dsh-plugin-readproof
已验证dsh-plugin-readproof · v0.3.2 · Apache-2.0
DeepSeek Harness plugin for Readproof: stable readproof:// identity, freshness policy, snapshots, per-run manifests, diff, byte-exact replay, and evidence bundles as model-callable tools.
安装
dsh plugin add dsh-plugin-readproof 用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。
源码
标签
作者
说明文档
dsh-plugin-readproof
Readproof as a DeepSeek Harness
plugin: the documents your agent reads get a stable readproof:// identity, a
freshness policy, content-addressed snapshots, per-run manifests, diff,
byte-exact replay, and portable evidence bundles — all as model-callable
tools.
The difference that buys: instead of "the model read some file", you get this run read exactly these bytes, from this source revision, at this time — a manifest id you can cite, diff against another run, replay byte-for-byte after the sources changed, and export as a tamper-evident in-toto bundle.
The tool names, descriptions, and result shapes mirror Readproof's MCP server
(docs/mcp.md, internal/mcp)
so the two surfaces cannot drift.
Tools
| Tool | What it does |
|---|---|
readproof_resources_list |
List every registered document with its source and policy. The discovery call. |
readproof_resolve |
Read one document; returns the bytes plus snapshot id, content hash, and source revision. |
readproof_history |
List a resource's snapshots, newest first, with the tags pointing at each. |
readproof_run_start |
Open a run — the container that records what this piece of work reads. |
readproof_run_mount |
Read a document and record it in the open run, at the next position. |
readproof_run_commit |
Freeze the run into an immutable manifest; returns the manifest id. |
readproof_manifest |
Show a committed manifest, by manifest id or run id. |
readproof_diff |
Compare two runs: added/removed/changed, the unified diff, and each side's source revision, observation time, and tag. |
readproof_replay |
Reconstruct a manifest's bytes from storage alone and re-hash them. include_content: true returns the bytes. |
readproof_tag_set |
Point a named tag at a snapshot, so it can be read as uri@tag. |
readproof_tag_list |
List a resource's tags and the exact uri@tag reference for each. |
readproof_tag_delete |
Remove a tag. The snapshot survives, and manifests that mounted it still replay. |
readproof_evidence_export |
Build an in-toto evidence bundle for a run. with_content: true embeds the bytes. |
Every tool returns two text blocks: a one-line summary a human can read at a
glance, and the full JSON payload. Failures come back as tool error
results with a readable message (resolve readproof://demo/nope: readproofd: resource not found), never as a crash, so the model can correct itself.
Reading has side effects. readproof_resolve and readproof_run_mount may fetch
from the source and record a new snapshot — that is how Readproof observes what
the model saw, and the tool descriptions say so.
toolPrefix renames the whole set: toolPrefix: 'context_' gives
context_resolve, context_run_mount, … and the descriptions
cross-reference the renamed tools correctly.
Prerequisites
A reachable readproofd. Either:
you already run one — set
endpoint(andREADPROOF_API_KEYif it was started with--api-key); orlet the plugin start one — set
spawn: truewithreadproofdonPATH. Build it from this repository:go build -o /usr/local/bin/readproofd ./cmd/readproofd go build -o /usr/local/bin/readproof ./cmd/readproof # the CLI, for `readproof evidence verify`
Register the documents you want the agent to read before it asks for them:
readproof --server http://127.0.0.1:8080 resource add readproof://demo/policies/refunds \
--source-type filesystem --path "$PWD/examples/refund-agent/policies/refunds.md" \
--policy require_fresh
(Drop --server and pass --data-dir instead to register against a local
data directory — including the one a spawn: true plugin uses.)
Install
A. As a bundle (the normal path)
From npm — the published tarball ships built dist/, so there is no build
step and no permission to run one:
dsh plugin --profile web add dsh-plugin-readproof
dsh web
Or from a checkout of this repository, which is what you want while developing against unreleased Readproof changes:
cd sdk/typescript && npm ci && npm run build && cd - # the file: dependency below
cd integrations/deepseek-harness/dsh-plugin-readproof && npm install && npm run build && cd -
dsh plugin --profile web add ./integrations/deepseek-harness/dsh-plugin-readproof
dsh web
dsh plugin add links the package and appends dsh-plugin-readproof to the
profile's dsh.profile.bundles, which activates
cordis.patch.yml — one insert row with id: readproof.
Check it landed without booting:
dsh --profile web --dump-config # shows a "# == dsh-plugin-readproof" layer
Any profile name works (--profile readproof, --profile tui, …), but note that
dsh plugin initializes a brand-new profile from @deepseek-ai/dsh-base
alone, and base composes no runnable app: dsh --profile readproof on a profile
holding only base and this plugin has nothing to boot. Add the plugin to a
profile that already has a surface — web and tui are the built-in ones —
or add a surface bundle to yours. (dsh web is an alias for
dsh --profile web; web does not accept the launcher's --profile,
so dsh --profile readproof web is an error.)
Override any of the defaults in the profile's own
$DSH_HOME/profiles/web/cordis.patch.yml. A patch replaces a row's whole
config, so restate every key you need, not just the changed one:
- insert:
- id: readproof
name: dsh-plugin-readproof
config:
endpoint: https://readproofd.internal
sessionRuns: true
toolPrefix: readproof_
Remove it with dsh plugin --profile web remove dsh-plugin-readproof.
The package is built TypeScript: npm run build must have produced dist/
before dsh plugin add, because a local (or git) install runs no build
script of ours. Publishing to npm with dist/ built at pack time removes
that step for users.
B. Local development overlay
No install and no profile — mount the checkout directly. Run this from the repository root:
cd integrations/deepseek-harness/dsh-plugin-readproof && npm install && npm run build && cd -
sed "s#__READPROOF_REPO__#$PWD#" integrations/deepseek-harness/readproof-plugin.cordis.yml > /tmp/readproof-plugin.cordis.yml
dsh web --patch /tmp/readproof-plugin.cordis.yml --no-open
The sed is not decoration. A plugin path in a patch overlay must be
absolute: a patch contributes configuration but does not change the
profile directory the loader resolves module paths from, and only a row's
config is !!js-interpolated, so the path cannot be computed at load
time either. readproof-plugin.cordis.yml therefore
carries a __READPROOF_REPO__ placeholder.
It defaults to spawn: true, so it needs readproofd on PATH (or READPROOFD_BIN
set) and nothing else. Keep npx tsc -w running: the loader hot-replaces
the plugin when the built file changes, and because every registration is a
reversible Cordis effect, the old tools are unregistered first.
C. Zero-code MCP overlay
If you would rather not run TypeScript at all, Readproof's own MCP server works
through @deepseek-ai/dsh-mcp-client:
dsh web --patch "$PWD/integrations/deepseek-harness/readproof-mcp.cordis.yml"
This one needs no absolute-path surgery: the row names
@deepseek-ai/dsh-mcp-client (a package, resolved from the dsh
installation) and computes the readproof binary and data directory in !!js
config expressions — READPROOF_BIN and READPROOF_HOME override them. The overlay
also carries a commented --server variant that passes READPROOF_API_KEY
explicitly, because the stdio bridge scrubs credential-looking ambient
variables before launching the child.
The tools then appear under the MCP client's server-qualified names —
mcp__readproof__readproof_resolve, mcp__readproof__readproof_run_mount,
mcp__readproof__readproof_evidence_export, and so on.
What you give up on that path: session runs (this plugin's own feature),
the toolPrefix, the system-prompt section, and Readproof's MCP resources
(resources/list / resources/read) — DSH's MCP client bridges tools only.
What you gain: no build step, and one fewer process model to reason about.
Configuration
| Field | Type | Default | What it does |
|---|---|---|---|
endpoint |
string | http://127.0.0.1:8080 |
Base URL of a running readproofd. Ignored when spawn is true. |
apiKey |
string | '' |
Bearer token. Leave it empty and set READPROOF_API_KEY — a patch file is usually in version control. |
spawn |
boolean | false |
Start a private readproofd child instead of using endpoint. |
readproofdPath |
string | readproofd |
Executable used when spawn is true. |
dataDir |
string | ~/.readproof |
--data-dir for the spawned readproofd. ~ is expanded. |
addr |
string | 127.0.0.1:18080 |
--addr for the spawned readproofd; also determines the endpoint. |
spawnTimeoutMs |
number | 10000 |
How long to wait for the spawned readproofd to answer /healthz. |
sessionRuns |
boolean | true |
Mirror every model-driven readproof_resolve into a run keyed by the DSH session. |
toolPrefix |
string | readproof_ |
Prefix for every registered tool name. |
systemPromptSection |
boolean | true |
Contribute a short "how to use Readproof" section to the system prompt (order 150, the tool-guidance band). |
maxInlineBytes |
number | 1048576 |
Cap on inline content per result. Past it text is cut on a UTF-8 boundary and a marker naming the content hash is appended. |
With spawn: true the plugin does not finish loading until the child
answers /healthz, and the plugin's disposer kills it — an HMR reload or an
unload takes the child with it rather than leaving a listener on the port.
Session runs
With sessionRuns: true (the default), every readproof_resolve a model makes
is also recorded in a Readproof run of its own, keyed by the DSH session. You do
not have to ask the model to drive readproof_run_start / readproof_run_mount, and
you still get a replayable, diffable, exportable manifest for the session.
- The run id is
dsh-<session id>, andreadproof_resolve's result carriessession_run: { run_id, position }so the model can cite it. - The mount is the read: one fetch, and the run records precisely the bytes the model received.
- The run is committed when the session ends — the plugin listens to both
agent/disposedandsession/disposed, because which of them a deployment emits depends on its composition. - It is also committed lazily the moment anything asks for its manifest:
readproof_manifest,readproof_run_commit,readproof_diff,readproof_replay, orreadproof_evidence_exporton that run id commits it first, so those tools work mid-session rather than failing with "manifest not found". - A committed run refuses further mounts, so a session that keeps reading
after its run was committed continues in the next epoch:
dsh-<session>-2,-3, … Each epoch is its own manifest. - Unloading the plugin commits every open run before shutting down.
The session id comes from exec.agent.id inside the tool body — Agent.id
is documented as "the single identity shared with session"
(@deepseek-ai/dsh-agent). A tool call with no agent (a direct
ctx.tools.execute from another plugin, or a test) has no session, so it is
resolved normally and not mirrored; session_run is simply absent from the
result.
Set sessionRuns: false to turn all of this off and leave the run lifecycle
entirely to the model.
Try it
With readproof://demo/policies/refunds registered:
- "List the Readproof resources you can read." →
readproof_resources_list - "Read
readproof://demo/policies/refundsand tell me the refund window." →readproof_resolve, which records a snapshot (and, with session runs on, a manifest entry). - "Tag that snapshot as
prod, then readreadproof://demo/policies/refunds@prod." →readproof_tag_set, then a pinned read withdecision: "use_tag". - Edit the document, then: "Read the resource again — did it change? What
does
@prodsay now?" The plain read returns the new bytes;@prodstill returns the old ones. - "Start a run called
demo-1, mountreadproof://demo/policies/refunds, commit it, and tell me the manifest id." → thereadproof_run_*trio. - "Diff
demo-1againstdemo-2and explain why the answer changed." →readproof_diff, including the source-revision and observed-at "why". - "Replay
demo-1and export an evidence bundle for it." →readproof_replay, thenreadproof_evidence_export.
Everything the model did is reproducible from the CLI against the same
deployment: readproof --server <endpoint> manifest demo-1, … replay demo-1,
… evidence export demo-1.
Tests
npm install && npm test
The tests are not mocked. They build readproofd and readproof from this repository,
start a real readproofd on a free port over a scratch data directory, register
the refund-agent policy, compose a real Cordis app (@deepseek-ai/dsh-system-prompt
@deepseek-ai/dsh-tools+ this plugin) and drive the tools throughctx.tools.execute— the same pipeline the agent loop drives. No model is involved. They cover the run trio, diff across a real source edit, replay returning the pre-edit bytes, tag pinning, session runs,spawn: trueincluding the disposer killing the child, and an evidence bundle whose merkle root matches the SDK'smerkleRootand which the Goreadproof evidence verifyaccepts.
Requires Go and Node on PATH, and network access on first npm install.
Publishing
Publishing is automated: pushing a v* tag runs
.github/workflows/publish-npm.yml,
which publishes @readproof/sdk first and this package second, both with
--provenance. docs/releasing.md has the
whole procedure.
How the SDK dependency works in this repository
In this monorepo package.json depends on @readproof/sdk as
file:../../../sdk/typescript, so npm ci && npm test exercises the
unreleased SDK. The publish step (.github/workflows/publish-npm.yml, or a
manual publish following the same two lines) rewrites that dependency to the
published semver range before npm publish, because npm sends the
manifest it read from package.json, not the one inside the tarball.
Consumers therefore see "@readproof/sdk": "^<version>".
"@readproof/sdk": "^0.3.1",
If a pack is interrupted between the two hooks, `package.json.prepack-backup`
survives; the next `prepack` refuses to run and tells you to restore it
(`node scripts/postpack.mjs`).
### Other notes
- `npm publish` with `dist/` built at pack time, so
`dsh plugin --profile <name> add dsh-plugin-readproof` installs prebuilt code
and needs no build permission from the user.
- Tag the GitHub repository with the **`dsh-plugin`** topic so it is
discoverable, and keep the `dsh-plugin` keyword in `package.json`.
- Installing from git instead (`dsh plugin add github:you/repo#<sha>`)
requires a `prepare` script here *and* a pnpm `allowBuilds` entry from the
user — which is permission to execute this package's code at install
time. Prefer npm or a `pnpm pack` tarball.
`@deepseek-ai/cordis` and `@deepseek-ai/dsh-tools` are **peer** dependencies
(with dev copies for building and testing), matching what the harness's own
packages do — a plugin must use the profile's single copy of the tool
registry, not a nested duplicate. `@deepseek-ai/schemastery` is an ordinary
dependency, the same choice `@deepseek-ai/dsh-tools` makes for it.
## Limitations
- **Text only.** The Readproof TypeScript SDK decodes a resolve's bytes to a UTF-8
string before this plugin sees them, so unlike `readproof mcp` there is no
base64 branch: a genuinely binary resource comes back mangled. Register
text documents; use `readproof_replay`/`readproof_evidence_export --with-content` and
the CLI for anything else.
- **Readproof's MCP resources are not bridged.** This plugin exposes tools only.
A model reads a document with `readproof_resolve`, not through a resource
listing.
- **Session-end commit depends on the composition.** `agent/disposed` and
`session/disposed` are both observed, but a host that tears its process
down without emitting either leaves the run open until something asks for
its manifest (or the plugin unloads). The lazy commit exists precisely
because that guarantee is not universal.
- **Session runs record `readproof_resolve` only.** A `readproof_run_mount` the model
drives itself goes into the run *it* names, which is the point of that
tool; the two are deliberately separate.
- **No approval hook.** Resolving can reach out to an `http` or `github`
source. This plugin registers no `tools/pre-execute` policy, so it
inherits whatever the deployment's own guards decide.
- **Run start is probed, not queried.** `readproofd` reports a duplicate run id as
a 500, so opening a session run treats a 500 as "id taken" and tries the
next epoch (up to 16 times). A different 500 would be misread as a
collision and eventually surface as "no unused run id".
- **One `readproofd` per plugin instance.** Mounting the plugin twice with
different endpoints would register conflicting tool names; use one row.