Skip to content

Governed shell workspace

Local orchestrator turns and @agent fleet conversations use protected per-turn journals. Fixed file tools execute through checked file handles in the trusted runtime; arbitrary commands execute in the project-selected Docker or gVisor worker. Both use prepared contracts, Cedar and the same approval queue. Invalid configuration and unavailable transports fail before inference. Malformed .symbi/constraints.toml stops startup before terminal raw mode. There is no implicit host project mount.

Configure useful file access

Create an operator-owned data directory and declare its access ceiling in symbiont.toml:

[sandbox]
tier = "docker"

[sandbox.docker]
image = "python:3.12-slim"
user = "1000:1000"
volumes = ["/absolute/project/data:/workspace:rw"]

File operations use the runtime service user's permissions within these ceilings; the configured worker UID/GID applies to arbitrary commands. Pin the image digest in a controlled deployment and provide sh for commands. The fixed file broker does not need Python in the image. Use ro for read-only data and declare only the needed directories. Project policy, agent and tool definitions, runtime configuration, credentials, .git and audit storage are control data and cannot be exposed by a project data mount.

Paths in read_file, search, edit_file and save_artifact are relative to /workspace. For this example, result.txt refers to /absolute/project/data/result.txt. Absolute paths, parent traversal, .git and .symbiont paths are rejected before policy evaluation. The broker uses directory descriptors and rejects symlinks, multiple hard links and nonregular files. Search reports skipped entries and limits. Reads, write content and commands are bounded to 32 KiB; search also limits files, inspected bytes and traversal depth. No arbitrary program receives a workspace directory through these tools. Reads and searches return the bounded snapshots captured during preparation; authorization includes their paths and hashes. Search follows the most specific ceiling when declarations overlap, inspects at most 512 files and 1 MiB, and reports truncation or other limits explicitly.

save_artifact validates the supplied DSL, Cedar or ToolClad content before an approval request, then writes it under a writable data ceiling. It can create parent directories there. Install reviewed artifacts into trusted project configuration through the operator's normal workflow. The broker cannot rewrite project policy or registered tool definitions.

Writes bind the destination, exact content hash, held parent directory and any prior file identity/content hash into approval. New files publish atomically without replacing a competing entry. Existing files are updated through their retained inode after checking that the path and content still match. They preserve the inode and permissions. Broker edits use a nonblocking file lock; independent host writers must honor flock to serialize their changes with the broker. An existing-file update is not an atomic replacement: a process or storage failure can leave partial content and an unresolved action. Inspect the journal and file before retrying. A changed file or destination refuses the held edit; prepare and review a new call. Missing artifact directories are created only after approval.

shell commands receive private scratch space and no host mounts. Use the fixed file tools for workspace I/O, or a ToolClad manifest with explicit filesystem grants on a route exposing that manifest, when a program needs input/output files.

Review an exact action

Cedar sees normalized arguments and the immutable executor, contract and selected sandbox metadata. Each edit_file, save_artifact and enabled shell call requires its own expiring, single-use approval. --yes skips conversational confirmation; it does not bypass this runtime requirement. --allow-shell advertises shell and enables its policy path; the command still needs approval and runs inside the selected worker.

Press Ctrl+G, select the request, press Enter to review the complete request, then press a to approve or d to deny. The review includes normalized paths, exact content or command, contract hash and selected boundary. File calls also show execution_transport: fixed_workspace_file_broker and the effective file_access descriptor. The retained worker_program_hash field identifies the compiled broker implementation. Changing project configuration after startup does not silently replace the approved executor's profile. Restart the shell to select new configuration.

Fleet runners retain the registered principal and take the intersection of registered tools and their selected tool list. delegate remains unavailable to fleet runners. Canonical .symbi and legacy-extension .dsl files can declare ORGA conversational agents with literal capabilities and empty with blocks:

metadata { description = "Review workspace files", executor = "orga" }
agent reviewer() {
    capabilities = ["read"]
    with sandbox = "docker", timeout = 20.seconds {}
}

The loader retains the complete source bytes and selects each declaration by its exact name. Capabilities and execution settings come from that declaration alone. A declared sandbox overrides the project default before backend validation, so an unused unavailable default does not prevent an explicitly selected Docker worker. The timeout bounds both the turn and worker. The runner freezes its selected profile before inference and approval; changing a file during a held action does not replace the approved source or boundary. Startup audit and each prepared call include source and declaration hashes, execution mode and selected settings.

This route executes ORGA conversations with external Cedar policies and the supported inline effect rules. Inline allows cannot waive Cedar or mandatory approval. File-wide and selected-agent rules use actual normalized calls; unsupported rule expressions, require and audit are refused. Functions, parameters, return types, nonempty with bodies, unknown with requirements and non-ORGA executors remain unsupported. Metadata is limited to literal description, version, author and executor strings. Complete canonical behavior and broader policy execution remain pending.

Duplicate names across files are refused rather than selected by directory order. Reloading removes missing, ambiguous or refused definitions from the fleet and clears cached runners. Existing admitted runs retain their original contract. Dynamic DSL spawn_agent cannot replace a canonical registration. Direct ask, send_to, composition and registry prompt-only inference cannot enforce canonical requirements and reject these recipients; use the governed fleet @name route. A prompt-only TOML agent continues to use the project profile.

Inspect execution evidence

Response metadata includes the audit run ID. Orchestrator and fleet turns share the display and public reference history while retaining separate signed journals. /audit lists public references (run_id, path, public_key) and the recent live display. Each turn writes a separate signed journal under .symbiont/governed/; the live buffer is only a display. Up to 256 references are displayed with an omitted count. Journal files remain available independently of that display limit. Preserve the public key through a trusted channel and verify the expected run and terminal record.

Ctrl+C requests cancellation; the UI reports that cleanup is pending. A retained owner handles caller cancellation, and normal shell exit waits for these owners. One-shot command owners retain cleanup acknowledgements, including after a call deadline drops its dispatch future. Run termination waits for these acknowledgements; missing or failed cleanup is an explicit run failure. Runtime process loss still depends on the independent supervisor and its recovery contract. Failed storage can leave an incomplete signed prefix. See protected audit and Docker containment.

Provider failures and incomplete terminations, including timeouts, return an error with the audit reference. The shell does not retry a whole turn automatically after tool effects. Inspect recorded outcomes before explicitly retrying. These changes do not establish complete containment: remaining transport coverage, durable effect reconciliation, full canonical DSL behavior/policy execution and the outer evaluation lab remain in progress.

Shipping regression test

python3 scripts/test-shell-governance.py \
  --binary target/debug/symbi-shell \
  --supervisor target/debug/symbi-sandbox-supervisor \
  --report /tmp/shell-governance.json

The test uses a local provider fixture, an actual terminal, the local Gate queue, real brokered file effects and an independently pinned ephemeral audit key. It also checks cancellation, wrapped transcript visibility, startup refusal and canonical fleet approval, denial and unavailable selection. It requires tmux, OpenSSL and Docker for cleanup observations. Missing prerequisites are failures, not passing containment evidence. The Rust shell regression suite also exercises normalized Cedar input, exact content, fleet identity, rejected links and delayed removal of a worker with a detached descendant.