AileronControlPlane

Sandbox Agent Images

Sandbox launch runs the agent command inside the selected container image. Aileron prepares and validates the image, but the image must already contain the agent CLI.

Use sandbox check to validate an image before starting a daemon-backed session:

aileron sandbox check --runtime=docker --agent=claude
aileron sandbox check --runtime=docker --build=never --agent=codex

The check uses the same composition plan and minimal launch validation as aileron launch --sandbox=...: /bin/sh, the /home/agent/workspace mount, workspace write access, and the requested agent command on PATH.

Support Matrix

AgentCommandSandbox image supportMCP under --sandbox=dockerNotes
Claude CodeclaudeAgent Feature (local build)✓ via --mcp-configFirst-class Feature below. The default launch path builds the image locally from the Feature, because @anthropic-ai/claude-code is all-rights-reserved and cannot be redistributed as a published image (#1451). Use sandbox check --agent=claude before launch.
CodexcodexAgent Feature (published image)✓ via bind-mounted config.tomlFeature below. @openai/codex is Apache-2.0, so its per-agent image is published and pulled build-free. Sandbox launch writes a generated config.toml to a host tempdir and bind-mounts it into /home/agent/.codex/config.toml (ADR-0024). Host ~/.codex/config.toml is never touched.
GoosegooseCommand contract only✓ via --with-extensionList the agent Feature in Tier 1, or install the CLI in a BYO image; no maintained Feature yet.
OpenCodeopencodeCommand contract only✓ via workspace opencode.jsonLauncher writes opencode.json into the launch directory; the workspace bind-mount makes it readable in-container.
PipiCommand contract only✓ via --mcp-configShares Claude’s MCP wiring.
Other agentsvariesUnsupportedn/aAdd an Aileron launch agent and an image recipe before relying on sandbox launch.

Under --sandbox=docker the launcher resolves the host-built aileron-mcp binary, bind-mounts it read-only at /usr/local/bin/aileron-mcp, builds an mcpEnv rewritten for the runtime (host.docker.internal on Docker), and calls each agent’s ConfigureMCP hook. Four of the five agents (Claude, Pi, Goose, OpenCode) work without any agent-side code change because their config is either inline-with-exec (--mcp-config, --with-extension) or workspace-local (opencode.json in the bind-mounted workspace). Codex is the one exception. Its host ~/.codex/config.toml is irrelevant inside the container, so the launcher writes a generated config.toml to a host tempdir and bind-mounts it. See ADR-0024 and the manual walkthrough for the load-bearing flow.

Docker is the only supported sandbox runtime in v4. Podman is planned but not yet supported; its host.containers.internal host alias is the deferred re-add path, and passing --runtime=podman fails with podman runtime is not supported yet (v4 is Docker-only); see ADR-0014 (see ADR-0014).

The harness-free ghcr.io/alrubinger/aileron-sandbox-base intentionally does not include agent CLIs (ADR-0017). Each agent install is authored once as a devcontainer Feature, the single source of truth that Aileron CI bakes into prebuilt per-agent images and that customers compose for Tier 1. The prebuilt per-agent image is the zero-build Tier 0 default, owned by #965. Use Tier 1 when you want Aileron’s base runtime plus an agent plus your own tools, or Tier 2 when your team owns the full image.

Default Launch Paths

With no .devcontainer in the project, aileron launch --sandbox=docker <agent> resolves one of three default paths from the requested agent:

  • Published agent (build-free): the launcher resolves the prebuilt per-agent image ghcr.io/alrubinger/aileron-sandbox-<agent> and pulls it. There is no sandbox init, no Dockerfile, and no local image build. The published agent today is codex, whose @openai/codex CLI is Apache-2.0 and so redistributable.
  • Recipe’d-but-not-publishable agent (local build): the launcher composes the base image with the agent Feature and builds the image locally on your host through @devcontainers/cli, then launches it. claude takes this path: @anthropic-ai/claude-code is all-rights-reserved with no redistribution grant, so Aileron never bakes it into a published image (#1451). The build needs only Docker on the host (the managed toolchain provisions Node and the CLI; see Managed Toolchain Build). Aileron synthesizes the composed devcontainer.json into a managed temp workspace folder for the build, so your working directory is never written to. The resulting image is tagged locally and reused across launches, so only the first launch builds.
  • Empty or unsupported agent (local base): sandbox plan/build (which pass no agent) and agents with no recipe keep the local agent-less base.

sandbox check --agent=<agent> resolves the same path, so a passing check matches what launch will run.

For a published agent, the launcher resolves the image in two ways. A release build with a recorded digest pin pulls a fixed image by @sha256 (see Reproducible releases below). Otherwise it resolves a floating tag: a release build with no recorded pin pulls latest (which the image workflows move only on a v* tag), and a dev build off main pulls edge (republished on every merge to main that touches the image source, and on workflow_dispatch). So latest always names the most recent release and a dev run never clobbers it. A version-pinned tag (<aileron-version>-<agent-cli-version>) needs the agent CLI version, which the launcher does not know at resolve time, so the floating tag is the fallback when no digest is pinned. The freshness policy that keeps edge current is owned by #1088.

Credential sealing is runtime-side and image-provenance-agnostic (ADR-0025), so a locally-built Claude image launches with credentials sealed exactly as a published image would.

Prebuilt Per-Agent Images

Aileron CI publishes one multi-arch image per publishable agent to GHCR. Each image is baked from the GHCR sandbox base plus that agent’s devcontainer Feature install script, so the Feature stays the single source of truth. The published set is computed from composition.PublishedAgents (the publishable agents), so an agent whose CLI license forbids redistribution is never published.

AgentImage
Codexghcr.io/alrubinger/aileron-sandbox-codex

Claude Code is not published: @anthropic-ai/claude-code is all-rights-reserved with no redistribution grant, so its image is built locally from the Feature at launch instead (#1451). See the Claude Code Feature section.

Each image is built for linux/amd64 and linux/arm64, so a docker pull resolves the manifest for your platform automatically.

The base image (sandbox-base.yml) and the per-agent images (sandbox-agents.yml) are two separate workflows, both triggered directly by a v* tag push, so they run concurrently on the same tag. A per-agent build FROMs the version-pinned base tag, so before that FROM it waits for the base tag to publish: it polls the registry on a bounded retry loop until the concurrently-building base tag appears. A base that never publishes surfaces a precise timeout error naming the absent tag rather than failing mid-build.

On a merge to main the two workflows run in sequence rather than concurrently. The base workflow runs on the main push (filtered to image-affecting paths) and republishes base:edge. The per-agent workflow then runs as a workflow_run cascade keyed on the base workflow’s completion, so it starts only after the base finishes its multi-arch push and composes the agents onto the freshly republished base:edge, never a stale one. The cascade leg runs only when the triggering base run succeeded on main. This keeps the image source and the edge tags in lockstep, so an in-repo change to the image (for example a new in-image entrypoint helper) cannot leave dev/main consumers pulling a base that predates it.

The tag scheme is <aileron-version>-<agent-cli-version> plus two floating tags: latest, moved only by a v* tag release, and edge, republished on every merge to main that touches the image source and on a workflow_dispatch. The <aileron-version> is the Aileron release the image was built from. The <agent-cli-version> is the agent CLI version baked into the image, resolved at build time from the installed package. A git-traceability tag git-<sha> is also published.

Pull the most recent release (latest), or the latest dev publish off main (edge):

docker pull ghcr.io/alrubinger/aileron-sandbox-codex:latest
# tip of main, republished on every image-affecting merge to main:
docker pull ghcr.io/alrubinger/aileron-sandbox-codex:edge

Pull a pinned version, for example Aileron 0.0.1 with the Codex CLI at 0.2.0:

docker pull ghcr.io/alrubinger/aileron-sandbox-codex:0.0.1-0.2.0

CI smoke-tests every published image for launchability before it ships. The smoke asserts the agent CLI resolves on PATH and that the launcher’s image validation succeeds.

The baked CLI is smoke-tested with aileron --version, which is vault-bypassing (see commandsBypassingVault in cmd/aileron/vault_state.go) and so prints without an unlocked vault. A real environment image boot with AILERON_API_URL injected is the other supported way to exercise the baked CLI end to end. Do not verify a published image with a bare docker run <edge> aileron skill launch --help. The skill command is not on the vault-bypass allowlist, so it enters the vault state machine and errors on vault state before printing help. That error is expected behavior for a bare invocation, not a defect in the image bake.

A daily watcher workflow (sandbox-agents-watch.yml) keeps the edge images fresh against upstream agent-CLI releases. It watches the publishable agents (computed from composition.PublishedAgents, so a non-publishable agent like Claude Code is never watched) by polling npm for each agent’s latest CLI version, for example @openai/codex. It compares each against the CLI version baked into the edge image, recovered from the dev-<cli-version> tag co-located on the edge manifest digest. On drift it re-triggers sandbox-agents.yml, which rebuilds from the unpinned Feature install scripts and so bakes the latest CLI. The refreshed build publishes edge and dev-<cli-version>. Dev and main consumers pull edge, so they pick up new agent CLIs automatically without a release.

latest and the release-pinned <aileron-version>-<agent-cli-version> tags move on v* releases only, by design. A released user on latest stays pinned to that release’s CLI version until the next release. This is intentional. A release is an immutable point and latest names the most-recent release, so a background job must never clobber it. Keeping latest fresh between releases is an accepted gap, not a bug.

The watcher supports a dry_run workflow_dispatch input for demonstration, which detects and reports drift without dispatching a rebuild. It uses only GITHUB_TOKEN and bakes no credentials anywhere.

Reproducible Releases: Digest Pinning

The floating latest tag is mutable. The freshness watcher republishes images between releases, so a latest-resolving launcher could pull a different image than the one a release was cut against. Digest pinning (#1233) removes that drift: a release pins each per-agent image to its immutable @sha256 digest, so the same release binary always pulls the same image.

The pins live in a committed lockfile, internal/sandbox/composition/agent-images.lock.json, embedded into the binary via go:embed. It maps each published agent to a sha256:... digest. PublishedAgentImage consumes it: a release build (a real version, which resolves to latest) returns ghcr.io/alrubinger/aileron-sandbox-<agent>@sha256:... when the lockfile records the agent, and falls back to the floating tag otherwise. A dev build keeps edge and is never pinned, so tip-of-main development always tracks the freshest image. An empty lockfile means no agent is pinned yet and releases keep pulling latest, the pre-#1233 behavior.

Regenerate the lockfile at release prep, after the release’s images are published and before tagging the binary:

task generate:agent-digests

The generator (internal/tools/agentdigests) resolves each published agent’s manifest digest from the registry via docker buildx imagetools inspect and writes the lockfile in canonical form. Commit the result, then tag the release. Override the resolved tag with TAG=<tag> (default latest). The generated file is deterministic, so a regenerate with no registry change is a no-op diff. A test asserts the committed lockfile is canonical, so a hand-edit or a stale regenerate fails CI.

Reproducible Toolchains: Node Version + Checksum Pin

The managed-toolchain devcontainer build (#1525) installs a fixed Node.js into the sandbox. To keep that install reproducible it is pinned the same way images are: a committed lockfile, internal/sandbox/container/tools.lock.json, embedded into the binary via go:embed. It records the pinned Node version and, per supported platform (darwin-arm64, darwin-x64, linux-arm64, linux-x64, win-x64), the sha256 of that platform’s distribution archive. The version pin lives alongside the @devcontainers/cli pin in internal/sandbox/container/runtime.go.

VerifyNodeChecksum is the regression guard the toolchain fetcher calls at the network boundary. It looks up the pin for the requested version and the build platform’s GOOS/GOARCH and rejects a downloaded archive whose sha256 disagrees, naming the platform and both hashes, so a tampered or drifted distribution fails the build rather than installing silently. Unlike the agent-images lock, the Node toolchain has no floating fallback: a version pin with no checksums is rejected at parse time.

Regenerate the toolchain lock when bumping the pinned Node version:

task generate:tools-lock VERSION=22.14.0

The generator (internal/tools/toolslock) resolves each supported platform’s archive sha256 from Node’s published SHASUMS256.txt and writes the lockfile in canonical form. It does not verify the detached GPG signature on that file; the signature-verified fetch pipeline is a separate concern under #1525. The generated file is deterministic, so a regenerate with no version change is a no-op diff. A test asserts the committed lockfile is canonical, so a hand-edit or a stale regenerate fails CI.

Refreshing the embedded Node release keyring

The signature-verified fetch pipeline trusts a baked-in set of GPG public keys, not a key fetched at runtime. Those keys live in internal/sandbox/nodedist/nodekeys.asc, embedded into the binary via go:embed and parsed by DefaultKeyring in internal/sandbox/nodedist/keyring.go. The asset holds one armored public-key block per current Node release signer.

The authoritative source for these keys is the nodejs/node repository: its README maintains the canonical list of release-signing GPG key fingerprints (the “Release keys” / “Release Schedule” signer table). Those fingerprints, and only those, are what the embedded asset must contain. Do not derive or auto-pin fingerprint constants from the asset itself: the trust anchor is the human-verified nodejs/node key list, and re-deriving constants from the committed blob would launder an unverified key into a “pinned” one.

Refresh the asset by hand when Node rotates a release signer (for example when a new pinned version’s SHASUMS256.txt is signed by a key not yet in the keyring):

  1. Open the current release-key list in the nodejs/node README and note the full fingerprint of each active signer.

  2. For each fingerprint, fetch the public key from a keyserver and verify its fingerprint matches the README exactly before trusting it:

    gpg --keyserver hkps://keys.openpgp.org --recv-keys <FINGERPRINT>
    gpg --fingerprint <FINGERPRINT>   # confirm it matches the nodejs/node README
  3. Re-export the verified keys as armored blocks and write them, concatenated (one block per signer), to internal/sandbox/nodedist/nodekeys.asc:

    gpg --armor --export <FINGERPRINT_1> <FINGERPRINT_2> ... > internal/sandbox/nodedist/nodekeys.asc

TestDefaultKeyringVerifiesPinnedRelease (internal/sandbox/nodedist/keyring_test.go) is the regression guard for this asset. It verifies the genuine SHASUMS256.txt signature Node published for the pinned version against the embedded keyring, so it fails until the asset is refreshed with the signer of the pinned release. A red TestDefaultKeyringVerifiesPinnedRelease after a Node version bump is the signal that the keyring needs the procedure above.

Managed Toolchain Build

A Tier 1 devcontainer that declares features cannot be built with raw docker build, so Aileron routes it through @devcontainers/cli, which needs a Node runtime. The managed toolchain is the default. Aileron provisions a verified, pinned Node and the pinned CLI itself, so Docker is the only host prerequisite for a Features build on Linux. The host does not need Node installed.

The host-npx toolchain is the opt-out. It resolves both Node and the CLI through the host’s npx (npx --yes @devcontainers/cli@<pinned>), so the host must have Node installed when you select it. Opt out on a Features build with the --toolchain flag:

aileron sandbox build --toolchain=host-npx
aileron sandbox check --toolchain=host-npx --agent=claude

The same selection is available through the AILERON_SANDBOX_TOOLCHAIN environment variable, which aileron launch also reads. Precedence is flag, then environment, then the default. The default is the managed toolchain (#1530); pass --toolchain=host-npx or set AILERON_SANDBOX_TOOLCHAIN=host-npx to opt out.

The managed toolchain is verified by a real devcontainer build in CI on Linux, which is the required, gating environment. macOS and Windows managed-build verification is a known gap: GitHub-hosted macOS runners ship no Docker engine and Windows runners default to Windows containers, so a real Linux-base Features build cannot run there, and CI does not run a leg that cannot pass. Managed-build coverage on those platforms is pending Docker-capable (self-hosted) runners. Until then, the host-npx path is the supported route on macOS and Windows: set AILERON_SANDBOX_TOOLCHAIN=host-npx (it needs host Node), or supply the escape hatch.

On first managed build Aileron fetches the pinned Node distribution into a content-addressed cache keyed by the distribution’s verified sha256, and verifies it against tools.lock.json at the network boundary. It then installs the pinned @devcontainers/cli into a separate cache directory keyed by the CLI version. Both caches short-circuit on a warm hit, so subsequent builds reuse them without re-downloading. The two build paths differ only in the invocation prefix that precedes the build subcommand (npx --yes @devcontainers/cli@<pinned> for host-npx versus the managed node binary plus the CLI’s JS entrypoint for managed); the build --workspace-folder … --image-name … --build-arg … tail is identical.

For a hermetic or offline host, point Aileron at a pre-staged Node binary and CLI entrypoint with the escape hatch, which skips provisioning entirely:

aileron sandbox build --toolchain=managed \
  --node=/opt/node/bin/node \
  --devcontainer-cli=/opt/devcontainer-cli/devcontainer.js

The escape hatch is also available through AILERON_SANDBOX_NODE and AILERON_DEVCONTAINER_CLI. Both paths must be supplied together and must exist on disk; a half-configured escape hatch is rejected rather than partially provisioned.

Prefetch and offline builds

aileron sandbox warm pre-stages the managed toolchain ahead of the first build. It fetches and verifies the pinned Node distribution and installs the pinned @devcontainers/cli into the content-addressed cache, exactly as a managed build does on a cold cache. Run it once on a host with network access:

aileron sandbox warm

Warm is idempotent. A warm cache short-circuits without re-downloading. Warm applies only to the managed toolchain, so aileron sandbox warm --toolchain=host-npx is rejected because the host-npx path has nothing to pre-fetch.

Once warmed, aileron sandbox build --offline (and aileron sandbox check --offline) resolve the managed toolchain from that cache with no network access:

aileron sandbox build --offline
aileron sandbox check --offline --agent=claude

Offline mode computes the Node cache key from the committed tools.lock.json pin rather than downloading the signed checksums to learn it, so it never touches the network. The cache key already comes from that pin, so re-asserting it against the pin is a cheap consistency check rather than a tamper guard: the offline path does not re-hash the cached Node contents. On a cold cache the build fails with an actionable error that names aileron sandbox warm, so run warm with network access first.

This is distinct from the escape hatch above. The escape hatch points at pre-staged Node and CLI binaries you manage yourself. The --offline route resolves the Aileron-managed, pin-verified toolchain from Aileron’s own cache.

Claude Code Feature

The Claude Code agent Feature installs the claude CLI onto ghcr.io/alrubinger/aileron-sandbox-base. It is the single source of truth you compose for Tier 1, and the recipe Aileron builds locally on your host for the default launch path. The Feature lives at images/sandbox-features/claude/ (devcontainer-feature.json plus install.sh).

Claude Code is not redistributed as a published image: @anthropic-ai/claude-code is all-rights-reserved with no redistribution grant, so Aileron never bakes it into a GHCR image (#1451). The default launch path therefore builds the image locally from the Feature instead of pulling one. The build needs only Docker on the host; the managed toolchain provisions Node and @devcontainers/cli (see Managed Toolchain Build). The composed devcontainer.json is synthesized into a managed temp workspace folder, so your working directory is never written to, and the locally-tagged image is reused across launches so only the first launch builds.

The locally-built Claude image needs no per-image aileron-mcp copy of its own. It composes FROM ghcr.io/alrubinger/aileron-sandbox-base, the published base that already bakes aileron-mcp at /usr/local/bin/aileron-mcp and stamps the ai.aileron.mcp.version label (see images/sandbox-base/Containerfile.published and ADR-0024). A child image built FROM that base inherits both the baked binary and the label, and the devcontainer CLI adding its own metadata label does not clear the inherited one. So the launcher reads ai.aileron.mcp.version on the local Claude image, resolves the baked path, and skips the host bind-mount of aileron-mcp entirely. The local Claude path stays on the baked path and dodges the host-mount friction by inheritance, with no Claude-specific Dockerfile or COPY step.

For the default local-build path, launch directly with no .devcontainer in your project:

aileron launch --sandbox=docker claude

For the Tier 1 customization path, list the Claude Feature in your devcontainer.json (see Scaffold a Starter Devcontainer), then validate and launch:

aileron sandbox build --runtime=docker
aileron sandbox check --runtime=docker --agent=claude
aileron launch --sandbox=docker claude

Claude Code still owns its own authentication flow. Do not bake Claude, Anthropic, cloud, or Aileron credentials into the image.

Codex Feature

The Codex agent Feature installs the codex CLI onto ghcr.io/alrubinger/aileron-sandbox-base. The @openai/codex npm package ships prebuilt musl binaries, so it installs cleanly on the Alpine base. @openai/codex is Apache-2.0, so unlike Claude its image is baked into the prebuilt, published Codex image; it is also composable for Tier 1. The Feature lives at images/sandbox-features/codex/ (devcontainer-feature.json plus install.sh).

For the Tier 0 zero-build path:

aileron launch --sandbox=docker codex

For the Tier 1 customization path, list the Codex Feature in your devcontainer.json, then validate and launch:

aileron sandbox build --runtime=docker
aileron sandbox check --runtime=docker --agent=codex
aileron launch --sandbox=docker codex

Codex owns its own authentication flow. Do not bake OpenAI, cloud, or Aileron credentials into the image.

BYO Image Contract

A BYO image must provide:

  • /bin/sh
  • a writable /home/agent/workspace bind mount when launched by Docker
  • the requested agent command on PATH

On Linux + Docker the launcher additionally routes the container through the aileron-remap-agent-uid entrypoint (see the proxy contract below). A BYO image that runs as a non-root user must ship that helper so the workspace bind mount is writable by the agent. See Workspace ownership on Linux.

Validate a BYO image by setting customizations.aileron.image in .devcontainer/devcontainer.json and running:

aileron sandbox check --runtime=docker --build=never --agent=claude

BYO Image Proxy Contract

aileron launch --sandbox=docker runs the HTTPS proxy by default (ADR-0019). The launcher mounts a session-scoped CA at /etc/aileron/proxy/ca.pem, sets standard proxy env (HTTPS_PROXY, HTTP_PROXY, NO_PROXY), and runs the agent through the aileron-run-with-proxy-ca wrapper. For the proxy to terminate TLS without breaking the agent’s HTTPS clients, the in-container trust store must include that CA before the agent starts.

aileron sandbox check --agent=<command> validates the proxy contract for every --runtime=docker invocation. The check exits non-zero with an actionable message when the image is missing any of the required pieces below. The launch-time validation runs the same script.

A BYO image meets the proxy contract by providing two helpers on PATH:

HelperPurpose
aileron-install-proxy-caInstalls the mounted CA into the in-container trust store. Must accept --check to dry-run the trust-store probe without writing anything, and must accept an optional positional CA file argument (default ${AILERON_SANDBOX_PROXY_CA_FILE:-/etc/aileron/proxy/ca.pem}). Exits 0 on success, 2 when the CA file is missing or empty, 126 when invoked unprivileged for an install, 127 when the underlying trust-store tooling is missing.
aileron-run-with-proxy-caEntrypoint wrapper that installs the CA as root, then drops privileges to the agent user and executes the requested agent command. The launcher always starts the container through this wrapper when the proxy is in force.
aileron-remap-agent-uidEntrypoint wrapper that, started as root, remaps the in-container agent user/group to the numeric uid/gid owning the mounted workspace, then execs the rest of its argv still as root. The launcher prepends it on Linux + Docker so the workspace bind mount is writable by the agent. See Workspace ownership on Linux.

The canonical implementations ship with the ghcr.io/alrubinger/aileron-sandbox-base image. BYO authors who derive from another base distro can write drop-in equivalents. The launcher only cares about the CLI shape, not the trust-store mechanism. Pick the mechanism that matches the base:

Base distroInstall file atApply withNotes
Debian / Ubuntu/usr/local/share/ca-certificates/aileron-sandbox-proxy-ca.crtupdate-ca-certificatesRequires the ca-certificates package.
Alpine/usr/local/share/ca-certificates/aileron-sandbox-proxy-ca.crtupdate-ca-certificatesProvided by the ca-certificates package. The sandbox-base image’s helper already works on Alpine because Alpine’s update-ca-certificates accepts the same input directory.
RHEL / Fedora / Amazon Linux/etc/pki/ca-trust/source/anchors/aileron-sandbox-proxy-ca.crtupdate-ca-trust extractRequires the ca-certificates package. Write a small wrapper that mirrors the Debian helper’s CLI but switches the install path and update command.

Two operational requirements apply to every equivalent helper:

  1. The CA must be installed as root once at container start, before the agent process runs. This is what aileron-run-with-proxy-ca guarantees by switching back to the agent user with exec after the install.
  2. The install step must be idempotent. The same helper is invoked on every container start, and the same CA is installed every time. Existing update-ca-certificates / update-ca-trust implementations are naturally idempotent.

Validate a BYO image meets both the agent and proxy contracts with:

aileron sandbox check --runtime=docker --build=never --agent=claude

The check reports support: ok only when the agent command and both proxy helpers are present and the --check probe succeeds. To launch without the proxy (useful for images that cannot meet the contract during initial bring-up), pass --sandbox-proxy=off on aileron launch. sandbox check does not honor that opt-out; it always exercises the full contract so BYO authors see the same failures the launcher would see.

Workspace ownership on Linux

aileron launch --sandbox=docker bind-mounts your current working directory into the container at /home/agent/workspace. The container runs as the image’s non-root agent user. On Linux + Docker the workspace bind mount keeps the host directory’s owner uid (your operator uid, e.g. 1000), while the agent user has its own uid baked into the image. When those differ, the directory’s 0755 permissions deny the agent write access, so the agent cannot create files in your workspace.

To fix this, the launcher routes the container through the aileron-remap-agent-uid entrypoint on Linux + Docker. Started as root, it reads the workspace owner’s uid/gid, remaps the in-container agent user/group to match, then drops to the agent user before the agent runs. The remap composes with the proxy contract: the launcher chains aileron-remap-agent-uid ahead of aileron-run-with-proxy-ca so the order is remap uid, then install the CA, then drop to the agent user.

This is scoped to Linux + Docker. On macOS and Windows, Docker Desktop’s file-sharing layer translates uids at the boundary, so the mismatch never surfaces and the remap is skipped.

A third operational requirement therefore applies to BYO images that run as a non-root user on Linux:

  1. The aileron-remap-agent-uid helper must be on PATH and the image must let it run as root at startup before dropping to the agent user. The helper needs usermod/groupmod (the shadow package on Alpine, passwd/shadow-utils on Debian/RHEL) and a stat that supports -c (GNU coreutils). The canonical implementation ships with the sandbox-base image.

This addresses a different failure than SELinux relabeling: the :z relabel handles SELinux MAC denials on enforcing hosts, while the uid remap handles the DAC permission mismatch that occurs on every Linux + Docker host regardless of SELinux. Both can apply on the same host.

Current Limits

The support matrix covers image contents only. It does not add live discovery refresh.