跪拜 Guibai
← Back to the summary

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.

3.png

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.

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:

  1. prepareProfile(name) loads the profile and reads its bundles list
  2. homePatches loads the home-level patch ($DSH_HOME/cordis.patch.yml)
  3. overlays loads temporary overlays specified by the command-line --patch
  4. bundlePatches extracts patches from each bundle layer of the profile
  5. composeEntries composes 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:

  1. 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.
  2. (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).
  3. boot returns 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:

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:

  1. Copy a template profile directory to $DSH_HOME/profiles/my-profile/
  2. Edit package.json, set dsh.profile.bundles to list the bundles you need
  3. Create cordis.patch.yml, replace or insert entries by id
  4. Use dsh plugin --profile my-profile add <pkg> to install out-of-tree plugins
  5. Run dsh --profile my-profile
  6. Use dsh --profile my-profile --dump-config to 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

Comments

Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.

AI智能灌水助手

How does it compare to tui? I feel tui is clean and fast.

怕浪猫

Is that the one from opencode?