Skip to main content

Add a sandbox provider

A SandboxProvider gives AstraBox the sandboxes Agent programs run in. It creates a sandbox, reaches it again by id, renews, pauses and destroys it, and returns a handle to the sandbox's files, commands, long-lived processes, and ports.

AstraBox includes one provider, open_sandbox, which calls an OpenSandbox lifecycle service. Write another provider when a deployment's sandboxes come from a different platform.

What AstraBox and the provider each own​

AstraBoxThe provider
Which sandbox a conversation uses, and when to create, pause, or replace oneCreating a sandbox from a SandboxCreateSpec, and connecting to it by id
Sessions, workspace planning, credentials, and starting the Agent programDestroying a sandbox, and proving that it is gone
The native conversation state, kept in AstraBox's databaseThe sandbox's files, commands, long-lived processes, and port addresses
Refusing configuration the backend has not declared it can runDeclaring what the backend can run, and implementing what it declares

The full contract, with every type, is in astrabox/seams/sandbox.py and astrabox/seams/sandbox_data.py.

Return a handle​

create_sandbox and connect return a SandboxHandle. AstraBox uses these members of it, and nothing else:

MemberContract
sandbox_idThe id the provider reaches the sandbox by again.
filesSandboxFiles: absolute paths taken literally, POSIX permission bits such as 0o640, account names for owners, and SandboxPathNotFound, SandboxPathRejected, or SandboxUnreachable for a failure.
commandsSandboxCommands: one shell command line run as root with an empty stdin. A non-zero exit status is data in the SandboxCommandResult; a deadline passed raises TimeoutError and the sandbox stops the command.
processesSandboxProcesses: long-lived processes, such as a terminal's shell or the Pi program, that any replica attaches to by id. One attachment holds a process; another may take it over. Output is numbered in bytes, stdout and stderr together, and replays from an offset the sandbox still holds or fails with SandboxProcessReplayGap. A process that exited runs again when the next attachment arrives.
get_endpoint(port)A SandboxEndpoint: an absolute http or https URL that reaches the in-sandbox port, and the headers every request to it must carry.
get_signed_endpoint(port, expires_at=...)A SandboxEndpoint whose URL itself carries a credential until expires_at, for a client that cannot send headers. A backend that cannot sign raises the 501 SANDBOX_SIGNED_ENDPOINT_UNSUPPORTED.

A provider whose sandboxes cannot keep long-lived processes leaves supports_process_sessions false and returns UnsupportedSandboxProcesses(name) from processes; every operation on it raises the 501 SANDBOX_PROCESS_SESSIONS_UNSUPPORTED. close(), when the handle has one, releases local resources only and never destroys the sandbox.

Reuse execd for files, commands, and processes​

Every bundled sandbox image carries OpenSandbox's execd v1.1.0 at /opt/astrabox/opensandbox/execd (see the Container Reference). When the provider's sandboxes run an execd on port 44772, and get_endpoint(44772) returns a URL and headers that reach it, build the handle's three data-plane faces from astrabox.providers.execd instead of writing them:

from astrabox.providers.execd import EXECD_PORT, ExecdCommands, ExecdFiles, ExecdProcesses


async def data_plane(sandbox_id, get_endpoint):
execd = await get_endpoint(EXECD_PORT)
return (
ExecdFiles.at_endpoint(execd.url, execd.headers, sandbox_id=sandbox_id),
ExecdCommands.at_endpoint(execd.url, execd.headers, sandbox_id=sandbox_id),
ExecdProcesses.at_endpoint(execd.url, execd.headers, sandbox_id=sandbox_id),
)

These implementations carry execd's wire rules: its mode format, its exit status reporting, its replay frames, and its connection closing. With them, the provider declares supports_process_sessions = True.

execd runs as root and executes whatever a request that reaches it asks for. Put a per-sandbox credential in the headers get_endpoint returns, where the sandbox's own workload cannot read it, or let only AstraBox reach the port.

A provider can instead implement the three protocols with its platform's own APIs. It then has to keep the same contract, including writing a file's permission bits and owner in the same operation, so verify it with the conformance suite described below.

Declare what the backend can do​

Capability flags on the provider class say what the backend supports. Registration refuses a flag declared true without the methods its row in SANDBOX_CAPABILITY_RULES names, so a half-declared capability fails when the provider is installed rather than at the first conversation that needs it.

FlagThe provider also implements
supports_create_network_policyadmit_runtime_egress; creates each sandbox under the Environment's network access rules
supports_isolated_sessionsthe isolated-session operations and count_active_isolated_sessions; read_memory_headroom and read_listening_ports have defaults that read the sandbox through commands
supports_isolated_session_recoveryfind_isolated_sessions_by_workspace
supports_pausepause and resume
supports_client_poolthe client-pool operations, owns_unclaimed_sandbox, and client_pool_readiness
supports_correlated_createfind_sandbox_by_assignment
supports_egress_credential_injectionapply_credential_vault
supports_inventory_listinglist_sandboxes and describe_sandbox
supports_process_sessionsthe handle's processes

AstraBox refuses configuration that needs a capability the selected backend does not declare when that configuration is saved: an Environment on a backend without supports_create_network_policy; sandbox sharing without supports_isolated_sessions; the Pause idle behavior without supports_pause; prepared capacity without a ready client pool; and, without sandbox sharing, the Pi Agent program or an Agent's terminal panel without supports_process_sessions.

supports_correlated_create decides what a platform restart in the middle of a create can recover. With it, AstraBox asks find_sandbox_by_assignment for the sandbox an interrupted start may have made, and continues with it. Without it, AstraBox creates each sandbox once and never asks: a create that raises fails the conversation's start with that error, and a start that a platform restart interrupted is refused with 409 SANDBOX_CREATE_INTERRUPTED instead of making a second sandbox, so the person starts the conversation again. A sandbox the backend made for that interrupted start is not known to AstraBox; the backend's own expiry reclaims it. Retrying a request that failed in transit is the provider's own client's job, for requests the backend accepts twice.

Prove what happened to a sandbox​

kill(sandbox_id) returns True only when the backend established that this sandbox is gone; a delete it accepted but did not confirm returns False. AstraBox never calls it directly. It calls confirm_destroyed, which turns that answer, and a second observation from probe, into a verdict. A backend that cannot probe raises NotImplementedError from probe; any other exception counts as a probe that failed, and the destruction stays unconfirmed.

endpoint_names_sandbox(endpoint) returns the sandbox id an endpoint string of this backend encodes, or None when the address does not say. AstraBox uses a stored endpoint only when the backend confirms it names the expected sandbox, and otherwise resolves it again from the sandbox id.

Enable the backend​

ASTRABOX_SANDBOX_BACKENDS lists the backends a deployment enables, and ASTRABOX_SANDBOX_BACKEND names the default for an Environment that selects none. A record that names an installed backend the deployment has not enabled fails with SANDBOX_BACKEND_DISABLED. List the environment variables only the provider reads in owned_settings: AstraBox warns at startup about each one set while the provider is installed but not enabled.

A backend that mounts persistent workspace storage returns a WorkspaceRoute from workspace_route(). Without one, a deployment that configures a workspace volume with this backend is refused at startup.

Package the provider​

Declare the provider in its Python distribution:

[project.entry-points."astrabox.providers.sandbox"]
example = "astrabox_sandbox_example:ExampleSandboxProvider"

Set the class's name to the entry-point name. AstraBox instantiates and registers the class when it starts. The class may also set seams_api_version; an incompatible declared version fails at startup instead of at the first conversation.

Verify the provider​

Bind the structural contract suite in the provider's unit tests:

from astrabox.testing.provider_conformance import SandboxProviderContractSuite

from astrabox_sandbox_example import ExampleSandboxProvider


class TestExampleSandbox(SandboxProviderContractSuite):
def make_provider(self):
return ExampleSandboxProvider()

It checks that the abstract methods are implemented, the capability flags are booleans, and every declared capability carries what its rule requires.

Then bind the data-plane suite in a live test against a real sandbox of the provider, one case per clause:

import pytest

from astrabox.testing.sandbox_data_conformance import (
DATA_CONTRACT,
PROCESSES_CONTRACT,
DataContractBox,
run_contract_check,
)


@pytest.mark.parametrize("check", DATA_CONTRACT + PROCESSES_CONTRACT, ids=lambda check: check.name)
async def test_data_contract(check, live_handle):
await run_contract_check(check, DataContractBox(
files=live_handle.files,
commands=live_handle.commands,
scratch="/tmp/data-contract",
account="agent",
processes=live_handle.processes,
))

Leave PROCESSES_CONTRACT out, and pass no processes, for a provider without supports_process_sessions. account names an account, with a group of the same name, that the sandbox image has and that the files face does not itself run as.

Finally, follow a conversation through a real deployment that enables the provider: start a conversation, read and write files in the Files panel, run a command in the terminal and interrupt it, let the sandbox go idle, and send another message.