Files
deepseek-harness/docs/rfc/implemented/architecture/2026-06-13-capability-seams.md
T
Tianyi Cui e6fad266a6 docs(rfc): define and enforce a uniform RFC format; adopt it across the corpus
Define the in-file RFC contract in docs/rfc/README.md § The file format:
the header block (`# RFC: <title>` plus a dateless Status enum
cross-checked against the lifecycle folder), the per-lifecycle body
skeleton (a Problem opener everywhere; Proposal/Alternatives considered/
Acceptance criteria/Risks in proposed/; present-tense Decision/
Consequences with proposal-era headings banned in implemented/; the
frozen proposal shape in rejected/), and a mandatory Alternatives
considered section with a date-fenced grandfather comment for pre-format
RFCs whose alternatives are not reconstructible from the record.

Enforce it with a new doc-sync gate, scripts/verify-rfc-format.ts, and
normalize all 112 RFCs to it: ~15 Status-line spellings collapse to the
enum, 29 Context openers become Problem, the 39 legacy-format XXX debt
markers are resolved and banned from reappearing, proposal-era sections
in implemented RFCs are rewritten to shipped reality (including the
web/fs/subagent seam RFCs' migration plans and test checklists, closing
the doc-tiers deferred-work item on the web seam), every RFC gains an
Alternatives considered section or the grandfather comment, and the
bilingual pair is re-mirrored and re-recorded.

Move the generated index tables out of README.md into a fully generated
docs/rfc/INDEX.md — gen-rfc-index now writes the whole file, and
verify-rfc-classification checks its freshness and rejects index-shaped
rows in the curated README — which makes room for the format contract to
live in the README front door instead of a separate FORMAT.md.

The decision record, and the first RFC written in the new format, is
docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md.
2026-07-05 22:58:25 +08:00

3.3 KiB

RFC: Capability seams — interface / implementation / consumer split

Status: implemented

Problem

The harness has swappable capabilities — bash execution today, sandboxed/remote executors and alternative model providers tomorrow. A capability has three concerns that change at different rates and for different reasons: the contract (what the capability is), the implementation (how it runs), and the consumer surface (what the model and other plugins program against). Bundling them in one package couples those rates of change — swapping a local executor for a sandboxed one would churn the tool schemas the model sees, even though the model-facing contract never changed.

This is distinct from "who provides vs. needs a capability at runtime", which Cordis already answers with services + inject (a provider registers ctx.bash; a consumer declares inject: ['bash'] and its fiber pends until the service exists). That mechanism is necessary but doesn't dictate package boundaries; this RFC does.

Decision

A swappable capability is three packages:

  1. Interface — an abstract service + the vocabulary types, owning the ctx.<key> and depending only on cordis (e.g. dsh-bash: BashExecutor, BashRunResult, BashTask).
  2. Implementation — a concrete subclass loaded as a plugin (e.g. dsh-bash-local: subprocesses, process-group kills, spill-file truncation). Sandboxed/remote backends are sibling packages implementing the same interface.
  3. Consumer — what the model and plugins see (e.g. dsh-tool-bash: the bash/bash_output/bash_kill tool schemas). Consumers inject the interface key and never import implementation types.

Implementation and consumer then evolve independently: a sandboxed executor replaces dsh-bash-local without touching a tool schema.

The split is not mandatory when the parts are genuinely one concern: the LLM seam folds interface + consumer into dsh-llm (the consumer is the loop itself, not a swappable schema surface) with adapters as the implementation packages. Don't split preemptively — a capability with one conceivable implementation and one consumer stays one package until a second appears.

Alternatives considered

  • One combined package — rejected because it recouples the three rates of change the split exists to separate (the whole point).
  • @cordisjs/plugin-capability — a different axis entirely: it is a permission/capability-security service (named permissions with inheritance, tested against a session via ctx.capability.test), a candidate for the deferred permissions/sandbox work on the tools/pre-execute deny/ask seam, NOT a mechanism for swapping implementations. Confusing the two ("capability") is the trap this RFC names.

Consequences

More packages and more boilerplate per capability (a package.json/tsconfig/README trio, the inject wiring). Bought: implementations and consumers ship and version independently, and a new backend never risks the model-facing contract. The rule is documented in AGENTS.md § Conventions ("Capability seams are three packages") and architecture.md § "Capability seams"; the bash trio is the reference template. When to fold vs. split is a judgment call the architecture doc spells out — this RFC records why the default is to split.