Opcionales¶
Funciones con coste propio en AWS, apagadas por defecto: sólo se activan al pasar la opción o instanciar la clase. Cada docstring lleva su bloque "Coste y activación". Guías: Secretos, Índice de metadatos, Montajes S3, Tamaños, Eventos y webhooks, Exportación OTLP, Pasarela de secretos, Templates declarativos, Pilas opcionales, Volúmenes EFS (experimental), Dominio propio (experimental) y Funciones opcionales.
Pilas opcionales¶
OptionalStacks(*, region: str | None = None, session: boto3.session.Session | None = None, provisioner: StackProvisioner | None = None)
¶
Construirlo no hace ninguna llamada a AWS.
components() -> tuple[StackComponent, ...]
¶
Metadata pura del catálogo; nunca llama a AWS.
parameter_changes(component: str | StackComponent, *, stack_name: str | None = None, parameters: dict[str, str] | None = None, artifact_bucket: str | None = None) -> tuple[ParameterChange, ...]
¶
Qué parámetros cambiaría deploy() con estos mismos argumentos,
sin desplegar nada (sólo un DescribeStacks): lo que rayito stack
deploy imprime antes de pedir confirmación. Los que no se pasan y la
pila ya tiene no aparecen: deploy() los conserva.
deploy(component: str | StackComponent, *, stack_name: str | None = None, parameters: dict[str, str] | None = None, artifact_bucket: str | None = None, tags: dict[str, str] | None = None, wait: bool = True, wait_timeout: float = DEFAULT_WAIT_TIMEOUT_SECONDS) -> StackStatus
¶
Crea la pila o, si ya existe, la actualiza. Al crear, los
parámetros no pasados toman su valor por defecto del catálogo; al
actualizar, los no pasados conservan el valor con el que la pila está
desplegada (UsePreviousValue), así redesplegar sin repetir cada
parámetro no deshace la configuración anterior.
AsyncOptionalStacks(*, region: str | None = None, session: boto3.session.Session | None = None, provisioner: StackProvisioner | None = None)
¶
StackComponent(name: str, description: str, cost: CostStatement, parameters: tuple[StackParameter, ...] = (), artifacts: tuple[StackArtifact, ...] = (), capabilities: tuple[str, ...] = (), supported: bool = True)
dataclass
¶
StackParameter(name: str, description: str, required: bool = False, default: str | None = None, no_echo: bool = False)
dataclass
¶
StackStatus(name: str, state: str | None, outputs: dict[str, str] = dict(), reason_code: str | None = None, parameters: dict[str, str] = dict())
dataclass
¶
CostStatement(creates: tuple[str, ...], idle_monthly: str, per_use: tuple[str, ...] = (), removal: str = 'destroy() borra todo lo que esta pila creó', source: str = '')
dataclass
¶
El bloque "Coste y activación" de cada componente, de forma
estructurada: creates son los recursos que la pila crea, idle_monthly
el coste en reposo, per_use el coste por uso (vacío si no lo hay),
removal qué se conserva o se borra con destroy y source la
referencia con fecha de la página de precios consultada.
StackArtifact(name: str, parameter_key: str, bucket_parameter_key: str | None = None)
dataclass
¶
Un fichero que deploy() sube a artifact_bucket antes de
desplegar la plantilla (código Lambda); parameter_key es el parámetro
de la plantilla al que se pasa la clave S3 resultante
(rayito/stacks/<componente>/<sha256>.zip, _packaging.artifact_key).
bucket_parameter_key, si la
plantilla también necesita el bucket (p. ej. Code.S3Bucket de una
Lambda), es el parámetro al que deploy() pasa el mismo
artifact_bucket: así ni la CLI genérica ni la fachada de la función
tienen que repetirlo como --param.
Montajes S3¶
S3Mount(bucket: str, prefix: str = '', read_only: bool = True, allow_overwrite: bool = False, allow_delete: bool = False)
dataclass
¶
Un bucket (o un prefijo suyo) montado en el guest por rayd vía
mount-s3/FUSE. Sólo tiene efecto sobre rayito-base-caps (o una
variante derivada por tamaño): _role_policy.require_caps_for lo
comprueba antes de lanzar cuando la imagen se conoce.
read_only=True (el valor por defecto) es la postura de mínimo
privilegio: allow_overwrite/allow_delete sólo tienen efecto, y sólo
se aceptan, con read_only=False.
Coste y activación¶
Activa: Sandbox.create(mounts={"/mnt/data": S3Mount(...)}); construir
un S3Mount no llama a AWS.
Recursos y llamadas AWS: ningún recurso nuevo; mount-s3 hace las
peticiones S3 normales (GetObject/ListObjectsV2, y
PutObject/DeleteObject con read_only=False) con el execution
role del sandbox.
Coste aproximado: $0 propio de Rayito; las peticiones y el
almacenamiento normales de S3 del bucket (us-east-1, consultado
2026-10-01); la pila s3-mounts es $0 (sólo IAM).
IAM: RayitoS3MountAccess (rayito stack deploy s3-mounts) en el
execution role, y el bucket en RAYITO_ALLOWED_MOUNT_BUCKETS de la
imagen.
Cómo apagarla: no pases mounts=; rayito stack destroy s3-mounts
quita la política (no borra objetos ni el bucket).
Ejemplo:
Sandbox.create("rayito-base-caps", execution_role_arn=role_arn,
mounts={"/mnt/data": S3Mount(bucket="mi-bucket", prefix="team7/")})
MountStatus(state: MountPhase, last_error_class: str | None = None)
dataclass
¶
Lo que sbx.mounts[path] expone, leído de ConfigureStatus (o del
resultado de la última Configure): last_error_class es None salvo
cuando state == "failed".
Eventos y webhooks¶
LifecycleEvents(*, stack_name: str = DEFAULT_STACK_NAME, region: str | None = None, session: boto3.session.Session | None = None, stacks: OptionalStacks | None = None, gateway: EventsGateway | None = None)
¶
deploy(*, artifact_bucket: str, log_group_name: str, reconciler_interval_minutes: int = DEFAULT_RECONCILER_INTERVAL_MINUTES, tags: dict[str, str] | None = None, wait: bool = True) -> StackStatus
¶
Despliega infra/events-webhooks.yaml.
Coste y activación¶
Activa: una llamada explícita a este método (o rayito events deploy).
Recursos y llamadas AWS: un secreto de Secrets Manager (la clave
HMAC del stack), una tabla DynamoDB on-demand con streams, tres
funciones Lambda con un log group cada una, una suscripción de
CloudWatch Logs, dos colas SQS de fallos, una regla de
EventBridge Scheduler (rate(reconciler_interval_minutes),
mínimo 2) y cuatro políticas IAM gestionadas.
Coste aproximado: $0,40/mes el secreto; DynamoDB, Lambda y SQS son
on-demand/por uso ($0 en reposo); el Scheduler invoca el
reconciliador cada reconciler_interval_minutes minutos
(~$0,0000002 por invocación, us-east-1, 2026-09-30).
IAM: la pila emite una política por tarea; adjunta a cada identidad
sólo la suya: EventsLauncherPolicy (events=: lee la clave
del stack), EventsReaderPolicy (get_events: sólo filas de
eventos) y EventsWebhookAdminPolicy (register_webhook/
list_webhooks/delete_webhook: sólo filas WEBHOOK). Todas
incluyen DescribeStacks sobre la pila. EventsOperatorPolicy
(la unión de las tres) queda obsoleta.
Cómo apagarla: destroy() (fuerza el borrado del secreto: cualquier
webhook registrado deja de poder verificarse); desvincula antes
esas políticas de quien las tenga, o la pila acaba en
DELETE_FAILED (AWS_API_NOTES.md Q108).
Ejemplo:
ev = LifecycleEvents()
ev.deploy(artifact_bucket="mi-bucket", log_group_name="/rayito/rayito-base")
destroy(*, wait: bool = True) -> None
¶
Borra la pila entera: el secreto del stack (force-delete), la
tabla con todos sus eventos y webhooks, las tres Lambdas, la
suscripción, las colas de fallos y el scheduler. No toca los secretos
de cada webhook (rayito/webhooks/..., de SecretStore) ni el log
group de la imagen, que esta pila nunca creó. Si alguna de sus
políticas (EventsLauncherPolicy, EventsReaderPolicy,
EventsWebhookAdminPolicy o EventsOperatorPolicy) sigue vinculada a
algún usuario o rol, CloudFormation no puede borrarla y la pila
termina en DELETE_FAILED (StackException): desvincúlala y repite
(AWS_API_NOTES.md Q108).
AsyncLifecycleEvents(*, stack_name: str = DEFAULT_STACK_NAME, region: str | None = None, session: boto3.session.Session | None = None, stacks: OptionalStacks | None = None, gateway: EventsGateway | None = None)
¶
WebhookInfo(webhook_id: str, url: str, types: tuple[str, ...])
dataclass
¶
Lo que register_webhook/list_webhooks exponen; nunca el secreto
(ni su nombre en Secrets Manager, ni mucho menos su valor).
EventRecord(event_id: str, sandbox_id: str, kind: str, kill_reason: str | None, generation: int, occurred_at_ms: int, image_arn: str, image_version: str)
dataclass
¶
Una fila de get_events(). sandbox_execution_id es
f"{sandbox_id}#{generation}" (documentado para el payload del
webhook); sandbox_template_id es el ARN de la imagen.
Templates declarativos¶
Template(spec: TemplateSpec | None = None)
¶
Builder inmutable de un TemplateSpec. Template() crea uno vacío
(sin imagen base): llama a from_base_image() antes de compilar.
from_base_image(name: str = DEFAULT_BASE_IMAGE_NAME, *, version: str | None = None) -> Self
¶
Parte del zip de codeArtifact de name (ya publicada con
rayito image publish): Template.build() lo descarga
(s3:GetObject), le añade las instrucciones de cable compiladas y
vuelve a subir el resultado. version fija una versión en vez de
la última ACTIVE.
skip_cache(*, skip_cache: bool = True) -> Self
¶
0.6 no tiene caché de capas propia (investigación §3.4): esto
hace que Template.build()/build_in_background() se comporten
como con force=True — envían un build nuevo aunque ya exista una
versión con la misma configuración.
set_start_cmd(start_cmd: str, ready_cmd: ReadyCommand | str | None = None, *, user: str = DEFAULT_TEMPLATE_USER, workdir: str | None = None, envs: dict[str, str] | None = None) -> Self
¶
Hornea /etc/rayito/template.json (rayito.template/1). Al
arrancar, un rayd 0.6 o posterior lo lee y, antes del /ready del
build (el snapshot ya lo lleva en marcha), lanza start_cmd como
proceso gestionado (visible en commands.list()); si hay
ready_cmd, /ready responde 503 hasta que el comando sale con 0, y
500 al agotarse
ReadyPoll.timeout_seconds (wait_for_port/wait_for_url/
wait_for_process/wait_for_file en _ready_cmds.py; una cadena
cruda se sondea con la cadencia por defecto, 0,5 s durante 60 s).
Requiere que la imagen base se haya publicado con rayd 0.6 o
posterior: un rayd anterior ignora el fichero.
to_dockerfile() -> str
¶
El Dockerfile real que Template.build() sube (investigación
§3.6: to_dockerfile() ya existe en E2B).
to_json() -> str
¶
Serialización estable del TemplateSpec (depurar/diffear un
template entre builds; no es lo que rayd lee: eso es
/etc/rayito/template.json, un subconjunto sólo del StartSpec).
AsyncTemplate(spec: TemplateSpec | None = None)
¶
Bases: Template
Template con build/build_in_background/get_build_status/
exists en versión async (asyncio.to_thread, como el resto del SDK
async: ver sandbox_async/*). El resto de la superficie (DSL,
to_dockerfile, to_json) es pura y se hereda tal cual.
BuildInfo(template_id: str, build_id: str, alias: str)
dataclass
¶
Lo que devuelve un build que terminó bien. template_id es el ARN
de la imagen (pásalo a Sandbox.create(template_id)); build_id es
"<imageVersion>/<buildId>", para get_build_status/template logs.
BuildHandle(arn: str, version: str, name: str, region: str | None = None, session: Any = None)
dataclass
¶
Lo que devuelve build_in_background(): deja la petición ya
aceptada (create-microvm-image/update-microvm-image respondió) y
guarda lo necesario para que get_build_status() vuelva a preguntar
sin repetir el build.
BuildStatus(state: BuildState, info: BuildInfo | None = None, error_message: str | None = None)
dataclass
¶
El estado de un build en curso o terminado. info/error son
mutuamente excluyentes y ambos None mientras state ==
"IN_PROGRESS".
ReadyCommand(cmd: str, poll: ReadyPoll)
dataclass
¶
Un ready_cmd compilado: el comando de shell y el sondeo con el que
rayd lo ejecuta.
timeout(seconds: float) -> ReadyCommand
¶
Sustituye ReadyPoll.timeout_seconds por seconds (debe ser >
0); la cadencia de sondeo no cambia.
ReadyPoll(interval_seconds: float, timeout_seconds: float)
dataclass
¶
Cuánto y con qué cadencia rayd sondea ready_cmd antes de declarar
el build fallido (ver _ready_cmds.py).
TemplateSpec(base: BaseImageRef | None = None, steps: tuple[WireStep, ...] = tuple(), start: StartSpec | None = None, skip_cache: bool = False)
dataclass
¶
El estado inmutable que acumula el builder Template (_dsl.py):
de qué imagen parte, en qué orden se compilan las instrucciones de
cable, y el StartSpec opcional. skip_cache no activa ninguna caché
de capas propia (0.6 no tiene una, investigación §3.4): sólo fuerza
Template.build(..., force=True) a reconstruir en vez de reusar una
versión existente con la misma configuración.
BaseImageRef(kind: str, name: str, version: str | None = None)
dataclass
¶
Qué imagen compone from_base_image(). name es el nombre o ARN de
la imagen ya publicada (rayito-base, rayito-base-caps, ...);
version fija una versión en vez de "la última ACTIVE" (por defecto
None).
StartSpec(start_cmd: str, ready_cmd: str | None = None, user: str = DEFAULT_TEMPLATE_USER, workdir: str | None = None, envs: tuple[tuple[str, str], ...] = (), ready_poll: ReadyPoll | None = None)
dataclass
¶
Lo que horneado en /etc/rayito/template.json lee rayd (dominio
compartido con rayd_core::template::StartSpec, mismo esquema JSON):
el comando que arranca como proceso gestionado y, si lo hay, el que
rayd sondea antes de responder 200 en /ready.
RunStep(cmd: str)
dataclass
¶
RUN <cmd>, ejecutado con /bin/sh -c.
CopyStep(src: str, dst: str)
dataclass
¶
COPY <src> <dst>: src es una ruta relativa al contexto (bajo el
directorio desde el que se llama a Template.build), dst una ruta
absoluta en la imagen.
EnvStep(key: str, value: str)
dataclass
¶
ENV <key>=<value>: horneado en la imagen y en todos sus
snapshots. Nunca un secreto (investigación §3.7): set_envs no acepta
nada que deba rotar.
WorkdirStep(path: str)
dataclass
¶
WORKDIR <path>: afecta a los RunStep/CopyStep posteriores y al
workdir por defecto del start cmd.
UserStep(user: str)
dataclass
¶
USER <user>: afecta a los RunStep posteriores. DockerfileRenderer
siempre vuelve a USER root antes del CMD de rayd final
(investigación §3.5: "rayd sigue siendo PID 1").
wait_for_port(port: int) -> ReadyCommand
¶
Listo cuando algo escucha en port (TCP, loopback).
wait_for_url(url: str, *, status: int = 200) -> ReadyCommand
¶
Listo cuando url responde status (curl, sólo cabeceras).
wait_for_process(name: str) -> ReadyCommand
¶
Listo cuando hay un proceso cuyo nombre contiene name (pgrep -f).
wait_for_file(path: str) -> ReadyCommand
¶
Listo cuando path existe.
Secretos¶
SecretStore(*, region: str | None = None, session: boto3.session.Session | None = None, prefix: str = DEFAULT_SECRET_PREFIX, kms_key_id: str | None = None)
¶
CRUD de secretos de Rayito sobre AWS Secrets Manager, en tu cuenta.
Coste y activación¶
Activa: SecretStore(...) es la opción explícita del CRUD; construirlo no
llama a AWS: el cliente boto3 secretsmanager se crea en la primera
llamada a un método.
Recursos y llamadas AWS: un secreto de Secrets Manager por create
(CreateSecret); update = DescribeSecret + PutSecretValue
(+ UpdateSecret si cambias metadata); get_info/exists =
DescribeSecret; list = ListSecrets (eventualmente consistente:
un secreto recién creado o cambiado puede tardar ~3-5 s en salir);
destroy = DescribeSecret + DeleteSecret (sin ventana de
recuperación).
Coste aproximado: $0,40 por secreto y mes hasta destroy + $0,05 por
10 000 llamadas (us-east-1, 2026-09-30).
IAM: secretsmanager:CreateSecret, PutSecretValue, UpdateSecret,
DescribeSecret, DeleteSecret sobre ...:secret:rayito/* y
secretsmanager:ListSecrets sobre * (política RayitoSecretsAdmin
de infra/secrets-access.yaml); con kms_key_id=, kms:GenerateDataKey
y kms:Decrypt sobre esa clave.
Logs: ni el valor ni el nombre en los logs de Rayito; el DEBUG de
botocore sí imprime el valor (no lo actives con secretos).
Cómo apagarla: no instancies SecretStore; borra con destroy() los
secretos que ya no uses (se facturan hasta entonces).
Ejemplo:
store = SecretStore(region="us-east-1")
info = store.create("openai", "sk-...", metadata={"team": "ml"})
store.update("openai", "sk-nuevo")
page = store.list(limit=20)
store.destroy("openai")
name es un nombre bajo prefix (rayito/ por defecto; '' para
ninguno) o un ARN completo. Los errores nunca repiten el nombre ni el
valor. Una instancia es reutilizable y segura entre hilos.
region: str | None
property
¶
La región pedida (o la de la sesión); None = la cadena por defecto.
create(name: str, value: str, *, metadata: Mapping[str, str] | None = None) -> SecretInfo
¶
CreateSecret con la versión 1. Si el nombre aún se está borrando
(destroy reciente: AWS tarda ~20-30 s en liberarlo), reintenta con
backoff y jitter durante CREATE_RETRY_BUDGET_SECONDS (60 s) con el
mismo ClientRequestToken y después lanza SecretException.
created_at/updated_at son el reloj del cliente (get_info da los
de AWS).
update(name: str, value: str, *, metadata: Mapping[str, str] | None = None) -> SecretInfo
¶
Nueva versión n + 1 (DescribeSecret + PutSecretValue); con
metadata, además la sustituye (UpdateSecret). Los lectores con
SecretCache ven el valor nuevo al vencer su TTL o con refresh().
Más de una llamada cada 600 s para el mismo secreto avisa una vez
(UserWarning).
get_info(name: str) -> SecretInfo
¶
DescribeSecret. Un secreto programado para borrarse es
SecretNotFoundException.
list(*, limit: int | None = None, next_token: str | None = None) -> SecretPage
¶
Una página de ListSecrets filtrada por el prefijo (sin los
programados para borrarse). limit 1-100. Es eventualmente
consistente: un secreto recién creado o actualizado puede tardar
~3-5 s en aparecer (o en reflejar el cambio); get_info no.
destroy(name: str) -> bool
¶
DescribeSecret y, si existe, DeleteSecret(ForceDeleteWithoutRecovery=True):
deja de facturarse y no se puede recuperar. True si lo borró;
False si no existía (o ya estaba programado para borrarse, u otro
lo borró a la vez), sin llamar a DeleteSecret: el borrado forzado
de AWS no distingue un nombre inexistente.
read_value(ref: SecretRef) -> str
¶
GetSecretValue de una referencia; lo usa SecretCache (y sólo
ella: nadie más debería leer valores sin caché). Nunca lee un secreto
de firma de webhook (refuse_webhook_signing_secret).
cache_identity() -> tuple[str | None, object | None]
¶
La parte (región, credenciales) de la clave de SecretCache.
api() -> SecretsManagerApi
¶
El cliente secretsmanager, construido en el primer uso.
SecretCache(*, ttl_seconds: int | float = DEFAULT_TTL_SECONDS, store: SecretStore | None = None, region: str | None = None, session: boto3.session.Session | None = None, prefix: str | None = None)
¶
Caché en memoria de valores de secretos: lo que hace que secrets=
nunca traiga el secreto en cada llamada.
Coste y activación¶
Activa: secret_cache=SecretCache(ttl_seconds=300) fija la caché (y su
TTL) que usa secrets=; sin él, secrets= usa una caché compartida
del proceso por (región, sesión) con TTL 300, creada la primera vez.
Construirla no llama a AWS.
Recursos y llamadas AWS: GetSecretValue en el primer uso de cada
(región, credenciales, secreto, versión) y otra vez sólo al vencer el
TTL o con refresh(); un acierto hace 0 llamadas. Una sola petición
en vuelo por clave (10 hilos a la vez = 1 llamada).
Coste aproximado: con TTL 300, ≤ 12 llamadas/hora por secreto y proceso
≈ $0,04/mes ($0,05 por 10 000 llamadas, us-east-1, 2026-09-30); el
secreto en sí cuesta $0,40/mes aparte.
IAM: secretsmanager:GetSecretValue (política RayitoSecretsReader de
infra/secrets-access.yaml) y kms:Decrypt si el secreto usa una CMK.
Cómo apagarla: no pases secrets= ni secret_cache=. ttl_seconds=0 no
existe (sería traer en cada llamada): 1..86400.
Ejemplo:
cache = SecretCache(ttl_seconds=600, region="us-east-1")
sbx = Sandbox.create(secrets={"OPENAI_API_KEY": "openai"}, secret_cache=cache)
sbx.commands.run("python agent.py") # 1 GetSecretValue
sbx.commands.run("python agent.py") # 0: acierto
cache.refresh("openai") # fuerza una lectura nueva
Los valores viven sólo en la memoria de este proceso; repr/str nunca
los muestran. Un secreto que no existe no se guarda.
get(secret: str | SecretRef) -> str
¶
El valor (de la caché si sigue vigente; si no, GetSecretValue).
Cada llamada descarta antes los valores vencidos de esta caché.
aget(secret: str | SecretRef) -> str
async
¶
get para asyncio: un acierto no sale del bucle; un fallo corre en
asyncio.to_thread con la misma petición única por clave.
refresh(name: str | SecretRef | None = None) -> None
¶
Fuerza una lectura nueva: con name, la hace ya (y lanza si el
secreto no existe); sin él, descarta todo y cada secreto se relee en
su siguiente uso.
invalidate(name: str | SecretRef | None = None) -> None
¶
Descarta los valores de name (todas sus versiones si es un str) o
de todos.
sweep() -> int
¶
Descarta los valores vencidos (sin llamar a AWS) y devuelve cuántos: un valor no sobrevive en memoria a su TTL más allá del siguiente uso de la caché.
SecretRef(name: str, version_id: str | None = None, version_stage: str | None = None)
dataclass
¶
Una referencia a un secreto, nunca su valor: name es un nombre bajo el
prefijo del SecretStore ("openai" → rayito/openai) o un ARN
completo (arn:..., que se usa tal cual). version_id o
version_stage (como mucho uno) fijan la versión; sin ninguno se lee
AWSCURRENT. Una cadena simple en secrets= equivale a SecretRef(name).
selector: tuple[str, str]
property
¶
La versión pedida como parte de la clave de caché.
SecretInfo(secret_id: str, name: str, version: int, metadata: dict[str, str], created_at: datetime, updated_at: datetime)
dataclass
¶
Metadatos de un secreto (los mismos campos que e2b.SecretInfo). El
valor es de sólo escritura y nunca vuelve en ninguna lectura.
secret_id es el ARN de Secrets Manager (E2B usa sec_…); version es
el entero que Rayito codifica en el ClientRequestToken (1 al crear, +1
en cada update; 0 si la versión actual no la escribió Rayito);
metadata vive en Description (≤ 2048 caracteres codificados).
SecretPage(items: list[SecretInfo], next_token: str | None)
dataclass
¶
Una página de SecretStore.list(): next_token es None en la última.
Índice de metadatos¶
DynamoDbIndex(table_name: str, *, region: str | None = None, session: boto3.session.Session | None = None, on_write_failure: WriteFailurePolicy = 'terminate', ttl_margin_seconds: int = DEFAULT_TTL_MARGIN_SECONDS)
¶
Índice de metadatos de sandboxes sobre una tabla DynamoDB de tu cuenta.
Coste y activación¶
Activa: index=DynamoDbIndex("rayito-sandboxes") en Sandbox.create()
(escribe una fila por sandbox), Sandbox.list()/paginate() con
metadata= (filtra también SUSPENDED sin sondear Health) y
PoolConfig(index=) (escribe al rellenar). Construirlo no llama a
AWS: el cliente boto3 dynamodb se crea en su primer uso.
Recursos y llamadas AWS: dynamodb:PutItem (1 por sandbox creado,
condicional attribute_not_exists(pk)) y dynamodb:BatchGetItem (1
por página de list-microvms, ≤ 100 claves, lectura eventualmente
consistente). Ningún recurso: la tabla la despliegas tú
(infra/metadata-index.yaml). Nunca DeleteItem (el TTL borra).
Coste aproximado: on-demand, us-east-1, consultado 2026-09-30: ~1 WRU por
create ≈ $0,000000625; 0,5 RRU por ítem listado; $0,25/GB-mes;
10 000 sandboxes/mes < $0,10; tabla vacía $0
(https://aws.amazon.com/dynamodb/pricing/on-demand/).
IAM: dynamodb:PutItem (escritor) y dynamodb:BatchGetItem (lector)
sobre el ARN de la tabla, en las credenciales del llamante.
Cómo apagarla: no pases index= (None por defecto) y borra el stack
de la tabla si ya no la usas.
Ejemplo:
idx = DynamoDbIndex("rayito-sandboxes", region="us-east-1")
sbx = Sandbox.create(metadata={"user": "42"}, index=idx)
sbx.pause()
items = list(Sandbox.list(metadata={"user": "42"}, states=["SUSPENDED"], index=idx))
on_write_failure decide qué hace create() si PutItem falla:
'terminate' (por defecto) termina el MicroVM recién lanzado (salvo
keep_on_failure=True) y lanza IndexWriteException; 'warn' avisa en
el logger rayito.index y devuelve el sandbox, que no aparecerá en los
listados con índice. ttl_margin_seconds es el margen sobre la vida
máxima del sandbox antes de que la fila caduque. region/session son
los de la tabla (por defecto la cadena de boto3); sin región,
create() lanza InvalidArgumentException antes de run-microvm, sin
lanzar ningún MicroVM. Reutilizable y seguro entre hilos.
now() -> float
¶
Segundos epoch con los que se descartan filas caducadas.
put(record: IndexRecord) -> None
¶
PutItem condicional: nunca sobrescribe una fila existente.
batch_get(sandbox_ids: Sequence[str]) -> dict[str, IndexRecord]
¶
Las filas vigentes de sandbox_ids, en trozos de 100 con lectura
eventualmente consistente; reintenta UnprocessedKeys con un backoff
acotado y, si siguen sin procesar, lanza (nunca una lista incompleta
en silencio). Las filas caducadas o con otra forma se descartan.
prepare() -> None
¶
Comprobación previa sin llamadas a AWS que create() hace antes de
run-microvm: construye el cliente dynamodb (construirlo no llama a
AWS) y lanza InvalidArgumentException si no hay región, así un
error de configuración nunca lanza (ni factura) un MicroVM.
api() -> DynamoDbApi
¶
El cliente dynamodb, construido en el primer uso.
Tamaños¶
SizeRequest(memory_mib: int)
dataclass
¶
size=SizeRequest(memory_mib=3000): una petición explícita en MiB,
para cuando ningún nombre corto ("512mb".."8gb") encaja exactamente.
Siempre se redondea hacia arriba, nunca hacia abajo: un sandbox nunca
recibe menos memoria de la que se pidió.
Pasarela de secretos¶
SecretGateway(upstream: str, headers: Mapping[str, str | SecretRef], allow: Sequence[tuple[str, str]], rate_per_minute: int = 0)
dataclass
¶
Una entrada de gateways={"nombre": SecretGateway(...)} en
Sandbox.create(): un único upstream fijo al que rayd reenvía,
inyectando en cada petición permitida el valor de cada cabecera de
headers (resuelto de Secrets Manager por su nombre, nunca aquí) y
rechazando lo que allow no cubre o exceda rate_per_minute.
Coste y activación¶
Activa: gateways={"nombre": SecretGateway(...)} en Sandbox.create()
(o SandboxPool.take(gateways=)); sin él, rayd no abre ningún listener de
loopback y el SDK no hace ninguna llamada a ConfigureSandbox ni a
Secrets Manager.
Recursos y llamadas AWS: secretsmanager:GetSecretValue una vez por
cabecera y TTL de la SecretCache que ya usa secrets= (un acierto
no llama a AWS), y una por cabecera en cada sbx.gateways.refresh();
ningún recurso nuevo (reutiliza infra/secrets-access.yaml).
Coste aproximado: el de SecretCache ($0,05 por 10 000 llamadas,
us-east-1, 2026-09-30) más el del secreto en sí si no existía ya.
IAM: secretsmanager:GetSecretValue en las credenciales del llamante,
igual que secrets= (no el execution role: la pasarela corre en
rayd, no necesita rol de ejecución).
Cómo apagarla: no pases gateways= (por defecto None).
Ejemplo:
sbx = Sandbox.create(gateways={
"anthropic": SecretGateway(
upstream="https://api.anthropic.com",
headers={"x-api-key": "anthropic"},
allow=[("POST", "/v1/messages")],
rate_per_minute=600,
)
})
url = sbx.gateways["anthropic"].url
sbx.commands.run("python agent.py", envs={"ANTHROPIC_BASE_URL": url})
GatewayStatus(port: int, last_error_class: str | None = None)
dataclass
¶
Espejo de SecretGatewayRouteStatus: el estado de una ruta tras el
Configure (o ConfigureStatus) más reciente.
url: str
property
¶
"http://127.0.0.1:<port>": no es secreto (nada más que uid 1000
puede alcanzar el puerto dentro del sandbox).
GatewayHandle(statuses: Mapping[str, GatewayStatus], *, refresher: Callable[[], Mapping[str, GatewayStatus]] | None = None, async_refresher: Callable[[], Awaitable[Mapping[str, GatewayStatus]]] | None = None, recovered: bool = False)
¶
Bases: Mapping[str, GatewayStatus]
sbx.gateways: un mapeo de sólo lectura nombre -> GatewayStatus.
Sin gateways= es siempre un GatewayHandle vacío cuyo refresh()/
arefresh() no hacen nada, así que llamarlos nunca es una rama especial
para quien los use. Con gateways=, releen cada cabecera de Secrets
Manager (aunque la SecretCache no haya vencido) y mandan un
Configure nuevo — la forma de rotar un secreto sin recrear el
sandbox. Cada ruta conserva su puerto; si rayd rechaza la sección,
lanzan y el estado anterior sigue en pie.
Sandbox (síncrono) pasa refresher; AsyncSandbox pasa
async_refresher (mismo patrón que SecretCache.get/aget): cada uno
sabe envolver al otro, así que da igual cuál dio el handle.
recovered: bool
property
¶
True si connect() lo reconstruyó desde ConfigureStatus (no
sabe rotar secretos); False si viene de create(gateways=).
refresh() -> None
¶
Para Sandbox. Un AsyncSandbox usa arefresh(): llamar a
refresh() desde una corrutina bloquearía su propio loop.
arefresh() -> None
async
¶
Para AsyncSandbox. Sobre un handle síncrono, corre el
refresher en un hilo (como SecretCache.aget sobre get).
Exportación OTLP¶
Guía: Exportación OTLP.
TelemetryExport(interval_s: int = DEFAULT_INTERVAL_S, service_name: str = DEFAULT_SERVICE_NAME, names: NameStyle = 'rayito', auth: OtlpAuth = OtlpAuth.execution_role())
dataclass
¶
telemetry= de Sandbox.create()/AsyncSandbox.create(). Validado
al construirse: un valor fuera de rango es InvalidArgumentException
antes de run-microvm, nunca una sección rechazada por rayd después
de lanzar la MicroVM.
OtlpAuth(kind: Literal['execution_role', 'bearer'], secret_name: str | None = None)
dataclass
¶
Cómo firma rayd cada exportación (research §6.3: opciones B1/B1').
execution_role() es SigV4 sobre las credenciales IMDS del execution
role: exige rayito-base-caps (o una variante derivada, por tamaño).
bearer(secret_name=...) es experimental (B1'): secret_name se
resuelve igual que en secrets= y SecretStore, bajo el prefijo
rayito/ ("otlp-key" lee rayito/otlp-key; un ARN completo se usa
tal cual), así que basta la política RayitoSecretsReader de
infra/secrets-access.yaml. El valor se lee al enviar la sección por
la SecretCache compartida (_section.resolve_bearer_token) y viaja a
rayd por ConfigureSandbox, nunca por variables de entorno (regla 4
de ADR-014) ni en texto plano en ningún lado; funciona en
rayito-base, sin caps.
TelemetryHealth(exported: int = 0, dropped: int = 0, last_error_class: str | None = None)
dataclass
¶
sbx.get_telemetry_status(): espejo de TelemetryExportStatus
(ConfigureService.ConfigureStatus). Todo ceros/None cuando rayd
nunca aplicó una sección (no hay telemetry=, o la imagen no soporta
la función).
Volúmenes EFS (experimental)¶
VolumeStore(*, file_system_id: str, region: str | None = None, session: boto3.session.Session | None = None)
¶
CRUD de volúmenes (access points) de un sistema de ficheros EFS que
ya existe (desplegado con rayito stack deploy efs-volumes o uno
propio). Una instancia es reutilizable y segura entre hilos.
Coste y activación¶
Activa: VolumeStore(...); construirlo no llama a AWS (el cliente boto3
efs se crea en el primer método). Experimental: lo que devuelve se
monta con Sandbox.create(volumes={ruta: vol}) sobre la imagen
opcional rayito-base-caps-efs (rayito image publish --with-efs);
ese create() añade una DescribeMountTargets por sistema de
ficheros si el volumen no trae mount_target_ip.
Recursos y llamadas AWS: ningún recurso nuevo (el sistema de ficheros lo
crea infra/efs-volumes.yaml, por separado); create = CreateAccessPoint,
get/list = DescribeAccessPoints, destroy = DeleteAccessPoint.
Coste aproximado: los access points no tienen cargo propio listado; el
sistema de ficheros se factura por infra/efs-volumes.yaml (ver ese
componente en rayito stack list).
IAM: elasticfilesystem:CreateAccessPoint, DescribeAccessPoints,
DeleteAccessPoint (y DescribeMountTargets para montar sin
mount_target_ip) sobre el sistema de ficheros (credenciales del
LLAMANTE, no del execution role del MicroVM).
Cómo apagarla: no instancies VolumeStore; borra con destroy() los
volúmenes que ya no uses (el directorio que cubrían no se borra, sólo el
access point: borrar los datos exige montar el sistema de ficheros desde
un sandbox, fuera de alcance en 0.6).
Ejemplo:
store = VolumeStore(file_system_id="fs-0123abcd", region="us-east-1")
vol = store.create("datos-agente-7")
store.get("datos-agente-7")
store.list()
store.destroy("datos-agente-7")
create(name: str) -> EfsVolume
¶
CreateAccessPoint con uid/gid 1000:1000 y la ruta raíz
/rayito-volumes/<name> (research doc §4.5). Idempotente: llamarla
dos veces con el mismo nombre no crea dos access points
(ClientToken es un hash de file_system_id+nombre). AWS no
devuelve el access point ya creado para un ClientToken repetido:
responde AccessPointAlreadyExists, que esta función atrapa para
devolver get(name) en su lugar, reintentado durante
LIST_VISIBILITY_BUDGET_SECONDS porque el listado de EFS tarda en
mostrar un access point recién creado (Q125).
get(name: str) -> EfsVolume
¶
DescribeAccessPoints(FileSystemId=...) filtrado por la
etiqueta rayito:volume=<name>. VolumeNotFoundException si no
existe ninguno con ese nombre.
list() -> tuple[EfsVolume, ...]
¶
Todos los access points del sistema de ficheros con la etiqueta
de volumen de Rayito, paginando DescribeAccessPoints hasta
agotar NextToken. Eventualmente consistente, como el propio
DescribeAccessPoints (Q125): un volumen recién creado puede tardar
unos segundos en aparecer y uno recién borrado seguir apareciendo
(también en get).
destroy(name: str) -> bool
¶
DeleteAccessPoint; los ficheros bajo su directorio raíz no se
borran (honesto: borrarlos exigiría montar el sistema de ficheros,
fuera de alcance en 0.6). True si existía y se borró; False si
no existía ningún volumen con ese nombre.
api() -> EfsApi
¶
El cliente efs, construido en el primer uso.
AsyncVolumeStore(*, file_system_id: str, region: str | None = None, session: boto3.session.Session | None = None)
¶
EfsVolume(file_system_id: str, access_point_id: str, name: str | None = None, region: str | None = None, read_only: bool = False, mount_target_ip: str | None = None)
dataclass
¶
Un volumen EFS: el access point que un sandbox puede montar bajo
volumes={path: EfsVolume(...)}. Validado en construcción, sin ninguna
llamada a AWS; name/region son metadata del cliente (VolumeStore
los rellena al crear o leer uno), nunca parte de la identidad que
manda al guest.
VolumeStatus(state: MountState, last_error_class: str | None = None)
dataclass
¶
Lo que sbx.volumes[path] reporta (una ConfigureStatus por
lectura). last_error_class es una cadena cerrada de
efs_volumes.proto (credentials_expired, flush_timeout, stale,
iam_denied...), nunca un mensaje de AWS.
EfsVolumes(*, stack_name: str = DEFAULT_STACK_NAME, region: str | None = None, session: boto3.session.Session | None = None, stacks: OptionalStacks | None = None, network: NetworkInspector | None = None, efs: Callable[[], EfsFileSystemApi] | None = None)
¶
Volúmenes EFS en tu propia VPC: comprueba, despliega, consulta y
borra el componente efs-volumes. Construirlo no llama a AWS.
Coste y activación¶
Activa: una llamada explícita a deploy(vpc_id=, subnet_ids=) (o
rayito stack deploy efs-volumes --param VpcId=... --param
SubnetIds=...). check() es de sólo lectura y no crea nada.
Experimental: Sandbox.create(volumes=...) monta sobre la imagen
opcional rayito-base-caps-efs (rayito image publish --with-efs),
con el execution role y este conector como único egress=: no
puede usar además INTERNET_EGRESS.
Recursos y llamadas AWS: check() = ec2:DescribeVpcs,
DescribeVpcAttribute, DescribeSubnets, DescribeRouteTables.
deploy() crea, sólo dentro de la VPC dada y etiquetado: un sistema
de ficheros EFS cifrado (clave KMS gestionada por AWS), un mount
target por subred, dos grupos de seguridad nuevos (el de los mount
targets sólo admite TCP 2049 desde el del conector), el
AWS::Lambda::NetworkConnector de salida a la VPC para MicroVMs, su
rol de operador y la política RayitoEfsVolumeClient. Nunca modifica
la VPC, sus subredes, tablas de rutas, NACLs ni grupos existentes.
Coste aproximado: $0 con el sistema de ficheros vacío; $0,30/GB-mes
(Standard) y $0,016/GB-mes tras 30 días (IA); Elastic Throughput
$0,03/GB leído y $0,06/GB escrito; sin cargo listado por mount
targets, access points ni ENIs del conector (us-east-1, 2026-09-11).
IAM: el llamante necesita cloudformation:*Stack* sobre la pila, crear
los recursos de arriba (CAPABILITY_IAM), los ec2:Describe* de
check() y, para borrar el sistema de ficheros,
elasticfilesystem:DescribeFileSystems/DescribeMountTargets/
DescribeAccessPoints/DeleteAccessPoint/DeleteFileSystem; el
execution role del MicroVM, la política CallerPolicyArn de la
salida (ClientMount/ClientWrite sólo sobre este sistema de
ficheros y sus access points; nunca ClientWrite sobre
read_only_access_point_arns).
Cómo apagarla: destroy() borra la pila (conector, grupos, mount
targets, rol y política) y conserva el sistema de ficheros con sus
datos; destroy(delete_file_system=True) borra además sus access
points y el propio sistema de ficheros: todo lo que deploy() creó.
Tras rayito stack destroy efs-volumes (que siempre lo conserva),
delete_file_system("fs-…") borra el que quedó.
Ejemplo:
efs = EfsVolumes(region="us-east-1")
network = {"vpc_id": "vpc-0123456789abcdef0", "subnet_ids": ["subnet-0123456789abcdef0"]}
report = efs.check(network)
efs.deploy(network)
store = efs.volume_store()
efs.destroy(delete_file_system=True)
check(*, vpc_id: str, subnet_ids: Sequence[str] | str) -> EfsNetworkReport
¶
Valida, sin crear nada, que la VPC y las subredes sirven: que
existen, que las subredes son de la VPC y están en AZs distintas,
que les quedan IPs libres y si la VPC resuelve DNS; avisa de una
sola AZ y de cómo saldría a internet un sandbox. El informe incluye
lo que deploy() crearía y su coste. Sólo llama a
ec2:DescribeVpcs/DescribeVpcAttribute/DescribeSubnets/
DescribeRouteTables.
deploy(*, vpc_id: str, subnet_ids: Sequence[str] | str, allow_write: bool = True, access_point_arns: Sequence[str] = (), read_only_access_point_arns: Sequence[str] = (), connector_name: str | None = None, tags: dict[str, str] | None = None, wait: bool = True) -> StackStatus
¶
Corre check() y, sólo si ningún hallazgo es FAIL, despliega
(o actualiza) la pila en la VPC dada. access_point_arns acota la
política RayitoEfsVolumeClient a esos access points; vacío, a
cualquier access point de la cuenta y región sobre este sistema de
ficheros. allow_write=False sólo concede ClientMount.
read_only_access_point_arns deja esos access points en sólo
lectura aunque allow_write sea True (la política les deniega
ClientWrite): es lo que hace de sólo lectura un volumen, porque la
opción ro del montaje no impide escribir por el túnel de
efs-proxy (AWS_API_NOTES.md §16 Q133). Ver el bloque "Coste y
activación" de la clase.
volume_store() -> VolumeStore
¶
Un VolumeStore sobre el sistema de ficheros de la pila ya
desplegada (lee FileSystemId de sus salidas). VolumeException
si la pila no existe.
destroy(*, delete_file_system: bool = False, wait: bool = True) -> None
¶
Borra la pila. El sistema de ficheros se conserva siempre
(DeletionPolicy: Retain) salvo con delete_file_system=True, que
después borra sus access points y el propio sistema de ficheros (y
con él todos los datos): eso exige wait=True, porque
DeleteFileSystem falla mientras quede un mount target.
delete_file_system(file_system_id: str) -> None
¶
Borra un sistema de ficheros que esta pila creó y conservó (p. ej.
tras rayito stack destroy efs-volumes, que siempre lo deja): espera
a que no le quede ningún mount target, borra sus access points y
después el sistema de ficheros, con todos sus datos. Se niega
(VolumeException) si no lleva la etiqueta que infra/efs-volumes.yaml
le pone (rayito=efs-volumes): nunca borra uno ajeno. Uno que ya no
existe es un no-op.
AsyncEfsVolumes(*, stack_name: str = DEFAULT_STACK_NAME, region: str | None = None, session: boto3.session.Session | None = None, stacks: OptionalStacks | None = None, network: NetworkInspector | None = None, efs: Callable[[], EfsFileSystemApi] | None = None)
¶
Ver EfsVolumes (su bloque "Coste y activación" vale igual aquí).
EfsNetworkReport(vpc_id: str, subnet_ids: tuple[str, ...], availability_zones: tuple[str, ...], findings: tuple[NetworkFinding, ...], cost: CostStatement)
dataclass
¶
Lo que EfsVolumes.check() devuelve sin crear nada: los hallazgos,
las AZs que cubrirían los mount targets y lo que deploy() crearía
con su coste (el CostStatement del componente efs-volumes, la misma
fuente que rayito stack list).
ok: bool
property
¶
True si ningún hallazgo es FAIL: deploy() se niega si no.
NetworkFinding(level: FindingLevel, code: str, message: str)
dataclass
¶
Un resultado de la comprobación: code es un conjunto cerrado
(vpc-missing, vpc-unavailable, vpc-dns-support,
vpc-dns-hostnames, subnet-missing, subnet-other-vpc,
subnet-unavailable, subnet-same-az, subnet-free-ips, single-az,
internet-egress).
Dominio propio (experimental)¶
Guía: Dominio propio. Sin verificar aún de punta a punta en AWS real.
CustomDomain(*, public_domain: str, stack_name: str | None = None, kvs_arn: str | None = None, region: str | None = None, session: boto3.session.Session | None = None, provisioner: StackProvisioner | None = None, kvs_writer: KeyValueStoreWriter | None = None, clock: Callable[[], float] = time.time)
¶
Dominio propio sobre CloudFront (experimental). Construirlo no hace ninguna llamada a AWS.
Experimental: la API puede cambiar en una minor. La CloudFront
Function de enrutado y la creación de su KeyValueStore se comprobaron
en AWS real (AWS_API_NOTES.md Q121, Q140), pero crear la distribución
y servir tráfico por ella no (Q141: la cuenta de pruebas lo deniega con
una SCP). Si lo pruebas, cuéntalo en un issue de GitHub.
Coste y activación¶
Activa: CustomDomain(public_domain=...).deploy(certificate_arn=...).
Recursos y llamadas AWS: una distribución CloudFront, su CloudFront
Function de enrutado y un KeyValueStore (infra/custom-domain.yaml,
rayito stack deploy custom-domain). En uso, register()/
unregister()/refresh() llaman a DescribeKeyValueStore/PutKey/
DeleteKey (AWS_API_NOTES.md §29); nada de esto se llama sin
invocarlos explícitamente. No hay refresher automático en este
cambio (seguimiento no bloqueante, ver _stacks/components/
custom_domain.py): llama a refresh() tú mismo antes de que
caduque el JWE de una ruta (DOM-7).
Coste aproximado: CloudFront ~$0,085/GB + $0,0075/10 000 peticiones
HTTPS (datos salientes, us-east-1); CloudFront Functions ~$0,10 por
1 000 000 de invocaciones (una por petición); KeyValueStore $0 en
reposo, ~$0,50 por 1 000 000 de lecturas (las de la Function) y ~$5
por 1 000 000 de llamadas de gestión (PutKey/DeleteKey de
register/unregister/refresh) — tres líneas separadas, no una
cifra combinada (_stacks/components/custom_domain.py, cifras de
lista de CloudFront Functions/KeyValueStore desde su lanzamiento,
reconfirmar en la etapa de aceptación AWS).
IAM: cloudformation:*Stack* para deploy/destroy (vía OptionalStacks);
cloudfront-keyvaluestore:DescribeKeyValueStore/PutKey/DeleteKey sobre
el KVS de la pila para register/unregister/refresh.
Cómo apagarla: no llames a deploy(); destroy() borra la distribución,
la Function y el KVS — ningún dato de sandbox se conserva, las
rutas son efímeras.
Ejemplo:
domain = CustomDomain(public_domain="sbx.example.com")
domain.deploy(certificate_arn="arn:aws:acm:us-east-1:...:certificate/...")
route = domain.register(
"ws-7", 8000, endpoint=sbx.endpoint, jwe=jwe,
traffic_token=secrets.token_urlsafe(32), ttl_seconds=2400,
)
route.host # "8000-ws-7.sbx.example.com"
domain.unregister("ws-7", 8000)
deploy(*, certificate_arn: str, alternate_domain_names: Sequence[str] | None = None, tags: dict[str, str] | None = None, wait: bool = True, wait_timeout: float = CUSTOM_DOMAIN_WAIT_TIMEOUT_SECONDS) -> StackStatus
¶
Despliega (o actualiza) infra/custom-domain.yaml. certificate_arn
debe estar en us-east-1 (requisito de CloudFront) y cubrir los
nombres de la distribución: *.<public_domain> por defecto.
alternate_domain_names sustituye ese comodín por una lista
explícita (_domain.validate_alternate_domain_names; normalmente
host_for(alias, port) de rutas conocidas), p. ej. si otra
distribución ya tiene *.<public_domain>. Las rutas cuyo host no
esté en la lista no llegan a esta distribución. No cambia el coste.
Tras desplegar, apunta un CNAME/alias de DNS de cada nombre al
DistributionDomainName de la salida.
kvs_arn() -> str
¶
El ARN del KeyValueStore de la pila: el pasado al construir
CustomDomain, o el de la última deploy()/status(). Nunca llama
a AWS por sí sola: si no hay ninguno, llama a status() primero.
host_for(alias: str, port: int) -> str
¶
El hostname público de una ruta; no llama a AWS ni exige que la
ruta esté ya registrada (pura, _domain.route_host).
register(alias: str, port: int, *, endpoint: str, jwe: str, traffic_token: str | None = None, public: bool = False, ttl_seconds: int) -> CustomDomainRoute
¶
Escribe las dos claves de la ruta (j:/m:) con ETag
encadenado (_write_route(..., rollback=True): ninguna de las dos
claves existía antes para este label, así que si la segunda
escritura falla, la primera se borra best-effort antes de relanzar
— nunca se deja un JWE vivo sin sus metadatos). Una ruta es pública
(sin traffic_token que comprobar) sólo con public=True, nunca
por omisión: sin traffic_token ni public=True se rechaza antes
de tocar el KVS (SEC-T25 — el valor por defecto nunca es
"pública").
refresh(route: CustomDomainRoute, *, jwe: str, ttl_seconds: int) -> CustomDomainRoute
¶
Reescribe j:<label> con un JWE nuevo (el TokenRefresher del
transporte ya renueva el suyo a los 45 min,
_transport.TOKEN_REFRESH_AFTER_MINUTES; esto hace lo mismo para la
copia que vive en el KVS) y también m:<label>, para que su
expiración no quede obsoleta: mismo endpoint/hash de
traffic_token que route ya tenía, sólo expires_at al día.
_write_route(..., rollback=False): a diferencia de register(),
aquí ambas claves ya existían con una ruta sana sirviendo tráfico,
así que un fallo a medias (p. ej. el PutKey de m: tras el de
j:) no debe borrar lo que sí se escribió — eso dejaría una ruta
que funcionaba sin su j: o su m: por un error transitorio del
KVS.
unregister(alias: str, port: int) -> None
¶
Idempotente: una ruta que ya no está no es un error
(ResourceNotFoundException del DeleteKey se traga aquí, no en
el adaptador — llegar aquí ya exige que el almacén exista).
AsyncCustomDomain(*, public_domain: str, stack_name: str | None = None, kvs_arn: str | None = None, region: str | None = None, session: boto3.session.Session | None = None, provisioner: StackProvisioner | None = None, kvs_writer: KeyValueStoreWriter | None = None, clock: Callable[[], float] = time.time)
¶
CustomDomain para asyncio (experimental): los mismos métodos,
cada uno en un hilo porque boto3 es bloqueante. Construirlo no hace
ninguna llamada a AWS. El bloque "Coste y activación" y el alcance de lo
verificado en AWS real son los de CustomDomain.
CustomDomainRoute(alias: str, port: int, public_domain: str, endpoint: str, traffic_token_sha256: str, expires_at: datetime)
dataclass
¶
Una ruta ya registrada: su hostname y cuándo caduca el JWE que
cubre. endpoint/traffic_token_sha256 son los metadatos que
register() ya escribió en m:<label>; refresh() los necesita de
vuelta para reescribir esa clave con la misma endpoint/hash y sólo
expires_at al día.