Files
bot-bottle/docs/research/host-dispatch-to-container-agents.md
T
didericis cc5e772519
test / run tests/run_tests.py (push) Successful in 13s
docs: replace stale .sh paths with claude_bottle/*.py equivalents
Cleans up references to the pre-refactor bash layout (cli.sh,
lib/*.sh, scripts/*.sh) across README, Dockerfile, the pipelock PRD,
and research notes. Refreshes line numbers in the oauth-token note
against the current cli/start.py.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 00:27:25 -04:00

4.1 KiB

Host Dispatch to Container Agents

Question

Can host Claude decide which claude-bottle container to spin up for a task, while guaranteeing the work executes in the container and not on the host?

Claude Code Agent Mechanisms

Claude Code provides two mechanisms for defining reusable agent behavior:

Skills (.claude/skills/<name>/SKILL.md) run inline in the main conversation context. They're reusable workflows invoked via /skill-name, with optional tool pre-approval.

Subagents (.claude/agents/<name>.md) run in an isolated context window with a custom system prompt and a declared tool allowlist. They're invoked by natural language, @agent-name, or claude --agent. The tools: frontmatter is enforced — the subagent cannot call tools not in the list. (See Claude Code subagents docs, "Choose the subagent scope" and "Write subagent files" sections.)

"Isolated context window" means only conversational isolation (fresh LLM state, summarized output). It is not process, filesystem, or network isolation. Subagents still run on the host with full user permissions.

The Reliability Problem

The previous approach used an MCP server to bridge host Claude and claude-bottle containers. It failed because host Claude had both work-capable tools (Edit, Write, Bash) and MCP dispatch tools. Claude could choose to do the work itself rather than dispatch, with no enforcement mechanism to prevent it.

Why Tool Restriction Solves It

Claude Code's subagent tools: allowlist is architecturally enforced — not a prompt-level suggestion. If the host subagent is defined with only container-dispatch tools and no Edit/Write/Bash, it is incapable of doing implementation work. Dispatch becomes the only available path.

Reliable Dispatch Architecture

Three pieces in combination give a 100% guarantee:

  1. Restricted host subagent — a .claude/agents/claude-bottle-dispatch.md with tools: limited to MCP container tools and git-read operations. No Edit, Write, or arbitrary Bash.

  2. MCP server — exposes tools the restricted host can call:

    • list_agents() — available agents from the manifest (host Claude decides which to use)
    • run_agent(agent_name, task) — starts a container non-interactively, returns a job ID
    • get_status(job_id) — check running/done
    • get_output(job_id) — read results
  3. Non-interactive container run modecli.py run <agent> "<task>" passes the task to claude --print inside the container and captures output. Currently cli.py start is interactive only; this mode does not yet exist.

Proposal

Build host-dispatch-to-container in two deliverables:

Deliverable 1: Non-interactive run mode for claude-bottle

Extend cli.py with a run <agent> <task> subcommand. Starts the container, writes the task prompt to a file inside it (same docker cp pattern used for --append-system-prompt-file), invokes claude --print with the prompt, streams stdout back to the host, and exits when Claude finishes. Results committed and pushed from inside the container as usual.

Deliverable 2: MCP server wrapping claude-bottle

A minimal MCP server (bash or node) exposing list_agents, run_agent, get_status, get_output. Registered in the host Claude Code settings so a restricted dispatch subagent can call it.

The combination enforces the container boundary at the tool layer, not the prompt layer — making it structurally impossible for host Claude to do implementation work itself.

Critical: the tool restriction only applies within the dispatch agent's context. A normal Claude session has its full toolset and may never invoke the dispatch agent regardless of its description. The dispatch agent must be the entry point for the session, not an optional subagent a full-tool host might call. Two ways to enforce this:

  • Launch with claude --agent claude-bottle-dispatch — makes the dispatch agent the primary agent for the session.
  • Set agent: claude-bottle-dispatch in the project .claude/settings.json — same effect automatically for any claude invocation in that directory.

Without one of these, the guarantee does not hold.