Skip to content

Managed CLI containment

Agents with metadata { executor = "claude_code" } run Claude Code in the project-selected Docker, gVisor or Firecracker worker. Containers receive scratch /workspace and a private channel mount. Firecracker receives scratch /tmp and fixed vsock capabilities for tools and inference, without host mounts or a NIC. Source mounts belong to governed container tool backends. Policy, approval state, provider credentials and signed journals stay in the runtime.

Configure a container image or matching VM rootfs containing Claude Code and Python 3. The reference code reviewer also needs Git. Missing images or backends fail without host fallback. The independent sandbox supervisor owns worker creation, lifetime and cleanup, including detached descendants and runtime process loss. Container deployments require a responsive Docker daemon. VM deployments require Linux/KVM, a matching guest service, kernel and VMM; see Firecracker setup. The worker image and host deployment remain trusted components.

Project configuration

Keep the control project separate from the source repository being reviewed. Its tools/, agents/, policies/, .symbiont/ and other control paths cannot be exposed through backend mounts. For example:

[sandbox]
tier = "docker"

[sandbox.docker]
image = "your-provisioned-cli-image"
network_mode = "none"
volumes = ["/srv/source:/source:ro"]

[managed_cli.inference]
base_url = "https://api.anthropic.com"
model = "claude-sonnet-4-5-20250929"
api_key_env = "REVIEW_PROVIDER_KEY"
max_requests = 32
max_output_tokens_per_request = 4096
request_timeout_seconds = 60
beta_headers = ["context-management-2025-06-27"]

The credential variable is read by the runtime while preparing its private inference broker. No provider request or child process starts until admission has passed policy, required approval and durable pre-effect recording. It is not inherited by the child. The child uses a fixed placeholder credential and a loopback adapter to the private Unix socket (containers) or runtime-issued vsock port 4052 (VMs). An explicit local HTTP endpoint is supported. URL credentials, query parameters, fragments, redirects and ambient proxies are rejected or disabled. Only the configured model and Messages/token-counting routes are available. Provider-hosted tools, remote MCP servers and URL-based document/image sources are refused. Only the thinking-history retention hint is supported for context management; server-side compaction is refused. Optional beta_headers are supplied by the operator, not the worker. Provider failures cannot expose raw response headers or error bodies.

The child profile keeps the selected isolation and resource settings. Containers replace source mounts with the private channel and force network mode none. VMs expose only guest loopback and the two issued broker capabilities, without an external network interface. Backend tools retain their independently authorized profile. The operator may explicitly grant backend network access; this is separate from the CLI's private inference channel.

Tool and policy configuration

allowed_tools names an exact subset of registered ToolClad tools. Built-in CLI names and wildcard permission expressions are not accepted as registry entries. The reference reviewer uses read_file, list_files, grep_files, git_diff, git_staged_diff, git_log and git_status. The first three now use fixed source queries against Linux Docker/gVisor ceilings or explicit read-only Firecracker source_roots. The CLI receives authorized results through its tool channel, without host mounts. The Git tools use isolated repository snapshots with networking disabled and fixed Git configuration. Firecracker receives bounded Git snapshots in a sealed guest filesystem; see Git queries.

metadata {
    executor = "claude_code"
    allowed_tools = "read_file,list_files,grep_files"
}

The runtime disables built-in tools with --tools "", loads only its explicit MCP configuration, disables automatic project/plugin discovery, and uses dontAsk for the child's permission prompts. Each MCP call still goes through the runtime's prepared-call dispatcher: normalize and validate, obtain any mandatory exact approval, evaluate Cedar, persist the pre-effect record, execute the grant, and persist the outcome. The child cannot supply principal, trusted context, approval, policy or sandbox overrides. Request identities are single-use within a connection, and reconnecting does not replenish budgets.

Policies in policies/managed-cli/ authorize the spawn and individual tools. Shared files directly under policies/ also apply. A minimal read policy is:

permit(principal, action == Action::"tool_call::claude_code", resource);
permit(principal, action == Action::"tool_call::read_file", resource)
when { context.invocation.arguments.path == "src/main.rs" };

The complete source's supported inline allow/deny policies restrict both the claude_code admission action and normalized broker calls before approval. An inline allowlist must name claude_code explicitly. Unsupported selected or file-wide rules are refused before worker or provider startup. This does not interpret executable DSL bodies; see inline policies. Execution metadata remains file-wide. Inline policy selection includes global rules and the selected agent's rules; sibling-agent policies do not apply.

Admission is a fixed, single-use prepared contract containing argv, explicit environment, task, stdin, bounds, inference settings and selected tool scope. Its sandbox describes the actual scratch child, and tool_sandbox describes the broker backends. The profile is retained during review; use an immutable image reference when deployment requires image content binding. claude_code is reserved for admission and cannot be a registered managed tool. Set metadata human_approval = true to require admission review; false or omitted keeps direct operator launches subject to source and Cedar policy. Other values are rejected. Each tool's approval requirement remains independent.

Enable --approval-terminal to resolve mandatory approvals from the runtime operator's controlling terminal. Each answer must name the exact displayed request; --approval-timeout bounds display and decision waiting. Without a relay, manifests requiring approval fail closed. SDK callers can attach an EscalationGate and the shared approval queue to GovernedToolSession. Host SDK applications can use ManagedCliActionExecutor and dispatch_host_action for the separate admission; wrap both executors with the same retained source policy. Admission must precede broker tool calls, and the host method is not exposed through MCP. See approval lifetime.

For Docker/gVisor, --target is a host directory mapped to an explicit backend mount; it defaults to the control project's current directory. For Firecracker, --target is an absolute guest path without parent traversal, defaulting to [sandbox.firecracker].working_dir. Relative file tool arguments resolve beneath that backend's working directory. The CLI itself always starts in its own scratch directory. Each VM backend call has a fresh rootfs view and private /tmp.

A VM target does not automatically upload a host repository. Provision tool executables in the rootfs. Backend tools may explicitly request bounded [filesystem] inputs and a new output; the CLI child receives only their authorized results. The CLI and tool VMs share the same base image, so readable files baked into it are also readable by the CLI. Keep private backend data in scoped grants. Fixed source read/list/search operations use the host broker. Git queries receive private repository snapshots; Firecracker seals its imported tree read-only before running the fixed driver. These routes refuse unsupported links and special files, and bounded searches report incomplete results. See file grants and Git snapshots.

Git helpers, hooks and network access remain disabled. See Git source queries for supported repository layouts and the metadata grant. --plugin-dir is rejected; managed runs do not load plugins.

Bounds and audit

--max-turns bounds the child conversation. The runtime separately caps tool calls at eight times that value, up to 1,000. --budget-timeout is a wall-clock bound including initialization. --budget-tokens is a reserved output-token allowance: every Messages request consumes its requested max_tokens before send, without refunds after cancellation or errors. Input traffic is separately bounded by request count and bytes; this option is not a total billed-token or currency limit. Requests are at most 1 MiB and responses at most 4 MiB. Responses, including SSE, are buffered until complete, so downstream streaming is delayed.

The runtime creates a required per-run signed journal in .symbiont/governed/<session>.jsonl. The directory is private, the journal and persistent signing key use mode 0600, and worker mounts cannot expose them. Records contain a sequence, hash chain and Ed25519 signature over canonical JSON. Signatures cover the exact embedded payload bytes; verifiers must not reformat numbers or whitespace before checking them. Writes are synchronized to storage before authorization proceeds. The admission pre-effect record includes the source-bound fingerprint and any exact approval receipt. The worker's final observation records process status, output hashes and byte lengths; it is separate from broker tool observations. The VM CLI retains verified nonzero exit codes and stderr as failed execution; child-reported success cannot replace the observed status. Closing a session waits for worker cleanup acknowledgement before terminal audit. Cancellation or a dropped dispatch cannot record successful completion. Fixed runtime diagnostics preserve deadline and protected-credential rejection causes without copying upstream response data into errors. Inference requests and responses are correlated by identifiers and hashes; credentials are excluded. This audit key is separate from AgentPin identity, which continues to use ES256.

The runtime prints the audit public key. Retain it through a trusted channel for later verification. ProtectedJournal::verify checks signatures, chain, sequence and principal against that key. A valid prefix without a terminal record is incomplete evidence. Interrupted dispatch, interrupted inference or cleanup failure cannot produce a successful terminal session record. Journals are bounded to 64 MiB; storage failure stops the run. These journals are per-run records, not an automatic replay or recovery mechanism.

Signed durable storage also applies to ordinary CLI, HTTP reasoning and scheduler invocations; see run-audit.md for invocation-bound records and verification. Default DSL reason() and tool_call() also require protected per-run journals. Direct DSL inference and composition have protected per-provider-call journals; coordinator, delegated and chat-platform turns also retain required audit. Scheduled ORGA execution and Firecracker command/stdio/PTY/managed transports are implemented. Governed workers using one supervisor state directory share worker capacity admission, including managed sessions and their tool workers. Size the pool for this overlap; a session occupying the only slot cannot start another worker and receives an explicit refusal. Unsupported scheduling transports, public browser execution and additional VM input/output capabilities remain separate work. See the branch guide for route scope and operator deployment assumptions. Docker validation does not establish gVisor socket compatibility.

The CLI flags are documented in the official CLI reference.

Selecting an agent boundary

Both ordinary and managed symbi run resolve execution settings from the selected agent before inference. For example:

agent reviewer(input: String) -> String {
    with sandbox = "docker", timeout = 30.seconds {
        return input;
    }
}

A single-agent file can be addressed by its filename alias. A file containing several agents requires the filename selector to match one unique declared name. Sibling definitions do not supply execution settings or enter the selected agent's ordinary reasoning prompt. Syntax errors, duplicate names, unknown tiers, and conflicting sandbox or timeout attributes are errors. Statements belong in with blocks. A reasoning process has one sandbox and one lifetime; switching between conflicting block settings is not supported.

An explicit agent tier overrides the project default before backend validation. If omitted, the project default applies. Each tier retains its own configured profile: gVisor uses [sandbox.gvisor.docker], without inheriting Docker mounts. Agent deadlines tighten existing tool and process limits and cannot extend them. Unavailable selected backends fail without host fallback.

Registered HTTP Input agents use the registry's name and source, validate the selected tier against the registered security tier, and build one frozen executor before inference. A disagreement is a configuration error. Configuration changes during a run cannot replace that snapshot. Standalone SDK servers can supply a trusted custom executor; the embedder owns that executor's isolation guarantees. Scheduled agent registration resolves the named agent's source, including legacy .dsl files. Scheduled ORGA execution now uses this selection with real invocation results and cleanup. Managed CLI scheduling remains unavailable. Firecracker oneshot commands and parsers now use the matching guest transport, which also supports MCP stdio and PTY sessions. The VM managed CLI uses these streams with runtime-issued tool and inference capabilities. Its rootfs needs Python and the native CLI; the minimal BusyBox test image is insufficient.