examples/support-agent · Readproof 0.3.2 · open model on Ollama

A support agent you can replay, diff, and prove.

A customer-support agent answers tickets from three policy documents. Every ticket is one Readproof run: the agent mounts the policies, hands the model exactly those bytes, commits a manifest, and stores the manifest id next to the answer. When a policy changes, diff names which document moved and why, replay reconstructs the exact text the old answer was based on, and evidence exports a bundle the Go CLI verifies — while the house-style document, mounted by tag, does not move until someone promotes it.

What makes this example worth reading

no Claude, no API key

Open model via Ollama

Any chat model Ollama serves: local (llama3.2), remote, or Ollama Cloud (deepseek-v4-flash:cloud was used for the transcript). A deterministic fake model runs the whole story with no network at all.

three freshness contracts

Same agent, different rules per document

refunds is require_fresh (money), shipping is allow_stale for an hour, tone is mounted as @prod — edits are inert until promoted.

provable

Replay, diff, evidence

Every guarantee is a property of Readproof, not the model: swap the model or use the fake one and the replay still matches, the diff still names the revision, the Go verifier still passes.

Architecture

What talks to what

Four moving parts, all local. The agent is a TypeScript CLI built on the Readproof TypeScript SDK; it talks HTTP to a readproofd server (the scenario starts a throwaway one) and to Ollama for the model. The Go CLI readproof appears once, as the independent auditor that verifies the evidence bundle.

src/cli.ts setup · ask · replay · diff … src/agent.ts run = rp.run({id: "ticket-N"}) mount ×3 → answer → commit src/model.ts builds the prompt from the mounted bytes readproofd (HTTP, :18090) resolve · runs · manifests · tags SQLite + content-addressed blobs Ollama (:11434) chat(model, messages, stream) or the fake deterministic model data/tickets.jsonl {ticket, question, answer, model, manifest_id, run_id, entries[], at} context/policies/*.md refunds.md · shipping.md · tone.md (filesystem sources) readproof (Go CLI) evidence verify bundle.json merkle root · re-hash · replay argv SDK: resolve, run.mount, commit, diff, replay, buildEvidence exactly the mounted bytes append record fetch per policy cross-check by replay replay / diff / evidence read from readproofd only — the policy files are never consulted again.
One ticket = one run. The model sees exactly the bytes the manifest records; everything that comes later (replay, diff, evidence) reads the store, never the files.

The governed documents

Three policies, three freshness contracts

The documents are deliberately tiny so the whole story fits in one screen. What matters is that each one gets a different freshness rule, declared once in src/config.ts and applied by readproofd on every resolve.

ResourceFilePolicy (on resolve)How the agent mounts itWhy
readproof://acme/policies/refundscontext/policies/refunds.mdrequire_freshbare URIMoney is involved: re-verify the source on every ticket. Unchanged bytes dedupe to the same hash.
readproof://acme/policies/shippingcontext/policies/shipping.mdallow_stale, max_age_seconds: 3600bare URIChanges rarely; an hour-old copy is fine and saves a fetch.
readproof://acme/policies/tonecontext/policies/tone.mdrequire_fresh…/tone@prod (tag)House style is a deployment decision: a tag delivers exactly the promoted snapshot, no fetch, policy not consulted.

Their contents (soft-wrapped here; each file is one paragraph on one line)

# Refund policy

Products can be refunded within 30 days of delivery. Refunds go
to the original payment method within 5 business days.
# Shipping policy

Orders ship within 2 business days. Standard delivery takes 3-5
business days, express 1-2. Tracking is emailed as soon as the
label is created.
# House style for support replies

Keep replies under 120 words. Use plain language and no jargon.
Name the policy you relied on, in the form "per the refund
policy". Close with one concrete next step the customer can take.

They are registered as filesystem sources with absolute paths (computed from import.meta.url), because readproofd resolves a filesystem path relative to its own working directory, never the example's. SUPPORT_CONTEXT_DIR points at a different directory — the tests use that to register a throwaway copy they can edit freely.

The core loop

How a ticket is answered

  1. Open a run named after the ticket

    rp.run({ id: "ticket-1001" }). Run ids are deterministic (runIdFor), so show/replay need no lookup table. The run starts lazily on the first mount.

  2. Mount the three policies, in a fixed order

    mountSpecs() returns ["readproof://acme/policies/refunds", "readproof://acme/policies/shipping", "readproof://acme/policies/tone@prod"]. Each run.mount(spec) resolves the resource under its policy (or by tag) and records it as the next ordered manifest entry. The agent keeps {uri, ref?, snapshot_id, content_hash, content} per mount. Order is a hard Readproof invariant — it is committed to the manifest and folded into the evidence Merkle root.

  3. Build the prompt from the mounted bytes and nothing else

    No second read of the files, no retrieval of its own. The system message is the tone document's text plus a fixed instruction; the user message is every document under a header naming its URI and the first 12 hex of its content hash, then the question.

    system:
    <contents of tone.md>
    
    Answer only from the policies given. Name the policy you relied
    on. If the policies do not cover it, say so.
    
    user:
    ### readproof://acme/policies/refunds (sha256:72be2c034713)
    <contents of refunds.md>
    
    ### readproof://acme/policies/shipping (sha256:e8178eaf5ca5)
    <contents of shipping.md>
    
    ### readproof://acme/policies/tone (sha256:c04b1f6dbc3c)
    <contents of tone.md>
    
    I bought headphones 20 days ago. Can I still get a refund?

    Those headers are what tie a sentence in the answer back to a line in the manifest, for about a dozen tokens.

  4. Stream the answer

    ollama.chat({ model, messages, stream: true }); tokens go to stdout as they arrive so a slow local model still looks alive. model: <name> is printed once so the record can store which model actually answered.

  5. Commit the manifest

    run.commit() freezes the three entries into an immutable manifest and returns its id.

  6. Append the ticket record

    One JSON line in data/tickets.jsonl: ticket, question, answer, model, manifest_id, run_id, the entries (without content), and a timestamp. The manifest id next to the answer is the whole trick: months later replay 1001 reconstructs the exact policy text that produced that reply.

src/model.ts

The model layer

Model resolution

1. OLLAMA_MODEL wins. 2. Otherwise ollama.list() and the first model whose name does not contain embed (Ollama happily lists nomic-embed-text, which cannot chat). 3. Otherwise: set OLLAMA_MODEL or: ollama pull llama3.2.

Where Ollama is

The ollama JS client (0.6.3) does not read OLLAMA_HOST — it hardcodes 127.0.0.1:11434. The example reads the variable itself and passes new Ollama({ host }), so a remote or cloud-backed Ollama works with one env var.

Errors

Connection failures (ECONNREFUSED, fetch failed, DNS) become one line naming OLLAMA_HOST and asking whether ollama serve is running. Everything else is passed through.

Fake mode

SUPPORT_FAKE_MODEL=1 replaces the chat call with a deterministic function that answers from the refund policy's first sentence, byte-identical output for byte-identical input. The run, mounts, manifest, tag, diff, replay and evidence are the same code path — which is the point: the guarantees are Readproof's, not the model's. Tests and CI always run this way.

Products can be refunded within 30 days of delivery.
(per readproof://acme/policies/refunds, sha256:72be2c034713)
— asked: I bought headphones 20 days ago. Can I still get a refund?

Use it

Run it

Prerequisites

  • Go 1.26+ — the scenario builds readproof and readproofd from this repo.
  • Node 18+ — the agent is TypeScript and uses the global fetch.
  • Ollama with a chat model for the real run (ollama serve, ollama pull llama3.2), or OLLAMA_HOST pointing at a remote/cloud instance — or skip it with the fake model.

One command

cd examples/support-agent
npm install

# needs Ollama; picks the first chat model it finds
npm run scenario

# or name the model explicitly
OLLAMA_MODEL=llama3.2 npm run scenario

# what produced the transcript below (Ollama Cloud)
OLLAMA_MODEL=deepseek-v4-flash:cloud npm run scenario

# no Ollama and no network — deterministic fake model
SUPPORT_FAKE_MODEL=1 npm run scenario

scripts/scenario.sh is self-contained: it builds readproof and readproofd, builds the SDK if needed, starts a throwaway readproofd on :18090 with its own data directory, runs the twelve steps, and restores the two policy files it edits — always, including on failure and on Ctrl‑C (it backs them up by copy, not with git, so it can never clobber edits you had in flight). It leaves its scratch directory behind (evidence bundle, readproofd log) and prints the path.

Against your own readproofd

# 1. a host-side readproofd, so it can read the policy files
go build -o readproofd ./cmd/readproofd
./readproofd --addr :8080 --data-dir ~/.readproof &

# 2. point the example at it (READPROOF_SERVER_URL also works)
export READPROOF_ENDPOINT=http://localhost:8080

# 3. register the policies, answer a ticket, then replay it
npm run agent -- setup
npm run agent -- ask 1001 \
  "I bought headphones 20 days ago. Can I still get a refund?"
npm run agent -- replay 1001
Containerized readproofd. docker compose up's readproofd cannot see your host filesystem, so the filesystem sources would fail there. Run readproofd on the host, or switch the policies to GitHub/HTTP sources (see Extend it).

Real output

The scenario, step by step

Everything below is real output from OLLAMA_MODEL=deepseek-v4-flash:cloud bash scripts/scenario.sh on 2026‑08‑21, ids and hashes trimmed. The two answers are the model's own words.

  1. setup — put the policies under Readproof

    # register the three policies and pin the house style as @prod
    $ npm run agent -- setup
      readproofd http://localhost:18090 ok
      register   readproof://acme/policies/refunds
                   -> …/policies/refunds.md (require_fresh)
      register   readproof://acme/policies/shipping
                   -> …/policies/shipping.md (allow_stale, max age 3600s)
      register   readproof://acme/policies/tone
                   -> …/policies/tone.md (require_fresh)
      tag        readproof://acme/policies/tone@prod -> snap_01M0HW18V0…
    
      3 policies governed by Readproof, 3 registered just now.

    Idempotent: run it twice and it registers nothing and moves no tag. If a URI is already registered against a different path, it warns with both paths.

  2. ask 1001 — while the policy says 30 days

    # one ticket = one run: mount three policies, answer, commit
    $ npm run agent -- ask 1001 \
        "I bought headphones 20 days ago. Can I still get a refund?"
      ticket:   1001
      question: I bought headphones 20 days ago. Can I still get a refund?
    
      model: deepseek-v4-flash:cloud
      Yes, you can still get a refund. Per the refund policy, products
      can be refunded within 30 days of delivery, and your purchase is
      20 days old, so you're within the window. The refund will go to
      your original payment method within 5 business days. To proceed,
      please start a return request through your account or reply with
      your order number.
    
      manifest: manifest_01M0HW1AQ9…
      POS  URI@REF                      SNAPSHOT          HASH
      0    …/acme/policies/refunds      snap_01M0HW194M…  sha256:72be2c034713
      1    …/acme/policies/shipping     snap_01M0HW194S…  sha256:e8178eaf5ca5
      2    …/acme/policies/tone@prod    snap_01M0HW18V0…  sha256:c04b1f6dbc3c
  3. Somebody edits the refund policy

    # 30 days → 14 days, straight on the file
    $ sed 's/within 30 days/within 14 days/' \
          context/policies/refunds.md > refunds.new
    $ mv refunds.new context/policies/refunds.md

    Nobody told the agent. Nothing was redeployed.

  4. ask 1002 — same question, different answer

    # same question, nothing redeployed — require_fresh re-verified
    $ npm run agent -- ask 1002 \
        "I bought headphones 20 days ago. Can I still get a refund?"
      model: deepseek-v4-flash:cloud
      Per the refund policy, products can be refunded within 14 days
      of delivery. Since your purchase was 20 days ago, it's past that
      window, so a refund isn't possible. If you have a different
      issue, like a defect, please check the warranty or contact
      support for options. Next step: reply with your order number if
      you'd like us to review any other concerns.
    
      manifest: manifest_01M0HW1CFV…
      POS  URI@REF                      SNAPSHOT          HASH
      0    …/acme/policies/refunds      snap_01M0HW1AZH…  sha256:3117512b66c3
      1    …/acme/policies/shipping     snap_01M0HW194S…  sha256:e8178eaf5ca5
      2    …/acme/policies/tone@prod    snap_01M0HW18V0…  sha256:c04b1f6dbc3c

    The decision flipped. require_fresh is why: Readproof re-verified the source and delivered a new snapshot. Shipping and tone are byte-identical to ticket 1001.

  5. diff 1001 1002 — which document moved, and why

    # which document moved between the two tickets, and why
    $ npm run agent -- diff 1001 1002
      --- ticket 1001 (manifest_01M0HW1AQ9…)
      +++ ticket 1002 (manifest_01M0HW1CFV…)
    
      ~ readproof://acme/policies/refunds
          (snap_01M0HW194M… -> snap_01M0HW1AZH…)
        why: source revision sha256:72be2c034713 -> sha256:3117512b66c3;
             observed 2026-08-21T09:57:02Z -> 2026-08-21T09:57:04Z
        --- a/readproof://acme/policies/refunds
        +++ b/readproof://acme/policies/refunds
        @@ -1,4 +1,4 @@
         # Refund policy
    
        -Products can be refunded within 30 days of delivery. Refunds
           go to the original payment method within 5 business days.
        +Products can be refunded within 14 days of delivery. Refunds
           go to the original payment method within 5 business days.
    
      = readproof://acme/policies/shipping  (snap_01M0HW194S…)
      = readproof://acme/policies/tone      (snap_01M0HW18V0…)
    
      1 resource changed, 0 added, 0 removed, 2 unchanged

    "Why did the agent answer differently?" — answered with a document name, a revision, and a timestamp.

  6. replay 1001 — the bytes the old answer was actually based on

    # rebuild the bytes ticket 1001 was actually answered from
    $ npm run agent -- replay 1001
      ticket:   1001
      manifest: manifest_01M0HW1AQ9… (answered 2026-08-21T09:57:04Z)
    
        [0] readproof://acme/policies/refunds
              recorded sha256:72be2c034713…
              replayed sha256:72be2c034713…   MATCH
            | # Refund policy
            |
            | Products can be refunded within 30 days of delivery.
            |   Refunds go to the original payment method within 5
            |   business days.
              live source: CHANGED -> sha256:3117512b66c3…
    
        [1] readproof://acme/policies/shipping
              … MATCH   live source: unchanged
        [2] readproof://acme/policies/tone
              … MATCH   live source: unchanged
    
      Replay verified: 3/3 entries match.
      1 of them no longer matches the live source — the manifest, not
      the source, is what a replay reads.

    The file on disk says 14 days. The replay says 30, because that is what the agent read at 09:57:04, reconstructed from Readproof's store with no fetch. Replay is strict: any hash mismatch exits non-zero.

  7. evidence 1001 — for someone who doesn't trust you

    # 1. the TypeScript SDK builds the bundle
    $ npm run agent -- evidence 1001 \
        --out ticket-1001.bundle.json --with-content
      evidence bundle written to …/ticket-1001.bundle.json
        entries:     3 (with embedded content)
        merkle root: e8d8572c0d97…
        replay:      all entries match
    
      verify it with the Go CLI:
        readproof --server http://localhost:18090 \
          evidence verify …/ticket-1001.bundle.json
    
    # 2. the Go CLI checks it independently
    $ readproof --server http://localhost:18090 \
        evidence verify ticket-1001.bundle.json
      evidence verified: 3 entries, merkle root e8d8572c0d97…,
      embedded content 3/3 re-hashed, replay match 3/3

    The bundle is an in-toto Statement built by the TypeScript SDK's buildEvidence(); the Go verifier recomputes the Merkle root, re-hashes the embedded bytes, and cross-checks the store by replay. Flip one base64 character and it exits non-zero — the tests assert exactly that.

  8. Edit the house style — and watch nothing happen

    # 1. edit the house style document
    $ printf '\nAlways open with a one-line summary of the decision.\n' \
        >> context/policies/tone.md
    
    # 2. ask again — the tone entry does not move
    $ npm run agent -- ask 1003 \
        "I bought headphones 20 days ago. Can I still get a refund?"
      model: deepseek-v4-flash:cloud
      Per the refund policy, products can be refunded within 14 days
      of delivery. Since your headphones were delivered 20 days ago,
      the refund window has passed, so a refund is not available. As a
      next step, please contact our support team if you have any other
      questions or concerns.
    
      manifest: manifest_01M0HW1GMK…
      POS  URI@REF                      SNAPSHOT          HASH
      0    …/acme/policies/refunds      snap_01M0HW1E27…  sha256:3117512b66c3
      1    …/acme/policies/shipping     snap_01M0HW194S…  sha256:e8178eaf5ca5
      2    …/acme/policies/tone@prod    snap_01M0HW18V0…  sha256:c04b1f6dbc3c

    The tone entry is snap_01M0HW18V0… — the same snapshot ticket 1001 used, three edits ago. A tag delivers exactly the snapshot it points at: no fetch, policy not consulted. Editing the file is not deploying it.

  9. Deploying it is: promote

    # 1. move the prod tag to the resource's current snapshot
    $ npm run agent -- promote tone
      readproof://acme/policies/tone@prod -> snap_01M0HW6DMN…
    
    # 2. the move is recorded in the snapshot history
    $ npm run agent -- history tone
      readproof://acme/policies/tone
      SNAPSHOT           OBSERVED              CONTENT_HASH         TAGS
      snap_01M0HW6DMN…   2026-08-21T09:59:51Z  sha256:8ed369c0dc9b  prod
      snap_01M0HW6D8B…   2026-08-21T09:59:50Z  sha256:c04b1f6dbc3c

    promote with no snapshot id resolves the resource first, then promotes what its policy says is current. The previous snapshot is still there; promote tone <old-snapshot-id> rolls back.

src/cli.ts

Command reference

All commands: npm run agent -- <command> [args]; npm run agent -- --help prints this. Runtime errors print one line and exit 1 (no usage dump); misuse prints usage and exits 2.

CommandWhat it doesReads / writes
setupChecks /healthz, registers the three policies idempotently (get → 404 → create; warns on a path mismatch), resolves tone once and tags it prod if no tag exists.readproofd
ask <ticket> <question…>One Readproof run: mount ×3 → model → commit → append. Prints the streamed answer, the manifest id and the entries table.readproofd, Ollama, tickets.jsonl
show <ticket>The stored record plus the manifest read back from readproofd (getManifest).tickets.jsonl, readproofd
replay <ticket>rp.replay(manifest_id); asserts every entry matches (exit 1 otherwise), prints recorded vs replayed hashes and the bytes, then resolves each URI live and prints unchanged / CHANGED -> hash.readproofd
diff <a> <b>rp.diff(manifestA, manifestB); per entry: status, the why: line (source revision, observed-at, ref — only fields present), unified diff; summary counts.readproofd
evidence <ticket> [--out f] [--with-content]buildEvidence + encodeEvidence; prints merkle root and the exact Go verify command.readproofd, writes the bundle
promote <policy> [snapshot-id]Moves the prod tag. With no id: resolves first, then promotes the resource's current snapshot.readproofd
history <policy>Snapshots newest first with their tags.readproofd

<policy> accepts a full readproof:// URI or a short name (refunds, shipping, tone).

Configuration

Environment variables

VariableDefaultEffect
READPROOF_ENDPOINThttp://localhost:8080Base URL of the readproofd to use. READPROOF_SERVER_URL (the Go CLI's variable) is accepted as a fallback so one export serves both halves.
READPROOF_API_KEYBearer token, if readproofd runs with --api-key.
OLLAMA_HOSThttp://localhost:11434Where Ollama is. Read by the example and passed to the client explicitly.
OLLAMA_MODELfirst non-embedding model listedWhich chat model answers.
SUPPORT_FAKE_MODEL1/true/yes/on = deterministic fake model, no Ollama.
SUPPORT_CONTEXT_DIR./context/policiesWhere the policy files live (tests point it at a throwaway copy).
SUPPORT_DATA_DIR./dataWhere tickets.jsonl is written (the scenario uses its scratch dir).

Under the hood

Every file

Everything under examples/support-agent/:

PathWhat is in it
package.jsonScripts build · agent (node dist/src/cli.js) · scenario · test · clean. Dependencies: @readproof/sdk (file:../../sdk/typescript), ollama 0.6.3; dev: typescript 5.9.3, @types/node 22.20.1.
tsconfig.jsonStrict, noUncheckedIndexedAccess, ESM output to dist/.
.gitignorenode_modules/, dist/, data/, *.bundle.json.
README.mdThe same story as this page, in markdown.
context/policies/refunds.md · shipping.md · tone.md — the three governed documents (fixtures).
src/config.tsEvery knob in one place: READPROOF_ENDPOINT and API key, OLLAMA_HOST/MODEL, FAKE_MODEL, POLICY_DIR, DATA_DIR, the POLICY_RESOURCES table, mountSpecs(), resolvePolicyURI().
src/model.tsanswer(): Ollama streaming chat or the fake model. systemPrompt() = tone + instruction; userPrompt() = documents with uri and hash headers; resolveModel(); ollamaError().
src/agent.tssetup() (idempotent registration + prod tag), ask() (one run per ticket), loadTicket(), appendTicket(), checkHealth().
src/cli.tsThe eight commands, the table/diff/replay printers, one-line runtime errors.
scripts/scenario.shBuilds readproof, readproofd, the SDK and the example; runs a throwaway readproofd on :18090; the twelve steps; restores the fixtures via trap.
test/agent.test.tsSeven end-to-end tests against a real readproofd with the fake model (below).

data/tickets.jsonl

The ticket record

Append-only, one JSON object per line, written by ask and read by show/replay/diff/evidence (last record for a ticket wins). The bytes the model saw are not stored here — they live in Readproof, addressed by the manifest.

{
  "ticket": "1001",
  "question": "I bought headphones 20 days ago. Can I still get a refund?",
  "answer": "Yes, you can still get a refund. Per the refund policy, …",
  "model": "deepseek-v4-flash:cloud",
  "manifest_id": "manifest_01M0HW1AQ9…",
  "run_id": "ticket-1001",
  "entries": [
    { "uri": "readproof://acme/policies/refunds",
      "snapshot_id": "snap_01M0HW194M…",
      "content_hash": "sha256:72be2c03…" },
    { "uri": "readproof://acme/policies/shipping",
      "snapshot_id": "snap_01M0HW194S…",
      "content_hash": "sha256:e8178eaf…" },
    { "uri": "readproof://acme/policies/tone", "ref": "prod",
      "snapshot_id": "snap_01M0HW18V0…",
      "content_hash": "sha256:c04b1f6d…" }
  ],
  "at": "2026-08-21T09:57:04.512Z"
}

Field names follow the SDK types exactly: resource.uri is always the bare URI and the tag is reported separately as resource.ref — the same split the manifest uses, so a moved tag can never change what a committed manifest replays.

test/agent.test.ts · CI

Tests and CI

npm test builds, then runs node --test dist/test/*.test.js with SUPPORT_FAKE_MODEL=1. The suite go builds readproof and readproofd, starts a throwaway readproofd on a free port, copies the three fixtures into a temp directory and registers those, then asserts:

  1. setup registers three resources with their declared policies and creates tone@prod — and is idempotent.
  2. ask commits a manifest with three entries in mount order, the third carrying ref === "prod".
  3. Editing refunds.md produces exactly one changed diff entry with source_revision_a !== source_revision_b and both observed_at fields; the others are unchanged.
  4. Replaying the first ticket still matches and returns the old bytes while a live resolve returns the new ones.
  5. Editing tone.md leaves the tone entry's snapshot identical (pinned by the tag) while the refunds entry moves.
  6. The evidence bundle's subject[0].digest.sha256 equals merkleRoot() recomputed from its entries; readproof evidence verify exits 0 — and exits non-zero after one byte of content_b64 is flipped.
  7. The repository's own fixtures were never touched.

CI job support-agent-example (.github/workflows/ci.yml): set up Go and Node, build the SDK, npm ci, npm run build, npm test. No Ollama in CI, by design.

Concepts

How it maps to Readproof

Readproof conceptIn this example
Identityreadproof://acme/policies/{refunds,shipping,tone} — stable names, independent of the files behind them.
Policyrequire_fresh on refunds (money), allow_stale 1h on shipping (rarely changes), require_fresh on tone but mounted by tag, so it never fetches.
Tagtone@prod, moved only by promote — an explicit, revertible deployment of house style.
RunOne per ticket, id ticket-<id>; mount() resolves and records.
ManifestThe three ordered entries commit() freezes; its id is stored with the answer.
Diffdiff 1001 1002, with per-side source_revision / observed_at / ref.
Replayreplay 1001 — strict SHA256 reconstruction, no source fetch.
Evidenceevidence 1001 — in-toto Statement, Merkle root over the entries, verified by the Go CLI.

Honest notes

Gotchas and limits

  • promote resolves first. current_snapshot_id only moves on resolve, so "promote the current snapshot" right after an edit would otherwise re-promote the pre-edit one. The example resolves, then tags.
  • The tone document is sent twice — as the system prompt (it is the house style) and in the documents list (so "exactly the mounted bytes" stays literal). A few dozen tokens; drop the duplicate if you prefer.
  • The ticket log is append-only JSONL; re-asking a ticket id appends a second record and the last one wins. It is a demo store, not a database.
  • The real run depends on your Ollama. Model quality and latency are the model's; the transcript used Ollama Cloud's deepseek-v4-flash:cloud. Tests never touch a model.
  • The scenario leaves its scratch directory (bundle + readproofd log) and prints the path; delete it when done.
  • Filesystem sources need a host-side readproofd. A containerized readproofd cannot read the example's files.

Next

Extend it

Add a policy

Append an entry to POLICY_RESOURCES in src/config.ts (uri, name, file, policy, optional mountRef) and drop the file in context/policies/. setup registers it; mountSpecs() mounts it in order; the manifest, diff, replay, and evidence pick it up unchanged.

Govern a GitHub or HTTP document

Swap policySource() to { kind: "github", github: { owner, repo, path, ref } } or { kind: "http", http: { url, headers } }; the source revision becomes the commit SHA or ETag, and diff names it. Then a containerized readproofd works too.

Swap the model

OLLAMA_MODEL=… for any Ollama model; for another provider replace answer() in src/model.ts — keep the rule that the prompt is built only from entries.

Let an agent harness do the mounting

The same tools exist over MCP (readproof mcp) for Claude Code, Cursor, or DeepSeek Harness, and as a LangGraph node in examples/langgraph-ts — the manifest id lands in the checkpoint instead of a JSONL line.

Page: examples/support-agent/guide.html · code: examples/support-agent/ · Readproof 0.3.2 · output captured 2026‑08‑21.