tools+web+docs: the registry hash ledger — public tamper-evidence in elevenmessenger/releases (#800)
Each corpus build emits manifest.sha256.json (hash of every published file under the contributor's prefix, written after the fail-closed verify so it hashes exactly what publishes; uploaded with the corpus so the bucket is self-describing). The accrual runbook commits it to the PUBLIC releases repo at prompt-registry/<slug>.json — that file's git history is the ledger: a few KB per accrual, and any after-the-fact change to a published transcript shows as a dated hash change without re-preserving the changed bytes. The page footer links the ledger history.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
3 files changed,
+34
−1
docs/status.md+8 −1tools/prompt-registry.mjs+19 −0web/status/prompts.html+7 −0
diff --git a/docs/status.md b/docs/status.md| index 41d109d..29b90d6 100644 |
| --- a/docs/status.md |
| +++ b/docs/status.md |
| @@ -411,7 +411,14 @@ Accruing a machine's sessions: |
| set +a` — or anywhere with equivalent PutObject rights). Sync **only your |
| own prefix**; the manifest upload is a `cp`, never a sync with `--delete` |
| at the `prompts/` root, which would erase other contributors. |
| -3. `prompts.html` itself is a committed viewer page: copy it to the box's |
| +3. Commit the hash ledger entry (the tool prints the exact command): the |
| + corpus's `manifest.sha256.json` goes to the **public** |
| + `elevenmessenger/releases` repo at `prompt-registry/<slug>.json` via |
| + `gh api -X PUT`. Its git history is the registry's tamper-evidence — a |
| + few KB per accrual, and any after-the-fact change to a published |
| + transcript shows as a dated hash change. (The manifest also uploads with |
| + the corpus, so the bucket is self-describing.) |
| +4. `prompts.html` itself is a committed viewer page: copy it to the box's |
| `/opt/lchat/status-pages/` like the other pages; the 5-minute publish |
| re-uploads it. |
diff --git a/tools/prompt-registry.mjs b/tools/prompt-registry.mjs| index f3f6932..02b3e48 100644 |
| --- a/tools/prompt-registry.mjs |
| +++ b/tools/prompt-registry.mjs |
| @@ -76,6 +76,7 @@ |
| // prefixes) are the real net for labelled secrets. |
| import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync, rmSync, renameSync, statSync } from "node:fs"; |
| import { execSync } from "node:child_process"; |
| +import { createHash } from "node:crypto"; |
| import { join, basename } from "node:path"; |
| import os from "node:os"; |
| @@ -570,6 +571,19 @@ if (!checkOnly) { |
| console.error("\n" + bad + " file(s) failed post-redaction scan — corpus NOT published, left in " + tmp); |
| process.exit(1); |
| } |
| + // The hash ledger entry: sha256 of every published file under this slug, |
| + // committed to the PUBLIC elevenmessenger/releases repo (its git history is |
| + // the tamper-evidence — see docs/status.md § The prompt registry). Written |
| + // after verification so it hashes exactly what publishes; also uploaded |
| + // with the corpus so the bucket is self-describing. |
| + const hashed = {}; |
| + for (const f of readdirSync(join(tmp, "prompts", slug)).sort()) { |
| + hashed[f] = "sha256:" + createHash("sha256").update(readFileSync(join(tmp, "prompts", slug, f))).digest("hex"); |
| + } |
| + const ledger = { generated_at: index.generated_at, contributor, slug, |
| + totals: index.totals, files: hashed }; |
| + writeFileSync(join(tmp, "prompts", slug, "manifest.sha256.json"), JSON.stringify(ledger, null, 1)); |
| + |
| rmSync(join(outRoot, "prompts"), { recursive: true, force: true }); |
| mkdirSync(outRoot, { recursive: true }); |
| renameSync(join(tmp, "prompts"), join(outRoot, "prompts")); |
| @@ -587,4 +601,9 @@ if (!checkOnly) { |
| (manifestLive |
| ? "\n aws s3 cp " + join(outRoot, "prompts", "contributors.json") + " s3://<status-bucket>/prompts/contributors.json --cache-control max-age=300" |
| : "\n ⚠ manifest built OFFLINE — do NOT upload contributors.json (it would drop other contributors); re-run online first")); |
| + console.log("ledger (public tamper-evidence — commit the hash manifest to elevenmessenger/releases):" + |
| + "\n gh api -X PUT repos/elevenmessenger/releases/contents/prompt-registry/" + slug + ".json" + |
| + " -f message='prompt-registry: accrue " + slug + "'" + |
| + " -f content=\"$(base64 -i " + join(outRoot, "prompts", slug, "manifest.sha256.json") + ")\"" + |
| + " -f sha=\"$(gh api repos/elevenmessenger/releases/contents/prompt-registry/" + slug + ".json --jq .sha 2>/dev/null || true)\""); |
| } |
diff --git a/web/status/prompts.html b/web/status/prompts.html| index b9363fd..1934505 100644 |
| --- a/web/status/prompts.html |
| +++ b/web/status/prompts.html |
| @@ -156,6 +156,13 @@ |
| <dl class="legend" id="legend"></dl> |
| </details> |
| + <p class="meta">🤖 Verifying the record: each contributor's corpus carries a |
| + <code>manifest.sha256.json</code> naming the hash of every published file, and its |
| + history in the public |
| + <a href="https://github.com/elevenmessenger/releases/commits/main/prompt-registry">releases |
| + repo's hash ledger</a> is the tamper-evidence — if a published transcript ever |
| + changed after the fact, that history shows when.</p> |
| + |
| <div class="card totals" id="totals" aria-live="polite"></div> |
| <h2>Sessions</h2> |