The Scribe — services/the-scribe/
A cultural-conduit exemplar for HERALD per [CHARTER §5.6] and [CHARTER §2.5].
"A conduit that adds transcription envelopes summarising long traces into compact, signed digests. Useful for audit, for legal discovery, for the agent equivalent of a court reporter. The Scribe never modifies originals; it walks beside them." — [CHARTER §2.5]The Scribe receives HERALD envelopes addressed to it (or simply submitted to it for the record), groups them by trace, and on demand emits one signed transcription envelope that cites every envelope in the trace. The originating envelopes are preserved byte-for-byte; the transcript walks beside them, never inside them. This service is the Charter exemplar — minimal, in-memory, reviewable. Not a production deployment.
What this is and isn't
- Is: an Express HTTP service implementing the conduit shape from [CHARTER §5.6] using the T-008 verify-then-act pattern at
demo/conduit/src/conduit.mjs. Builds on thelib/herald-core-ts/envelope build/sign/verify primitives. - Isn't: a Herald, a CT log, a registry, or anything that mints reputation. The Scribe is a conduit — it observes, summarises, signs.
- Isn't: a key-bearing instance of
sigil:scribe. Perregistries/reserved-sigils.md§1 rule 1 and §2.5, reserved role-sigils never sign. A real Scribe instance binds tosigil:scribevia a Herald-issuedreserved_role_bindingattestation (see T-027's skeleton inlib/herald-core-ts/src/reserved-resolution.ts); the instance's own sigil signs every transcript.
Run
This sandbox uses a relative import path that, when promoted to the canonical services/the-scribe/ location, resolves to the in-tree lib/herald-core-ts/ reference library. The sandbox includes a symlink at manthan/state/findings/T-124/lib/herald-core-ts so that the same import path works during review.
cd learning/herald-protocol/manthan/state/findings/T-124/services/the-scribe
npm install
npm testThe Scribe is library-shaped: createScribe({ scribeSigil, scribeSigner, keys }) returns an Express app. The npm start script exists to refuse to start without a wired keys registry — the embedder must wire identity, not the script. See test/scribe.test.mjs for ten worked examples.
API
| Method | Path | Purpose |
|---|---|---|
| GET | /health | Liveness; returns the Scribe's instance sigil and counts. |
| POST | /envelopes | Ingest one HERALD envelope. Verifies via verifyEnvelope, persists into the in-memory trace store, returns { status: "stored", id, trace } on success. |
| POST | /transcripts | Body { traceId }. Emits one signed transcription envelope summarising every envelope already stored for that trace, citing each by id. |
| GET | /traces/:traceId | List envelope ids stored for the trace, in receipt order. |
| GET | /transcripts/:envId | Fetch a previously-emitted transcript envelope by its id. |
| GET | /originals/:envId | Return the byte-equal original of a stored envelope (the never-modify proof rail; embedder may set exposeOriginals: false). |
Refusals are signed refusal envelopes per [EXT §10] — not just HTTP errors. Reasons used by The Scribe (all drawn from the registered set in lib/herald-core-ts/src/refusal.ts):
envelope_malformed— invalid JSON, or an envelope shape that the verifier cannot parse.envelope_invalid_signature— signature failed verification or the envelope was tampered with.sigil_unknown— the signing sigil is not in the Scribe's keys registry.envelope_expired— the envelope'sexpiresfield is past.envelope_replay— a(from, nonce)pair seen before.trace_parent_invalid— orphaned or parent-trace-mismatched envelope.rate_limit_exceeded— per-sender rate cap (ratePerMinute, default 60).
What's in the transcription attestation
Per [CHARTER §2.5] and registries/reserved-sigils.md §2.5, "transcripts MUST be new envelopes citing originals." The Scribe issues one transcription attestation whose value shape is:
{
"type": "transcription",
"value": {
"of_trace": "<traceId>", // the trace summarised
"cites": [ // ordered citation rail
{
"id": "<envelope id>",
"from": "sigil:...",
"ts": 1746432000,
"parent": "<parent id or null>",
"canonical_sha256": "<hex>", // pin to the exact bytes signed
"attestation_types": ["model", "..."]
}
],
"envelope_count": 2,
"first_ts": 1746432000,
"last_ts": 1746432042,
"cites_digest_sha256": "<hex>", // sha-256 of the cites list
"scribed_at": 1746432043,
"by": "sigil:<scribe instance>" // not sigil:scribe
}
}The attestation shape is one defensible interpretation of the Charter's prose. See INTERPRETATION.md for the underspecification analysis and follow-on questions.
The transcript envelope itself
fromis the Scribe instance's own sigil, neversigil:scribe.parentisnull— the transcript is its own genesis, because the trace it summarises is owned by other senders and must not be re-rooted under the Scribe (per the never-modify rule from [CHARTER §5.6]: "never modifies originals; produces new envelopes that cite the originals").- The citation rail lives in the
transcriptionattestation, not inparent/trace. - The transcript verifies under
lib/herald-core-ts/src/envelope.ts#verifyEnvelopestandalone.
File layout
services/the-scribe/
├── README.md (this file)
├── package.json
├── src/
│ └── scribe.mjs (Express service factory; cited inline)
└── test/
└── scribe.test.mjs (10 tests: success, multi-envelope, 4 refusals, 2 edge)Citations
[CHARTER §2.5]— definition ofsigil:scribe(CHARTER.md lines 116-118).[CHARTER §5.6]— the conduit-bestiary entry for The Scribe (CHARTER.md lines 623-627).[CHARTER §6.2]— the speculative section that the task brief refers to as "transcription"; seeINTERPRETATION.mdfor why the citation does not match a transcription-attestation definition and how the Scribe nonetheless implements one.registries/reserved-sigils.md §2.5— reserved-sigil binding semantics (lines 99-110).lib/herald-core-ts/src/reserved-resolution.ts— T-027 binding-attestation skeleton.demo/conduit/— the T-008 pass-through gateway pattern this service generalises.[EXT §10]andlib/herald-core-ts/src/refusal.ts— the refusal envelope shape and registered reason set.