Architecture · 07

Capability Seams and the Tool Execution Pipeline

Separate Definition, Provider, and Consumer roles, then trace policy, approval, guards, dispatch, sandboxing, results, and concurrency.

Reading time
18 minutes
Sources verified

Use a seam to stabilize capability, not implementation

A capability seam is a runtime contract shared by implementations that may differ by environment. DeepSeek Harness organizes each seam into Definition, Provider, and Consumer roles. The Definition owns stable types and service behavior. A Provider supplies that behavior for a particular substrate. A Consumer calls the contract without importing the concrete provider. Composition then selects the implementation, so changing local storage to sandboxed storage does not force tool code to change.

This is dependency inversion with lifecycle consequences. Cordis mounts providers into a Context, consumers declare injection, and disposal removes registrations with their owning scope. A seam is not a generic registry for every concern: it should describe one coherent capability and keep transport-specific details behind the provider boundary.

text
Definition: ctx.fs contract and normalized operations
Providers: fs-local | fs-sandbox | fs-e2b
Consumer: tool-fs
Composition chooses one provider; tool-fs keeps the same dependency.
  • Definition owns semantics and public types.
  • Provider translates those semantics to an environment.
  • Consumer depends on the seam, never a provider package.
  • Bundle or profile selects and configures the mounted provider.
Definition
Owns service types and behavior.
Provider
Registers one implementation selected by composition.
Consumer
Depends on the service contract rather than the provider package.
Simplified capability seam using the filesystem family as a concrete example. Official source ↗

Trace a seam before extending it

Start in docs/capability-seams.md and find the service row. Follow its Definition package, all Providers, then Consumers. This prevents two common architecture errors: importing a local implementation into a portable tool, or adding a second service that duplicates an existing contract. The tools service itself is core rather than a replaceable provider seam, while subprocess, shell, filesystem, sandbox, approval, web, and persistence are composed capabilities around it.

bash
rg "ctx\.(fs|sandbox|approval|tools)" docs/capability-seams.md
rg "inject:.*fs|ctx\.fs" packages/fs packages/core
rg "ctx\.sandbox" packages/sandbox packages/shell

A valid extension test mounts a Definition-compatible Provider, invokes it through a Consumer, disposes the owning Context, and proves the registration is gone. If behavior survives disposal, look for an untracked listener, timer, global singleton, or resource opened outside the plugin lifecycle.

Register a typed tool at the tools boundary

Tool plugins register a ToolDefinition with a name, description, input parameters, mandatory output declaration, and execute body. The registry validates model arguments, snapshots immutable execution identity and arguments, validates the returned lossless JSON value, and renders model-facing content. The body receives an AbortSignal and must forward or observe it until owned work has quiesced.

typescript
ctx.tools.register(defineTool({
  name: 'lookup_ticket',
  description: 'Read one ticket by id.',
  parameters: { id: { type: 'string', required: true } },
  output: {
    schema: { type: 'object', properties: { title: { type: 'string' } }, additionalProperties: false },
    render: (_args, value) => [{ type: 'text', text: value.title }],
  },
  async execute({ id }, exec) {
    return tickets.read(id, { signal: exec.signal })
  },
}))

Follow the exact pipeline in order

The Agent Loop first appends durable tool/call before execution. The tools registry snapshots arguments, assigns an opaque token, and runs tools/pre-execute. An allow continues; deny skips the body; ask calls ctx.approval and continues only for allowed-once. Registered monotonic guards run after pre-policy. tools/execute then wraps canonical dispatch for concerns such as timeout, retry, or metrics. The body may reach filesystem or subprocess enforcement. tools/post-execute may accept, block, replace, or add context.

After normalization, the definition-owned synchronous finalizeContent callback gets the last content-only invariant. The live tools/result notification observes a frozen authoritative outcome. Only afterward does the Agent Loop append durable tool/result. Do not confuse tools/result, a live observe-only event, with tool/result, the replayable session event.

text
durable tool/call
→ tools/pre-execute
→ approval for ask
→ monotonic guards
→ tools/execute wrapper → tool body → provider/sandbox
→ tools/post-execute
→ normalize → finalizeContent
→ live tools/result
→ durable tool/result
  1. The durable tool/call event is recorded before policy and dispatch.
  2. Pre-execute resolves allow, deny, or ask; approval resolves an ask before monotonic guards.
  3. The execute waterfall wraps the tool body; post-execute can accept, block, replace, or add context.
  4. Normalization and finalizeContent precede the live tools/result notification.
  5. The Agent Loop then records one durable, model-facing tool/result.
Simplified canonical tool execution path. Denials and thrown failures converge on normalization and one final outcome. Official source ↗

Keep guard, approval, and sandbox responsibilities distinct

Pre-execute policy makes reorderable allow, deny, or ask decisions. Approval answers a one-shot human question; it is not a persistent grant store, and an absent or unanswerable provider fails closed. A monotonic guard is owner policy that later listeners cannot reverse: it returns a denial reason or abstains. Put invariants that must never be reordered or upgraded from denial into a guard.

A sandbox constrains execution after permission has been granted. Sandbox policy resolves the mode and canonical workspace root; an enforcing Provider wraps exact argv or fences filesystem mutation. It does not decide whether the user intended the action. Approval without confinement can authorize excessive reach; confinement without policy can still permit an unwanted action inside the boundary.

text
Request: write /workspace/report.md
pre-policy: ask because mutation is sensitive
approval: allowed-once
guard: deny if path violates owner rule
sandbox: enforce workspace-write at execution
result: success or one stable denial/error outcome
  • Pre-policy: deployment ordering and allow/deny/ask.
  • Approval: one request, one answer, fail closed when unavailable.
  • Guard: monotonic non-overridable owner rule.
  • Sandbox: operating-system or provider enforcement of allowed reach.

Preserve one call and one result

Every accepted model call must settle through the normal result path. Invalid arguments, denial, refused approval, pre-aborted work, wrapper failure, body failure, post-policy failure, timeout, and normalization failure all become a single authoritative ToolExecutionResult. The Agent Loop records exactly one model-facing tool/result for each tool/call. A failure must not vanish, throw past the loop, or generate a second compensating result.

Post-policy can replace content or canonical value, but not both in one accept decision. Replacing content is presentation-only and is not a confidentiality barrier; replace or block the value when programmatic consumers must not receive it. Finalization runs exactly once even for normalized failures, then observers receive the frozen outcome.

typescript
const result = await ctx.tools.execute({
  callId, name, arguments: rawArgs, signal, agent,
})
// Append one durable tool/result from `result`.
// Never append once in catch and again in finally.

Understand which stage may rewrite what

The stages are deliberately asymmetric. Pre-execute may decide allow, deny, or ask, but it cannot rewrite the immutable execution input. A tools/execute wrapper surrounds dispatch and may substitute only its operational signal; it cannot rename a tool, exchange arguments, or replace identity. This makes audit correlation reliable even when timeout and metrics plugins are composed dynamically. The registry reattaches the original caller signal immediately before entering the body.

Post-execute owns controlled result transformation. An accept decision may replace presentation content while retaining the canonical value, or replace the value and trigger validation plus rerendering. A block converts feedback into a valueless failure. Tool-deferred contexts precede decision contexts on accept, while a block discards tool-deferred contexts and exposes only contexts explicitly supplied by the blocking policy. finalizeContent is narrower still: it is synchronous, total, and content-only.

text
Immutable through pipeline: token, callId, tool name, frozen arguments
Wrapper-mutable: dispatch signal only
Post accept: replace content OR replace canonical value; attach contexts
Post block: valueless failure with explicit blocking contexts
Finalizer: replace final content only
Observer: read frozen outcome; no mutation

These restrictions prevent a policy plugin from silently turning an approved read into an unapproved write, prevent cached results from bypassing the active output declaration, and give incident responders one identity to follow. If a plugin needs a genuinely different operation, it should initiate a nested tool call with its own call identity and parent token rather than mutating the current execution. Document every rewrite because model-visible content and machine-readable value serve different consumers and can create different security consequences.

Classify concurrency per call and commit in model order

A visible definition may declare isConcurrencySafe(arguments). executionMode returns parallel only when that classifier returns exactly true. Unknown, hidden, undeclared, invalid, or throwing classifications are exclusive. The Agent Loop uses exclusive calls as barriers and admits safe calls through a bounded rolling pool. It reclassifies immediately before start because visible composition or policy may have changed.

Physical completion order is not transcript order. Pre-execute handling follows model order; safe bodies may overlap; post-execute and durable result commitment proceed in model call order. This retains deterministic model history while gaining concurrency for independent reads. Never mark an operation safe merely because it is asynchronous: shared mutation, rate-limited identities, external ordering, and read-after-write dependencies require exclusivity.

text
Model calls: read A, read B, update C, read D
Classify:    parallel, parallel, exclusive, parallel
Run A/B in bounded pool; commit A then B
Barrier: run and commit C alone
Then run and commit D
typescript
isConcurrencySafe(args) {
  return args.operation === 'read' && args.consistency === 'snapshot'
}

Handle cancellation and deferred context without adjacency bugs

Cancellation is cooperative. A caller signal is mandatory; around-execute wrappers may temporarily replace only the dispatch signal, while the registry re-fuses the original signal before the body. Cancellation before dispatch skips all policy and body phases and still publishes one result. After invocation, cancellation can replace only a success; a denial, timeout, wrapper, tool, or post-policy failure remains the more specific outcome.

A tool may defer additional context for delivery after its final result reaches the loop. Post-policy can also attach ordered contexts. These messages are injected only after the active batch has committed all tool results, preserving call/result adjacency. Composite Code Mode sub-calls omit additionalContexts for the same reason and carry parent execution correlation internally.

typescript
async execute(args, exec) {
  exec.deferContext({ id: crypto.randomUUID(), message: { role: 'user', content: 'Verify the generated report.' } })
  return performWork(args, { signal: exec.signal })
}

Verify success and every failure branch

A successful integration proves more than a pleasant final answer. Capture durable events and counters. Assert immutable call identity, validated arguments, one body invocation, correct provider selection, one result, model-order commitment, and disposal. For a parallel batch, deliberately delay the first body so the second finishes first, then prove durable results remain in original call order.

text
Test matrix
allow → body once → success result
deny → body zero → one denial result
ask + unavailable → fail closed, body zero
guard denial after pre allow → body zero
body throws → one normalized error result
post blocks → one valueless failure
B finishes before A → durable results A, B
cancel → owned work stopped before settlement

For sandbox tests, request one permitted workspace mutation and one path escape. Verify the allowed file and stable denial, then inspect the resolved profile for tools, approval, permission preset, sandbox policy, and enforcing filesystem or subprocess provider. Never test escape behavior against valuable host data.

Use a production and safety review

Pin the preview revision, treat tool schemas and event shapes as compatibility contracts, and log only redacted identity and stable error fields. Default to the smallest visible tool catalog, narrow credentials, workspace confinement, deterministic unattended policy, bounded timeouts, and idempotent external effects. An automatic retry wrapper must understand whether the first attempt may already have committed a side effect.

text
Release checklist
[ ] Definition documented; Providers and Consumers mapped
[ ] input/output schemas closed where appropriate
[ ] guards cannot be reordered around owner policy
[ ] unavailable approval fails closed
[ ] sandbox root and mode verified per execution
[ ] serial/parallel classifier tested with shared state
[ ] every call produces one result
[ ] cancellation reaches owned subprocesses
[ ] logs redact arguments, content, paths, and credentials

When a run surprises you, locate the first boundary that diverged: durable call, pre-policy, approval, guard, wrapper, body/provider, post-policy, finalization, live observation, or durable result. That sequence turns a vague ‘tool failed’ report into an actionable component diagnosis.

Official sources