ADR-0017: Sandbox Composition
Revision note, 2026-06-15: This ADR originally described a launch-scoped static
tools.txtmanifest and/usr/local/binconnector shims as the in-container tool surface. #959 ↗ retired that surface;aileron-mcp(ADR-0024) is now the sole in-container tool surface. The composition tiers and the read-only manifest mounts are unchanged. The base image keepswgetfor general tooling, but it is no longer a launch-validated shim requirement. Passages below have been updated to reflect the retired surface.
Revision note, 2026-06-16: Umbrella #1080 ↗ moves each agent install from a per-agent baked Dockerfile layer to a devcontainer Feature. The base image stays harness-free and gains no agent CLIs. Aileron CI bakes prebuilt per-agent images from the same Feature, and those images become the Tier 0 default (their publishing and launch-time auto-resolution are owned by #965 ↗, forward-referenced here). Customers compose the same agent Feature alongside their own tooling Feature for the customization tier. The build engine, the diamond-inheritance resolution, and the CLI surface prose below are updated to reflect the Feature model. The CLI changes that this model implies land in sibling issues (#1082 ↗, #1083 ↗, #1084 ↗); this ADR records the model and the build-engine decision.
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: prebuilt per-agent image | No .devcontainer/devcontainer.json; launch auto-resolves the prebuilt per-agent image ghcr.io/alrubinger/aileron-sandbox-<agent> baked from the agent Feature. Publishing and launch-time auto-resolution are owned by #965 ↗. The bare ghcr.io/alrubinger/aileron-sandbox-base:<version> stays the harness-free root that each agent Feature layers onto. |
| Tier 1: devcontainer | .devcontainer/devcontainer.json exists; Aileron composes the sandbox from the Features it lists. The starter path lists ghcr.io/alrubinger/aileron-sandbox-base:<version> as the base plus an agent Feature plus an optional customer-tooling Feature under features. |
| 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 the approval UI adds the runtime behavior. (The mediation slot was originally also intended to anchor container-only shell mediation; that surface was prototyped under #801 ↗ and withdrawn in #952 ↗. See ADR-0021 (Withdrawn).)
The Aileron-owned base image contains only the runtime substrate: shell utilities and the proxy/session bootstrap helpers. CA installation hooks layer onto this base as those features land. The base image stays harness-free and does not carry language runtimes, third-party development tools, or agent CLIs.
Composition model: Features
Each agent install is authored once as a devcontainer Feature. A Feature is a self-contained install unit that runs on top of any base, so the agent CLI is no longer a baked layer inside a per-agent image. A customer’s own tooling is its own Feature and stays agent-agnostic. A Tier 1 sandbox is composed by listing the base image and these Features in devcontainer.json:
{
"image": "ghcr.io/alrubinger/aileron-sandbox-base:<version>",
"features": {
"ghcr.io/alrubinger/aileron-features/<agent>:0": {},
"ghcr.io/acme/internal-tools:1": {}
}
}The agent Feature is the single source of truth. Aileron CI bakes the prebuilt per-agent images (#965 ↗) from the same Feature that customers compose directly. There is one install recipe per agent, consumed two ways.
Diamond-inheritance resolution
Docker FROM is single-parent. A per-agent baked layer cannot also carry arbitrary customer tooling unless the customer rebuilds the whole agent layer on their own base, because “base + customer tools” and “base + agent” are two lineages that never merge into “base + customer tools + agent”. Features compose independently onto one base, so “base + customer tools + agent” is expressible without merging two image lineages. This is the diamond-inheritance resolution that authoring the agent as a Feature provides. The spawn-sandbox technology decision in ADR-0014 and the harness-free base in ADR-0024 are the substrate the Feature model builds on.
Build engine
Composing standard features requires either the official @devcontainers/cli or a reimplementation of Feature resolution and ordering. The decision is to drive sandbox builds through @devcontainers/cli so standard features compose without Aileron reimplementing the resolver. A raw docker build that runs each Feature’s install script in order is retained only as a contingency fallback should the CLI dependency prove unworkable. It is not a co-equal option, and it is also the existing path for a Tier 1 plan that declares no features.
As of #1083 ↗, composition.Discover parses the features block into the plan (raw option payloads carried verbatim, never interpreted by Aileron), and the Tier 1 build path branches on it. When a Tier 1 plan carries features, the build routes through @devcontainers/cli (resolved as a pinned npx --yes @devcontainers/cli@<version>), which reads features from devcontainer.json and shells out to Docker under the hood. The @devcontainers/cli invocation flows through the same container.Runner seam as docker build, so no second runtime is introduced and the Docker-only guard (ADR-0014) still applies. A Tier 1 plan without features keeps the raw docker build path unchanged (the recorded fallback). A Tier 2 BYO image (ADR-0024) still short-circuits to the as-is image; any features it lists are parsed for inspection but inert. Local Feature composition is complete; publishing and launch-time auto-resolution of Feature/agent refs remain owned by #965 ↗ under umbrella #1080 ↗.
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 in-containeraileron-mcptool surface and the data plane. Later proxy work can addHTTPS_PROXYand session CA configuration without changing the composition tiers; see ADR-0019.
CLI surface
aileron sandbox init scaffolds a Feature-composing .devcontainer/devcontainer.json for the customization tier. The scaffold lists ghcr.io/alrubinger/aileron-sandbox-base:<version> as the base image and an agent Feature plus an optional customer-tooling Feature under features, so users compose their own tooling alongside the agent through standard devcontainer workflows. Under the Feature model init no longer scaffolds a per-agent .devcontainer/Dockerfile, and the --agent flag is removed because the agent is selected by the listed Feature rather than a baked recipe.
aileron sandbox plan is an inspection helper that reports the normalized tier/image/dockerfile plan, including a features: summary line listing the parsed Feature references when the devcontainer declares any.
aileron sandbox build is the first user-facing build consumer of that plan. It resolves the Tier 0 prebuilt per-agent image and composes the Tier 1 devcontainer Features through the build engine recorded above. Tier 2 BYO images are selected as-is; launch validates the minimal runtime contract before running the agent. The features-composing build path lands in #1083 ↗.
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=off|auto|docker 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 container. 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 that the in-container aileron-mcp and the data plane use. Local daemon URLs are rewritten to the Docker host alias host.docker.internal. Podman’s host.containers.internal alias is the deferred re-add path for when Podman returns.
v4 is Docker-only. Podman is deferred to a later track, not rejected. An explicit aileron launch --sandbox=podman is rejected at validation time with unsupported sandbox runtime "podman" (want off, auto, or docker). The runtime abstraction seam is preserved, so re-adding Podman is re-enabling it in resolveRuntime and the support matrix. See ADR-0014 and umbrella issue #1050 ↗ for the descope rationale.
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. The daemon loads installed connector specs from that metadata to validate connector operations on the HTTPS data plane. The agent reaches Aileron’s tools through aileron-mcp (ADR-0024), which sets the launch session id from AILERON_SESSION_ID so daemon-side approvals retain session context. The static tools.txt//usr/local/bin shim surface that #796 first shipped was retired in #959 ↗. Live discovery refresh so a newly-installed action surfaces without an MCP restart is tracked in #897 ↗.
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: live discovery refresh (#897 ↗), proxy/session-CA bootstrap (#896 ↗), and agent image recipes (#894 ↗) build on this substrate only when later runtime layers need them. Shell interception (ADR-0021, Withdrawn) was an earlier candidate and is no longer planned.
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
- Issue #1080 ↗ — agent-as-Feature sandbox model umbrella
- Issue #965 ↗ — prebuilt per-agent image publishing and auto-resolution
- ADR-0014 — spawn sandbox technology
- ADR-0024 — harness-free base and in-container MCP parity
- 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 (Withdrawn)