ADR-0017: Sandbox Composition
Context
Aileron is moving from a host-launched MCP-first runtime toward the Aileron Way: the agent runs inside a container Aileron defines, with credentialed HTTPS traffic flowing through the Aileron data plane and shell/runtime boundaries mediated inside the container.
That shift creates an image-composition question: who decides what is in the agent container? Aileron needs to own the security substrate, but users still need ordinary development tools such as gh, kubectl, language runtimes, private CLIs, and internal certificates.
An Aileron-specific tool resolver was considered and rejected. A schema like aileron.yaml with tools: [gh, kubectl, node@20] would make Aileron responsible for package resolution, install recipes, version drift, and ecosystem-specific failure modes. That is not Aileron’s lane.
Decision
Use .devcontainer/devcontainer.json as the canonical project-local sandbox composition substrate. Aileron reads standard devcontainer build/image fields and stores Aileron-specific settings under customizations.aileron.
Aileron supports three tiers:
| Tier | Contract |
|---|---|
| Tier 0: base image | No .devcontainer/devcontainer.json; Aileron uses aileron/sandbox-base:<version> directly. |
| Tier 1: devcontainer | .devcontainer/devcontainer.json exists; Aileron composes the sandbox using its build/image settings. The starter path is .devcontainer/Dockerfile extending aileron/sandbox-base:<version>. |
| Tier 2: BYO image | customizations.aileron.image names a fully custom image. Aileron uses it as supplied and injects the runtime contract at launch. |
The Aileron extension block starts narrow:
{
"customizations": {
"aileron": {
"image": "ghcr.io/acme/agent:2026-05-29",
"mediation": "default",
"approval_surface": "both"
}
}
}image selects the BYO-image tier. mediation and approval_surface are declared here so the config surface exists before shell mediation and the approval UI add the runtime behavior.
The Aileron-owned base image contains only the runtime substrate: shell utilities, discovery files, and the minimal HTTP client used by generated connector shims. Proxy/session bootstrap, CA installation hooks, and shell mediation files layer onto this base as those features land. The base image does not carry language runtimes or third-party development tools.
Single-binary alignment
This ADR follows the updated sandbox runtime direction:
- Aileron uses one
aileronbinary with multiple modes. - This composition contract does not introduce an
aileron-mcpimage or launch path. - The canonical credentialed-action path is HTTPS through the Aileron proxy/data plane.
- Runtime bootstrap supplies
AILERON_API_URL,AILERON_TOKEN, and launch session metadata for the current action-shim path. Later proxy work can addHTTPS_PROXYand session CA configuration without changing the composition tiers; see ADR-0019.
CLI surface
aileron sandbox init scaffolds:
.devcontainer/devcontainer.json.devcontainer/Dockerfile
The Dockerfile extends aileron/sandbox-base:<version> and includes commented snippets for common tools. The snippets are guidance, not a runtime resolver. Users own their container contents using normal Docker/devcontainer workflows.
aileron sandbox plan is an inspection helper that reports the normalized tier/image/dockerfile plan.
aileron sandbox build is the first user-facing build consumer of that plan. It builds Tier 0 from Aileron’s local sandbox-base image definition and Tier 1 from the devcontainer Dockerfile through Docker or Podman. Tier 2 BYO images are selected as-is; launch validates the minimal runtime contract before running the agent.
aileron sandbox check --agent=<command> prepares the selected image with the same build policy as launch and validates that the image can run the requested agent command before starting a daemon-backed session. This is the user-facing preflight for the sandbox agent image support matrix tracked in #894 ↗.
aileron launch --sandbox=auto|docker|podman consumes the same build path to prepare the selected image, validates that it can execute /bin/sh, use a writable /home/agent/workspace mount, and resolve the agent command on PATH, then runs the agent command inside a one-shot Docker/Podman container. When generated connector shims are mounted, launch also validates that wget is available because those shims use it to reach AILERON_API_URL. The project is mounted at /home/agent/workspace and used as the container working directory. Launch passes the session-scoped Aileron daemon env into the container, including AILERON_API_URL for the daemon /v1 API used by sandbox-side execution shims, plus discovery hints AILERON_TOOLS_FILE=/etc/aileron/tools.txt and AILERON_SHIMS_DIR=/usr/local/bin. Local daemon URLs are rewritten to the runtime host alias (host.docker.internal for Docker, host.containers.internal for Podman).
Launch build behavior is controlled by --sandbox-build=auto|always|never. auto is the default and builds Tier 0/Tier 1 images only when the selected local image is missing. always forces a rebuild. never fails if the selected image is not already present. The explicit aileron sandbox build command keeps its manual-build behavior.
Sandbox launch also bind-mounts Aileron’s installed action manifests and connector store metadata read-only under /opt/aileron/manifests/actions and /opt/aileron/manifests/connectors when the corresponding host directories exist. When installed action manifests declare connector dependencies, launch renders a session-scoped static tools.txt manifest and bind-mounts it read-only at /etc/aileron/tools.txt; it also renders read-only connector shim scripts under /usr/local/bin for --help discovery and explicit installed-action execution through AILERON_API_URL. Shim calls include the launch session id when AILERON_SESSION_ID is available so daemon-side approvals can retain session context. This launch-scoped static discovery/action surface is the first complete sandbox runtime path for #796. Live tools.txt refresh and watcher processes are deferred until dynamic in-session connector install/update needs them.
The sandbox-base image has a dedicated CI/publish workflow. Pull requests build the image for linux/amd64 and linux/arm64 without publishing. Release tags publish the same multi-arch image to GitHub Container Registry as ghcr.io/alrubinger/aileron-sandbox-base:<version>.
Consequences
Users with existing devcontainers get an upgrade path rather than a parallel Aileron-only config file.
Aileron keeps a clear boundary: it owns mediation, credentials, approvals, audit, and runtime bootstrap; users own development tooling in the image.
The first implementations establish the contract, image-build substrate, container execution path, validation, and launch-scoped discovery/action substrate. That is the #796 cut line before #801: live discovery refresh (#897 ↗), proxy/session-CA bootstrap (#896 ↗), agent image recipes (#894 ↗), and shell interception (ADR-0021) build on this substrate only when later runtime layers need them.
Alternatives Considered
Aileron-specific YAML resolver. Rejected. It would require Aileron to maintain install recipes and version semantics for every common development tool.
Dockerfile only. Rejected as the top-level contract because devcontainer.json already standardizes Dockerfiles, images, features, mounts, and editor/tooling interop.
Bake common CLIs into sandbox-base. Rejected. It bloats the trusted base image and makes Aileron responsible for unrelated tool maintenance.
References
- Issue #796 ↗ — sandbox composition
- Issue #747 ↗ — runtime-first milestone
- Issue #894 ↗ — sandbox agent image support matrix
- Issue #895 ↗ — connector specs and generated HTTPS shims
- Issue #896 ↗ — HTTPS proxy/data-plane mediation
- Issue #897 ↗ — dynamic discovery refresh
- ADR-0015 — old host launch audit boundary
- ADR-0018 — single-binary runtime model
- ADR-0019 — HTTPS data-plane mediation
- ADR-0020 — connector specs and shims
- ADR-0021 — shell-layer mediation