Skip to main content

Runtime preparation integrations

Install one deployment-owned RuntimePreparationProvider through astrabox.providers.runtime_preparation:

[project.entry-points."astrabox.providers.runtime_preparation"]
enterprise = "my_deployment.preparation:PreparationProvider"

The entry point targets a subclass of astrabox.seams.RuntimePreparationProvider with a stable name. Conflicting providers fail startup. With no provider, the platform prepares Agents and Sessions without callbacks.

One provider answers every point at which a deployment prepares an Agent or a Session. Each method has a default, so implement only the points you need:

MethodWhen the platform calls it
planWhenever it plans an Agent's runtime; synchronous, no I/O
synchronize, recoverOnce per prepared box, before it becomes waiting inventory
prepare_sessionBefore the engine initializes: cold start, claim, reattachment, sandbox replacement
prepare_turnBefore every engine write, under the turn guard
read_user_actions, perform_user_actionWhen the Session's owner reads or acts on its prerequisites

The platform owns ordering, placement, fencing, file delivery and failure handling. The provider owns external identities, declaration formats, credential policy and prerequisites. It never starts a consent flow by itself, registers an Agent per Session, or dispatches work.

Declare when a Session is required​

plan(*, config) -> RuntimePreparationPlan is synchronous and performs no I/O. Its detached resolved Agent configuration contains no requesting Session. session_bound=True means the engine cannot be initialized through EngineAdapter.prepare_runtime before a real Session's materials exist.

The platform still prepares the image, workspace account, Plugin/Skills caches, control service and shared seats. Waiting inventory contains no personal Session material. Its receipt records engine_initialized=False. Claiming that inventory retains its placement, delivers the Session material and activates the engine without treating it as an already initialized vendor runtime. Failures discard exactly the claimed unit; they do not silently switch to a cold start.

The provider name and declaration participate in the runtime generation. Changes invalidate waiting inventory. With session_bound=False, ordinary initialized prewarming and protected Vault credential substitution retain their existing path. Session-bound resource prewarming also works without Credential Vault; it leaves model and environment credentials out of unclaimed inventory and resolves them at claim. It does not implement unavailable Vault capabilities.

This controls initialization performed by the engine adapter. Required system services and wrappers must already be baked into the selected image. The provider cannot retrofit a wrapper into a box or defer an image's own entrypoint.

Synchronize Agent resources​

async synchronize(*, context: AgentPreparationContext) -> None runs after the actual Plugin and Skills caches exist, before waiting inventory is published. It runs once for each prepared box, so an Agent without prepared capacity is never synchronized; claim, cold start, reattachment and input refresh do not call it. Per-conversation declarations belong in prepare_turn.

ContextMeaning
operation.agent_idComplete stable Agent ID; the external identity key
operation.operation_idDurable invocation identity for idempotency and recovery
creator_user_idPersisted Agent creator
config_version, preparation_id, runtime_generationAuthored version, pool lifetime and resolved recipe
configDetached resolved Agent configuration
sandboxStandard SandboxHandle, including its file interface
packagesOrdered source declarations and actual cache checkout paths
skills_pathActual prepared Skills directory, or None

Read actual prepared files and merge the deployment's declarations. Keep the external identity keyed by Agent ID across versions and pool members. Skip unchanged external writes in the provider. Do not log trusted configuration.

The existing Agent repository's compare-and-set serializes external updates across replicas, members and generations. The host checks the version, refresh revision, pool lifetime and generations before and after synchronization and before publishing inventory. A superseded result is refused.

An exception, timeout, cancellation or host loss leaves an unresolved operation. Its deadline permits async recover(*, operation: AgentPreparationOperation); it does not permit a new uncertain write. Return True only after that exact operation can never write again, including delayed requests. Recovery must be idempotent and must not fence newer operations. The default returns False. A successful synchronize must leave no outstanding external writes. An expired lease or absent remote result alone does not prove quiescence.

Deliver Session materials​

async prepare_session(*, context: SessionPreparationContext) -> RuntimeMaterials runs before engine activation on cold start, claim, reattachment and sandbox replacement. Its principal_id comes from the persisted Session owner. The context includes Session/Agent IDs, resolved configuration, runtime generation, sandbox ID and workload identity. No fake Session is supplied during Agent prewarming.

Return RuntimeFile(name, content: bytes) entries and an environment mapping. Use context.file_path(name) when an environment value names a returned file. The platform writes each file into the workload's private home with ownership and mode 0600, outside the task workspace. File names must be safe basenames; paths remain stable for that Session and sandbox. Material bytes and environment values are excluded from their dataclass representations and public errors. A Session-bound provider must return material.

The environment stays fixed for the life of the runtime it starts. An engine adapter must declare supports_startup_environment() and deliver EngineStartupContext.runtime_env before native initialization. The built-in Claude Code adapter does so through its SDK options; other built-in adapters refuse Session environment rather than ignore it. Platform-owned environment names cannot be overwritten. A refusal blocks activation with RUNTIME_PREPARATION_FAILED; the exception text is not exposed.

The Claude Agent SDK probes the CLI with claude -v before it starts it, without the options environment. An image wrapper that gates the CLI on Session material should pass -v and --version through unchanged.

Prepare each engine write​

async prepare_turn(*, context: TurnPreparationContext) -> RuntimeMaterials runs immediately before every engine write, after the platform reloads the Session, confirms that the turn still holds its slot and claims a durable per-Session guard. It must be idempotent for context.dispatch_attempt_id, which identifies one write attempt including reconnect retries. Return refreshed files only; the platform writes them to the same path prepare_session used, so context.file_path(name) names the same file. A non-empty environment fails the write. Provider-owned state may also be renewed here through context.sandbox_handle.

The hook runs under a host-clock deadline. A timeout, exception or self-cancel refuses the write with TURN_PREPARATION_FAILED and the input never reaches the engine. Set turn_validity_seconds when prepared state expires: the host calls prepare_turn again for the same attempt before the validity lapses while the write is live, and a failed refresh quarantines the guard. Turn preparation requires a sandbox backend that declares sandbox_is_profile_exclusive and implements owns_sandbox, so that the live handle is proven to belong to the persisted binding.

Require a user action​

Raise PreparationUserActionRequired(message=..., url=...) from prepare_session or prepare_turn when the user must act first. The host validates the message, status and optional HTTP(S) URL and returns a non-retryable TURN_PREPARATION_USER_ACTION_REQUIRED (409) with that payload. Any other exception stays generic, because provider text can carry secrets. The console keeps the refused input and its images for an explicit retry; nothing is resent automatically.

read_user_actions(context=...) returns SessionUserActionState, which the console reads through GET /api/v1/sessions/{session_id}/user-action. perform_user_action(context=..., action_id=...) runs an operation the user chose from the state's actions through POST at the same path. The host checks that the operation is currently offered; operations must be idempotent for their current pending requirement across replicas. The context carries the persisted Session ID, its authenticated owner and Agent ID. Reads require read scope, operations write scope. Provider errors fail the request with SESSION_USER_ACTION_FAILED rather than grant readiness, and the console blocks sending while a prerequisite is unknown or pending. A status read is advisory: prepare_turn remains the authority for every caller, including those that do not use the console.

from astrabox.seams import SessionUserActionCommand, SessionUserActionState

pending = SessionUserActionState(
ready=False,
user_action={"status": "pending", "message": "Complete the requested consent.",
"url": "https://identity.example/consent"},
actions=(SessionUserActionCommand("check-current-consent", "Check consent"),),
)
ready = SessionUserActionState(ready=True)

Do not dispatch input, initiate consent, re-register an Agent or launch background writes from any of these callbacks. Return material from the deployment's existing credential system. Cancellation must leave no outstanding provider writes; acknowledged platform file delivery completes before the invocation unwinds.

Validation scope​

tests/e2e/runtime_preparation_test.py runs every callback on a real deployment with a fixture provider and an image-baked gate in front of the real Claude CLI. It is marked deployment_shape, so the live lanes exclude it; run it against a deployment built with that fixture. Its token document is synthetic. This is not acceptance of any particular credential service, image or sandbox backend.