How DeepSeek Harness Assembles Its Plugin Tree: Bundles, Profiles, and Whole-Row Patches
DeepSeek Harness Source Code in Action Chapter 3: Profile / Bundle / Patch — dsh's Assembly System
Series: DeepSeek Harness Source Code in Action Original Repository: https://github.com/deepseek-ai/deepseek-harness
Change one line of configuration, and the entire product's form changes.
In the last chapter, we discussed the Cordis plugin engine and learned that all of dsh's features are plugins. But you might have been wondering: how are these plugins assembled together? Who decides which plugins to load and which ones override others? This chapter dissects dsh's assembly system—three concepts, one startup chain, and one set of replacement semantics.
I'm Pa Lang Mao. In this chapter, we'll dive into the source code of apps/cli/src/ and packages/boot/ to see what happens from a single command to a plugin tree.
3.1 Distinguishing Three Concepts: Bundle / Profile / Patch
dsh's assembly system has three core concepts. First, define each in one sentence, then elaborate.
- Bundle: A distribution format for Cordis configuration items and mount code. It is a "package" of a group of plugins, and the inserted content can always be overridden by upper-layer patches. Its patch file path is declared in its own
package.json'sdsh.bundlefield. - Profile: A named assembly stored in the Harness home directory. It lists the bundles it stacks (
dsh.profilefield), stores its installed out-of-tree plugins, and saves the user's owncordis.patch.yml.webandheadlessare delivered as templates with the distribution. - Patch: Locates an entry by id and replaces its entire config, or inserts a new entry. It is not a deep merge; it is a whole-row replacement.
The relationship between the three can be understood like this:
Profile = An assembly manifest
│
├── bundles list (in order)
│ ├── dsh-base (Base layer: model adapters, tools, persistence, sandbox, approval policies, settings, credentials, telemetry)
│ ├── dsh-web-app (Web application layer: host, API gateway, HMR)
│ └── dsh-headless (Serverless single-runner)
│
├── cordis.patch.yml (User-layer overrides)
│ └── Replace or insert entries by id
│
└── Out-of-tree plugins (installed via dsh plugin)
Patch layers (stacked bottom-up):
Empty config []
→ Bundle patches (in the order listed by the profile)
→ Profile's own cordis.patch.yml
→ Home-level $DSH_HOME/cordis.patch.yml
→ --patch command-line temporary overlay
→ Final configuration tree
Comparison table:
| Concept | What it is | Who creates it | Can be overridden by |
|---|---|---|---|
| Bundle | A packaged distribution of a group of plugins | dsh official or third-party | Profile patch / Home patch / CLI overlay |
| Profile | An assembly manifest | User (or templates delivered with the distribution) | Upper-layer patches |
| Patch | Whole-row replacement by id | User-written | Higher-level patches |
The three concepts are declared in their respective package.json files via the dsh field: dsh.profile lists a profile's bundles, and dsh.bundle points to a bundle's patch file.
Golden Quote: Bundles are bricks, Profiles are blueprints, and Patches are the modifications you draw on the blueprint with a pen.
3.2 Startup Chain Breakdown: From bin.ts to Plugin Tree
Now let's trace a complete startup process. Take dsh --profile web as an example.
Step 1: Argument Parsing
The entry point is apps/cli/src/bin.ts, whose three-mode dispatch we saw in the last chapter. Here we focus on the profile mode:
// apps/cli/src/bin.ts (excerpt)
const invocation = parseDshArgs(process.argv.slice(2), readVersion())
switch (invocation.mode) {
case 'profile': {
const { runProfile } = await import('./profile-boot.ts')
await runProfile({
environment: loadLayeredEnv('dsh'),
profile: invocation.profile,
patchFiles: invocation.patches,
args: invocation.args,
})
break
}
// ...
}
parseDshArgs returns a discriminated union; the mode field determines which branch to take. In profile mode, it passes the environment snapshot, profile name, patch overlay paths, and internal arguments to runProfile.
Step 2: Composing Patch Layers
runProfile is defined in apps/cli/src/profile-boot.ts. It first calls composeProfile to compose the patch layers:
// apps/cli/src/profile-boot.ts (excerpt)
function composeProfile(name: string, patchFiles: readonly string[]): ComposedProfile {
const profile = prepareProfile(name)
const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? []
const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
const bundlePatches = profile.layers.flatMap(layer => layer.patches)
const rows = new Map<string, EntryOptions>()
for (const row of composeEntries([bundlePatches, profile.patches, homePatches, overlays])) {
if (typeof row.id === 'string') rows.set(row.id, row)
}
// ... agent-presets root path injection + telemetry switch
return { profile, bundlePatches, homePatches, overlays: composedOverlays, rows }
}
The logic of this code is:
prepareProfile(name)loads the profile and reads its bundles listhomePatchesloads the home-level patch ($DSH_HOME/cordis.patch.yml)overlaysloads temporary overlays specified by the command-line--patchbundlePatchesextracts patches from each bundle layer of the profilecomposeEntriescomposes the four layers of patches into a final entry list and builds an id index
Note the argument order of composeEntries: [bundlePatches, profile.patches, homePatches, overlays]. This is the stacking order—later ones override earlier ones.
Step 3: Rewriting the Root Configuration
There is an easily overlooked detail in prepareProfile:
// apps/cli/src/profile-boot.ts (excerpt)
export function prepareProfile(name: string, userLayer = true): Profile {
healProfilesModuleFallback(INSTALL_ANCHOR)
const profile = loadProfile(NAME, name, INSTALL_ANCHOR, undefined, { userLayer })
writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG)
return profile
}
PROFILE_ROOT_CONFIG is an empty list [], rewritten on every startup. Why? Because Cordis's Loader has a write-back mechanism—when a plugin self-uninstalls, it persists the current configuration tree. If not reset, the rows composed last time would be solidified into the root file, and the insertion operations of each bundle would be duplicated on the next startup.
The content of PROFILE_ROOT_CONFIG is just two comment lines plus an empty array:
# dsh profile root — an empty entry list. The tree is composed as patches:
# each bundle in package.json's dsh.profile.bundles, then cordis.patch.yml, then any
# --patch overlays. Edit cordis.patch.yml, not this file.
[]
The comments explicitly state: the entire configuration tree is composed by patch stacking; do not edit this file directly.
Golden Quote: Starting from an empty list every time is like wiping the whiteboard clean every morning. This isn't OCD; it's engineering discipline to prevent state accumulation.
Step 4: Boot Mounting
After composing the patch layers, the boot function is called to mount the configuration tree:
// apps/cli/src/profile-boot.ts (excerpt)
const ctx = await boot(NAME, rootConfig, structuredClone(allPatches(composed)), (hostCtx) => {
app.current = hostCtx
hostCtx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, options.environment)
provideCmdline(hostCtx, {
args: options.args,
exit: code => void shutdown.shutdown(code),
})
})
Note three details:
structuredClone(allPatches(composed))— deep clones the patch list. The comment explains why: Include inserts rows by reference into the mount tree, and subsequent id-targeted patches modify these objects in place. Without cloning, user-layer overrides would permeate into the bundle's in-memory objects, making it impossible to restore when overrides are revoked.(hostCtx) => { ... }is the host callback, executed before the configuration tree entries are mounted, used to provide global services (environment snapshot, command-line arguments, exit interface).bootreturns the root Context, which is the root node of the Cordis plugin tree.
Step 5: HMR Hot Reloading
After boot completes, hot-reload watchers for patch files are also set up:
// apps/cli/src/profile-boot.ts (excerpt)
await watchUserPatches(ctx, {
binName: NAME,
filename: composed.profile.patchPath,
compose: composeLive,
})
await watchUserPatches(ctx, {
binName: NAME,
filename: homePatchPath(),
compose: composeLive,
})
The two watchers monitor the profile-level and home-level cordis.patch.yml respectively. When the file changes, composeLive is called to recompose the patch layers (excluding bundle layers, as they cannot be hot-reloaded), and then the Cordis Loader is notified to remount the affected entries.
The implementation of composeLive has an important cloning logic:
const composeLive = (): PatchOptions[] => structuredClone([
...composed.bundlePatches,
...loadOptionalPatches(NAME, composed.profile.patchPath) ?? [],
...loadOptionalPatches(NAME, homePatchPath()) ?? [],
...composed.overlays,
])
Each recomposition does a fresh clone, for the same reason as Step 4—to prevent reference aliasing of insert rows from causing state permeation.
Startup Chain Summary
1. dsh --profile web
2. parseDshArgs → mode='profile', profile='web'
3. runProfile → composeProfile
3a. prepareProfile → loadProfile + rewrite empty root config
3b. Load homePatches + overlays + bundlePatches
3c. composeEntries composes the final entry list
4. boot(NAME, rootConfig, patches, hostCallback)
4a. Cordis Loader reads the configuration tree
4b. Instantiates plugins by entry
4c. Each plugin registers ctx services
4d. Returns root Context
5. watchUserPatches × 2 (profile-level + home-level HMR)
6. web-app bundle's plugin starts Web Server → http://127.0.0.1:3080
3.3 Patch Replacement Semantics: Whole-Row Replacement by id, Not Deep Merge
This is the easiest pitfall in dsh's assembly system.
Patch's replacement semantics are "locate an entry by id and replace its entire config." Note: entire config, not deep merge, not partial override. If you patch an entry, the config you provide is the final result; the original config disappears completely.
Let's look at a comparison. Suppose a bundle layer defines an entry:
# Bundle layer
- id: tool-bash
config:
timeoutMs: 30000
retryPolicy:
maxRetries: 3
backoffMs: 1000
env:
DSH_SHELL: bash
Now you want to change the timeout and write a patch:
Deep merge semantics (dsh does NOT work this way):
# Hypothetical deep merge result (incorrect)
- id: tool-bash
config:
timeoutMs: 60000 # ← overridden
retryPolicy: # ← retained
maxRetries: 3
backoffMs: 1000
env: # ← retained
DSH_SHELL: bash
dsh's actual semantics (whole-row replacement):
# Actual result: config is replaced entirely
- id: tool-bash
config:
timeoutMs: 60000 # ← only this
# ← retryPolicy is gone
# ← env is gone
If you want to change one field but keep others, you need to write all fields in the patch. This is a design decision, not a bug.
Why this design? Because deep merge semantics are unintuitive—when multiple patch layers stack, deep merge behavior becomes hard to predict. Whole-row replacement, while more verbose to write, makes the result completely predictable: what you see is the final value, with no need to infer how it was merged.
Golden Quote: Deep merge is a sticky note; whole-row replacement is a contract. The former is easy to write but hard to read; the latter is hard to write but easy to read. dsh chose readability.
Command to view the actual configuration tree:
dsh --profile web --dump-config
This prints the final composed configuration tree, letting you confirm which entries are loaded and which have been replaced by patches. --dump-default-config prints only the bundle layer, excluding user layers and overlays.
3.4 CLI Mode Overview
dsh's CLI has three modes (in bin.ts's switch) and two syntactic sugars (in args.ts). Complete command quick reference:
| Command | Mode | Description |
|---|---|---|
dsh --profile web |
profile | Start the web profile |
dsh web |
profile | Alias for --profile web |
dsh --profile headless "task" |
profile | Single task, print result and exit |
dsh --profile tui --patch ./extra.yml |
profile | Custom profile + extra overlay |
dsh --profile tui --resume <session> |
profile | Resume a specified session (--resume is an internal parameter) |
dsh --profile web --dump-config |
dump-config | Print the full config tree for the web profile |
dsh --profile web --dump-default-config |
dump-config | Print the bundle layer for the web profile (excluding user layer) |
dsh plugin --profile tui add <pkg> |
plugin | Install a plugin for the tui profile |
dsh plugin --profile tui remove <pkg> |
plugin | Remove a plugin |
Applicable scenarios for each mode:
profile mode: Daily use. Starts a profile's service (Web Server, headless runner, etc.), passing arguments to the profile's internal application plugins.
dump-config mode: Debugging and documentation. Does not start a service, only prints the configuration tree. When your profile's behavior is wrong, use this first to see if the actual configuration is what you expect. Note that dump-config does not accept application arguments (--dump-config and internal arguments are mutually exclusive) because the configuration tree is determined before application arguments are injected.
plugin mode: Plugin management. Forwards arguments as-is to pnpm, executing in the profile directory. Equivalent to cd $DSH_HOME/profiles/<name> && pnpm <args>.
Definition of the web alias:
// apps/cli/src/args.ts (excerpt)
const web = program.command('web')
.description('boot the web profile (alias of --profile web)')
.helpOption(false)
.allowUnknownOption()
.passThroughOptions()
.enablePositionalOptions()
.argument('[args...]', 'arguments for the web app')
.option('--patch <path>', 'extra patch-list overlay', collect)
.option('--dump-config', 'print the composed web-profile tree and exit')
.action((args: string[], options: BootOptions) => {
rejectParentOptions('web')
resolved = resolveBoot(web, 'web', options, args)
})
Note rejectParentOptions('web')—the web subcommand does not accept the parent command's --profile, --patch, --dump-config. This prevents meaningless combinations like dsh --profile tui web.
3.5 Default Assembly: dsh-base, dsh-web-app, dsh-headless
dsh delivers three bundles with the distribution, forming two default profiles.
dsh-base: The First Base Layer
dsh-base is the first layer of every profile. It contains:
| Content | Description |
|---|---|
| Model Adapters | LLM adapters registered to ctx.llm |
| Tools | Core tools registered to ctx.tools |
| Persistence | Session log storage (JSONL / SQLite) |
| Sandbox & Approval | Process sandbox, filesystem constraints, operation approval |
| Settings | ctx.settings service |
| Credentials | ctx.credentials service |
| Telemetry | Optional OpenTelemetry telemetry |
dsh-base also handles platform gating. On Windows (win32), only the PowerShell (pwsh) stack is installed; bash-sandbox and tool-bash are disabled. On POSIX (Linux/macOS), only the bash stack is installed. This is controlled by the disabled field in the bundle layer based on platform conditions.
dsh-web-app: Browser Application
dsh-web-app adds on top of dsh-base:
- Web host (host-side service)
- API gateway
- Browser plugin table (client-side module registration)
- HMR (Hot Module Replacement) support
dsh web is the combination of dsh-base + dsh-web-app.
dsh-headless: One-Shot Runner
dsh-headless adds a headless runner on top of dsh-base—it receives a task string, prints the result after execution, and exits. Completely serverless.
dsh --profile headless "run the tests" is suitable for CI (Continuous Integration) scenarios: no UI needed, no interaction, run and leave.
Custom Profile Steps
If you want to create your own profile:
- Copy a template profile directory to
$DSH_HOME/profiles/my-profile/ - Edit
package.json, setdsh.profile.bundlesto list the bundles you need - Create
cordis.patch.yml, replace or insert entries by id - Use
dsh plugin --profile my-profile add <pkg>to install out-of-tree plugins - Run
dsh --profile my-profile - Use
dsh --profile my-profile --dump-configto inspect the configuration tree
When using a new profile name for the first time, dsh automatically initializes the template.
Golden Quote: A Profile is not a configuration file; it's an assembly contract. You sign (declare bundles), and dsh delivers according to the contract (mounts the plugin tree).
Chapter Summary
| Key Point | Description |
|---|---|
| Bundle | A packaged distribution of a group of plugins; inserted content can be overridden by upper layers |
| Profile | A named assembly manifest listing bundles + storing out-of-tree plugins + saving cordis.patch.yml |
| Patch | Replaces an entry's entire config by id; not a deep merge |
| Stacking Order | Empty [] → bundle patches → profile patch → home patch → --patch overlay |
| Startup Chain | parseDshArgs → runProfile → composeProfile → boot → HMR watcher |
| Root Config Rewrite | Rewritten to empty [] on every startup to prevent config bloat from Loader write-back |
| structuredClone | Deep copy of patch objects to prevent reference aliasing of insert rows from causing state permeation |
| CLI Three Modes | profile (start) / dump-config (view config) / plugin (manage plugins) |
| Three Bundles | dsh-base (foundation) / dsh-web-app (Web application) / dsh-headless (one-shot runner) |
| Platform Gating | win32 installs only pwsh stack; POSIX installs only bash stack |
Next Chapter Preview
The assembly system is clear, and the plugin tree is mounted. In the next chapter, we'll dive into dsh's data core—Session logs. You'll see why dsh's chat history is actually just a projection of the log, and why this design decision has far-reaching implications.
I'm Pa Lang Mao. This chapter on the assembly system is quite dense; I suggest bookmarking it for repeated review. How is configuration assembled in your agent project? Let's chat in the comments.
Series Progress: 3/8 | Next Chapter: Session Logs—The Single Source of Truth
Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.
How does it compare to tui? I feel tui is clean and fast.
Is that the one from opencode?