---
title: "Where a harness begins"
description: "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."
pubDate: 2026-09-23
author: tusgino
tags: [ai-engineering, architecture, agents, infrastructure]
series:
  id: tu-llm-den-harness
  title: "From LLM to Harness"
  order: 5
cover: /images/covers/harness-bat-dau-tu-cho-nao.svg
coverAlt: "A harness plugin tree with the four core parts highlighted among the other plugins"
lang: en
faq:
  - q: "How does a loop differ from a harness?"
    a: "A loop only coordinates: call the model, run tools, repeat. A harness is everything else — where history is durably recorded, tools are registered, the context budget is held, and permission is checked."
  - q: "Why split it into plugins instead of one block?"
    a: "Because you need to replace one part without touching the other three. A solid block forces you to edit the loop every time you change a model, a storage backend, or a permission policy."
  - q: "Is there a privileged core you have to patch?"
    a: "In the harness I read, no. Everything including the model adapter, the session log and the loop itself is a plugin, so each is replaceable from configuration."
  - q: "How large is the surface the model sees?"
    a: "Far smaller than the machinery behind it. In the harness I read: 338 workspace packages, but only 70 tool names the model can see."
---

<TLDR>A 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 — and all four must be removable.</TLDR>

## 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.

```yaml title="cordis.patch.yml — change one row, touch no source"
# 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 verbatim
```

Both 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:

<Quote cite="dsh, docs/architecture.md">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.</Quote>

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.

```diagram
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.

<Callout type="warn" title="Do not confuse configurable with pluggable">
A YAML file full of parameters is still a solid block if you cannot mount a new component into it. The test is not "can I change a number" but "can I mount a package that did not exist before, without editing source".
</Callout>

## Do this now

<Steps>
<Step title="Name the four parts before writing the loop">Settle the log, the registry, the budget and the permission layer as four separate interfaces. If you cannot name them, the loop will swallow them.</Step>
<Step title="Give every part a name in configuration">A stable `id` per component, so a patch can target it. With no name there is no seam to replace.</Step>
<Step title="Measure the surface the model sees, and keep it small">Count the tools and the fields the model can see. That number should grow far more slowly than the packages behind it.</Step>
<Step title="Unplug a plugin and check it leaves cleanly">Mount it, then remove it. If orphaned tools or dangling listeners remain, registration was not a reversible effect.</Step>
</Steps>

## 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.

<Quote cite="tusgino">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.</Quote>

<KeyTakeaways>
- A loop only coordinates; the harness is the log, the registry, the context budget and the permission layer.
- Give each part a stable `id` in 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.
</KeyTakeaways>
