Sandbox Composition
Sandbox composition is the contract for deciding which container image an agent session runs in. It is defined by ADR-0017 and implemented by the aileron sandbox CLI.
This page covers the user-facing workflow. Runtime launch support can scaffold, inspect, build, run the agent command in the prepared sandbox image, and register aileron-mcp as the in-container tool surface. Live discovery refresh is a follow-on runtime layer tracked in #897 ↗.
Choose a Composition Tier
| Tier | Use when | How to configure |
|---|---|---|
| Tier 0: prebuilt per-agent image | You want a working agent sandbox with no init and no local build. | Do not create .devcontainer/devcontainer.json; launch auto-resolves the prebuilt per-agent image baked from the agent Feature. Publishing and auto-resolution are owned by #965 ↗. |
| Tier 1: devcontainer | You want Aileron’s base image plus an agent plus your own project tools. | Create .devcontainer/devcontainer.json listing the base image, an agent Feature, and an optional customer-tooling Feature under features. |
| Tier 2: BYO image | Your team already owns a compliant image. | Set customizations.aileron.image in .devcontainer/devcontainer.json. |
Scaffold a Starter Devcontainer
Tier 1 is the customization tier. You reach for it when you want Aileron’s base image plus an agent plus your own project tools in one sandbox. aileron sandbox init scaffolds a Feature-composing .devcontainer/devcontainer.json (ADR-0017). It writes only that file. It does not write a per-agent Dockerfile, and it takes no agent flag:
aileron sandbox initThe scaffold composes the base image with the Claude agent Feature as the worked example, and demonstrates a commented customer-tooling Feature slot:
{
"name": "Aileron sandbox",
"image": "ghcr.io/alrubinger/aileron-sandbox-base:<version>",
"features": {
// The agent Feature installs the agent CLI onto the base image. Swap
// "claude" for another published agent (e.g. "codex"), or list several.
"ghcr.io/alrubinger/aileron-features/claude:0": {}
// Add your own tooling as its own Feature alongside the agent Feature:
// "ghcr.io/acme/internal-tools:1": {}
},
"customizations": {
"aileron": {
"mediation": "default",
"approval_surface": "both"
}
}
}The agent is selected by the agent Feature you list, and your own tooling is its own Feature alongside it. Swap the agent reference or uncomment the tooling slot to suit your project. The same agent Feature is the single source of truth that Aileron CI bakes into the prebuilt per-agent images (#965 ↗), so the customization tier and the zero-build default share one install recipe per agent. The features-composing build path is implemented in #1083 ↗.
How the agent Features are published
The agent Features under images/sandbox-features/<agent>/ are published to GitHub Container Registry by the .github/workflows/sandbox-features.yml workflow. The workflow runs devcontainer features publish on push to main when images/sandbox-features/** changes, and on release tags. Pull-request runs validate the Feature manifests without publishing.
Each Feature manifest stays on the 0.0.1 house version. devcontainer features publish emits the tag set 0.0.1, 0.0, 0, and latest from that manifest. The scaffold pins the broadest in-house major tag, :0, so a 0.0.x patch bump republishes the 0 tag and the scaffold keeps resolving without re-scaffolding.
The first publish requires a one-time manual step. CI cannot set the visibility of a GHCR package on its first push, so each new package starts private. aileron sandbox build pulls Features anonymously and performs no docker login, so each package must be flipped to public in its GHCR package settings after the first publish. This is a one-time operator step per package, not a recurring CI action.
Add a CLI Capability
A command-line tool that talks to a third-party API needs a credential. Aileron seals that credential at the network boundary so the agent in the sandbox never holds it. A CLI’s complete credential story ships as one CLI-capability unit on that tool’s devcontainer Feature, under customizations.aileron.cli. See ADR-0026 for the decision this section documents, and ADR-0017 for the Feature composition it builds on.
A unit declares the tool’s name, its single vault key, an optional presence tier, an acquisition block, and a sealing list. The gh Feature is the worked example:
{
"customizations": {
"aileron": {
"cli": {
"name": "gh",
"key": "user/github",
"presence": {
"builtin": "base"
},
"acquisition": {
"mode": "device-flow",
"container_name": "aileron-auth-github",
"login_cmd": ["gh", "auth", "login", "--hostname", "github.com", "--git-protocol", "https", "--web"],
"token_cmd": ["gh", "auth", "token", "--hostname", "github.com"],
"browser_shim": "echo"
},
"sealing": [
{
"host": "github.com",
"scheme": "basic",
"emit_mechanism": "inject",
"username": "x-access-token"
},
{
"host": "api.github.com",
"scheme": "bearer",
"emit_mechanism": "sentinel-swap",
"sentinel": {
"value": "ghp_AILERONSENTINELAAAAAAAAAAAAAAAAAAAAA",
"env": "GH_TOKEN"
}
}
]
}
}
}
}The single key: user/github is the one source of the tool’s credential identity. It derives the acquisition store_at, the credential kind (the first path segment, here user), and the credential_ref of every sealing entry. The unit must not re-declare any of those derived fields. Re-declaring one is a load error so the key stays the single source.
The acquisition block is the device-flow login. It runs the login_cmd interactively in a container named container_name, then reads the token back out with token_cmd. Device flow is the shipped acquisition mode this umbrella. Each sealing entry seals one outbound host’s credential at the proxy boundary. The github.com entry seals git-over-HTTPS with HTTP basic auth and injects unconditionally. The api.github.com entry seals gh with a bearer token, planting a non-secret sentinel the proxy swaps for the real credential at egress.
The host reads the unit from the resolved image’s devcontainer.metadata OCI label at launch, then projects it into the same capture and proxybinding schemas the rest of Aileron already uses. You write one unit on one Feature, and no central file in core changes. The state plane and any non-base presence declaration are reserved and carry no behavior this umbrella.
Inspect the Plan
Use sandbox plan to see what Aileron currently infers from the project:
aileron sandbox planWith no .devcontainer/devcontainer.json, the output is Tier 0:
tier: base
image: ghcr.io/alrubinger/aileron-sandbox-base:edgeThe base image tag tracks the CLI: a dev build off main resolves the floating edge tag, and a released CLI resolves latest (the most recent release).
With the starter scaffold, the output is Tier 1 and names the composed devcontainer:
tier: devcontainer
image: ghcr.io/alrubinger/aileron-sandbox-base:edge
devcontainer: .devcontainer/devcontainer.jsonBuild the Image
Use sandbox build to build the image selected by the plan:
aileron sandbox buildAileron detects Docker from PATH. You can choose it explicitly:
aileron sandbox build --runtime=docker
aileron sandbox build --runtime=docker --tag=ghcr.io/acme/agent-dev:localDocker is the only supported sandbox runtime in v4. Podman is planned but not yet supported (ADR-0014); passing --runtime=podman fails with podman runtime is not supported yet (v4 is Docker-only); see ADR-0014.
Build behavior by tier:
| Tier | Build behavior |
|---|---|
| Tier 0 | Resolves the prebuilt per-agent image baked from the agent Feature. Publishing and auto-resolution are owned by #965 ↗. |
| Tier 1 | Composes the devcontainer Features onto the base image and tags the result as a deterministic local aileron/sandbox-project:<hash> image unless --tag is supplied. The recorded build engine is @devcontainers/cli so standard features compose (ADR-0017); the features-composing build path lands in #1083 ↗. |
| Tier 2 | Does not build. The BYO image is reported as-is; launch validates it before agent startup. |
When building the base image outside the source tree, set AILERON_SANDBOX_BASE_CONTEXT to the directory containing the sandbox-base Containerfile.
Release tags also build the sandbox-base image for linux/amd64 and linux/arm64 and publish it to GitHub Container Registry as ghcr.io/alrubinger/aileron-sandbox-base:<version> and the floating latest. A workflow_dispatch run publishes the same multi-arch image under the floating edge tag, the tip-of-main build that dev CLIs resolve. Pull-request runs build both platforms without publishing, so image regressions are caught before release.
Check Agent Support
Use sandbox check to validate that the selected image can run an agent command before starting a daemon-backed launch session:
aileron sandbox check --runtime=docker --agent=claude
aileron sandbox check --runtime=docker --build=never --agent=codexsandbox check uses the same composition plan, build policy, and minimal image validation as sandbox launch. It reports the selected tier, runtime, image, command, and support: ok when the command is available. Agent-specific image recipes and support status live in the sandbox agent image matrix.
Run During Launch
Use --sandbox on aileron launch to have launch prepare the composition-selected image and start the agent inside it:
aileron launch --sandbox=auto claude
aileron launch --sandbox=docker codex
aileron launch --sandbox=docker gooseauto detects Docker from PATH. docker selects the runtime explicitly. The default is --sandbox=off, which preserves the current direct host launch path. Podman is planned but not yet supported (ADR-0014); passing --sandbox=podman fails with podman runtime is not supported yet (v4 is Docker-only); see ADR-0014.
Launch uses --sandbox-build=auto by default. Build policy options are:
| Policy | Behavior |
|---|---|
auto | Use the selected image if it already exists locally; build Tier 0/Tier 1 images only when missing. A published image carrying a floating tag (edge/latest) is always re-pulled so it re-resolves to the current upstream digest each launch; version-pinned published tags are cached locally. |
always | Rebuild Tier 0/Tier 1 images before validation and launch. |
never | Do not build; fail with an actionable error if the selected image is missing locally. |
Examples:
aileron launch --sandbox=docker --sandbox-build=always claude
aileron launch --sandbox=docker --sandbox-build=never codexaileron sandbox build remains the explicit manual build command and always invokes the selected runtime build for Tier 0/Tier 1.
The project directory is mounted at /home/agent/workspace, and the agent starts there. Launch passes session-scoped Aileron daemon env into the container, including AILERON_URL, AILERON_API_URL, AILERON_COMMS_URL, AILERON_SESSION_ID, AILERON_APPROVAL_URL, and the sandbox image metadata (AILERON_SANDBOX_IMAGE, AILERON_SANDBOX_TIER, AILERON_SANDBOX_RUNTIME). AILERON_API_URL points at the daemon’s /v1 API and is the stable endpoint for sandbox-side data-plane operations. For local daemon URLs, launch rewrites the container-facing host to host.docker.internal for Docker. Podman’s host.containers.internal alias is the deferred re-add path, not yet supported.
The HTTPS proxy bootstrap is default-on for --sandbox=docker. Sandbox launch generates a session-local CA, mounts the public CA at /etc/aileron/proxy/ca.pem, and sets standard proxy env (HTTPS_PROXY, HTTP_PROXY, NO_PROXY) plus Aileron metadata (AILERON_SANDBOX_PROXY_MODE, AILERON_SANDBOX_PROXY_URL, AILERON_SANDBOX_PROXY_CA_FILE). The proxy URL uses standard proxy userinfo so clients can send Proxy-Authorization; it carries the launch session id and, when present, the local daemon token. Images used with this mode must provide aileron-install-proxy-ca and aileron-run-with-proxy-ca (see the BYO image proxy contract); the current sandbox-base image includes both and launch validation checks both before the agent starts. The container starts through aileron-run-with-proxy-ca, installs the mounted CA as root, then drops back to the agent user before executing the requested agent command.
Use --sandbox-proxy=auto|on|off (default auto) or AILERON_SANDBOX_PROXY=auto|on|off to control bootstrap. The flag wins over the env var; the env var wins over the default. auto resolves to on for docker and to off for every other --sandbox mode. on forces bootstrap; if the selected sandbox mode cannot support bootstrap (e.g. --sandbox=off), launch refuses with an actionable error before the container starts. off skips bootstrap for the session, and the daemon records a sandbox.proxy.disabled audit event with reason user_opt_out.
When bootstrap is requested but the selected image lacks the BYO contract helpers, launch fails preflight before the container starts, prints an actionable error citing the contract docs and the --sandbox-proxy=off opt-out, and records a sandbox.proxy.disabled audit event with reason preflight_failed. Non-container sandbox modes record reason unsupported_sandbox_mode. Tip: pre-existing pipelines that set AILERON_SANDBOX_PROXY_BOOTSTRAP should switch to AILERON_SANDBOX_PROXY; the former is no longer honored.
The daemon-side /sandbox-proxy/requests boundary can proxy recognized bodyless HTTPS requests with daemon-side credential injection, and /connector-operations/run can route eligible connector operations through that boundary with GET/DELETE/HEAD args encoded as query parameters and POST/PATCH/PUT args sent as JSON request bodies. The daemon also recognizes standard proxy-shaped requests, authenticates their Proxy-Authorization, completes authenticated CONNECT host:443 TLS interception with the session CA, and routes decrypted requests through the same sandbox proxy boundary when they uniquely match an installed connector spec operation by method, host, and path. Smoke coverage confirms standard proxy URL userinfo can authenticate a normal HTTPS client through this transparent path. Missing or ambiguous transparent matches fail closed.
When installed action manifests or connector store metadata exist on the host, launch mounts them read-only under /opt/aileron/manifests/actions and /opt/aileron/manifests/connectors. The daemon loads installed aileron.connector.v1.json specs to validate connector operations on the HTTPS data plane (see Sandbox Connector Specs).
aileron-mcp is the sole in-container tool surface. The launcher bind-mounts the host-built aileron-mcp at /usr/local/bin/aileron-mcp and registers it with the agent so MCP-capable agents see the same first-class tool catalog they do under host launch (ADR-0024). The static tools.txt//usr/local/bin shim surface was retired in #959 ↗; the two reasons it was once load-bearing, BYOCLI tool-catalog cost and shim-based credential mediation, are both gone. Live discovery refresh so a newly-installed action surfaces without an MCP restart is tracked in #897 ↗.
Before running the agent, launch validates the selected image with the same env, mount, and workdir shape it will use for the agent. The image must:
- execute
/bin/shcommands through the selected container runtime - use
/home/agent/workspaceas the working directory - allow a temporary file to be written in the mounted workspace
- resolve the agent command on
PATH
The agent command must already exist in the selected image. For Tier 1, list the agent Feature in your devcontainer.json so the agent CLI installs onto the base. Tier 2 uses the BYO image as supplied while Aileron’s runtime injection remains limited to session env, manifest mounts, and the aileron-mcp tool surface. See the sandbox agent image matrix for the current support contract and recipes.
Use a BYO Image
Set customizations.aileron.image when your team owns the complete image:
{
"customizations": {
"aileron": {
"image": "ghcr.io/acme/agent:2026-05-29",
"mediation": "default",
"approval_surface": "both"
}
}
}In BYO-image mode, launch uses the image as supplied and layers on Aileron’s session env, manifest mounts, and the aileron-mcp tool surface. Images that participate in the v4 HTTPS proxy must include aileron-install-proxy-ca and aileron-run-with-proxy-ca helpers that meet the BYO Image Proxy Contract. aileron sandbox check --agent=... validates both contracts for every Docker run.
Cache Volumes
Tools the agent runs inside the sandbox (package managers, language toolchains) build up caches that are expensive to rebuild on every launch. Declare an Aileron-managed cache under customizations.aileron.cache_paths to persist one across launches in a named Docker volume:
{
"customizations": {
"aileron": {
"cache_paths": [
// Each entry is keyed by (cli, identity). The same key reuses the same
// volume across launches; a different identity keys a fresh store.
{ "cli": "npm", "identity": "acme-workspace", "container_path": "/home/agent/.npm" },
{ "cli": "pip", "identity": "acme-workspace", "container_path": "/home/agent/.cache/pip" }
]
}
}
}Each entry has three fields:
| Field | Meaning |
|---|---|
cli | The tool the cache belongs to. Required. It is part of the volume key and is sanitized into the volume name for legibility. |
identity | The credential or workspace identity the cache is scoped to. Re-authenticating to a different identity keys a different volume, so caches never leak across identities. It is hashed into the volume name and never written to the host in plaintext. |
container_path | The absolute in-container mount point for the cache. Required. |
On launch, Aileron mounts each declared cache as a read-write Docker named volume named aileron-cache-<cli>-<hash-of-identity>. Docker creates the volume on first mount and reuses it on every launch with the same key. The volume holds plaintext on the host, which is acceptable for v4 single-user/local mode; it is not an encrypted or sealed mount.
Eviction is manual. There is no max-age or auto-expiry policy. Remove every Aileron-managed cache volume with:
aileron sandbox cache clear
aileron sandbox cache clear --runtime=dockercache clear removes only volumes carrying the aileron-cache- prefix and is safe to run between launches; Docker recreates a volume on the next launch that mounts it.
What Belongs in the Image
Put ordinary project tooling in the devcontainer: language runtimes, CLIs, package managers, private CA bundles, and internal helper tools.
Do not put Aileron credentials or user secrets in the image. The agent reaches Aileron’s tools through aileron-mcp, which calls the daemon API with the launch token and session context. Credentialed network flows route through the Aileron HTTPS proxy/data plane.
What This Does Not Do Yet
This runtime path does not add live discovery refresh or polished arbitrary-client proxy support. The launcher wires aileron-mcp as an in-container stdio subprocess so MCP-capable agents see the same first-class tool catalog they do under host launch (ADR-0024 and the manual walkthrough). Eligible connector operations flow through the daemon HTTPS proxy boundary with credential injection and connector.proxy.proxied audit records, including JSON request bodies for POST, PATCH, and PUT. Proxy-bootstrap launches can install the session CA in the container trust store before the agent starts, authenticate standard proxy-shaped requests back to the daemon, and route uniquely matched decrypted CONNECT requests through the daemon credential boundary; standard proxy URL userinfo has smoke coverage for this path. Follow-on work adds broader proxy/client integration and polish for arbitrary HTTPS clients (#896 ↗), live discovery refresh only if dynamic in-session connector changes require it (#897 ↗), and final credentialed HTTPS audit semantics (ADR-0019). Container-only shell-layer interception was prototyped under #801 ↗ and withdrawn in #952 ↗; container isolation, the HTTPS proxy, and tool-level HITL cover the named risks (see ADR-0021, Withdrawn).