Capabilities and security: one boundary doing all the work

This is the most important chapter for anyone weighing adoption. The tool pipeline is the strongest-engineered part of the codebase. The permission and sandboxing model around it is a single real boundary carrying the entire load, with several capabilities living outside it — and the code is unusually honest about saying so.

How a tool is defined and run

A tool is authored one of two ways. First-party tools use defineTool(...) (packages/core/tools/src/schema.ts:544), which compiles a parameter spec and validates arguments inside the execute body (schema.ts:588-592). Anything satisfying the ToolDefinition interface (packages/core/tools/src/index.ts:222) — MCP tools, dynamic Cordis tools — is a raw tool. Registration performs three load-time checks (supported output schema, positive timeout, no shadowing the reserved run_code transport name; index.ts:1045-1056) but deliberately does not validate description or parameters — it only snapshots them (index.ts:1256-1266).

Registrations live in scoped layers so a subagent can be handed a restricted tool set (index.ts:811, :1152). Execution is a well-built pipeline: arguments are snapshotted to lossless JSON and deep-frozen (index.ts:1364, :1416); the caller's cancellation signal is re-fused over any wrapper replacement so a middleware cannot detach cancellation (index.ts:1536-1544); results are validated against the declared output schema; and tools/result observers receive a frozen snapshot (index.ts:1657-1660).

The pipeline's genuine strengths:

What a "capability seam" is

A seam is a Cordis Service subclass plus a declare module Context augmentation, owned by an interface-only package, with N implementation packages registering under the same key (packages/sandbox/sandbox/src/index.ts:146-176). Consumers read ctx.sandbox and never name a backend; swapping is composition-only. This is the same pattern as the plugin model, applied to capabilities: the file sandbox, the shell, the filesystem, subprocess execution, and the LLM adapter are all seams.

The security model, as one picture

the one real enforcement boundary kernel process sandbox bwrap · Landlock · Seatbelt · Windows-ACL subprocess argv — bash / pwsh confines where file writes land fails closed when no runner is available fs fence — advisory only “a policy check in TRUSTED code over a MODEL-CONTROLLED path, NOT a kernel boundary” reads are never fenced, under every mode outside the boundary network egress sandbox vocabulary excludes the network: curl | sh, ssh, git push — no prompt, no gate run_code worker thread, full Node API reachable — file access confined by nothing (env-var gated) web surface no authentication — loopback-only by construction is the entire model headless surface no approval answerer mounted — every ask and escalation deterministically denies DSH_PERMISSION_MODE=danger-full-access one env var removes the sandbox and the approval prompts together
One kernel boundary (teal, solid) carries the whole load. The fs fence is advisory (dashed). Everything orange lives outside the boundary.

There is exactly one real enforcement boundary: a kernel/OS-level process sandbox that wraps subprocess argv. It selects a runner by platform — linux: ['bwrap','landlock'], darwin: ['seatbelt'], win32: ['windows-acl'] (packages/sandbox/sandbox-local/src/index.ts:159-166) — and fails closed when none is available (:494-502). The seam contract forbids silent unconfined passthrough (packages/sandbox/sandbox/src/index.ts:154-156). This part is good.

Everything else is either advisory or absent, and here is the honest inventory:

1. The shell tool has no command-level permission gate at all. Its own source says so (packages/shell/tool-bash/src/index.ts:6):

 * TODO(permissions): deployment policy belongs in `tools/pre-execute` and
 * sandboxing executors; see docs/architecture.md § Where new behavior goes.

Under the shipped default mode (workspace-write), the model can run any command with no prompt as long as file writes land in the workspace — including curl | sh, git push --force, ssh, and arbitrary network egress. The sandbox vocabulary explicitly excludes the network (packages/sandbox/sandbox/src/index.ts:26-27: "Network and process visibility are outside this vocabulary"). The kernel sandbox limits where files land; it does nothing about what commands do over the network.

2. run_code executes outside the file sandbox entirely. The file fence (fs-sandbox) covers ctx.fs and the process sandbox covers subprocess argv, but run_code is a worker thread whose only limits are env, flags, and heap (packages/code-runtime/code-runtime-worker-thread/src/index.ts:378-387), evaluating the model's program as an AsyncFunction with the full Node API reachable. The module claims "bash-equivalent trust" (index.ts:3-4) but that undersells the gap: shipped bash is kernel-confined; run_code file access is confined by nothing. It is gated behind one env var (DSH_TOOLS_MODE), and nothing warns that turning it on widens the file boundary.

3. The web surface has no authentication — loopback-only by construction instead (see what-is-dsh.html). This is a defensible choice, honestly labeled, but it means anything that can issue a loopback request with a loopback Host header owns the machine.

4. The approval channel is absent in most surfaces. The only approval/request answerers in the repo are the ACP bridge (packages/acp/acp/src/index.ts:215) and the web host. The headless bundle mounts neither — so in headless, every ask decision and every sandbox escalation request deterministically denies. There is no interactive approval for the one-shot runner.

5. One env var removes both the sandbox and the prompts. DSH_PERMISSION_MODE=danger-full-access makes the shell executor bypass the sandbox (packages/shell/bash-sandbox/src/index.ts:91-93), makes the fs fence return the raw path (packages/fs/fs-sandbox/src/index.ts:129), and the base bundle sets the approval policy to never in the same mode. A single environment variable is the difference between confined and wide open.

6. The file fence is advisory, and reads are never fenced. fs-sandbox's own header is candid (packages/fs/fs-sandbox/src/index.ts:11-18): "a policy check in TRUSTED code over a MODEL-CONTROLLED path, NOT a kernel boundary" with an accepted residual TOCTOU. Reads pass through under every mode (:8), so read/glob/grep can exfiltrate anything the process can read regardless of permission mode.

The through-line: the security posture is honest but thin. Almost every gap above is documented in a code comment right next to the gap. That honesty is a real strength — but a reader who takes "sandboxed agent" at face value will over-trust it.

The permission-pipeline gaps

model emits arguments unvalidated snapshot lossless JSON, deep-frozen tools/pre-execute hooks and matchers see raw model output approval missing service ⇒ deny guards deny-only by type — no “allow” return value execute arguments validated here, inside the body output schema check result validated against declared schema tools/post-execute observers get a frozen snapshot Every policy stage — pre-execute, approval, guards — runs before validation: they all inspect unvalidated model output.
The pipeline in execution order. Validation lives inside execute — after every policy stage has already inspected the raw arguments.

Beyond the missing gates, the pipeline itself has ordering and shape problems:

The extension capabilities, by maturity

Things that ship but do not run

docs/tool-catalog.md presents tool-terminal, tool-lsp, tool-bash-persistent, mcp-client, tool-cordis, and e2b as first-class tools, but none is in bundle/base or bundle/headless. That is roughly 7,500 lines of terminal (2,306), LSP (2,486), and e2b (2,659) capability with no default composition — and therefore far less real-world exercise of their sandbox interactions than bash gets. The largest single unshipped area is packages/extensions/ (the dynamic-Cordis tool, tool-cordis/src/api-catalog.ts alone is 4,751 lines), reachable only in the web-app bundle.

The generated docs disagree with the code

The four capability/tool docs are generator-produced, and docs/capability-seams.md:471 even claims a "completeness guard" — yet they drift from the code in eight verifiable places. The pattern matters more than any single item (see weaknesses.html, theme 2): the gates verify that a doc matches its generator's output, not that the generator is correct.

Doc says Code says
ctx.lsp impl lsp-local (capability-seams.md:466) no such package; it is dsh-lsp-stdio, and tool-catalog.md:31 names it correctly — the two generated docs disagree with each other
ctx.codeRuntime impl code-runtime-worker (:455) it is dsh-code-runtime-worker-thread
ctx.shell impls omit pwsh-sandbox (:448) pwsh-sandbox registers ctx.shell and is the shipped Windows executor
pwsh "mirrors bash minus sandbox controls" (tool-catalog.md:264) tool-pwsh registers sandbox_permissions, justification, and calls approveEscalation — straightforwardly false
rendered bash schema has 5 properties, no escalation fields (tool-catalog.md:178-217) the shipped composition mounts bash-sandbox, so the real wire schema carries escalation fields; the catalog renders the unsandboxed variant
catalog uses sampleOverCapGlobResults: true (tool-catalog.md:27) base bundle sets it false
reorder-proof owner policy is a registered guard (tool-execution-pipeline.md:60) zero permission policies use tools.guard()
ctx.dynamicCordisRunner owns "the vm sandbox" (capability-seams.md:468) the implementation refuses the word: "is not containment: host-realm helper functions remain an escape route" (cordis-host-runner/src/sandbox.ts:6-7)

The two claims in docs/defensive-patterns.md we could check — the env scrub and the spill-file hardening — are accurate and verified in code (packages/subprocess/subprocess/src/index.ts:44; packages/spill/spill-local/src/store.ts:109-113). The hand-written doc holds; the generated ones drift.