The central claim
The root README.md calls dsh's architecture "everything is a plugin." Concretely:
[Cordis] is the framework under dsh: plugins contribute services, typed events, and reversible effects to a shared context. Every part of the product is a plugin, including the model adapter, the tool registry, the session log, and the agent loop itself, so every part is replaceable from configuration. —
docs/architecture.md
The word to hold on to is everywhere it says "a plugin": the model adapter (ctx.llm), the tool registry (ctx.tools), the session log (ctx.sessions), and the agent loop (ctx.agentLoop) are all just entries in a Cordis tree. There is no privileged core to patch — you extend dsh by mounting a plugin beside the others.
What plugins contribute to the context
A Cordis plugin is an object that either is a Service subclass or a plain function with an optional inject property and an apply(ctx) body. Once Cordis mounts it, it contributes three kinds of things to the shared ctx:
| Contribution | Mechanism | Example |
|---|---|---|
| Service | claims a stable ctx.<key> | ctx.llm, ctx.tools, ctx.sessions, ctx.agents |
| Typed event | declared via TypeScript declaration merging, dispatched with emit / waterfall / parallel / serial | agent/pre-step, tools/execute, turn/start |
| Reversible effect | installed via ctx.effect() or ctx.on(), unwound on unload | a prompt section, a tool schema, a listener |
Dependency is declared through inject: a plugin that names a service it needs waits until that service exists, so load order is expressed as service requirements rather than manual sequencing. Registrations are effects, so when a plugin unloads (a profile switch, a cordis.patch.yml edit, a reload), everything it installed rolls back predictably — nothing is left half-registered.
The underpinning framework is the vendored Cordis, whose design is documented in the paper A Programming Paradigm for Spatiotemporal Composability, and whose in-repo explanation is docs/cordis-primer.md. The package providing the "plugin" abstraction itself is @deepseek-ai/cordis under vendor/cordis.
No privileged core
Because every capability is a service, one provider swap changes the whole product. docs/architecture.md makes this explicit with the capability seam model: a swappable capability has three roles —
- Service Definition — declares the interface (e.g.
ctx.shell). - Service Provider — implements it (e.g.
dsh-bash-local,dsh-bash-sandbox). - Consumer — uses it, commonly a model-facing tool (e.g.
dsh-tool-bash).
One role alone is not a seam; adding a capability means designing all three. The packages/shell/shell/README.md shows the split in miniature:
| Package | Role |
|---|---|
@deepseek-ai/dsh-shell | Service Definition: ShellExecutor (ctx.shell) + vocabulary types |
@deepseek-ai/dsh-bash-local | Service Provider: local subprocesses |
@deepseek-ai/dsh-bash-sandbox | Service Provider: same mechanics, every spawn confined via ctx.sandbox |
@deepseek-ai/dsh-tool-bash | Consumer: the model-facing tool schemas |
Because the consumer talks only to the Service Definition, swapping dsh-bash-local for a containerized or remote executor needs no provider forks. Filesystem, subprocess, and subagent providers work the same way (see the capability-seams section of docs/architecture.md).
Profiles and bundles: named compositions
A running dsh is not a fixed program; it is a plugin tree composed at boot out of ordered layers.
- A profile is a named composition stored in the Harness home. It lists the bundles it stacks, holds any out-of-tree plugins it installs, and keeps the user's own
cordis.patch.yml.webandheadlessship as templates (PROFILE_TEMPLATESin app-boot). - A bundle is a distribution format for Cordis config rows and the code they mount.
dsh-base,dsh-web-app, anddsh-headlessare the shipped bundles.
Each declares itself under the dsh field of its own package.json:
// a profile package.json
{ "dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"] } } }
// a bundle package.json
{ "dsh": { "bundle": { "patch": "./cordis.patch.yml" } } }dsh.profile.bundles lists a profile's bundles in order; dsh.bundle.patch points at a bundle's patch file — the YAML that inserts the code rows. The composition machinery lives in packages/boot/app-boot/ (exports resolveProfileDir, initProfile, loadProfile, composeEntries, PROFILE_TEMPLATES, DEFAULT_PROFILE_BUNDLES, PROFILES_DIR, PROFILE_PATCH_FILENAME).
Layer order
Layers apply to an empty entry list in a strict order, from the CLI and profile machinery:
empty root
→ each bundle's patch (in dsh.profile.bundles order)
→ the profile's cordis.patch.yml
→ the home-level $DSH_HOME/cordis.patch.yml
→ any --patch overlaysUse apps/cli/src/args.ts to see the launcher flags themselves; the ordering doctrine lives in docs/architecture.md (Layers apply to an empty entry list in this order: each bundle in the profile's listed order, then the profile's cordis.patch.yml, then the home-level one, then any --patch overlay).
Inspecting the composed tree
Because a running tree is just a stack of patches, it is fully inspectable. This command prints the composed tree for the web profile and exits without booting a tree:
dsh --profile web --dump-config--dump-default-config prints the bundle layers only (no user layer, no --patch). The dump is rendered by renderConfigDump in app-boot using the include's own applyEntryPatches, so what it prints is exactly what would mount — the tooling cannot drift from the boot. Any row it prints can be replaced by a patch of your own. A patch targets a row by id, and replaces that row's wholeconfig (there is no deep-merge layer — a profile override must restate the fields it keeps), or inserts a new row.
Why this makes every part replaceable
The payoff is the "no privileged core" claim made concrete:
- A bundle's row set is decided by a patch file, not by code — override a row by id and the behavior changes.
- User preferences live in layers (per-profile, then home-level), each of which beats the bundle below it.
- Flags parsed by an app plugin (via
@deepseek-ai/dsh-cmdline'sparseCmdline) are read from lazy!!jsconfig, e.g.port: !!js ctx.webStartup.port ?? 3080, so even a launch flag is just the value of a config expression — write a literal over the expression and the flag silently stops winning. - Because registrations are reversible effects, live
cordis.patch.ymledits (via the HMR plugin) recompose the tree in place without a full restart.
Interaction and lifecycle detail — the webserver row example, the web-startup provider, and profile machinery — is covered in depth on Boot & CLI and The Extension (Cordis) System.
Further reading
- What Is DeepSeek Harness? — the product this philosophy drives.
- Boot & CLI — profiles, bundles, and
--dump-configin practice. - The Extension (Cordis) System —
inject, events, andctx.effect(). docs/cordis-primer.md— the Cordis in Five Ideas summary.docs/architecture.md— layer order and capability seams.packages/boot/app-boot/README.md— profiles andcomposeEntriesdetail.