Where a harness begins
The loop is only a coordinator. Four parts turn it into a harness: a durable log, a tool registry, a context budget, and a permission layer, all four pluggable.
A loop is not a harness
The last post ended with a loop 101 lines long, where the extra lines mark the places it used to break. Those 101 lines are still not a harness.
A loop does exactly one job: coordination. It calls the model, runs tools, repeats. It does not know where history is written, nor where a tool came from. It does not know who holds the context budget, or who authorises a call to run.
Those four questions are four parts. In a real harness, all four are plugins — which means all four can be replaced without editing the loop.
Four parts, and where they live
In the harness I read, every profile boots by loading 94 plugin entries (called rows) from a base bundle. The four core parts sit inside that list, each entry with its own id that can be patched from config:
| Part | Entry in config | Job |
|---|---|---|
| Durable log | session, session-persistence-jsonl | Records every event the model saw |
| Tool registry | tool-* (17 entries) | Declares which tools exist |
| Context budget | compaction-basic | Keeps requests inside the window |
| Permission layer | approval, permission, sandbox | Decides what may run |
Four names, four separate config entries, none hardcoded inside the loop body.
That is the key idea. The loop simply calls ctx.tools.execute() and does not need to know whether that tool is a local shell command or a remote MCP server. It appends to the session and does not care whether that session lives in memory or in a JSONL file on disk.
Replacing a part without editing code
This is where the idea becomes testable rather than just sounding good.
Every config entry has an id. To change how a plugin behaves, you write a patch targeting its id. The patch replaces the plugin’s entire configuration rather than blending into it — so the outcome is always predictable and never depends on file load order.
# The row in the bundle carries a name and no config:# - id: compaction-basic# name: '@deepseek-ai/dsh-compaction-basic'
- id: compaction-basic config: thresholdRatio: 0.8 # the package default retainRatio: 0.16 # recent conversation kept verbatimBoth keys are real parameters of the package, and the values above are the bundle author’s defaults. What you write into the patch is yours.
And the architecture document states the consequence outright:
There is no privileged core to patch: you extend dsh by mounting a plugin beside the others, and registrations are effects that unwind when their plugin unloads.— dsh, docs/architecture.md
Two clauses, and the second matters as much as the first. Whatever a plugin registers is unwound when that plugin is removed. No orphaned tools, no listeners left hanging after their plugin dies. That is the precondition for swapping parts at runtime rather than only at build time.
338 packages, 70 tool names
Now the number that clarifies the whole post.
The harness I read holds 338 packages in its workspace, spread across 55 package groups, with 14,004 files tracked. But the surface the model sees is just 70 tool names in the catalog.
That gap is not waste. It is the definition of a harness.
A good harness keeps the surface facing the model small and stable, while the machinery behind it is large and changeable. The model sees one stable tool list. Behind it, each of those tools may come from the base bundle, from an out-of-tree plugin, or from an MCP server. The model cannot tell the difference, and does not need to.
If that surface grows in step with the machinery, you do not have a harness. You have a pile of packages exposed to the outside.
View diagram data
type: architecture
title: The plugin tree of a harness
desc: From 338 workspace packages, one profile plugs in 94 plugin rows; the four core parts sit among them and are all replaceable from configuration
nodes:
- id: profile
label: Profile (web, headless, sdk)
kind: external
- id: bundle
label: Base bundle, 94 rows
kind: input
- id: log
label: Durable log
kind: store
focal: true
- id: registry
label: Tool registry
focal: true
- id: budget
label: Context budget
- id: perm
label: Permission layer
kind: security
- id: surface
label: 70 tool names the model sees
kind: external
edges:
- [profile, bundle]
- [bundle, log]
- [bundle, registry]
- [bundle, budget]
- [bundle, perm]
- [registry, surface]
- [perm, surface]Four profiles, one base bundle
One detail worth noticing: the profiles are not four copies. They layer over the same base bundle — dsh-base, 94 rows — and each adds its own part.
| Profile | Own rows | What it adds |
|---|---|---|
| web | 112 | The browser application |
| sdk-minimal | 32 | A minimal SDK tree, deliberately not using the base bundle |
| sdk | 7 | A JSON-RPC server |
| headless | 5 | A one-shot runner with no server |
| acp | 5 | An automation-only server |
Look at headless: five rows. A harness that runs once and exits needs five lines of configuration on top of the base bundle. There is no if (headless) branch anywhere in the loop.
That is the test I apply to any harness design: how many lines in the core does a new run mode cost? If the answer is more than zero, the parts are not genuinely removable.
Do this now
- Name the four parts before writing the loopSettle the log, the registry, the budget and the permission layer as four separate interfaces. If you cannot name them, the loop will swallow them.
- Give every part a name in configurationA stable
idper component, so a patch can target it. With no name there is no seam to replace. - Measure the surface the model sees, and keep it smallCount the tools and the fields the model can see. That number should grow far more slowly than the packages behind it.
- Unplug a plugin and check it leaves cleanlyMount it, then remove it. If orphaned tools or dangling listeners remain, registration was not a reversible effect.
The rule
The loop is the easiest part of a harness, and the least decisive. What decides is whether the four parts around it come out cleanly.
If you cannot swap the model adapter without editing the loop, you do not have a harness. You have a loop with a status light.— tusgino
- A loop only coordinates; the harness is the log, the registry, the context budget and the permission layer.
- Give each part a stable
idin configuration, so behaviour changes by patch rather than by source edit. - Keep the model-facing surface small and stable: 338 packages behind it, 70 tool names in front.
- Registration must be a reversible effect, otherwise removing a plugin leaves debris.
- The final test: how many lines in the core does a new run mode cost? The right answer is zero.