jjmlovesgit/dsh-plugin-coding-delegate

dsh-plugin-coding-delegate

A preventive governance plugin for DeepSeek Harness: the cloud planner is denied write and shell access, a local worker implements inside a bounded workspace, and promotion is hash-bound against a machine-checked or operator-attested record. Five boundaries, each failing closed.

Uncategorizedcoding-agentdeepseek-harnessdelegation

dsh-plugin-coding-delegate

Repo-native cryptographic attestation, anti-self-approval enforcement, and an execution perimeter for autonomous coding agents.

Every verified delegation leaves a signed record in the repository's own Git notes, bound to the commit it describes: the execution receipt and the human approval are two signatures made by two different keys, and the audit that checks both runs offline. Control mapping for SOC 2 CC8.1, ISO/IEC 42001 and the EU AI Act: docs/enterprise-governance.md.

Who this is for — the roles that are blocked today, and the obligation each one is trying to discharge: Target audience and market positioning.

A DeepSeek Harness (DSH) plugin that separates who specifies work from who implements it, and refuses to let the first party write the second party's code. It is a technical control, not a policy: enforcement happens in a tool-call hook before a write lands, so it does not depend on the model choosing to comply.

The metered model has no write tool and no shell. A fully prompt-injected architect can plan anything and still cannot change a byte of your host, because the permission is never granted.

The two guarantees this control exists to make

Two properties, both of the kind a pipeline usually asserts rather than enforces. Everything else in this README is in service of them.

1. Anti-self-approval: two principals, two keys, checked

A record under attestation spec 1.2 carries a worker receipt and a reviewer approval, with three signatures sealed in order: the worker signature over the payload, the reviewer signature over the payload *including* the frozen worker block, and the outer envelope seal last, because adding the reviewer block would invalidate an earlier seal.

  • The two keys must differ. Equal fingerprints are refused with self-approval refused. Red-team probe

10 signs a reviewer block with the worker's own key; without that check the record verifies perfectly.

  • The record must name the key that verifies it. A genuine signature from a key the record names but the

resolved trust root does not know is refused (probe 9).

  • Approval is a separate, human, two-phase act. --prepare runs the contract and

stages an untracked draft that cannot be committed; --approve --reviewer-identity "" re-verifies the draft against the anchored tree, adds the reviewer block, attaches once, reads the note back byte-identical, and only then removes the draft. It does not re-run the contract: the record must carry what phase one saw, not a second opinion collected later.

  • No plugin path can author a human claim. delegate_worker settles execution status only — the four

registry outcomes UNIT_PASSED, UNIT_FAILED, UNIT_UNVERIFIED and UNIT_FLAKY, which are verdicts and not signing keys. The three tool parameters that once let a caller name a reviewing human are gone, and a compile-time assertion keeps them gone.

Residual boundary. With the in-repo reviewer key in use, a committer who can edit both keys/delegation-reviewer.pub and the operator pin could substitute both roots. Pin the reviewer root out of band with DSH_REVIEWER_PUBKEY or DSH_REVIEWER_FINGERPRINT, and a second signature becomes evidence about a principal the record's author cannot reach. Details: dual attestation.

2. Repo-native provenance: the record cannot outrun its commit

Attestations are Ed25519-signed notes in refs/notes/delegations — objects in the same database as the code, so they clone and verify offline. Every claim in a record is re-derived at audit time from the anchored commit's tree, never from the working copy: a note that names one commit and lives on another, whose anchor is not an ancestor of main, or whose artifact or contract digest does not match the blob at that commit is refused. The full check list is under Cryptographic Provenance & Attestation Model, and is specified in full in docs/enterprise-governance.md §3.

Residual boundary. A Git ref is not write-once storage: notes can be edited, re-attached or removed, and the ref can be force-pushed. The audit catches every mutation that leaves a record behind — the red-team harness performs ten of them — and it refuses an empty ledger, but it cannot detect one note removed while others remain. That needs an external sink, and the auditor says so in its own closing line.

Auditing it yourself

Run these from a clone. The published npm package is the plugin (plugin/dist); the gate and signing tooling deliberately live in the checkout rather than in the tarball, so there is no standalone CLI to install and no extra artefact to trust:

npm run gate                                 # all seven commands (eight tools), offline
node scripts/verify-delegation-ledger.cjs    # the ledger audit alone
node scripts/redteam-probe.cjs               # ten attack classes; restores the ledger afterwards

A green run prints AUDIT PASSED, then the one limit that matters: *"this proves the records present are sound. It cannot detect a record that was deleted."* To check a published release against its tag — this one needs the network — run powershell -File scripts/verify-release-tarball.ps1 -Version -Tag v, substituting the release you are checking; it compares the tarball and the tag in both directions by Git blob hash.

The same gate runs on every push in .github/workflows/ci.yml, with fetch-depth: 0 and an explicit refs/notes/delegations fetch, because a depth-1 checkout would audit an empty ledger and report success. The v0.5.7 release commit was gated by run 210, which concluded success. A CI workflow holds no signing key, so the signer lifecycle step reports a named # SKIP there rather than passing silently.

Getting Started

dsh-plugin-coding-delegate puts an enforcement boundary between an architect model and an untrusted worker: it intercepts the handoff, keeps the worker from seeing the tests that judge it, sandboxes the verification run, and can sign a cryptographic note per unit.

Prerequisites

  • Node.js 22 or later — the verification sandbox uses --permission, which older releases do not have.
  • A DSH profile you actually boot (web, tauri, headless, or your own), with an architect model

and a reachable worker endpoint (local LM Studio at 127.0.0.1:1234, or a cloud worker you choose).

  • Optional: an Ed25519 private key at attestationPrivateKeyPath if you want the unit signed. Without one

the verdict is still produced and the attestation is withheld, not faked.

Step 1 — install into a profile, not into the repository

dsh plugin --profile web add dsh-plugin-coding-delegate

DSH does not read a plugins array from a config file. A profile is a directory at $DSH_HOME/profiles/ with its own package.json and cordis.patch.yml, and dsh plugin forwards to pnpm inside it. npm install in your project does not mount the tool.

Step 2 — confirm registration

The plugin ships a patch that inserts one entry; DSH applies it when the bundle is loaded. On startup:

[LOCAL_ROUTER_INIT] Tool 'delegate_worker' registered successfully on ctx.tools.

If you do not see that line, the active profile is not loading the plugin — check which profile the process booted, not the project you are standing in.

Step 3 — write a verification contract

A contract is the oracle for one unit. It must be a standalone script:

// tests/contract-add.cjs — .cjs, because the default package type is CommonJS
const assert = require('node:assert');
const { add } = require('../src/add.js');

try {
  assert.strictEqual(add(2, 3), 5, '2 + 3 must equal 5');
  assert.strictEqual(add(-1, 1), 0, '-1 + 1 must equal 0');
  console.log('CONTRACT_VERIFIED');
  process.exit(0);
} catch (error) {
  console.error('CONTRACT_FAILED: ' + error.message);
  process.exit(1);
}

Rules the sandbox enforces, each with the refusal you will see:

rule · refusal direct node only · REFUSED_UNSANDBOXED_RUNNER for node --test no shell chaining or redirection · REFUSED_SHELL_OPERATORS node runtime only · REFUSED_UNSANDBOXED_RUNTIME for python, sh, … the host binary only · REFUSED_SHADOW_RUNTIME for ./node or a temp path exit 0 on pass, non-zero on fail · the verdict is VERIFICATION_FAILED

Step 4 — dispatch, declaring the contract

Use delegate_worker to implement src/add.js, with contractFiles ["tests/contract-add.cjs"]
and the verification command node tests/contract-add.cjs.

contractFiles is the declaration the gate trusts. Passing that same path in contextFiles is refused with REFUSED_CONTRACT_AS_CONTEXT — the worker may not be shown the tests it is graded by.

What the boundary does, in order

architect dispatches
  ├─ contract/context segregation, before any file is read     REFUSED_CONTRACT_AS_CONTEXT
  ├─ worker emits fenced blocks
  │    └─ test paths refused before the write                  isTestPath
  ├─ verification runs: node, --permission, shell:false,
  │    prototypes frozen, sockets severed, env scrubbed
  ├─ verdict from the lattice: boundary > gate > flaky >
  │    coherence > unverified > success
  └─ attestation when a key is configured: Ed25519 note on
       refs/notes/delegations, digest-bound to the commit

The staged-bytes check (git show :) belongs to the promotion gate that runs at commit time, not to the delegation itself — a distinction that matters when you are reading which control caught a change.

Troubleshooting

  • `delegate_worker` not registered — the booted profile does not load the plugin. Registration is per

profile, not per project.

  • `spawn EPERM` on Windows — a freshly created directory can inherit an ACL that denies subprocess

creation; grant your account full control over the workspace.

  • `REFUSED_UNSANDBOXED_RUNNER` — your contract command still uses --test. Rewrite it as a script

that exits 0/1.

  • `REFUSED_CONTRACT_AS_CONTEXT` — remove the contract from contextFiles; declare the interface the

worker needs in a separate spec file.

For profile options, custom key paths, environment flags and enterprise wiring, see Install below; this section is the short path to a first verified delegation.

How it works: a contract out, a verdict back

Coding needs two different jobs done, and they want two different contexts. The plugin gives each one its own window and lets exactly two things cross between them.

The architect is the metered thinking model. It holds the design conversation, decides how the work decomposes, and writes a contract for each unit: what to build, where the worker may write, and the command that decides whether the unit passed. It does not write source. Reading it back is governed rather than forbidden: with the default sourceReadEgress: 'source' a read returns the file, rule 3 makes a read of a freshly-delegated one ask first, and sourceReadEgress: 'declarations' serves a type skeleton instead of the body.

The worker is whatever your GPU is already running. It receives one unit and everything that unit needs — nothing else — writes the code, and is discarded. A fresh context per unit costs nothing.

Direction · What crosses · What never crosses architect → worker · the contract: instruction, permitted paths, the verification command · the design conversation, the other units worker → architect · the verdict: pass/fail counts, a redacted failure structure, the names of files written · the code

The verification command is sandboxed, and that is a breaking change in 0.5.11. It runs as a node process under --permission, with filesystem access scoped to the workspace and the OS temp directory, core prototypes frozen, sockets and fetch severed, and the environment scrubbed. What that means for a contract command:

  • Direct scripts only. node . The runner form node --test is refused

(REFUSED_UNSANDBOXED_RUNNER) because the runner forks a child process per test file, the permission model blocks that fork, and granting it would reopen the execution vector the sandbox closes.

  • Exit codes decide. A contract asserts its invariants and exits 0 on pass, non-zero on failure.
  • Node only. python, sh, bash, pwsh and other runtimes are refused

(REFUSED_UNSANDBOXED_RUNTIME): the sandbox reaches node processes only, so anything else would run bare while the record claimed isolation.

  • No shadow binaries. ./node, temp/node.exe and other path-shaped binaries are refused

(REFUSED_SHADOW_RUNTIME); the spawn uses the host binary the plugin is already running under.

  • One command. Shell operators and redirection are refused, and the command is tokenised and spawned

with shell: false, so there is no intermediate shell to expand or chain anything.

Its raw output stays on the local machine; what travels back is the failure *structure*, with source stripped out.

Status: 0.5.11, published to npm and gated in CI. The mechanisms work and are tested, and their
limits are documented rather than glossed, including several that cannot be closed from inside the
process — see What this control does not cover and the consolidated