Status: Exploration / not yet approved. This proposes the
agent-runtimebox image's in-box loop — the one seam carried across Phases 0–3 of the agent-skills mechanism (seedocs/AGENT-SKILLS-CREWS-DESIGN.md). Nothing here is built yet.Superseding direction:
docs/AGENT-DATA-PLANE-SEPARATION-DESIGN.mdrecords the target where the loop runs outside the data box (box = data + tools only; skill + credential held by the agent, never in the box). This in-box note remains accurate for the shipped implementation and self-host single-principal use; read it together with the separation note for where this is headed.
The daemon-side agent-skills mechanism is shipped (Phases 0–3): a skill is
provisioned into its own box with a scoped JWT, its allowed_peers compile to
an eBPF egress policy, A2A delegation is gated + audited, and crews validate
their topology against the trust fabric. But every one of those phases left the
same seam: the box is provisioned, scoped, network-gated, and seeded — and
then nothing runs inside it. Concretely:
agent runreturns an emptyartifact_json(the box is up; no agent ran).agent call/SendAgentTaskreaches no A2A listener (Unavailable).crew runlands inRUNNING, neverCOMPLETED(boxes up; no choreography).
The in-box loop is what turns a provisioned-scoped-gated-traced box into a
working agent. It is the agent-runtime image's job, deliberately kept out of
the daemon so the daemon stays a pure control plane.
Seeded under /etc/containarium/agent/ at launch:
| File | Contents |
|---|---|
system_prompt.txt |
the skill's persona |
token |
a JWT scoped to exactly the skill's allowed_scopes (mode 0600) |
input.json |
the task input |
agent-card.json |
the skill's A2A discovery doc |
Plus: the agent-box MCP (cmd/agent-box) is already present in the box —
in-the-box shell + file ops over stdio. That is the in-box loop's tool surface.
The in-box loop is a long-lived process (the agent-runtime image's
entrypoint) that does two things:
┌──────────────────────── agent-runtime box ───────────────────────┐
│ │
/tasks ──▶│ A2A server (:8674) │
│ POST /tasks {AgentTask} ──┐ │
│ GET /agent-card ─────────┼─ serves agent-card.json │
│ ▼ │
│ ┌──────────────┐ tool calls ┌───────┐ │
│ seed (/etc/.../agent) │ agent loop │ ─────────────▶ │ agent │ │
│ - system_prompt.txt ─▶│ (Claude API │ shell/files │ -box │ │
│ - token (scoped JWT) │ tool-use) │ ◀───────────── │ MCP │ │
│ - input.json └──────┬───────┘ results └───────┘ │
│ │ │
│ │ artifact (JSON) │
│ ▼ │
│ /etc/.../agent/artifact.json │
└───────────────────────────────────────────────────────────────────┘
│ drives the model
▼
api.anthropic.com (Claude API)
A standard Claude API tool-use loop (Messages API — client.messages with
a manual loop, or the SDK tool runner). NOT Managed Agents — see the
alternative below; the whole premise is that the agent runs in our sandbox,
so we drive the model from inside the box.
- Model: from the skill manifest's
modelfield; defaultclaude-opus-4-8. - Thinking / effort: adaptive thinking (
thinking: {type: "adaptive"}) +output_config: {effort: "high"}for agentic work (per the Claude API guidance for long-horizon/tool-heavy tasks). - Tools: the agent-box MCP tools (shell, read/write/edit, process,
tail-log) are the loop's tool surface. A skill that needs platform actions
(create a container, read audit logs) additionally points the loop at the
platform MCP, authenticated with the seeded scoped
token— so the agent can only do what itsallowed_scopespermit. The two MCP surfaces stay distinct exactly asCLAUDE.mdrequires. - System prompt:
system_prompt.txt. Task input:input.json. - Artifact: the loop's final structured output, validated against the
skill's
agent_card.output_schema_json, written toartifact.json.
A small HTTP server on :8674 (the a2aPort the daemon already resolves):
GET /agent-card→ servesagent-card.json(Phase-1 discovery; the daemon seeds it, the box serves it).POST /tasks→ body is anAgentTask(protojson); run one agent-loop pass overtask.input_json; return anAgentArtifact. This is the listenerSendAgentTaskalready POSTs to.
| Credential | What it authenticates | How it arrives |
|---|---|---|
Scoped platform JWT (token) |
the agent → Containarium platform MCP (containers, audit, etc.), bounded by allowed_scopes |
already seeded by RunAgentSkill |
| Anthropic API key | the agent loop → Claude API (drives the model) | seeded via the tenant secrets mechanism (AES-256-GCM, mode 0400); never in the prompt or task input |
These are not interchangeable. The platform JWT must never reach Anthropic; the Anthropic key must never reach the platform API. The loop reads the key from the secret-injected env (or file) at startup, like any other in-box secret.
agent run: after the box is up, the daemon POSTs the seededinput.jsonto the box's/tasksand returns the resultingAgentArtifact—RunAgentSkillResponse.artifact_jsonis no longer empty. (Or the box runs the seeded input once at startup and writesartifact.json; the daemon reads it back. Decide in 4a.)agent call/SendAgentTask: already wired end-to-end on the daemon side and unit-tested against a stub peer — it starts working against real boxes the moment/taskshas a listener.crew run:RunCrewdrives the topology by issuingSendAgentTaskhops (threading the run'strace_id), collects the terminal artifact, and moves theCrewRunfromRUNNING→COMPLETED.
The in-box loop adds a new egress requirement: the box must reach
api.anthropic.com. Under observe-only (LOG_ONLY, the default) this just
shows up in the audit log. Before ENFORCE is armed, the agent-runtime box's
egress allowlist must include the Anthropic API (and DNS) — i.e.
api.anthropic.com belongs in the operator-supplied platform egress
(CONTAINARIUM_AGENT_EGRESS_CIDRS, or an egress_domains entry once domain
policy is wired) alongside the daemon API. Otherwise an armed agent box can
talk to its peers but not to the model — stranded. This is the concrete
instance of the "don't strand the agent" caveat from the Phase-2 docs.
- API-key custody: secrets store → mode 0400 → read once at startup. Never logged, never in the prompt/task input/artifact (which are audit-visible).
- Prompt injection: the task input is untrusted (it may come from
another agent or a tenant). Containment is exactly the trust fabric we built:
the loop runs in an isolated box, with a least-privilege scoped token, behind
an eBPF egress allowlist, fully audited. A compromised loop can do only what
its
allowed_scopes+allowed_peerspermit — that is the point of Phases 0–2. - Cost / runaway: bound the loop with
max_tokens, a task budget (output_config.task_budget, beta), and a wall-clock cap; surface token spend in the artifact metadata.
Anthropic's Managed Agents with config: {type: "self_hosted"} runs the
agent loop on Anthropic's orchestration layer while tool execution happens in a
container you control, via an outbound-polling worker. That is a real fit on
paper — less loop code to write. But it inverts control: Anthropic orchestrates,
the box must poll Anthropic's work queue, and the trust-fabric story ("every hop
sandboxed + network-gated + audited by us") gets muddier when the loop lives
off-box. For Phase 4 we lean to the in-box Claude API loop (full control,
the audit/network story stays clean, no dependency on Anthropic reachability
for orchestration). Managed-Agents-self-hosted is worth revisiting as an opt-in
runtime later — it would reuse the same box, secrets, and egress plumbing.
- 4a — in-box loop, single skill. Agent-runtime image: read seed, drive the
Claude API tool-use loop over the agent-box MCP, write
artifact.json. Closes the empty-artifact seam foragent run. - 4b — A2A server. Add
:8674serving/agent-card+/tasks. Closesagent call/SendAgentTaskagainst real boxes. - 4c — crew choreography.
RunCrewdrives hops + reportsCOMPLETED.
Each phase is independently demoable, mirroring how Phases 0–3 were built.
- Tool runner vs manual loop. The SDK tool runner is less code; a manual loop gives per-tool-call audit/gating hooks. Lean manual for the audit-everything posture, but evaluate the runner for 4a.
- Artifact-schema enforcement. Validate the loop's output against
output_schema_jsonin-box, or let the daemon validate on read? In-box keeps the daemon dumb; daemon-side gives a consistent rejection path. - Default model + effort per skill.
claude-opus-4-8+effort: highis the agentic default; should a skill be able to requestmedium/lowfor cheap/fast leaf agents? (The manifest already carriesmodel; consider aneffortfield.) - API-key scoping. One org key per deployment vs per-tenant keys (cost attribution, blast-radius). Ties into the secrets model.
- Runtime image distribution. ✅ Resolved via the release-artifact path:
make bundle-agent-runtimepackages the loop component, and theagent-runtimerecipe'spost_startrunsscripts/install-agent-runtime.shto pull agent-box + the bundle from the daemon's release (the daemon passes its version as the recipe'sreleaseparam) and install both onto PATH. Best-effort — a dev/unpublished release skips assembly and the box runs without the loop. (A prebuilt OCI/LXC image remains a future optimization to avoid per-box npm install.)
docs/AGENT-SKILLS-CREWS-DESIGN.md— the mechanism (Phases 0–3)docs/AGENT-SKILLS-QUICKSTART.md— the shipped CLI/MCP surface + trust fabric