One sentence
DeepSeek Harness is a plugin tree: a running dsh process is a set of Cordis plugins composed at boot from ordered patch layers, where every part of the product — model adapter, tool registry, session log, even the agent loop itself — is a plugin mounted beside the others and replaceable from configuration.
There is no privileged core to patch. You extend dsh by mounting a plugin beside the rest, and every registration is an effect that unwinds when its plugin unloads.
Cordis context as the backbone
Cordis is the vendored plugin framework underneath dsh. Every capability runs through one Context (ctx). A service claims a stable ctx.<key> — ctx.tools, ctx.llm, ctx.sessions — and other plugins find it by key rather than importing a concrete implementation. Dependencies are declared via inject so load order is expressed as service requirements, not a manual boot sequence.
Plugins contribute three things to the shared context:
- Services — a value installed at
ctx.<key>(the session store, the LLM adapter registry). - Typed events — names declared through TypeScript declaration merging and dispatched as
emit,waterfall,parallel, orserial. - Reversible effects — registrations (prompt sections, tool schemas, listeners, providers) installed through
ctx.effect()orctx.on()that unwind predictably on reload and teardown.
The result is that a capability "seam" has three roles — a Service Definition declaring the interface, a Service Provider implementing it, and a Consumer using it — and swapping one provider changes the whole product. For example, filesystem and subprocess providers share one execution world, so pointing them at a remote sandbox moves Bash, PTY, and LSP together with no provider forks.
Profiles and bundles
Two package.json-level concepts structure the tree:
| Concept | Definition | Declared where |
|---|---|---|
| Profile | A named composition stored under $DSH_HOME/profiles/<name>, listing the bundles it stacks, any out-of-tree plugins it installs, and the user's own cordis.patch.yml | dsh.profile.bundles in the profile's package.json |
| Bundle | A distribution format for Cordis config rows plus the code they mount — whatever it inserts stays patchable by layers above | dsh.bundle.patch in the bundle's package.json |
web and headless ship as profile templates. Three bundles cover the layers:
@deepseek-ai/dsh-base— the first layer of every profile: model adapters, tools, persistence, sandbox and approval policy, settings, credentials, telemetry.@deepseek-ai/dsh-web-app— adds the browser application.@deepseek-ai/dsh-headless— adds a one-shot runner with no server at all.
Layers apply to an empty entry list in order: each bundle in the profile's list, then the profile's cordis.patch.yml, then the home-level patch file, then any --patch overlay. A patch targets a row by id and replaces its whole config, or inserts new rows.
The core-packages table
packages/core holds the "product API spine" — the stable surface plugins build against:
| Package | Owns | ctx key |
|---|---|---|
core/session | The append-only SessionEvent log and in-memory store | ctx.sessions |
core/system-prompt | Prompt-section and tool-schema assembly | ctx.systemPrompt |
core/tools | The scoped tool registry and guarded execution pipeline | ctx.tools |
core/agent | The Agent interface, live registry, and agent/* events | ctx.agents |
core/agent-loop | The default driver implementing that interface | ctx.agentLoop |
core/scope | The per-agent scoped-registration primitive | library — no key |
llm/llm | Message and stream vocabulary plus the adapter seam | ctx.llm |
Note the deliberate split: core/agent owns the public Agent contract, while core/agent-loop is its single default implementation. Extension plugins depend on the agent seam (including when they need the initiating agent) and never on agent-loop directly, so the driver stays swappable. core/scope is the one non-service package: a dependency-free library (createScope/scopeOf/scopeTarget) that sits below session/ and system-prompt/ so they can consume it without a cycle.
Host and client split
dsh builds the same source tree against two faces (the root tsconfig.host.json and tsconfig.client.json, switched by --env.DSH_BUILD_FACE):
- Host — the Node process: runs the webserver, API gateway, sandbox, subprocess and filesystem providers, and the agent loop. This is what
dsh webstarts. - Client — the browser bundle served to the page. It mounts web-client modules and the browser halves of dual-half packages (for example the Cordis dynamic-package runner at
packages/extensions/cordis-client-runner), communicating with the host through the API gateway (packages/api/gateway).
A host-only package (e.g. @deepseek-ai/dsh-cordis-host-runner) is this process's own; a package with a browser half has to be carried out by a page, so the host suspends and the browser answers. The two faces never mix in one aggregate: the host aggregate excludes browser-half packages so each face keeps its own TypeScript program.
How layers compose at boot
Boot is one call in packages/boot/app-boot:
dsh web
└► bin.ts parseDshArgs → { mode: 'profile', profile: 'web', args, patches }
└► profile-boot.ts runProfile
├► composeProfile ─ healProfilesModuleFallback
│ loadProfile → bundle layers (base, web-app)
│ + profile cordis.patch.yml
│ + home cordis.patch.yml
│ + --patch overlays
│ + telemetry switch + shipped preset root
├► boot(name, rootConfig, patches, prepare)
│ new Context (Cordis)
│ ctx.plugin(Loader) cordis-plugin-loader
│ prepare: provide env + cmdlineArgs/appExit
│ mountRootInclude → 'cordis:include' builtin
│ Loader creates the root Include entry
│ Include reads cordis.yml (the empty profile root)
│ include applies the flattened patch list,
│ mounting every plugin row → ctx.sessions, tool registry, …
│ await loader, assertEntriesActivated
└► watchUserPatches keep cordis.patch.yml hot-reloaded via HMRThe prepare callback runs before any config-tree entry mounts, providing the launch environment snapshot and ctx.cmdlineArgs/ctx.appExit — launcher facts, not config. The mountRootInclude call mounts the statically imported Include as the cordis:include builtin plus cordis:group, so the config tree's rows resolve without depending on the included tree's own specifier resolution.
The plugin-tree layering diagram
┌──────────────────────────────────────────────┐
--patch │ overlay patches (CLI, highest precedence) │ ─┐
├──────────────────────────────────────────────┤ │
$DSH_HOME/ │ home cordis.patch.yml (every profile) │ │
cordis.patch. │ │ │
yml │ │ │ applied
├──────────────────────────────────────────────┤ │ bottom-up
profile/ │ profile cordis.patch.yml (user's own layer) │ │ (later wins)
cordis.patch. │ │ │
yml ├──────────────────────────────────────────────┤ ┘
│ @deepseek-ai/dsh-web-app (browser surface) │
│ └ or @deepseek-ai/dsh-headless (one-shot) │ patch layers
├──────────────────────────────────────────────┤ in dsh.profile
│ @deepseek-ai/dsh-base .bundles order
│ model adapters,tools,persistence,sandbox, │
│ approval,settings,credentials,telemetry │
└──────────────────────────────────────────────┘
root profile/cordis.yml = [ ] (empty entry list)Composition is the same single applyEntryPatches call the boot include makes — composeEntries reuses it so --dump-config prints exactly what the same invocation would mount.
Where to look for what (a reading map)
| You want to… | Look here |
|---|---|
| See the exact tree your machine boots | run dsh --profile web --dump-config |
| Compose profiles & bundles | packages/boot/app-boot/src/profile.ts (Profile, loadProfile, resolveBundleDir) |
| Boot a tree from a config | packages/boot/app-boot/src/index.ts (boot, mountRootInclude) |
Understand the CLI flags & dsh plugin | apps/cli/src/args.ts, apps/cli/src/plugin.ts |
| Read the session log / turn flow | packages/core/session/src/, docs/subsystems/session.md |
| See the Agent interface & events | packages/core/agent/src/runtime-types.ts, docs/subsystems/core.md |
| Trace a turn end-to-end | packages/core/agent-loop/src/agent.ts (ReactLoopAgent), docs/agent-lifecycle.md |
| Add a model provider | register its adapter on ctx.llm (packages/llm/llm/) |
| Add a model-facing tool | register on ctx.tools; its schema joins prompt assembly |
| Swap a provider world | the seams in docs/capability-seams.md |
| Change the loop itself | this map (the loop is core/agent-loop), and docs/architecture.md |
Events as extension points
Choosing the right event domain is the first decision in most changes:
- Session events are durable facts appended to the log and broadcast through
session/event— use one when the fact must survive a reload. - Agent events (
agent/*) carry a liveAgent— inbox, step, status, request, validation, continuation — use one to observe or intercept work in flight. - Capability events attach policy and adapters to a seam (
fs/*,tools/*,telemetry/*) without importing the loop.
The event map (docs/event-producer-consumer.md) lists every event's producers and consumers.
Further reading
- Boot process & CLI — what happens from
dsh webto a running server. - The extension (Cordis) system — services, typed events, reversible effects, dsh's Cordis wrappers.
- Runtime & agent lifecycle — agent lifecycle states, the setup window, the durable
session/eventstream, event dispatch. - Repo docs:
docs/architecture.md,docs/capability-seams.md,docs/module-graph.mdin the repository root. - Source:
packages/core/README.md,packages/boot/app-boot/src/profile.ts,packages/core/agent/src/runtime-types.ts.