Source-level analysis · deepseek-harness 0.1.0-rc.5

DeepSeek Harness Architecture:
A Source-Level Deep Dive

A running dsh is a tree of plugins composed at boot from a few YAML layers — the model adapter, the tool registry, the session log, and the agent loop itself are all replaceable from configuration. This article takes the machine apart, diagram by diagram.

Published 2026-08-14Verified against 0.1.0-rc.5201 MB monorepo · 40+ package groups
Based on a full read of the 0.1.0-rc.5 source tree (git 47f9438) · snapshot 2026-08-14

No privileged core: the five-layer view

DeepSeek Harness has no fixed core. At startup, the runtime is assembled from layers of YAML configuration into a tree of plugins — the model adapter, the tool registry, the session log, and the agent loop itself are all nodes in that tree, and every one of them can be replaced from configuration. The architecture docs state it directly: “there is no privileged core to patch.”

Three entry points — the CLI, the Web SPA, and the Python SDK — share one runtime. Every capability the runtime exposes eventually converges on a layer of standard interfaces the project calls capability seams; only behind those seams do the real model APIs, file system, and operating system appear.

ENTRY LAYER (CAN COEXIST ON ONE MACHINE)dsh CLI--profile web|headlessWeb SPAReact 18 + zustand · WS/HTTPPython SDKbundled Node 24 · NDJSON/stdioACP serverautomation JSON-RPCBOOT COMPOSITION LAYERStart from an empty config; apply bundles, user config and CLI flags as patches → one plugin treeapps/cli → packages/boot/app-boot · patch rule: whole-entry replacement, no deep mergemountKERNEL (VENDORED)Cordis 4.0.0-rc.7: Context · Fiber (clean unload) · event bus (5 dispatch modes)source vendored into the repo under the @deepseek-ai scope · 18 local modifications on recordAGENT RUNTIME LAYERAgentLoopadvances in turns and stepssee §4Tool runtimecheck → approve → run → finalizesee §5 · concurrency cap 10Session logappend-only event logmodel-visible means loggedvia ctx.*CAPABILITY SEAMS (EACH SLOT REPLACEABLE AS A WHOLE)ctx.llm · ctx.fs · ctx.shell · ctx.subprocess · ctx.sandbox · ctx.terminals · ctx.subagentsctx.web · ctx.skills · ctx.jobs · ctx.approval · ctx.sessionPersistence · ctx.codeRuntime …THE WORLD BEHIND THE SEAMSModel APIsdeepseek-official / pi-ai / local endpointsOperating systembwrap→Landlock / Seatbelt / Win ACLExternal agentsMCP servers / Claude Code / Codex / E2B
The five layers. The kernel’s only job is composing plugins; every product feature lives in a plugin.

An easily missed fact: Cordis is not an npm dependency — its source is vendored wholesale into the repo. The vendor/ directory copies nine upstream packages (cordis, loader, hmr, schemastery, …) under the @deepseek-ai scope, with a CI gate that prevents registry copies from sneaking back in. The stated rationale: a framework layer that is “auditable, patchable, pinned.” Eighteen local modifications are already on record, including a deadlock fix.

Boot: a plugin tree assembled from patches

One detail captures the design philosophy: the root config file of a profile contains an empty entry list — literally [] — and is rewritten on every boot. The entire plugin tree is produced by applying patch layers in order. What your dsh is is fully determined by which patches were applied, and in what order.

cordis.ymlempty entry list []① bundle layerdsh-base + web-app|headless② profile layerprofiles/x/cordis.patch.yml③ home layer~/.dsh/cordis.patch.yml④ --patch overlaysCLI flags, argv order⑤ launcher overlaypreset roots · telemetry flagassembled plugin treeprintable via --dump-configUser layers ② and ③ are watched; edits apply immediatelyeach change recomposes the full stack (bundles stay at the bottom, CLI overlays on top); a failed recomposition keeps the previous treePatch semantics: match by id → whole-entry config replacement (no deep merge); insert → append entry; unknown id → warningboot, --dump-config and hot reload share one composition code path — the dump is exactly what runs
Application order is precedence: later layers override earlier ones, and hot reload always re-slots user layers above bundles, below CLI overlays.

A bundle is an ordinary npm package that declares a patch file in its package.json ("dsh": {"bundle": {"patch": "./cordis.patch.yml"}}). A single patch in dsh-base assembles roughly 80 plugins — model, session, agent, tools, credential management — while the web-app and headless bundles only apply incremental overrides on top. Config values may also be !!js expressions, evaluated at mount time:

# packages/bundle/headless/cordis.patch.yml (verbatim)
- id: session-query-sqlite
  config:
    path: ':memory:'   # matched by id → whole-entry replacement
- insert:
    - id: headless-runner
      name: '@deepseek-ai/dsh-headless'
      inject: [headlessStartup]
      config:
        task: !!js ctx.headlessStartup.task

The Cordis kernel: fully reversible side effects

The accompanying 88-page paper, A Programming Paradigm for Spatiotemporal Composability, reduces to one implementation-level constraint: every resource a plugin registers while mounting — event listeners, tools, timers — must simultaneously record a cleanup function. Unloading runs those functions in reverse, restoring the runtime to its pre-mount state. Hot replacement without a process restart rests entirely on this mechanism.

plugin apply(ctx)declares deps; mounts when readyasFiberfiber.tsctx.effect()Disposer ledger (DisposableList)· event listener registered → “remove listener” recorded· tool registered → “unregister tool” recorded· timers / file watchers → “close” recorded for each· child plugin mounted → cascade disposal recordedunload / hot reloaddisposers run in reverse → all changes revertdisposal is idempotent; registering on a disposed fiber throwscascading unload: parent out, subtree outone changed config line = old plugin fully out, new one mountedFive dispatch modes: emit (fire-and-forget) · parallel · serial · bail · waterfall (chained; a listener that skips next() vetoes the chain — every interception point uses it)
Complete unload is what makes tool-cordis — a tool that lets the model write and mount plugins at runtime — a defensible idea: a bad mount is simply unloaded.

The lifecycle of a turn

Two definitions: a turn is one full cycle of processing queued input; a step is one model call plus all the tool executions it requests. A turn contains one or more steps — tool results and injected context drive the next step until the agent stops naturally. The diagram below shows the main event sequence; every event name is verified against agent.ts.

session events: logged, replayablewaterfall: plugins may interceptone-way notificationsinput queued → agent status: runningturn/startinbox.claim → agent/inbox/claimed ×Nsystem-prompt/assembleagent/pre-step (reject | enter)reject → turn ends blocked, no model callstep/start → user/message ×Nagent/request → request/header·contextllm/stream → assistant/chunk ×Nfailure → e.g. context overflow: compact, retry same stepassistant/message (references chunk seqs)tool pipeline (§5) → tool/call … tool/resultstep/end↺ inbox has input → next step, same turnagent/turn-stopping (serial) → turn/end → idleturn end reasons (closed set): completed | aborted |blocked | error | max-tokens | interrupted (crash recovery)once a step hits max-tokens, the turn’s outcome is locked
Orange events enter the session log and form the authoritative record; teal waterfalls are where plugins legally intervene — compaction, the hooks bridges and plan mode all mount there.

The tool pipeline: log first, authorize second

The most engineering-dense part of the codebase. Three design decisions stand out: the call is written to the log before anything executes; any anomaly in the approval path is treated as a rejection (fail-closed) — there is no default-allow; and results are committed strictly in the order the model requested them.

session: tool/call — logged before executiontools/pre-execute (hooks · permissions · sandbox)allowaskdeny (incl. rejected approvals)one-shot approval (ctx.approval)pushed to the UI → user decides → response returnsno answerer / timeout / error = not allowedallowed-onceguards: may deny, never allowtools/execute (timeout & retry wrappers)tool body runsfile writes pass an intent check; once started, cancel waitstools/post-execute (spill oversized output, transforms)normalize (throws → error results) → freezesession: tool/result (paired with tool/call)extra contexts queue into the next stepinjected as user messages (also logged)Concurrency rules· parallel tools: rolling pool, cap 10· exclusive tools get a barrier:  they run alone· permission checks run serially, in  model order; only bodies overlap· results commit strictly in request  order — a fast 2nd waits for the 1st· on abort: in-flight calls drain, unstarted  ones get synthetic results — log stays whole
Denied calls still travel the back half of the pipeline — the denial itself must leave a complete record. In Code mode, direct calls that bypass run_code are rejected before entering the pipeline at all.

The session log as the single source of truth

The core invariant: anything that enters a model request must be reconstructable from the session log. Three mechanisms enforce it. Structurally, model requests can only be derived from the log — no second code path exists. Every event is validated before append. And at runtime, an assertion plugin compares each outgoing request byte-for-byte against a fresh reconstruction from the log, aborting loudly on mismatch.

append-only event log (JSONL / SQLite)turn/start · seq 0user/message · surfaceOp: appendassistant/chunk ×N (packed into storage rows)assistant/message · references chunk seqstool/call → tool/result (meta for UI rebuild)summary · surfaceOp: replace [start,end]compaction never deletes: a summary eventshadows the range — originals stay auditablebatched writes · flush events are checkpointsderivemodel request (deriveMessages)runtime byte-level check: actual request ≡ reconstructionreplay → Web UI trajectory viewtool cards rebuilt from logged metadatafork a child sessionprefix up to a boundary; boundaries inside an open turn are rejectedresume / crash recoveryunclosed turns get synthetic closing events, then continue
One stream, four projections. Scheduled reminders and permission switches are stored the same way — as events in the same log, folded back into state on demand.

Context management is two-tiered: at every step boundary the runtime estimates token usage and compacts proactively past a threshold — pruning verbose tool output first, then summarizing older conversation with the session’s own prefix (deliberately preserved so KV-cache hits survive). If the provider actually returns a context-overflow error, compaction runs regardless of thresholds and the same step retries. Oversized tool output is handled separately (“spill”): the full text goes to disk, and the model sees only a head/tail preview plus a retrieval handle.

Capability seams: swap a provider, move the world

Every capability — files, processes, models — is split into three roles: a package that defines the interface, a package that implements it, and consumers that depend only on the interface. The practical payoff: moving the entire execution environment into a cloud sandbox takes two provider swaps in configuration.

consumer modules (unchanged by the swap)bash-localterminal-bash (PTY)lsp-stdioglob/grep (ripgrep)external subagents ×3tool-fs (read/write/edit)ctx.subprocessspawn / PTY primitive /resolveExecutablectx.fsopaque FsTarget handlesdefaultswap 2 config lineslocal worldsubprocess-local (node-pty)fs-sandbox (realpath + atomic writes)E2B cloud sandboxsubprocess-e2b (E2B PTY API)fs-e2bVerified in source: shells, persistent terminals, language servers, file search and external subagents all spawn through ctx.subprocess alone→ swap in the two E2B providers and all of it moves to the cloud; the host process, model calls and session log stay local
The point of the figure is the left-hand arrows: before and after the migration, not one of them is redrawn.

The main seams (defaults per the dsh-base bundle)

Seam (ctx key)Default providerSwappable toNotes
ctx.llmllm-deepseek (deepseek-official route)llm-pi-ai (multi-provider) / llm-replaypi-ai ships mounted but dormant; a settings block activates it. Both adapters can coexist.
ctx.fsfs-sandbox (extends fs-local)bare fs-local / fs-e2bIts own comment: containment, not a security boundary.
ctx.subprocesssubprocess-local (node-pty)subprocess-e2bThe PTY primitive lives in this seam, not in the terminal packages.
ctx.sandboxsandbox-localcustom runner commandLinux: bwrap first, Landlock fallback (a home-grown static C binary); macOS Seatbelt; Windows restricted token (write-only enforcement). Unavailable sandbox = refuse to run, never a bare exec.
ctx.subagentsspawn + fork (in-process)acp / codex / claude-code / dsh-sdkThe claude-code provider resolves the claude binary from PATH and drives it via the official agent SDK; codex spawns codex app-server --stdio.
ctx.codeRuntimeworker-thread (not in base)other language backendsWhere Code-mode programs run: an in-process worker executes the model’s TypeScript with types stripped; every tool call bridges back and re-enters the full pipeline.
ctx.webweb-search-deepseekexa / perplexity / http-fetchweb_fetch ships disabled; the comment cites SSRF risk.
ctx.approvaluser-approvalacp auto-answererPolicies are only ask / never — and never means reject-all, not allow-all.

The four presets — and the “Creator” that doesn’t exist

An agent preset selects the capability set for a single session; different sessions in one process can run different presets. Here the source diverges from the marketing: the official page and much of the press describe a fourth preset called “Creator” — but no such preset exists in the rc.5 source. The config directory contains exactly four: standard, code, cordis, minimal.

Capabilitystandardcodecordisminimal
bash / pwsh / fs / searchpersistent bash + editor only
plan / todo / goal / skills
subagents / jobs / web search
compaction
Code mode (tools only via run_code)the sole diff vs standard is one config line
tool-cordis (model edits the live composition)

A separate system, permission presets, is orthogonal to agent presets. It bundles two independent knobs into one user-facing option: sandbox level (read-only → workspace-write → full access) and approval policy (ask / never). Only two presets ship by default — workspace-write + ask, and full access + never — and every switch is recorded in the session log, so it replays.

The web trust model: origin checks, not authentication

The web deployment is two halves: a local host service (Node) and a React SPA. Commands go up as HTTP POST; real-time frames come down over two WebSocket channels. The essential point: there is no authentication — only an origin fence against DNS rebinding and cross-site requests, and the source comments state explicitly that the fence is not an auth layer.

Browser SPAReact 18 + zustandthe UI itself is 40+ pluginstrust fenceloopback Host onlycross-site rejectedhost (node:http)typed gateway (new)legacy RPC (52 methods)unmigrated methods fall back to legacyPOST /api/*WS ×2: events.mux (session frames) · events.hostPrivileged methods are loopback-only: settings, credentials and 16 more re-check origin even with LAN trust configured — non-local gets 403Credentials are write-only: set / unset / describe, no read method; the UI shows status only; resolved per use — a rotated key applies on the next requestthe approval loopa tool asking to escalate (e.g. write outside the workspace) must attach a justification → an audit event is logged (never shown to the model)→ the request is pushed to the matching tool card in the browser → the user picks allow-once or reject → the decision returns and is logged→ four possible outcomes: allowed-once / rejected / cancelled / unavailable — only allowed-once executes
From the source: the fence is “explicitly not an auth layer.” That is why disclosure #853 — any local process can call 60+ unauthenticated RPC methods — holds: by design, local processes sit inside the trust boundary.

What holds up, and what to watch

▲ Architecturally solid

  • Log consistency is enforced, not aspirational: byte-level request verification, complete records for denied calls, synthetic results on abort — replayability gets unusual investment.
  • The seam abstraction is field-tested: wholesale E2B migration, Claude Code and Codex mounted as subagents, twin model adapters coexisting — all evidence of stable interfaces over swappable implementations.
  • Inference-cost awareness runs deep: forks and compaction both deliberately preserve cache-hittable request prefixes, and cache-hit metrics surface throughout.
  • Security defaults are strict: no sandbox means no execution, approval anomalies mean rejection, credentials are write-only with 0600 file modes.

▼ Worth watching

  • No authentication, only a fence: any local process sits inside the trust boundary (the root cause of disclosure #853); remote access is simply disabled until a real auth layer lands.
  • Hot reload is actually off in both shipped deployments (the reload lifecycle is marked untested) — hot-swapping is an architectural capability, not yet a product reality.
  • Docs have started drifting from code: the advertised “Creator” preset does not exist, and a documented lsp-local package doesn’t either (it’s lsp-stdio).
  • Preview-stage rough edges are visible: two RPC generations coexist, the Windows sandbox restricts writes only, and web fetch is switched off entirely over SSRF concerns.

In one sentence: this is not an agent with a plugin system — it is a plugin system that happens to be assembled into an agent. The bet is not benchmark scores; it is making the agent runtime the kind of infrastructure a web framework is: self-hosted, auditable, and rearrangeable at will. Vendoring the framework, composing from an empty config, and byte-checking the log are all down payments on that bet.

Methodology & sources

This analysis is based on a full read of the deepseek-ai/deepseek-harness source at 0.1.0-rc.5 (git 47f9438, snapshot 2026-08-14), conducted as four parallel passes: kernel & composition, event flow, capability seams, and the product layer. Key files: docs/architecture.md, vendor/README.md, apps/cli/src/profile-boot.ts, packages/boot/app-boot, vendor/cordis/src/fiber.ts, packages/core/agent-loop/src/agent.ts, packages/core/tools/src/index.ts, packages/core/session, packages/bundle/base/cordis.patch.yml, packages/client/connection/src/api-request-trust.ts. The project is iterating fast — expect details to change between rc releases.

New to DeepSeek Harness? Start with our complete guide — installation, troubleshooting, pricing, and how it compares to Claude Code and Codex.

Changelog: 2026-08-14 — first published, verified against dsh 0.1.0-rc.5.