SherlockLiu Logo SherlockLiu
Back to all posts
Engineering

DeepSeek Harness: Composing an App From YAML, Not Code (Part 2)

SL
Aug 14, 2026 8 min read
DeepSeek Harness: Composing an App From YAML, Not Code (Part 2)

Series: Inside the DeepSeek Harness — Part 2 of 16


Part 1 established the one-line idea behind DeepSeek Harness: no privileged core, everything — including the agent loop — is a Cordis plugin mounted on a shared Context. That raises an obvious question. If nothing is hardcoded, something still has to decide which ~150 plugins get mounted, in what configuration, for a given dsh process. A web session and a headless CI run don’t want the same tool set. A sandboxed deployment and a trusted local dev box don’t want the same shell provider.

In most frameworks, that decision lives in code — an if (env === 'production') branch, a factory function, a DI container wired up in TypeScript. In dsh, it lives entirely in YAML, resolved through a small, disciplined merge algorithm before a single plugin’s apply() function ever runs. Concretely: your CI profile wants a locked-down sandbox and no interactive approval prompts; your local dev profile wants the opposite. Getting both, from the same base bundle, without forking anything, is what the rest of this post explains — profiles, bundles, and the five-layer patch stack that turns static config files into a running process, with the actual merge code, not a paraphrase of it.


A profile is not a config file — it’s a stack

The unit you actually interact with as a dsh user is a profile: a named directory in $DSH_HOME/profiles/<name>/ that lists which bundles to stack, plus your own overrides.

Profile package.json: bundles = [dsh-base, dsh-web-app] cordis.patch.yml — your own overrides cordis.yml — empty include anchor @deepseek-ai/dsh-base ~100 rows: llm, session, tools, agent-loop, bash, fs... @deepseek-ai/dsh-web-app server + browser client roster, agent-presets Two shipped templates: web dsh-base + dsh-web-app headless dsh-base + dsh-headless

Figure: a profile lists which bundles to stack; a bundle is a distribution format for config rows plus the code that mounts them.

A bundle is a distribution format: config rows (in its own cordis.patch.yml) plus the plugin code they reference. Two templates ship out of the box — web stacks dsh-base and dsh-web-app for a full browser app over HTTP/WebSocket; headless stacks dsh-base and dsh-headless for a one-shot task runner with no server. Both start from the same base layer. The important property: whatever a bundle inserts stays patchable by every layer above it. A bundle is not a black box you either take whole or fork — it’s a starting stack you’re free to override one row at a time.


Five layers, lowest first

Patches apply to an empty entry list, in a fixed order, and the order is the whole ballgame — it’s what determines whether your --patch flag or your bundle’s default wins when they touch the same row.

1 — bundle rows (each bundle's cordis.patch.yml, in dsh.profile.bundles order) 2 — profile patch (<profile>/cordis.patch.yml) 3 — home patch ($DSH_HOME/cordis.patch.yml, all profiles) 4 — --patch overlays (in argv order) 5 — launcher-generated overlays (agent-preset root, telemetry opt-out)

Figure: last layer wins on a given row. Your bundle's defaults are always the easiest thing to override — never the hardest.

That order is real code, not a diagram I’m inventing — runProfile’s allPatches() helper spells it out directly:

// apps/cli/src/profile-boot.ts:122-129 (the allPatches helper)
return [
  ...composed.bundlePatches,
  ...composed.profile.patches,
  ...composed.homePatches,
  ...composed.overlays,
]

(composeProfile, which builds composed from the bundle stack, profile directory, and $DSH_HOME, is declared at apps/cli/src/profile-boot.ts:142; runProfile itself at :207.) Notice what’s absent from this function: any conditional logic about what kind of change a row represents. That’s deliberate — the layer order decides precedence, and a completely separate function decides what a given patch actually does to the entry list. Those are two different concerns, and dsh keeps them in two different places.

That collapses layers 4 and 5 from the diagram above into the single overlays bucket — so it’s worth showing that the sub-ordering within that bucket is real too, not just asserted. composeProfile builds overlays by pushing in exactly this sequence (apps/cli/src/profile-boot.ts:148-165): command-line --patch files first (via loadOverlayPatches), then the launcher’s agent-preset root injection, then its telemetry opt-out — the same “later push wins” rule that governs the five layers applies one level down, inside the fifth bucket, for the same reason.


What a patch actually does: applyEntryPatches

Two patch shapes exist, and the distinction matters:

insert patch { insert: 'dsh-tool-bash', id: 'tool-bash' } appends a row (into a named group, or the root list) indexed immediately — a LATER layer can patch what an EARLIER layer just inserted id-targeted patch { id: 'system-prompt', config: {...} } REPLACES the row's whole config — not a deep merge unknown id, or name mismatch: warn + skip, never throw (one shared overlay, many surfaces)

Figure: two patch shapes, one function. Config replacement is whole-row, which is why patch authors write complete configs, not partial diffs.

The real function, applyEntryPatches, lives at vendor/include/src/index.ts:58. Three things about its implementation are worth knowing before you ever write a patch file yourself:

  1. It never mutates the input. Line 63 opens with structuredClone(data) — every patch pass works on a fresh copy, which is what makes it safe to re-run the same function on every HMR tick without cached values leaking between reloads.
  2. Config replacement is whole-row, not a deep merge. An id-targeted patch’s config block replaces the target row’s entire config. If you only want to change one field, you still write out the row’s complete config — there’s no partial-patch syntax to reach for instead.
  3. Failure is silent by design, not by accident. If a patch names an id that doesn’t exist, or its name doesn’t match the row it’s targeting, applyEntryPatches logs a warning and skips it — line 117 has the exact message: patch: name mismatch for %C (expected %C, got %C), skipping. It never throws. That’s not an oversight; it’s what lets one shared overlay patch file apply across multiple different surfaces (say, both the web and headless templates) where only some of the targeted rows are actually present on a given surface.

The boot sequence, for real

Here’s what actually happens when you run dsh --profile web "task", condensed from the real boot() function:

composeProfile(name, patchFiles) boot(): new Context(); ctx.baseUrl set ctx.provide('dshHomePath'); ctx.plugin(Loader) prepare(ctx) — launch env + cmdline args mountRootInclude → applyEntryPatches → root.update(rows) per row: import module, interpolate !!js, start fiber await loader.await() → assertEntriesActivated

Figure: patches are fully resolved into a flat row list before a single plugin loads — composition and execution are strictly sequential phases, never interleaved.

The real signature, unchanged from source:

// packages/boot/app-boot/src/index.ts:757-763
export async function boot(
  binName: string,
  absoluteConfigPath: string,
  patches?: PatchOptions[],
  prepare?: (ctx: Context) => Promise<void> | void,
  bareModuleBaseUrl?: string,
): Promise<Context>

Two details here connect straight back to Part 1’s invariants. First, activation is inject-driven, not row-order-driven — the same service-availability rule from Part 1’s Service primitive, applied here to whole rows: a row for dsh-tool-bash can sit above or below the row for dsh-shell in the YAML file; the fiber for dsh-tool-bash simply waits until something provides ctx.shell, however the config happened to be written. Second, assertEntriesActivated fails loud, with a per-row reason, rather than silently leaving a broken plugin half-mounted — and if anything in the tree fails, the entire partial tree gets disposed via the fiber mechanism from Part 1, not just the row that errored. That’s not an inference from the architecture — it’s a literal catch block:

// packages/boot/app-boot/src/index.ts:786-790 (abridged)
try {
  // ...ctx.baseUrl, ctx.provide, ctx.plugin(Loader), mountRootInclude,
  // loader.await(), assertEntriesActivated...
  return ctx
} catch (cause) {
  await ctx.fiber.dispose()  // line 790 — the whole partial tree unwinds
  throw new Error(`${binName}: ${stage}: ${detail}${stack}`, { cause })
}

Composition failure is atomic: boot() either hands back a fully-activated Context, or it disposes everything it managed to mount and throws — there’s no state where a caller gets back a half-built tree.


Live edits: the same two functions, running again

Nothing about this pipeline is boot-only. Edit your profile’s cordis.patch.yml while dsh is running, and the same two functions from above fire again: composeLive() rebuilds the layer-stack array, then applyEntryPatches reconciles it against what’s currently mounted.

// apps/cli/src/profile-boot.ts — composeLive (abridged)
const composeLive = (): PatchOptions[] => structuredClone([
  ...composed.bundlePatches,
  ...loadOptionalPatches(NAME, composed.profile.patchPath) ?? [],
  ...loadOptionalPatches(NAME, homePatchPath()) ?? [],
  ...composed.overlays,
])

watchUserPatches calls composeLive() on every file change, hands the recomposed layer stack to the running include entry, and lets applyEntryPatches do its usual diff — the entry tree then decides, row by row, whether the change is an in-place patch, a restart, or a dispose, rolling back individually on failure. This is the payoff promised at the end of Part 1: because every registration is an effect with a disposer, “recompute the config and reconcile” is not a special HMR code path bolted onto the side of the system. It’s the ordinary boot path, run again, on a smaller diff. There is exactly one algorithm for “what should be mounted,” whether that question gets asked once at startup or fifty times during a long edit session.


Trust, but verify: --dump-config

Because composition is a real, callable function rather than something that happens implicitly, you can ask dsh to show its work:

dsh --profile web --dump-config

renderConfigDump (packages/boot/app-boot/src/index.ts:379) runs the exact same applyEntryPatches call the real boot uses — not a simulated or best-effort approximation — and prints the resolved row list as YAML with a provenance comment on every row, naming which layer it came from. This is a small design choice with an outsized consequence: dumps, --patch flags, and actual boots cannot drift apart, because they’re not three implementations of the same idea — they’re one function called three ways. Anything you see in a dump can be overridden by a patch of your own, because a dump is a patch result.


Two boot shapes, one function underneath

Everything above describes the product CLI path — profiles, bundles, five patch layers, HMR. But boot() itself doesn’t know about any of that; it just takes a config path and an optional patch list. The demo binaries under packages/examples/*-demo (acp-demo, sdk-jsonrpc-demo, agent-spine-demo — published as @deepseek-ai/dsh-acp-demo and siblings) skip profiles and bundles entirely and call boot() directly against a leaf cordis.yml they compose by hand:

// packages/examples/acp-demo/src/bin.ts:29
const ctx = await boot(NAME, resolveConfigPath(values.config ?? './cordis.yml', snapshotMode))

Same function, same merge semantics, same fiber lifecycle — just a much shorter patch stack. That’s a useful thing to know if you ever want to embed dsh in something that isn’t the CLI: the profile machinery is a convenience layered on top of boot(), not a prerequisite for using it.


Coming up in this series

(All 16 parts are live today — no daily drip.)

Part Title What it covers
1 DeepSeek Harness: Inside the Open-Source Claude Code Rival The launch, the comparison, no privileged core, the four Cordis primitives
2 Composing an App From YAML, Not Code (this post) Profiles, bundles, five patch layers, boot, live HMR, --dump-config
3 DeepSeek Harness: Scope — Why a Live Agent Is the Key of Its Own Registration Shadowing, restriction, lineage vs. scope
4 Tool Execution in DeepSeek Harness: Guards and Approval Pre-execute, monotonic guards, post-execute, approval
16 DeepSeek Harness: Agent Presets as Data, Not Code Agent presets, PTC/Code Mode, the plugin ecosystem, compared to Claude Code’s agent types
5 DeepSeek Harness: Capability Seams — Making Bash Swappable for Sandboxed Bash The 3-role pattern end-to-end, across ~85 real seams
6 The Session Log: DeepSeek Harness’s Enforced Invariant The real invariant-checking code, surface projection
7 DeepSeek Harness: Persistence and Compaction — Crash-Safe by Construction JSONL/SQLite, torn-tail repair, the compaction lock bracket
8 DeepSeek Harness: Waterfalls — The One Event Pattern That Runs Everything Five dispatch modes, durable vs. live events, retry
9 DeepSeek Harness: The Agent Loop — Turns, Steps, and a Real Cancellation Bug The phase state machine, the inbox, a dated bug fix
10 DeepSeek Harness: The LLM Layer — One Message Format, Every Surface Message vocabulary, streaming, retry-via-log-replay
11 DeepSeek Harness: Subagents and Workflows — Composing Agents From Agents Provider kinds, continuable children, Ralph rounds
12 DeepSeek Harness: Defense in Depth — Sandboxing and Four Real Incidents bwrap/Landlock/Seatbelt/ACL, real production postmortems
13 DeepSeek Harness: Three Surfaces, One Spine — Web, Typert RPC, and SDK/ACP Typert codegen, the four-quadrant RPC envelope
14 Engineering Rigor: DeepSeek Harness’s Verification Gate The 100% coverage gate, real engineering-culture quotes
15 DeepSeek Harness: What a Second Production Harness Teaches dsh vs. the Agent Harness series, side by side

Part 16 shipped after this series’ initial 15 posts, once Agent Presets and Code Mode landed — it reads best right after Part 4, which is why its row sits there instead of at the end.

Next up: once a plugin is mounted, what does the model actually get to see? Part 6 is about the invariant this post kept gesturing at — model-visible ⟺ logged — and the append-only event log that makes it enforceable.


References

Comments