AileronControlPlane

Sandbox MCP — Manual Verification Walkthrough

This walkthrough exercises the v4 sandbox MCP wiring (ADR-0024) with a real agent and a real action. Use it to confirm a development build of Aileron is wired correctly, or to repro a sandbox-MCP issue with a known-good harness.

The integration test at internal/app/sandbox_mcp_test.go (build tag integration_sandbox) covers the same flow under automation with a stub upstream. This walkthrough is the human-facing companion when you want to watch each step against a real connector.

What you need

  • Docker installed and running.
  • A development build of the Aileron CLI and aileron-mcp siblings, both on PATH. task build:cli && task build:mcp produces them under ./build/.
  • jq available on PATH (used by the audit-verification command below).
  • The Google connector and its draft-email action installed, with a Google credential bound. The simplest path is the suite installer, which fetches the connector, installs its actions, and walks Google sign-in in one command: aileron action add-suite github://ALRubinger/aileron-connector-google/suite.toml@latest. (To install them individually instead: aileron connector install github://ALRubinger/aileron-connector-google then aileron action add github://ALRubinger/aileron-connector-google/actions/draft-email.)

What to validate (agents and runtimes)

The per-agent bar is “the agent sees aileron and its draft_email tool”. The only thing that varies per agent is how aileron-mcp is registered (see ADR-0024). Everything after registration is identical across agents.

AgentIdentifierMCP registration mechanism
Claudeclaude--mcp-config JSON
Pipi--mcp-config JSON
Goosegoose--with-extension
OpenCodeopencodeopencode.json in the workspace
Codexcodexbind-mounted config.toml

Supported runtimes are Docker on macOS, Linux, and Windows. Docker Desktop on Windows runs Linux containers through the WSL2 backend, which is the same sandbox-base image used on macOS and Linux. The launcher adds --add-host=host.docker.internal:host-gateway on Linux only. macOS and Windows Docker Desktop resolve host.docker.internal automatically.

Linux is automated

The Linux column is covered in CI, so you do not run it by hand:

  • TestSandboxMCPRegistration_Matrix (internal/launch/agents) asserts, per agent, that the ModeSandbox registration references the aileron-mcp binary and carries every daemon env var. It runs in the standard unit suite.
  • TestSandboxMCP (internal/app, build tag integration_sandbox) runs the aileron-mcp to daemon round-trip over both a host subprocess and a real Docker container, asserts tools/list exposes draft_email, and checks the approval.requested → approval.approved → execution.started → execution.succeeded audit chain. It runs in the CI integration job on Linux.

macOS and Windows are a light manual smoke

GitHub-hosted macOS and Windows runners cannot run Linux Docker containers, so those two columns are not automatable in CI. The container itself is the same Linux image on all three operating systems, and the host-side launcher code is unit-tested on macOS and Windows already, so the residual risk is only host Docker networking (host.docker.internal resolution). Cover it with a light manual smoke per agent rather than a full Gmail round-trip: launch the agent, confirm it lists aileron with draft_email, then exit. Record each in issue #962. A full round-trip (through to a real Gmail draft and approval) on at least one agent per OS is the optional gold-standard check; the steps below walk through it.

Run

Pick the agent to validate. Every other step is identical.

AGENT=claude   # one of: claude | pi | goose | opencode | codex
aileron launch "$AGENT"

aileron launch runs the agent in a Docker sandbox by default; no --sandbox flag is needed. The launcher will:

  1. Resolve the host-built aileron-mcp binary and bind-mount it read-only into the container at /usr/local/bin/aileron-mcp. The launcher skips this step when the resolved image already bakes aileron-mcp in. It detects a baked image by reading the ai.aileron.mcp.version label (issue #957). The published ghcr.io/alrubinger/aileron-sandbox-base image bakes the binary so sealed customer-operated runtimes launch without a host aileron-mcp. The local Tier 0 base build stays unbaked and keeps using this host-mount.
  2. Build the MCP environment (AILERON_URL rewritten to host.docker.internal:<port>, AILERON_SESSION_ID, AILERON_TOKEN).
  3. Register aileron-mcp with Claude Code via --mcp-config.
  4. Validate the container can command -v aileron-mcp AND aileron-mcp --version exits 0 (catches arch mismatch).
  5. Start the container with --add-host=host.docker.internal:host-gateway on Linux Docker.

Once the agent is running, confirm the aileron MCP server registered. Claude and Codex list servers with /mcp:

> /mcp

You should see one MCP server named aileron with the Aileron action catalog. Look for draft_email. Goose, OpenCode, and Pi surface their tool list differently. For those, ask the agent to list its available tools, or skip straight to the draft request below and confirm it invokes the Aileron tool. The session log (.aileron/session.log under the launch directory) records aileron-mcp’s discovery call against the daemon, which is the runtime-agnostic proof that registration succeeded.

Then ask:

> Draft an email to [email protected] saying I'm running late

The agent calls mcp__aileron__draft_email. The flow:

  1. aileron-mcp (inside the container) POSTs /v1/actions/draft-email/run to the daemon (on the host, via host.docker.internal) with X-Aileron-Session-Id set to the launch session.
  2. The daemon sees the action manifest declares [approval] and returns 202 Accepted with a review_url.
  3. aileron-mcp surfaces the review URL back to the agent, which surfaces it to you.
  4. Open the review URL in your browser, or run aileron approval approve <id> in another terminal.
  5. The daemon executes the action: fetches the OAuth credential from the vault, calls Gmail’s draft endpoint, returns the draft id.
  6. aileron-mcp’s check_action_status polling surfaces the result back to the agent.
  7. The audit log records the chain approval.requested → approval.approved → execution.started → execution.succeeded, all stamped with the launch session id.

Verify the audit chain. List the recent entries (there is no session filter; audit is keyed by audit_id, so look at the recent window right after the run):

aileron audit list --limit 20

Assert the four contractual events are present in one shot. audit list --json returns {"events":[...]}, and each event carries an event_type:

aileron audit list --limit 20 --json \
  | jq -r '[.events[].event_type] | sort | unique' \
  | jq -e 'index("approval.requested") and index("approval.approved")
           and index("execution.started") and index("execution.succeeded")' \
  && echo "PASS: audit chain complete" \
  || echo "FAIL: audit chain incomplete"

Verify the draft landed in Gmail’s draft folder. That is the upstream contract under test.

Refresh the tool surface without a restart

aileron-mcp discovers actions from the daemon’s /v1/actions at startup, then keeps the agent’s tool list current while the session runs. It advertises the MCP tools.listChanged capability during initialize and re-discovers actions on a background poll. When the action set changes, it swaps its cache and emits a notifications/tools/list_changed to the host, which re-pulls tools/list.

This means an aileron action add, aileron action enable, or aileron action disable in another terminal reaches the running agent within a poll cycle. Adding or enabling an action surfaces it; disabling one drops it. No agent restart.

The poll interval is AILERON_MCP_REFRESH_INTERVAL (a Go duration such as 5s, or a bare integer interpreted as seconds). It defaults to 5s. Set it to 0 to disable the poller and freeze the tool surface at the boot snapshot. The knob is only consulted when AILERON_URL is set.

A discovery failure during a refresh (daemon momentarily unreachable, vault locked) logs to stderr and leaves the existing tool surface intact. The working tool list is never overwritten with an error state, so a transient failure can never strand the agent without its tools.

When a restart is still required

The poller re-reads the action catalog, nothing else. A restart is genuinely required to pick up changes to the static configuration aileron-mcp reads once at boot:

  • AILERON_URL (which daemon to talk to)
  • AILERON_TOKEN (the bearer token)
  • AILERON_COMMS_URL and AILERON_SESSION_ID (the comms tool surface: read_messages, send_message, draft_reply, http_request)

These are launch-time wiring, not catalog state, so they fall outside the refresh path by design.

Record the result

The Linux column is automated (the tests above), so those cells are green when CI is green; you do not hand-run them. For macOS and Windows, the per-agent bar is the light smoke: the agent listed aileron with draft_email. Record those cells in issue #962 (its body, not a comment) with the environment (OS, arch, Docker version, Aileron CLI commit, agent version) and any deviations. If you also run the optional full round-trip, note that the audit assertion above printed PASS and the draft landed in Gmail.

Agent ↓ / Runtime →macOS Docker (manual smoke)Linux Docker (CI)Windows Docker (manual smoke)
Claudeautomated
Piautomated
Gooseautomated
OpenCodeautomated
Codexautomated

Codex is the highest-risk cell on any runtime because it is the only agent whose MCP registration uses a bind-mounted config.toml rather than a CLI flag or workspace file. Validate it deliberately and read the Codex troubleshooting note below.

Verify coexistence with a user MCP server (R3)

Aileron does NOT aggregate or proxy user-installed MCP servers under sandbox launch. Aileron is one MCP server; your own MCP servers connect through your own config and coexist independently.

To verify, add a user MCP server to your global Claude Code config (~/.config/claude-code/mcp.json or ~/Library/Application Support/Claude/claude_code_settings.json on macOS):

{
  "mcpServers": {
    "userthing": {
      "command": "/usr/local/bin/your-mcp-server",
      "env": {}
    }
  }
}

Re-launch under sandbox:

aileron launch claude

In Claude, /mcp should show BOTH aileron and userthing. Both work independently. The Aileron daemon never sees the userthing traffic.

Troubleshooting

mcp__aileron__draft_email missing from the tool list

The most common cause is a cross-arch host: an arm64 host bind-mounting into an amd64 container (or vice versa). The launcher’s validate step runs aileron-mcp --version for exactly this case and should fail before launch with a clear message. If the validate step passed but tools are missing, look at the session log (.aileron/session.log under the launch directory) for the daemon discovery call from aileron-mcp — a vault-locked or daemon-restart-during-launch race leaves aileron-mcp without action tools.

Tool-name collision with a user MCP server

Claude Code’s mcp__<server>__<tool> convention disambiguates by server. A user’s draft_email from userthing appears as mcp__userthing__draft_email alongside mcp__aileron__draft_email. Both work independently; the agent picks based on intent. There is no conflict.

Codex: sandbox runs non-interactively (approval + folder trust)

Under aileron launch --sandbox, the generated /home/agent/.codex/config.toml pre-sets Codex’s non-interactive keys so an ephemeral container runs end-to-end without operator prompts:

  • approval_policy = "never" suppresses Codex’s per-tool approval prompts (the ones you would otherwise hit on each MCP tool call, e.g. list_recent_emails / get_email).
  • sandbox_mode = "danger-full-access" defers OS isolation to the outer Aileron container.
  • a [projects."/home/agent/workspace"] block with trust_level = "trusted" pre-accepts Codex’s “trust the current folder?” prompt for the bind-mounted workspace.

This is sandbox-mode only. The outer container is the trust boundary (ADR-0015) and Aileron still mediates every action the agent calls through aileron-mcp + the gateway (the HITL approval chain in this walkthrough is unaffected — it runs in the daemon, not in Codex). A host-mode launch never touches ~/.codex/config.toml’s approval, sandbox, or folder-trust settings; you own those.

Codex: user devcontainer MCP entries masked

The Codex sandbox path bind-mounts a generated config.toml into the container at /home/agent/.codex/config.toml. Any user-shipped [mcp_servers.foo] entry in a devcontainer-baked config is silently masked by the launcher-provided file. Aileron does NOT promise a Codex multi-config-file workaround today — whether Codex reads additional ~/.codex/*.toml files is unverified. If you want Codex + sandbox with extra MCP servers, pre-merge your entries into a wrapper script that writes a combined config before aileron launch invokes Codex.

Agent crashed mid-approval

The daemon executes regardless of agent presence. If the agent crashes after the 202 response but before the user approves (or after they approve but before aileron-mcp polls the result), the action still runs to completion on approve, and the result sits in memory keyed by approval_id. The next daemon restart drops in-memory approval state. Either re-launch and re-invoke (idempotency depends on the action), or read the audit log directly. This decoupling is intentional per ADR-0009 (agent is never in the trust path).

host.docker.internal not resolving (Linux)

Linux Docker does not configure host.docker.internal automatically. The launcher adds --add-host=host.docker.internal:host-gateway on Linux Docker; this requires Docker 20.10+. If your daemon hangs trying to reach the daemon, confirm with:

docker run --rm --add-host=host.docker.internal:host-gateway alpine getent hosts host.docker.internal

macOS and Windows Docker Desktop handle this automatically. Podman’s native host.containers.internal alias is the deferred re-add path; Podman is planned but not yet supported in v4 (ADR-0014).

Baked image version skew

The published sandbox-base image ships its own aileron-mcp baked in. The launcher detects this through the ai.aileron.mcp.version label and skips the host-mount, so a baked image runs the version of aileron-mcp that was compiled into it. That version can differ from the host CLI’s version. ADR-0024’s host-mount lockstep guarantees the two match for the v4 default topology, but a baked image breaks that lockstep by design.

aileron sandbox check surfaces the difference. It compares the baked ai.aileron.mcp.version label against the host CLI version. A match prints an mcp: baked aileron-mcp <version> (matches host CLI) line. A mismatch prints a warning that names both versions and never fails the check. Skew is expected and managed for sealed customer-operated runtimes. Skew in the v4 default topology is worth investigating, since that flow expects the unbaked local image and a host-built aileron-mcp.

For the default unbaked flow, the host aileron-mcp sibling is still required. Build it with task build:cli && task build:mcp and keep both on PATH.

Sources

  • ADR-0008 — MCP is the canonical action-exposure surface (extended to sandbox launch).
  • ADR-0009 — agent is never in the approval trust path.
  • ADR-0018 — v4 single-binary model; sandbox MCP revival amended in.
  • ADR-0024 — the Path B1 architecture decision this walkthrough exercises.
  • Issue #953 — the tracking issue for sandbox MCP parity.
  • Issue #957 — baking aileron-mcp into the published sandbox-base image.