Skip to content

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, or serial.
  • Reversible effects — registrations (prompt sections, tool schemas, listeners, providers) installed through ctx.effect() or ctx.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:

ConceptDefinitionDeclared where
ProfileA 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.ymldsh.profile.bundles in the profile's package.json
BundleA distribution format for Cordis config rows plus the code they mount — whatever it inserts stays patchable by layers abovedsh.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:

PackageOwnsctx key
core/sessionThe append-only SessionEvent log and in-memory storectx.sessions
core/system-promptPrompt-section and tool-schema assemblyctx.systemPrompt
core/toolsThe scoped tool registry and guarded execution pipelinectx.tools
core/agentThe Agent interface, live registry, and agent/* eventsctx.agents
core/agent-loopThe default driver implementing that interfacectx.agentLoop
core/scopeThe per-agent scoped-registration primitivelibrary — no key
llm/llmMessage and stream vocabulary plus the adapter seamctx.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 web starts.
  • 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:

text
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 HMR

The 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

text
                 ┌──────────────────────────────────────────────┐
   --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 bootsrun dsh --profile web --dump-config
Compose profiles & bundlespackages/boot/app-boot/src/profile.ts (Profile, loadProfile, resolveBundleDir)
Boot a tree from a configpackages/boot/app-boot/src/index.ts (boot, mountRootInclude)
Understand the CLI flags & dsh pluginapps/cli/src/args.ts, apps/cli/src/plugin.ts
Read the session log / turn flowpackages/core/session/src/, docs/subsystems/session.md
See the Agent interface & eventspackages/core/agent/src/runtime-types.ts, docs/subsystems/core.md
Trace a turn end-to-endpackages/core/agent-loop/src/agent.ts (ReactLoopAgent), docs/agent-lifecycle.md
Add a model providerregister its adapter on ctx.llm (packages/llm/llm/)
Add a model-facing toolregister on ctx.tools; its schema joins prompt assembly
Swap a provider worldthe seams in docs/capability-seams.md
Change the loop itselfthis 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 live Agent — 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 web to 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/event stream, event dispatch.
  • Repo docs: docs/architecture.md, docs/capability-seams.md, docs/module-graph.md in the repository root.
  • Source: packages/core/README.md, packages/boot/app-boot/src/profile.ts, packages/core/agent/src/runtime-types.ts.