magenta-canon 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -236,6 +236,7 @@ magenta-canon sentinel repository # invariant-check a git repository
236
236
  magenta-canon sentinel artifact <tgz|dir> # invariant-check a packed artifact
237
237
  magenta-canon sentinel pack # npm pack, then check the result
238
238
  magenta-canon sentinel witness --ledger ledger.jsonl # witness rotation-authority check
239
+ magenta-canon sentinel ledger ledger.jsonl # ledger truth: sequence, chains, Merkle, checkpoints (new in 0.7.0)
239
240
  magenta-canon sentinel invariants # list the invariant registry
240
241
  ```
241
242
 
@@ -246,6 +247,17 @@ retired-key resurrection / pre-activation refusal, and keystore↔ledger
246
247
  agreement — re-derived independently from the published wire formats, so it
247
248
  can disagree with a wrong (or compromised) server.
248
249
 
250
+ The **Ledger Sentinel** (MC-LEDGER-001…021, new in 0.7.0) is the independent
251
+ judge of ledger truth: contiguous receipt sequence, hash-chain and issuer
252
+ signatures, RFC 6962 Merkle re-derivation, checkpoint monotonicity and
253
+ equivocation refusal, corruption fail-closed vs bounded crash-tail
254
+ quarantine, writer/lock authority (never auto-stolen), witness agreement,
255
+ and mirror consistency. It reports five distinct outcomes — integrity /
256
+ origin (pinned anchor only) / operational exclusivity (lock metadata only) /
257
+ not-evaluated / violation — and is the contract any future storage backend
258
+ (PostgreSQL, hosted) must satisfy before carrying authority
259
+ ([`docs/LEDGER_SENTINEL.md`](docs/LEDGER_SENTINEL.md), ships in the package).
260
+
249
261
  Sentinels are read-only, redacting (paths + fingerprints, never secret
250
262
  bodies), deterministic, network-free, and emit `magenta-sentinel-violation/1`
251
263
  evidence records; exit codes map to a scoped promotion decision (`eligible` /
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * magenta-canon — CLI entry point for the open-source reference implementation.
4
4
  *
@@ -3,7 +3,7 @@
3
3
  "scripts/demo-control-plane.js": "3a559f39d17c1a403988e731e053d0d071cbd41ab595fd6246910f852337ab2f",
4
4
  "scripts/intake-cli.js": "189014db22d6fccb034ed1a93ec11efe8905be820bc468366c68bb2cabaf8a97",
5
5
  "scripts/magenta-mirror.js": "cf701065f7a6e20f7f44a9b815396aeb5fe031c3f003bcd793b8014ea8801b85",
6
- "scripts/magenta-sentinel.js": "0280d7584524201551c61a93beb1f2ee4964e76b0e44eb7c9d15a0f5a95cafdc",
6
+ "scripts/magenta-sentinel.js": "74336873d7f7c10825750e34a79988497ba793df593587f1c971b494a4d29f5f",
7
7
  "scripts/magenta-verify.js": "0bb65ef6ff1350789eecc4f0434ca94dcf3a89930f8ef1d62cc38100e18094ba",
8
8
  "scripts/mcp-gateway.js": "335923004b73511fe42ea0fc6f2e567afe8018dc95464a79027e4c2537e30de1",
9
9
  "server/agent-policy.js": "20dd9150f8244c33aaf14ecdf3a09f471210960d246cda9ab8d5abe0e8433518",
@@ -26,6 +26,7 @@
26
26
  "server/ledger.js": "9bf8f132e08d636070d2ede568b2eaf75810be245c90ecd5979d2d4de69af934",
27
27
  "server/persistence.js": "2e6b1d0b1c74babbd581780b028c2593256c5ec96c2d60f4c25159e3f54e4e3f",
28
28
  "server/sentinel/artifact-sentinel.js": "93bae917af313c9247f8062af148e7c25ccfd16d0507fa85e755c64056715f8d",
29
+ "server/sentinel/ledger-sentinel.js": "37d1c7040ae2506454247d40b42bad6076273b577190eeb3a6571184849b1e71",
29
30
  "server/sentinel/promotion.js": "68ac5e54eb8c07d5c75ba0d2325d595b1583557840cc46322b2053dd9924a0be",
30
31
  "server/sentinel/repository-sentinel.js": "722e973c8860cf281b29db8c4a22ec3cf473afcb088247916e8278d5e11638cf",
31
32
  "server/sentinel/rules.js": "ab24435df4f6f5b10f8b721fde37fe7fa894453648f17f04923cab56020c459f",
@@ -39,7 +40,7 @@
39
40
  "server/witness.js": "b1a8209d4dae4ffafc3ca4a158474b369250472ba80cef03bd92048282be91a4",
40
41
  "shared/canonical.js": "476cee4b5c5702e8a2a198e79c2639cf8ff84000ffb92f68a04433ed2a2f5484",
41
42
  "shared/certificates.js": "7844e71a38e1b1324586c7d054b2f5c15222b86446318d1e6dea9bab504a4a73",
42
- "shared/invariants.js": "414a2aac034c9b28d0e32fc4afefb9b9ab723ed465b4aba1f8da98dab08363d4",
43
+ "shared/invariants.js": "7cde6f65dc014d9dd09849281734660369cbb0ea0f34faa583671efd2522d0c0",
43
44
  "shared/schema.js": "f89190990e3826e44c722d5e8f5b39fd59b4d3fb9ec047229930a17604e4646c",
44
45
  "shared/terminating-kernel.js": "8e2aa68d47d6780e4a70822482e3e410924e9d1af843f301a487da0af37ed047"
45
46
  }
@@ -43,6 +43,7 @@ const path = __importStar(require("node:path"));
43
43
  const repository_sentinel_1 = require("../server/sentinel/repository-sentinel");
44
44
  const artifact_sentinel_1 = require("../server/sentinel/artifact-sentinel");
45
45
  const witness_sentinel_1 = require("../server/sentinel/witness-sentinel");
46
+ const ledger_sentinel_1 = require("../server/sentinel/ledger-sentinel");
46
47
  const promotion_1 = require("../server/sentinel/promotion");
47
48
  const invariants_1 = require("../shared/invariants");
48
49
  const USAGE = `magenta-sentinel — deterministic invariant evaluation (Sentinel Mesh foundation)
@@ -53,6 +54,9 @@ Usage:
53
54
  sentinel pack [--json]
54
55
  sentinel witness --ledger <file> [--keystore <file>] [--passphrase-env <VAR>]
55
56
  [--anchor <pubkey-hex>] [--json]
57
+ sentinel ledger <ledger.jsonl> [--anchor <pubkey-hex>] [--keystore <file>]
58
+ [--passphrase-env <VAR>] [--mirror <mirror.jsonl>]
59
+ [--no-lock-inspection] [--json]
56
60
  sentinel invariants [--json]
57
61
 
58
62
  Exit: 0 eligible/diagnostic-only · 1 blocked · 2 requires review · 3 usage/unreadable
@@ -169,6 +173,36 @@ try {
169
173
  }
170
174
  process.exit(exitFor(promo));
171
175
  }
176
+ else if (sub === "ledger") {
177
+ const target = argv.find((a) => !a.startsWith("-"));
178
+ if (!target) {
179
+ console.error("sentinel ledger: missing <ledger.jsonl>\n\n" + USAGE);
180
+ process.exit(3);
181
+ }
182
+ const keystore = option(argv, "--keystore");
183
+ const mirror = option(argv, "--mirror");
184
+ const res = (0, ledger_sentinel_1.runLedgerSentinel)({
185
+ ledgerPath: path.resolve(target),
186
+ anchorPubkey: option(argv, "--anchor") ?? option(argv, "--expected-witness-key"),
187
+ keystorePath: keystore ? path.resolve(keystore) : undefined,
188
+ passphraseEnv: option(argv, "--passphrase-env"),
189
+ mirrorPath: mirror ? path.resolve(mirror) : undefined,
190
+ inspectLock: !flag(argv, "--no-lock-inspection"),
191
+ });
192
+ const promo = (0, promotion_1.decidePromotion)({ subject: `ledger:${res.subject_identity}`, findings: res.findings, violations: res.violations });
193
+ if (json)
194
+ console.log(JSON.stringify({ result: res, promotion: promo }, null, 2));
195
+ else {
196
+ printHuman("ledger", `${res.subject_identity.slice(0, 26)}… (${res.record_count} records: ${res.receipt_count} receipts, ${res.checkpoint_count} STH, ${res.rotation_count} rotation(s))`, res, promo);
197
+ for (const n of res.not_evaluated)
198
+ console.log(` [NOT EVALUATED] ${n.invariant_id}: ${n.reason}`);
199
+ if (res.incomplete_tail_quarantined)
200
+ console.log(" note: incomplete trailing append quarantined (carries no authority)");
201
+ if (res.witness_cross_run)
202
+ console.log(` witness cross-run: ${res.witness_cross_run.failed.length === 0 ? "agrees" : "DISAGREES"} (tip ${res.witness_cross_run.tip_fingerprint ?? "-"})`);
203
+ }
204
+ process.exit(exitFor(promo));
205
+ }
172
206
  else if (sub === "invariants") {
173
207
  if (json)
174
208
  console.log(JSON.stringify(invariants_1.INVARIANTS, null, 2));
@@ -0,0 +1,408 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runLedgerSentinel = exports.LEDGER_SENTINEL_VERSION = exports.LEDGER_SENTINEL_TYPE = void 0;
7
+ /**
8
+ * LEDGER SENTINEL — deterministic evaluation of ledger-truth invariants
9
+ * (MC-LEDGER-001…021) over a durable evidence ledger.
10
+ *
11
+ * The Ledger Sentinel defines what a lawful Magenta evidence ledger looks
12
+ * like. The File Ledger satisfies it today; Postgres, hosted ledgers, and
13
+ * mirrors must satisfy the SAME family later — the implementation is never
14
+ * its own judge.
15
+ *
16
+ * INDEPENDENCE: this module parses the JSONL ledger itself and re-derives
17
+ * every claim from the published wire formats — receipt chain
18
+ * (chainHash(prev, execution_hash)), execution-hash recomputation, issuer
19
+ * signatures (Ed25519 over the canonical receipt body, verified directly
20
+ * with tweetnacl), RFC 6962 Merkle roots (0x00 leaf / 0x01 node domain
21
+ * separation, re-implemented here on node:crypto), checkpoint accounting,
22
+ * and tail/corruption classification. It deliberately does NOT call
23
+ * FileLedgerStore.replay(), LedgerStore.importState(),
24
+ * TransparencyLog.rebuildLeaves(), or MemStorage.
25
+ *
26
+ * Shared primitives (documented, spec-level only):
27
+ * - shared/canonical.ts canonicalHash/chainHash/GENESIS_HASH — canonical
28
+ * JSON hashing IS the published protocol spec; the standalone verifier
29
+ * shares it for the same reason.
30
+ * - tweetnacl — the Ed25519 primitive itself.
31
+ * - witness-sentinel — a SIBLING in the same independent layer; the ledger
32
+ * sentinel cross-runs it for epoch/rotation agreement (MC-LEDGER-019)
33
+ * rather than re-implementing the witness model a third time.
34
+ *
35
+ * Read-only (MC-REL-004), redacting, deterministic, network-free.
36
+ */
37
+ const node_crypto_1 = require("node:crypto");
38
+ const node_fs_1 = require("node:fs");
39
+ const tweetnacl_1 = __importDefault(require("tweetnacl"));
40
+ const canonical_1 = require("../../shared/canonical");
41
+ const invariants_1 = require("../../shared/invariants");
42
+ const violation_1 = require("./violation");
43
+ const witness_sentinel_1 = require("./witness-sentinel");
44
+ exports.LEDGER_SENTINEL_TYPE = "ledger-sentinel";
45
+ exports.LEDGER_SENTINEL_VERSION = "1.0.0";
46
+ const SUPPORTED_RECORD_VERSION = 1;
47
+ const KNOWN_TYPES = new Set(["header", "receipt", "sth", "rotation"]);
48
+ // ── independent crypto (published wire formats only) ─────────────────────
49
+ const sha256 = (b) => (0, node_crypto_1.createHash)("sha256").update(b).digest("hex");
50
+ function fromHex(hex) {
51
+ const out = new Uint8Array(hex.length / 2);
52
+ for (let i = 0; i < out.length; i++)
53
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
54
+ return out;
55
+ }
56
+ function verifyDetachedUtf8(message, signatureHex, pubkeyHex) {
57
+ try {
58
+ return tweetnacl_1.default.sign.detached.verify(new TextEncoder().encode(message), fromHex(signatureHex), fromHex(pubkeyHex));
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ /** RFC 6962 (re-implemented): SHA256(0x00 || utf8(receipt_hash)) leaves,
65
+ * SHA256(0x01 || left || right) nodes, odd node promoted, empty = SHA256(""). */
66
+ function leafHash(receiptHash) {
67
+ return sha256(Buffer.concat([Buffer.from([0x00]), Buffer.from(receiptHash, "utf8")]));
68
+ }
69
+ function rootOver(leaves) {
70
+ if (leaves.length === 0)
71
+ return sha256(Buffer.alloc(0));
72
+ let level = leaves.slice();
73
+ while (level.length > 1) {
74
+ const next = [];
75
+ for (let i = 0; i < level.length; i += 2) {
76
+ if (i + 1 < level.length) {
77
+ next.push(sha256(Buffer.concat([Buffer.from([0x01]), Buffer.from(level[i], "hex"), Buffer.from(level[i + 1], "hex")])));
78
+ }
79
+ else
80
+ next.push(level[i]);
81
+ }
82
+ level = next;
83
+ }
84
+ return level[0];
85
+ }
86
+ const fp = (hex) => "key:" + (0, node_crypto_1.createHash)("sha256").update(hex, "utf8").digest("hex").slice(0, 16);
87
+ function finding(invId, observed, expected, paths) {
88
+ const inv = (0, invariants_1.getInvariant)(invId);
89
+ return {
90
+ invariant_id: inv.id, invariant_version: inv.version, severity: inv.severity,
91
+ authority_class: inv.authorityClass, disposition: inv.disposition,
92
+ observed, expected, paths: paths.sort(),
93
+ };
94
+ }
95
+ function pidAliveOnThisHost(pid) {
96
+ try {
97
+ // /proc check covers Linux; zombie state counts as dead-for-writing.
98
+ const stat = (0, node_fs_1.readFileSync)(`/proc/${pid}/stat`, "utf8");
99
+ const state = stat.slice(stat.lastIndexOf(")") + 2).trim().charAt(0);
100
+ return state !== "Z";
101
+ }
102
+ catch {
103
+ try {
104
+ process.kill(pid, 0);
105
+ return true;
106
+ }
107
+ catch (e) {
108
+ return e.code === "EPERM" ? true : false;
109
+ }
110
+ }
111
+ }
112
+ function runLedgerSentinel(opts) {
113
+ if (!(0, node_fs_1.existsSync)(opts.ledgerPath))
114
+ throw new Error(`ledger-sentinel: ledger not found: ${opts.ledgerPath}`);
115
+ const raw = (0, node_fs_1.readFileSync)(opts.ledgerPath);
116
+ const subjectIdentity = `ledger:${sha256(raw)}`;
117
+ const text = raw.toString("utf8");
118
+ const findings = [];
119
+ const notEvaluated = [];
120
+ // ── parse with bounded tail quarantine (MC-LEDGER-013) ────────────────
121
+ const lines = text.split("\n");
122
+ const incompleteTail = lines.length > 0 && lines[lines.length - 1] !== "";
123
+ const complete = incompleteTail ? lines.slice(0, -1) : lines;
124
+ const records = [];
125
+ let recordNo = 0;
126
+ const MAX_RECORD_BYTES = 2 * 1024 * 1024; // file-ledger bounds records at 1MiB; 2MiB is hard refusal
127
+ for (const line of complete) {
128
+ if (line === "")
129
+ continue;
130
+ recordNo++;
131
+ let rec = null;
132
+ let framingOk = true;
133
+ let defect;
134
+ if (Buffer.byteLength(line, "utf8") > MAX_RECORD_BYTES) {
135
+ framingOk = false;
136
+ defect = `record exceeds the ${MAX_RECORD_BYTES}-byte bound`;
137
+ }
138
+ else {
139
+ try {
140
+ rec = JSON.parse(line);
141
+ }
142
+ catch {
143
+ framingOk = false;
144
+ defect = "not valid JSON (complete record)";
145
+ }
146
+ }
147
+ if (framingOk) {
148
+ if (rec.v !== SUPPORTED_RECORD_VERSION) {
149
+ framingOk = false;
150
+ defect = `unsupported record version ${rec.v}`;
151
+ }
152
+ else if (typeof rec.t !== "string") {
153
+ framingOk = false;
154
+ defect = "missing record type";
155
+ }
156
+ else if (!KNOWN_TYPES.has(rec.t)) {
157
+ framingOk = false;
158
+ defect = `unknown record type '${rec.t}'`;
159
+ }
160
+ else if (rec.t !== "header" && (0, canonical_1.canonicalHash)(rec.payload) !== rec.payload_hash) {
161
+ framingOk = false;
162
+ defect = "payload_hash mismatch (record altered on disk)";
163
+ }
164
+ }
165
+ records.push({ recordNo, t: rec?.t ?? "?", v: rec?.v ?? -1, seq: rec?.seq, payload: rec?.payload, payload_hash: rec?.payload_hash, framingOk, framingDefect: defect });
166
+ }
167
+ const badFraming = records.filter((r) => !r.framingOk);
168
+ for (const b of badFraming) {
169
+ const isUnknownType = (b.framingDefect ?? "").startsWith("unknown record type");
170
+ findings.push(finding(isUnknownType ? "MC-LEDGER-020" : "MC-LEDGER-003", `record #${b.recordNo}: ${b.framingDefect}`, (0, invariants_1.getInvariant)(isUnknownType ? "MC-LEDGER-020" : "MC-LEDGER-003").statement, [`record#${b.recordNo}`]));
171
+ }
172
+ // Complete-but-invalid records anywhere = corruption fails closed.
173
+ if (badFraming.length > 0) {
174
+ findings.push(finding("MC-LEDGER-012", `ledger contains ${badFraming.length} complete-but-invalid committed record(s) — must be refused, never skipped`, (0, invariants_1.getInvariant)("MC-LEDGER-012").statement, badFraming.map((b) => `record#${b.recordNo}`)));
175
+ }
176
+ const good = records.filter((r) => r.framingOk);
177
+ const receipts = good.filter((r) => r.t === "receipt");
178
+ const sths = good.filter((r) => r.t === "sth");
179
+ const rotations = good.filter((r) => r.t === "rotation");
180
+ // ── MC-LEDGER-001: contiguous sequence ────────────────────────────────
181
+ const seqProblems = [];
182
+ receipts.forEach((r, i) => {
183
+ if (r.seq !== i + 1)
184
+ seqProblems.push(`record#${r.recordNo} seq=${r.seq} expected=${i + 1}`);
185
+ });
186
+ if (seqProblems.length > 0) {
187
+ findings.push(finding("MC-LEDGER-001", `sequence not contiguous: ${seqProblems.join("; ")}`, "receipt sequence is exactly 1..N in ledger order", seqProblems.map((s) => s.split(" ")[0])));
188
+ }
189
+ // ── MC-LEDGER-002: duplicate receipt identity ─────────────────────────
190
+ const seenHashes = new Map();
191
+ for (const r of receipts) {
192
+ const h = r.payload.receipt_hash;
193
+ const prev = seenHashes.get(h);
194
+ if (prev !== undefined) {
195
+ findings.push(finding("MC-LEDGER-002", `duplicate receipt_hash at record#${prev} and record#${r.recordNo}`, "each receipt is unique under its canonical identity", [`record#${prev}`, `record#${r.recordNo}`]));
196
+ }
197
+ else
198
+ seenHashes.set(h, r.recordNo);
199
+ }
200
+ // ── MC-LEDGER-004/005/006: chain, hash, signature ─────────────────────
201
+ let prevHash = canonical_1.GENESIS_HASH;
202
+ for (const r of receipts) {
203
+ const rec = r.payload;
204
+ if (rec.previous_receipt_hash !== prevHash) {
205
+ findings.push(finding("MC-LEDGER-004", `record#${r.recordNo}: previous_receipt_hash does not link to predecessor`, "every receipt links to the exact predecessor (genesis first)", [`record#${r.recordNo}`]));
206
+ }
207
+ const expectedExecution = (0, canonical_1.canonicalHash)({ action_hash: rec.action_hash, decision_hash: rec.decision_hash, timestamp: rec.timestamp });
208
+ if (rec.execution_hash !== expectedExecution || rec.receipt_hash !== (0, canonical_1.chainHash)(rec.previous_receipt_hash, rec.execution_hash)) {
209
+ findings.push(finding("MC-LEDGER-005", `record#${r.recordNo}: receipt/execution hash does not re-derive from the canonical body`, "receipt_hash = chainHash(prev, execution_hash); execution_hash re-derives", [`record#${r.recordNo}`]));
210
+ }
211
+ if (typeof rec.receipt_signature === "string" && rec.receipt_signature.length > 0) {
212
+ const { receipt_signature, ...body } = rec;
213
+ if (!verifyDetachedUtf8((0, canonical_1.canonicalHash)(body), receipt_signature, rec.issuer_pubkey)) {
214
+ findings.push(finding("MC-LEDGER-006", `record#${r.recordNo}: issuer signature INVALID under ${fp(rec.issuer_pubkey)}`, "every receipt signature verifies under its declared issuer key", [`record#${r.recordNo}`]));
215
+ }
216
+ }
217
+ else {
218
+ findings.push(finding("MC-LEDGER-006", `record#${r.recordNo}: durable receipt carries no issuer signature`, "every committed receipt is issuer-signed", [`record#${r.recordNo}`]));
219
+ }
220
+ prevHash = rec.receipt_hash;
221
+ }
222
+ // ── MC-LEDGER-007/008/009/010: tree & checkpoints ─────────────────────
223
+ // Leaves in commit order; accounting uses ledger positions.
224
+ const leafByPosition = [];
225
+ const receiptCountBefore = new Map(); // recordNo -> receipts committed before it
226
+ {
227
+ let count = 0;
228
+ for (const r of good) {
229
+ receiptCountBefore.set(r.recordNo, count);
230
+ if (r.t === "receipt") {
231
+ leafByPosition.push(leafHash(r.payload.receipt_hash));
232
+ count++;
233
+ }
234
+ }
235
+ }
236
+ const rootBySize = new Map();
237
+ let lastSize = -1;
238
+ for (const s of sths) {
239
+ const sth = s.payload;
240
+ const before = receiptCountBefore.get(s.recordNo);
241
+ if (!Number.isInteger(sth.tree_size) || sth.tree_size < 0 || sth.tree_size > before) {
242
+ findings.push(finding("MC-LEDGER-009", `record#${s.recordNo}: STH tree_size ${sth.tree_size} exceeds the ${before} receipts committed before it`, "tree_size never exceeds receipts committed at that point", [`record#${s.recordNo}`]));
243
+ continue;
244
+ }
245
+ const recomputed = rootOver(leafByPosition.slice(0, sth.tree_size));
246
+ if (recomputed !== sth.root_hash) {
247
+ findings.push(finding("MC-LEDGER-007", `record#${s.recordNo}: committed root at tree_size ${sth.tree_size} does not re-derive from the ledger's receipts`, "every signed root equals the independently recomputed Merkle root", [`record#${s.recordNo}`]));
248
+ }
249
+ const existing = rootBySize.get(sth.tree_size);
250
+ if (existing && existing.root !== sth.root_hash) {
251
+ findings.push(finding("MC-LEDGER-008", `two roots committed for tree_size ${sth.tree_size} (record#${existing.recordNo} vs record#${s.recordNo}) — equivocation at rest`, "at most one root per tree size", [`record#${existing.recordNo}`, `record#${s.recordNo}`]));
252
+ }
253
+ else if (!existing)
254
+ rootBySize.set(sth.tree_size, { root: sth.root_hash, recordNo: s.recordNo });
255
+ if (sth.tree_size < lastSize) {
256
+ findings.push(finding("MC-LEDGER-010", `record#${s.recordNo}: checkpoint tree_size ${sth.tree_size} regresses below ${lastSize}`, "checkpoint sizes never regress in ledger order", [`record#${s.recordNo}`]));
257
+ }
258
+ lastSize = Math.max(lastSize, sth.tree_size);
259
+ }
260
+ // Rotation boundary accounting (the witness model owns the rest).
261
+ for (const r of rotations) {
262
+ const before = receiptCountBefore.get(r.recordNo);
263
+ if (r.payload.effective_tree_size !== before) {
264
+ findings.push(finding("MC-LEDGER-009", `record#${r.recordNo}: rotation boundary ${r.payload.effective_tree_size} != ${before} receipts at its position`, "rotation boundary equals the receipt count at its ledger position", [`record#${r.recordNo}`]));
265
+ }
266
+ }
267
+ // ── MC-LEDGER-013/014: tail semantics ─────────────────────────────────
268
+ // An incomplete final line is lawful quarantine (013) and carries no
269
+ // authority (014). Execution ordering beyond bytes: not-evaluated.
270
+ notEvaluated.push({
271
+ invariant_id: "MC-LEDGER-014",
272
+ reason: "execution/evidence ordering beyond the ledger bytes is runtime context; the quarantined tail (if any) was verified to carry no authority",
273
+ });
274
+ // ── MC-LEDGER-015/016: writer & lock (operational metadata) ───────────
275
+ const lockPath = `${opts.ledgerPath}.lock`;
276
+ if (opts.inspectLock === false || !(0, node_fs_1.existsSync)(lockPath)) {
277
+ notEvaluated.push({
278
+ invariant_id: "MC-LEDGER-015",
279
+ reason: opts.inspectLock === false ? "lock inspection disabled" : "no lock file present (offline artifact or writer cleanly absent)",
280
+ });
281
+ notEvaluated.push({ invariant_id: "MC-LEDGER-016", reason: "no lock metadata to evaluate" });
282
+ }
283
+ else {
284
+ try {
285
+ // Published lock format: {pid, hostname, created, nonce, ledger}.
286
+ const lock = JSON.parse((0, node_fs_1.readFileSync)(lockPath, "utf8"));
287
+ if (typeof lock.pid !== "number" || typeof lock.hostname !== "string" || typeof lock.created !== "string") {
288
+ findings.push(finding("MC-LEDGER-016", "lock file metadata malformed (missing pid/hostname/created)", "lock metadata must positively identify its holder", [lockPath]));
289
+ }
290
+ else {
291
+ const alive = pidAliveOnThisHost(lock.pid);
292
+ if (alive === false) {
293
+ notEvaluated.push({
294
+ invariant_id: "MC-LEDGER-016",
295
+ reason: `lock holder pid ${lock.pid}@${lock.hostname} not found alive on this host — stale-lock recovery is a deliberate OPERATOR action; the sentinel never removes locks`,
296
+ });
297
+ }
298
+ // A live holder is lawful single-writer state; nothing to flag.
299
+ }
300
+ }
301
+ catch {
302
+ findings.push(finding("MC-LEDGER-016", "lock file is not valid JSON (forged or damaged lock metadata)", "lock metadata must positively identify its holder", [lockPath]));
303
+ }
304
+ }
305
+ // Configuration authority is runtime context.
306
+ notEvaluated.push({ invariant_id: "MC-LEDGER-017", reason: "configuration (env) is runtime context, not ledger bytes" });
307
+ notEvaluated.push({ invariant_id: "MC-LEDGER-018", reason: "file-ledger subjects carry OS-lock semantics; fencing tokens apply to future stores" });
308
+ // ── MC-LEDGER-019: witness agreement (cross-run the sibling sentinel) ──
309
+ let witnessCross = null;
310
+ if (badFraming.length === 0) {
311
+ try {
312
+ const w = (0, witness_sentinel_1.runWitnessSentinel)({
313
+ ledgerPath: opts.ledgerPath,
314
+ keystorePath: opts.keystorePath,
315
+ passphraseEnv: opts.passphraseEnv,
316
+ anchorPubkey: opts.anchorPubkey,
317
+ detectedAt: opts.detectedAt,
318
+ detector: opts.detector,
319
+ });
320
+ witnessCross = { failed: w.failed, tip_fingerprint: w.tip_fingerprint };
321
+ if (w.failed.length > 0) {
322
+ findings.push(finding("MC-LEDGER-019", `witness authority model disagrees with this ledger: ${w.failed.join(", ")}`, "ledger and witness views of the same bytes agree", w.failed));
323
+ }
324
+ }
325
+ catch (e) {
326
+ findings.push(finding("MC-LEDGER-019", `witness cross-run refused the subject: ${e.message}`, "ledger and witness views of the same bytes agree", ["witness-cross-run"]));
327
+ }
328
+ }
329
+ else {
330
+ notEvaluated.push({ invariant_id: "MC-LEDGER-019", reason: "framing corruption present; witness cross-run skipped (subject already refused)" });
331
+ }
332
+ // ── MC-LEDGER-021: mirror consistency ─────────────────────────────────
333
+ if (!opts.mirrorPath) {
334
+ notEvaluated.push({ invariant_id: "MC-LEDGER-021", reason: "no mirror file supplied" });
335
+ }
336
+ else if (!(0, node_fs_1.existsSync)(opts.mirrorPath)) {
337
+ throw new Error(`ledger-sentinel: mirror not found: ${opts.mirrorPath}`);
338
+ }
339
+ else {
340
+ const mirrorLines = (0, node_fs_1.readFileSync)(opts.mirrorPath, "utf8").split("\n").filter((l) => l !== "");
341
+ for (let i = 0; i < mirrorLines.length; i++) {
342
+ let m;
343
+ try {
344
+ m = JSON.parse(mirrorLines[i]);
345
+ }
346
+ catch {
347
+ continue;
348
+ }
349
+ if (typeof m.tree_size !== "number" || typeof m.root_hash !== "string")
350
+ continue;
351
+ if (m.tree_size > leafByPosition.length) {
352
+ findings.push(finding("MC-LEDGER-021", `mirror observed tree_size ${m.tree_size} beyond this ledger's ${leafByPosition.length} receipts (mirror ahead or ledger truncated)`, "mirrored checkpoints never contradict committed history", [`mirror#${i + 1}`]));
353
+ continue;
354
+ }
355
+ const localRoot = rootOver(leafByPosition.slice(0, m.tree_size));
356
+ if (localRoot !== m.root_hash) {
357
+ findings.push(finding("MC-LEDGER-021", `mirror root at tree_size ${m.tree_size} contradicts the ledger's recomputed root`, "mirrored (size, root) pairs match committed history", [`mirror#${i + 1}`]));
358
+ }
359
+ else if (!rootBySize.has(m.tree_size)) {
360
+ // The mirror observed a checkpoint this ledger no longer carries —
361
+ // a committed STH was deleted even though the receipts survive.
362
+ findings.push(finding("MC-LEDGER-021", `mirror observed a checkpoint at tree_size ${m.tree_size} that is absent from the ledger's committed STH history (checkpoint deletion)`, "every mirrored checkpoint exists in committed history", [`mirror#${i + 1}`]));
363
+ }
364
+ }
365
+ }
366
+ // ── MC-LEDGER-011: deterministic replay (self-attested here; the cross-
367
+ // implementation agreement is pinned by tests against the store and
368
+ // verifier — a finding appears only if THIS run detects divergence). ──
369
+ // (No runtime check beyond the above: every derived quantity used a single
370
+ // pass over the same bytes; tests prove three-way agreement.)
371
+ const ledgerInvariants = (0, invariants_1.ratifiedInvariants)().filter((i) => i.domain === "ledger").map((i) => i.id);
372
+ const notEvalIds = new Set(notEvaluated.map((n) => n.invariant_id));
373
+ const failed = Array.from(new Set(findings.map((f) => f.invariant_id))).sort();
374
+ const evaluated = ledgerInvariants.filter((id) => !notEvalIds.has(id) || failed.includes(id)).sort();
375
+ const passed = evaluated.filter((id) => !failed.includes(id)).sort();
376
+ const violations = findings.map((f) => (0, violation_1.buildViolationRecord)({
377
+ sentinelType: exports.LEDGER_SENTINEL_TYPE,
378
+ sentinelVersion: exports.LEDGER_SENTINEL_VERSION,
379
+ subjectType: "artifact",
380
+ subjectIdentity,
381
+ finding: f,
382
+ detectedAt: opts.detectedAt,
383
+ detector: opts.detector,
384
+ }));
385
+ const describe = (r) => r ? `#${r.recordNo}:${r.t}${r.t === "receipt" ? `(seq ${r.seq})` : ""}` : null;
386
+ return {
387
+ sentinel_type: exports.LEDGER_SENTINEL_TYPE,
388
+ sentinel_version: exports.LEDGER_SENTINEL_VERSION,
389
+ subject_type: "artifact",
390
+ subject_identity: subjectIdentity,
391
+ ledger_format: `magenta-file-ledger/v${SUPPORTED_RECORD_VERSION}`,
392
+ record_count: records.length,
393
+ receipt_count: receipts.length,
394
+ checkpoint_count: sths.length,
395
+ rotation_count: rotations.length,
396
+ first_record: describe(records[0]),
397
+ last_record: describe(records[records.length - 1]),
398
+ evaluated_invariants: evaluated,
399
+ not_evaluated: notEvaluated,
400
+ passed,
401
+ failed,
402
+ findings,
403
+ violations,
404
+ incomplete_tail_quarantined: incompleteTail,
405
+ witness_cross_run: witnessCross,
406
+ };
407
+ }
408
+ exports.runLedgerSentinel = runLedgerSentinel;
@@ -486,18 +486,408 @@ exports.INVARIANTS = [
486
486
  name: "reserved-postgres-ledger-invariants",
487
487
  version: 1,
488
488
  domain: "ledger",
489
- statement: "RESERVED: single-writer, fsync-equivalent durability, replay-equals-" +
490
- "file semantics for the Postgres ledger. Ratification happens in the " +
491
- "Postgres lane.",
489
+ statement: "RESERVED (superseded): ledger truth ratified as MC-LEDGER-001…021 by " +
490
+ "the Ledger Sentinel lane. Any future storage implementation (Postgres, " +
491
+ "hosted, mirror) must satisfy that family; it does not define its own.",
492
492
  severity: "high",
493
493
  enforcementPhase: ["runtime"],
494
494
  authorityClass: "none",
495
495
  disposition: "report",
496
496
  evidenceRequirements: [],
497
- remediation: "n/a (reserved)",
498
- owner: "postgres-lane",
499
- status: "reserved",
497
+ remediation: "n/a (superseded)",
498
+ owner: "ledger-sentinel",
499
+ status: "superseded",
500
500
  enforcedBy: [],
501
+ supersededBy: "MC-LEDGER-001",
502
+ }),
503
+ // ── Ledger truth (ratified by the Ledger Sentinel lane). The contract every
504
+ // storage implementation — File Ledger today, Postgres/hosted/mirrors
505
+ // later — must satisfy. The implementation is never its own judge. ──
506
+ inv({
507
+ id: "MC-LEDGER-001",
508
+ name: "contiguous-receipt-sequence",
509
+ version: 1,
510
+ domain: "ledger",
511
+ statement: "Committed receipt records carry sequence numbers exactly 1..N in ledger " +
512
+ "order — no gaps, no regressions, no duplicates, no unexplained holes.",
513
+ severity: "critical",
514
+ enforcementPhase: ["ci", "runtime"],
515
+ authorityClass: "none",
516
+ disposition: "block-release",
517
+ evidenceRequirements: ["record number", "expected vs observed seq"],
518
+ remediation: "Refuse the ledger; restore from a verified copy.",
519
+ owner: "ledger-sentinel",
520
+ status: "ratified",
521
+ enforcedBy: ["ledger-sentinel", "server/file-ledger-store.ts replay"],
522
+ supersedes: "MC-LEDGER-RSV-001",
523
+ }),
524
+ inv({
525
+ id: "MC-LEDGER-002",
526
+ name: "no-duplicate-receipt-identity",
527
+ version: 1,
528
+ domain: "ledger",
529
+ statement: "No two committed receipt records may share a receipt_hash; each durable " +
530
+ "receipt is unique under its canonical identity.",
531
+ severity: "critical",
532
+ enforcementPhase: ["ci", "runtime"],
533
+ authorityClass: "none",
534
+ disposition: "block-release",
535
+ evidenceRequirements: ["duplicate receipt_hash", "record numbers"],
536
+ remediation: "Refuse the ledger; investigate replay/duplication.",
537
+ owner: "ledger-sentinel",
538
+ status: "ratified",
539
+ enforcedBy: ["ledger-sentinel"],
540
+ }),
541
+ inv({
542
+ id: "MC-LEDGER-003",
543
+ name: "record-framing-integrity",
544
+ version: 1,
545
+ domain: "ledger",
546
+ statement: "Every committed ledger record is well-formed: supported version, known " +
547
+ "type, valid JSON framing, and payload_hash equal to the canonical hash " +
548
+ "of its payload. Altered records are refused, never repaired.",
549
+ severity: "critical",
550
+ enforcementPhase: ["ci", "runtime"],
551
+ authorityClass: "none",
552
+ disposition: "block-release",
553
+ evidenceRequirements: ["record number", "framing defect class"],
554
+ remediation: "Refuse the ledger; restore from a verified copy.",
555
+ owner: "ledger-sentinel",
556
+ status: "ratified",
557
+ enforcedBy: ["ledger-sentinel", "server/file-ledger-store.ts replay"],
558
+ }),
559
+ inv({
560
+ id: "MC-LEDGER-004",
561
+ name: "receipt-chain-continuity",
562
+ version: 1,
563
+ domain: "ledger",
564
+ statement: "Every receipt's previous_receipt_hash equals the preceding receipt's " +
565
+ "receipt_hash (genesis for the first) — removal, reordering, or " +
566
+ "insertion breaks the chain and is refused.",
567
+ severity: "critical",
568
+ enforcementPhase: ["ci", "runtime"],
569
+ authorityClass: "none",
570
+ disposition: "block-release",
571
+ evidenceRequirements: ["record number", "expected vs observed predecessor hash"],
572
+ remediation: "Refuse the ledger; audit against mirrors.",
573
+ owner: "ledger-sentinel",
574
+ status: "ratified",
575
+ enforcedBy: ["ledger-sentinel", "scripts/magenta-verify.ts"],
576
+ }),
577
+ inv({
578
+ id: "MC-LEDGER-005",
579
+ name: "receipt-hash-correctness",
580
+ version: 1,
581
+ domain: "ledger",
582
+ statement: "Every receipt_hash re-derives as chainHash(previous_receipt_hash, " +
583
+ "execution_hash), and execution_hash re-derives from the receipt's own " +
584
+ "action_hash, decision_hash, and timestamp.",
585
+ severity: "critical",
586
+ enforcementPhase: ["ci", "runtime"],
587
+ authorityClass: "none",
588
+ disposition: "block-release",
589
+ evidenceRequirements: ["record number", "expected vs observed hash"],
590
+ remediation: "Refuse the ledger.",
591
+ owner: "ledger-sentinel",
592
+ status: "ratified",
593
+ enforcedBy: ["ledger-sentinel", "scripts/magenta-verify.ts"],
594
+ }),
595
+ inv({
596
+ id: "MC-LEDGER-006",
597
+ name: "issuer-signature-validity",
598
+ version: 1,
599
+ domain: "ledger",
600
+ statement: "Every receipt's issuer signature verifies (Ed25519 over the canonical " +
601
+ "receipt body without the signature) under its declared issuer key. " +
602
+ "Recomputing hashes alone can never bypass signature validation.",
603
+ severity: "critical",
604
+ enforcementPhase: ["ci", "runtime"],
605
+ authorityClass: "issuer",
606
+ disposition: "require-independent-review",
607
+ evidenceRequirements: ["record number", "issuer key fingerprint"],
608
+ remediation: "Treat as forged evidence; freeze and review.",
609
+ owner: "ledger-sentinel",
610
+ status: "ratified",
611
+ enforcedBy: ["ledger-sentinel", "scripts/magenta-verify.ts"],
612
+ }),
613
+ inv({
614
+ id: "MC-LEDGER-007",
615
+ name: "merkle-root-rederivation",
616
+ version: 1,
617
+ domain: "ledger",
618
+ statement: "Every committed STH root_hash equals the independently recomputed " +
619
+ "RFC 6962 Merkle root over the first tree_size receipt leaves. No signed " +
620
+ "root may describe a different ledger.",
621
+ severity: "critical",
622
+ enforcementPhase: ["ci", "runtime"],
623
+ authorityClass: "witness",
624
+ disposition: "require-independent-review",
625
+ evidenceRequirements: ["STH record number", "expected vs observed root"],
626
+ remediation: "Refuse the ledger; audit against mirrors.",
627
+ owner: "ledger-sentinel",
628
+ status: "ratified",
629
+ enforcedBy: ["ledger-sentinel", "scripts/magenta-verify.ts", "file-ledger replay"],
630
+ }),
631
+ inv({
632
+ id: "MC-LEDGER-008",
633
+ name: "no-equivocation-at-rest",
634
+ version: 1,
635
+ domain: "ledger",
636
+ statement: "At most one root_hash may exist per tree_size across all committed " +
637
+ "checkpoints — two roots for one size is equivocation and is refused.",
638
+ severity: "critical",
639
+ enforcementPhase: ["ci", "runtime"],
640
+ authorityClass: "witness",
641
+ disposition: "freeze-authority-class",
642
+ evidenceRequirements: ["tree_size", "both conflicting roots", "record numbers"],
643
+ remediation: "Freeze ledger authority; independent review against mirrors.",
644
+ owner: "ledger-sentinel",
645
+ status: "ratified",
646
+ enforcedBy: ["ledger-sentinel", "file-ledger appendSth/replay", "magenta-mirror"],
647
+ }),
648
+ inv({
649
+ id: "MC-LEDGER-009",
650
+ name: "tree-size-accounting",
651
+ version: 1,
652
+ domain: "ledger",
653
+ statement: "Checkpoint and rotation accounting is exact: an STH's tree_size never " +
654
+ "exceeds the receipts committed before it, and a rotation's " +
655
+ "effective_tree_size equals the receipt count at its ledger position. " +
656
+ "Non-receipt records never contribute leaves.",
657
+ severity: "critical",
658
+ enforcementPhase: ["ci", "runtime"],
659
+ authorityClass: "none",
660
+ disposition: "block-release",
661
+ evidenceRequirements: ["record number", "claimed vs actual size"],
662
+ remediation: "Refuse the ledger.",
663
+ owner: "ledger-sentinel",
664
+ status: "ratified",
665
+ enforcedBy: ["ledger-sentinel", "file-ledger replay"],
666
+ }),
667
+ inv({
668
+ id: "MC-LEDGER-010",
669
+ name: "checkpoint-monotonicity",
670
+ version: 1,
671
+ domain: "ledger",
672
+ statement: "Committed checkpoint tree sizes never regress in ledger order: the " +
673
+ "authoritative head moves forward only.",
674
+ severity: "critical",
675
+ enforcementPhase: ["ci", "runtime"],
676
+ authorityClass: "witness",
677
+ disposition: "require-independent-review",
678
+ evidenceRequirements: ["record numbers", "sizes in violation"],
679
+ remediation: "Treat as rollback; freeze and review.",
680
+ owner: "ledger-sentinel",
681
+ status: "ratified",
682
+ enforcedBy: ["ledger-sentinel", "magenta-mirror (rollback refusal)"],
683
+ }),
684
+ inv({
685
+ id: "MC-LEDGER-011",
686
+ name: "deterministic-replay",
687
+ version: 1,
688
+ domain: "ledger",
689
+ statement: "Identical ledger bytes must replay — by any correct, independent " +
690
+ "implementation — to identical receipts, Merkle roots, checkpoint sets, " +
691
+ "and epoch interpretation. Divergent replay between the store, the " +
692
+ "verifier, and the sentinel is itself a violation.",
693
+ severity: "critical",
694
+ enforcementPhase: ["ci", "runtime"],
695
+ authorityClass: "none",
696
+ disposition: "block-release",
697
+ evidenceRequirements: ["diverging surface", "differing values"],
698
+ remediation: "Treat the ledger as ambiguous; refuse until resolved.",
699
+ owner: "ledger-sentinel",
700
+ status: "ratified",
701
+ enforcedBy: ["ledger-sentinel", "cross-implementation agreement tests"],
702
+ }),
703
+ inv({
704
+ id: "MC-LEDGER-012",
705
+ name: "corruption-fails-closed",
706
+ version: 1,
707
+ domain: "ledger",
708
+ statement: "A complete-but-invalid committed record anywhere in the ledger is " +
709
+ "refused: no skipping damaged records, no fresh blank universe, no " +
710
+ "memory fallback, no silent auto-discard of authoritative records.",
711
+ severity: "critical",
712
+ enforcementPhase: ["ci", "runtime"],
713
+ authorityClass: "none",
714
+ disposition: "block-release",
715
+ evidenceRequirements: ["record number", "corruption class"],
716
+ remediation: "Operator restores from a verified copy; never auto-repair.",
717
+ owner: "ledger-sentinel",
718
+ status: "ratified",
719
+ enforcedBy: ["ledger-sentinel", "file-ledger LedgerCorruptionError"],
720
+ }),
721
+ inv({
722
+ id: "MC-LEDGER-013",
723
+ name: "bounded-tail-quarantine",
724
+ version: 1,
725
+ domain: "ledger",
726
+ statement: "Only a provably incomplete FINAL append (no newline termination / " +
727
+ "truncated JSON at end-of-file) may be quarantined as a crash tail. A " +
728
+ "complete final record that is invalid fails closed like any other " +
729
+ "corruption.",
730
+ severity: "high",
731
+ enforcementPhase: ["ci", "runtime"],
732
+ authorityClass: "none",
733
+ disposition: "report",
734
+ evidenceRequirements: ["tail byte offset", "incompleteness proof"],
735
+ remediation: "Quarantine the tail; committed prefix remains authoritative.",
736
+ owner: "ledger-sentinel",
737
+ status: "ratified",
738
+ enforcedBy: ["ledger-sentinel", "file-ledger tail quarantine"],
739
+ }),
740
+ inv({
741
+ id: "MC-LEDGER-014",
742
+ name: "commit-before-authority",
743
+ version: 1,
744
+ domain: "ledger",
745
+ statement: "No authority attaches to an incomplete append: a crash before durable " +
746
+ "commit leaves no authoritative record, and where the protocol requires " +
747
+ "evidence-first semantics, execution must not outrun the durable record. " +
748
+ "(Execution ordering beyond the ledger bytes is reported not-evaluated " +
749
+ "in offline inspection — never silently passed.)",
750
+ severity: "critical",
751
+ enforcementPhase: ["runtime", "ci"],
752
+ authorityClass: "none",
753
+ disposition: "block-release",
754
+ evidenceRequirements: ["tail/ordering evidence where available"],
755
+ remediation: "Re-drive the action through the gate; never backfill evidence.",
756
+ owner: "ledger-sentinel",
757
+ status: "ratified",
758
+ enforcedBy: ["ledger-sentinel (tail)", "gate-first invariant (runtime)"],
759
+ }),
760
+ inv({
761
+ id: "MC-LEDGER-015",
762
+ name: "single-writer-exclusivity",
763
+ version: 1,
764
+ domain: "ledger",
765
+ statement: "Exactly one live writer may hold a ledger's write authority at a time; " +
766
+ "a second concurrent writer is refused. (Evaluable only with operational " +
767
+ "lock metadata; offline bundles report not-evaluated.)",
768
+ severity: "critical",
769
+ enforcementPhase: ["runtime", "ci"],
770
+ authorityClass: "operator",
771
+ disposition: "freeze-authority-class",
772
+ evidenceRequirements: ["lock holder identity", "liveness evidence"],
773
+ remediation: "Stop the rogue writer; verify ledger integrity afterward.",
774
+ owner: "ledger-sentinel",
775
+ status: "ratified",
776
+ enforcedBy: ["file-ledger O_EXCL lock", "ledger-sentinel (lock inspection)"],
777
+ }),
778
+ inv({
779
+ id: "MC-LEDGER-016",
780
+ name: "no-stale-lock-auto-theft",
781
+ version: 1,
782
+ domain: "ledger",
783
+ statement: "A ledger lock is never stolen automatically: recovery requires positive " +
784
+ "dead-holder evidence and a deliberate operator action.",
785
+ severity: "critical",
786
+ enforcementPhase: ["runtime", "ci"],
787
+ authorityClass: "operator",
788
+ disposition: "require-independent-review",
789
+ evidenceRequirements: ["lock metadata", "holder liveness determination"],
790
+ remediation: "Verify the holder is dead, then remove the lock manually.",
791
+ owner: "ledger-sentinel",
792
+ status: "ratified",
793
+ enforcedBy: ["file-ledger lock semantics", "ledger-sentinel (lock inspection)"],
794
+ }),
795
+ inv({
796
+ id: "MC-LEDGER-017",
797
+ name: "single-ledger-authority",
798
+ version: 1,
799
+ domain: "ledger",
800
+ statement: "Exactly one evidence authority may be configured: durable ledger and " +
801
+ "legacy state-file authority are mutually exclusive, and a configured-" +
802
+ "but-unavailable ledger fails startup rather than falling back. " +
803
+ "(Configuration is runtime context; offline inspection reports " +
804
+ "not-evaluated.)",
805
+ severity: "critical",
806
+ enforcementPhase: ["runtime"],
807
+ authorityClass: "operator",
808
+ disposition: "freeze-authority-class",
809
+ evidenceRequirements: ["conflicting configuration surfaces"],
810
+ remediation: "Fix the configuration; never run with ambiguous authority.",
811
+ owner: "ledger-sentinel",
812
+ status: "ratified",
813
+ enforcedBy: ["server/witness.ts config guards", "server/ledger.ts"],
814
+ }),
815
+ inv({
816
+ id: "MC-LEDGER-018",
817
+ name: "writer-fencing-contract",
818
+ version: 1,
819
+ domain: "ledger",
820
+ statement: "CONTRACT for future storage implementations (Postgres, hosted): write " +
821
+ "authority must be fenced (token/lease) such that a paused or partitioned " +
822
+ "old writer cannot commit after a new writer is authorized. The File " +
823
+ "Ledger satisfies the same intent via OS-exclusive locks; subjects " +
824
+ "without fencing metadata report not-evaluated.",
825
+ severity: "critical",
826
+ enforcementPhase: ["runtime"],
827
+ authorityClass: "operator",
828
+ disposition: "freeze-authority-class",
829
+ evidenceRequirements: ["fencing token/lease evidence"],
830
+ remediation: "Implement fencing before granting write authority.",
831
+ owner: "postgres-lane (future) / ledger-sentinel (contract)",
832
+ status: "ratified",
833
+ enforcedBy: ["contract — enforced when a fencing-capable store exists"],
834
+ }),
835
+ inv({
836
+ id: "MC-LEDGER-019",
837
+ name: "ledger-witness-agreement",
838
+ version: 1,
839
+ domain: "ledger",
840
+ statement: "The ledger's committed rotation placement and epoch interpretation must " +
841
+ "agree with the witness authority model (MC-WIT-001…011) for the same " +
842
+ "bytes; disagreement between the ledger view and the witness view is a " +
843
+ "violation of the whole subject.",
844
+ severity: "critical",
845
+ enforcementPhase: ["ci", "runtime"],
846
+ authorityClass: "witness",
847
+ disposition: "require-independent-review",
848
+ evidenceRequirements: ["witness-sentinel finding refs"],
849
+ remediation: "Freeze and review; audit against mirrors.",
850
+ owner: "ledger-sentinel",
851
+ status: "ratified",
852
+ enforcedBy: ["ledger-sentinel (cross-runs witness-sentinel)"],
853
+ }),
854
+ inv({
855
+ id: "MC-LEDGER-020",
856
+ name: "authority-record-typing",
857
+ version: 1,
858
+ domain: "ledger",
859
+ statement: "Every committed record carries a known, validated authority type " +
860
+ "(header, receipt, sth, rotation — and future typed Sentinel records). " +
861
+ "An unknown type bearing authority-shaped content is refused, not " +
862
+ "ignored.",
863
+ severity: "high",
864
+ enforcementPhase: ["ci", "runtime"],
865
+ authorityClass: "none",
866
+ disposition: "block-release",
867
+ evidenceRequirements: ["record number", "unknown type"],
868
+ remediation: "Refuse the ledger; classify the record first.",
869
+ owner: "ledger-sentinel",
870
+ status: "ratified",
871
+ enforcedBy: ["ledger-sentinel", "file-ledger replay (fail closed on unknown)"],
872
+ }),
873
+ inv({
874
+ id: "MC-LEDGER-021",
875
+ name: "mirror-consistency",
876
+ version: 1,
877
+ domain: "ledger",
878
+ statement: "No checkpoint observed by an external mirror may contradict the " +
879
+ "ledger's committed history: every mirrored (tree_size, root_hash) must " +
880
+ "match the ledger's root at that size. (Evaluated only when a mirror " +
881
+ "file is supplied; otherwise not-evaluated.)",
882
+ severity: "critical",
883
+ enforcementPhase: ["ci", "runtime"],
884
+ authorityClass: "witness",
885
+ disposition: "freeze-authority-class",
886
+ evidenceRequirements: ["mirrored vs committed root at the conflicting size"],
887
+ remediation: "Treat as equivocation; freeze and review.",
888
+ owner: "ledger-sentinel",
889
+ status: "ratified",
890
+ enforcedBy: ["ledger-sentinel (--mirror)", "magenta-mirror check"],
501
891
  }),
502
892
  inv({
503
893
  id: "MC-WIT-RSV-001",
@@ -109,3 +109,12 @@ level; sync writes briefly block the event loop per append (fine at
109
109
  reference-grade volumes). The hosted, transactional, lease-protected,
110
110
  fail-closed implementation is the PostgreSQL ledger lane
111
111
  (`docs/roadmaps/DURABLE_WITNESS_ARCHITECTURE.md`).
112
+
113
+ ## Independent evaluation (Ledger Sentinel)
114
+
115
+ The File Ledger no longer judges itself: `magenta-canon sentinel ledger
116
+ <ledger.jsonl>` re-derives sequence, hash-chain, Merkle, checkpoint,
117
+ corruption-refusal, and writer-authority truth from the bytes with an
118
+ independent implementation (MC-LEDGER-001…021 — see
119
+ `docs/LEDGER_SENTINEL.md`). The same invariant family is the contract the
120
+ future PostgreSQL ledger must satisfy before it carries authority.
@@ -0,0 +1,96 @@
1
+ # Ledger Sentinel
2
+
3
+ The Ledger Sentinel determines whether a Magenta evidence ledger is
4
+ **structurally, cryptographically, sequentially, and operationally lawful** —
5
+ independently of the implementation that wrote it. It ratifies and enforces
6
+ **MC-LEDGER-001…021**, the ledger-truth contract that every storage
7
+ implementation must satisfy: the File Ledger today; Postgres, hosted ledgers,
8
+ and mirrors later. **The implementation is never its own judge.**
9
+
10
+ Safe claim: *Magenta Canon can independently evaluate whether a ledger
11
+ preserves sequence, hash-chain integrity, Merkle consistency, checkpoint
12
+ monotonicity, witness epoch correctness, corruption refusal, and configured
13
+ writer authority.* It is **not** continuous production monitoring.
14
+
15
+ ## Usage
16
+
17
+ ```bash
18
+ magenta-canon sentinel ledger ledger.jsonl \
19
+ [--anchor <pubkey-hex>] # pinned witness anchor (origin assurance)
20
+ [--keystore <file>] # witness keystore (authority agreement depth)
21
+ [--passphrase-env <VAR>] # env-var NAME only; never a literal, never echoed
22
+ [--mirror <mirror.jsonl>] # external mirror records (MC-LEDGER-021)
23
+ [--no-lock-inspection] # skip operational lock metadata
24
+ [--json]
25
+ ```
26
+
27
+ Exit codes (shared sentinel contract): `0` eligible/diagnostic-only · `1`
28
+ blocked · `2` requires independent review · `3` usage error / unreadable
29
+ subject.
30
+
31
+ ## What it evaluates (MC-LEDGER-001…021)
32
+
33
+ | Group | Invariants |
34
+ |---|---|
35
+ | Sequence & uniqueness | 001 contiguous receipt sequence · 002 no duplicate receipt identity · 003 record framing integrity (version/type/payload_hash) |
36
+ | Hash chain | 004 previous-hash continuity from genesis · 005 receipt/execution-hash re-derivation · 006 issuer-signature validity (recomputed hashes can never bypass signatures) |
37
+ | Tree & checkpoints | 007 Merkle-root re-derivation (RFC 6962, re-implemented) · 008 no equivocation at rest (one root per size) · 009 exact tree-size/boundary accounting · 010 checkpoint monotonicity · 011 deterministic replay (store ↔ verifier ↔ sentinel agreement) |
38
+ | Durability & corruption | 012 corruption fails closed (no skip, no blank universe, no memory fallback) · 013 bounded tail quarantine (only a provably incomplete FINAL append) · 014 commit-before-authority |
39
+ | Writer authority | 015 single-writer exclusivity · 016 no stale-lock auto-theft (recovery = positive dead-holder evidence + operator action) · 017 single ledger authority (config) · 018 writer-fencing contract (the rule future Postgres/hosted stores must implement) |
40
+ | Cross-surface | 019 ledger/witness agreement (cross-runs the Witness & Rotation Sentinel on the same bytes) · 020 authority-record typing (unknown types refused, not ignored) · 021 mirror consistency (mirrored checkpoints can neither contradict nor be missing from committed history) |
41
+
42
+ ## Assurance honesty — five distinct outcomes
43
+
44
+ The sentinel never collapses these into one green light:
45
+
46
+ 1. **Integrity proven** — sequence/chain/Merkle/framing re-derived from bytes.
47
+ 2. **Origin proven** — only with an independently pinned `--anchor` (witness
48
+ cross-run); a subject can never vouch for its own origin.
49
+ 3. **Operational exclusivity proven** — only when live lock metadata is
50
+ present; offline bundles report not-evaluated.
51
+ 4. **Not evaluated** — runtime-context invariants (configuration, execution
52
+ ordering, fencing) and absent optional inputs (mirror, keystore, anchor)
53
+ are reported explicitly, never silently passed.
54
+ 5. **Violation confirmed** — a deterministic `magenta-sentinel-violation/1`
55
+ record binding invariant, subject hash, record number, expected vs
56
+ observed, severity, authority class, and disposition.
57
+
58
+ Lock semantics mirror the File Ledger's: a **live** holder is lawful
59
+ single-writer state; a **dead** holder is reported as an operator decision —
60
+ the sentinel never removes a lock (MC-LEDGER-016); forged/malformed lock
61
+ metadata is a finding.
62
+
63
+ ## Independence boundary (documented per shared primitive)
64
+
65
+ The sentinel parses the JSONL itself and re-derives every claim. It does
66
+ **not** call `FileLedgerStore.replay()`, `LedgerStore.importState()`,
67
+ `TransparencyLog.rebuildLeaves()`, or `MemStorage`. Shared primitives:
68
+
69
+ - `shared/canonical.ts` (`canonicalHash`/`chainHash`) — canonical JSON
70
+ hashing is the published protocol spec; the standalone verifier shares it
71
+ for the same reason.
72
+ - `tweetnacl` — the Ed25519 primitive.
73
+ - the **Witness & Rotation Sentinel** — a sibling in the same independent
74
+ layer, cross-run for epoch agreement (MC-LEDGER-019) instead of
75
+ re-implementing the witness model a third time.
76
+ - RFC 6962 Merkle hashing is **re-implemented** in the sentinel (not imported
77
+ from `transparency-log.ts`); cross-implementation agreement is pinned by
78
+ tests against the real store (MC-LEDGER-011).
79
+
80
+ ## Relationship to the rest of the trust loop
81
+
82
+ - **Verifier** proves a published evidence *bundle*; the Ledger Sentinel
83
+ proves the durable *ledger* behind it, including operational properties a
84
+ bundle cannot carry.
85
+ - **Witness Sentinel** owns rotation-authority truth; the Ledger Sentinel
86
+ defers to it and converts disagreement into MC-LEDGER-019.
87
+ - **Mirror** supplies the external observations for MC-LEDGER-021
88
+ (equivocation, rollback, and checkpoint deletion against an outside record).
89
+
90
+ ## The future Postgres contract
91
+
92
+ When the Postgres ledger is built it must satisfy **this same family** —
93
+ including MC-LEDGER-018 (fenced write authority: a paused or partitioned old
94
+ writer must be unable to commit after a new writer is authorized). Postgres
95
+ will be validated against the already-existing Ledger Sentinel; it does not
96
+ get to define ledger truth for itself.
@@ -3,21 +3,23 @@
3
3
  How Magenta Canon is packaged as an installable CLI, what ships in the tarball,
4
4
  what deliberately does not, and how to verify the package locally.
5
5
 
6
- > **Status: published on npm.** The current published release is **`0.5.0`**
7
- > (durable witness identity + authorized rotation continuity), carried on
8
- > **both** the `latest` and `next` dist-tags (published and registry-verified
9
- > 2026-06-12 see `reports/PUBLICATION_PACKET_0.5.0.md`). This refresh
10
- > prepares **`0.6.0`** — the **executable Sentinel Mesh foundation**:
11
- > Magenta Canon 0.6.0 introduces an executable Sentinel Mesh that protects
12
- > repository state, npm artifacts, dependency boundaries, release promotion,
13
- > witness identity, and cryptographically authorized witness-key rotation
14
- > (`magenta-canon sentinel` repository / artifact / pack / witness /
15
- > invariants). It does **not** include a production runtime kill switch, a
16
- > hosted Sentinel service, a Postgres Ledger Sentinel, an independent hosted
17
- > mirror, founder/root custody persistence, multi-tenancy, automated
18
- > third-party threat-intelligence intake, or complete Sentinel Mesh
19
- > coverage. `0.6.0` is **not yet published**; publishing remains an
20
- > explicitly-authorized step done on an authenticated machine.
6
+ > **Status: published on npm.** The current published release is **`0.6.0`**
7
+ > (the executable Sentinel Mesh foundation), carried on **both** the
8
+ > `latest` and `next` dist-tags (published 2026-06-12; registry shasum
9
+ > `20da40e742d47d1f4cf38beced1b17495bc6f300`, 64 files, owner-verified live
10
+ > install). This refresh prepares **`0.7.0`** — the **Ledger Sentinel**:
11
+ > Magenta Canon 0.7.0 adds an independent Ledger Sentinel
12
+ > (`magenta-canon sentinel ledger`) that evaluates sequence, hash-chain
13
+ > integrity, issuer signatures, Merkle consistency, checkpoint monotonicity,
14
+ > corruption handling, writer authority, witness agreement, and mirror
15
+ > consistency the ledger-truth contract (MC-LEDGER-001…021) that any
16
+ > future storage backend (PostgreSQL, hosted) must satisfy before carrying
17
+ > authority. It does **not** yet provide continuous hosted monitoring,
18
+ > Postgres storage, hosted independent mirror infrastructure, trust-root/
19
+ > issuer persistence, production execution kill switches, multi-tenant
20
+ > hosted services, or automatic multi-model threat ingestion. `0.7.0` is
21
+ > **not yet published**; publishing remains an explicitly-authorized step
22
+ > done on an authenticated machine.
21
23
  >
22
24
  > **Verdict-semantics migration (0.3.0):** scripts that grepped
23
25
  > `RESULT: VERIFIED` must match the new verdicts — `RESULT: INTEGRITY
@@ -28,7 +30,7 @@ what deliberately does not, and how to verify the package locally.
28
30
 
29
31
  ## Release posture
30
32
 
31
- The **`latest`** dist-tag tracks the current release (`0.4.0`), so a plain
33
+ The **`latest`** dist-tag tracks the current release (`0.6.0`), so a plain
32
34
  `npm i magenta-canon` resolves the current reference implementation; the **`next`**
33
35
  tag points at the same version. It remains a **proven reference implementation**,
34
36
  not production-hosted infrastructure yet — production durability and an
@@ -64,7 +66,8 @@ npm dist-tag add magenta-canon@<version> next
64
66
  | `0.3.0` | **verifier truth hardening** (spec v1.1, PR #27): layered verdicts; `--expected-witness-key` (independently pinned witness identity — a bundle's own key can no longer masquerade as trusted); `INCOMPLETE EVIDENCE` for zero-receipt bundles; `--require-receipt`/`--require-action-hash`/`--min-receipts` claim pinning; **receipt issuer signatures enforced**; `--json` machine output; 21-test adversarial battery; demo now pins the witness key and earns `ORIGIN AND INTEGRITY VERIFIED`. Plus: Durable Witness architecture report, MVAR v1 receipt-standard DRAFT + schema/vectors, public verifier-page design — **published**; `latest`/`next` aligned |
65
67
  | `0.4.0` | **durable file ledger** (Durable Witness PRs 1–2, #32/#33): `LedgerStore` seam; `FileLedgerStore` — append-only canonical-JSONL evidence ledger (`MAGENTA_LEDGER_FILE`), fsync-before-committed, exclusive writer lock with documented stale-lock recovery, strict boot replay (payload hashes, chain, issuer signatures, STH signatures, one-root-per-tree-size, recomputed Merkle roots), crash-tail quarantine vs corruption refusal, **witness-continuity enforcement** (regenerated witness identity refuses to continue an existing ledger), dual-authority prevention vs legacy `MAGENTA_STATE_FILE`; two-real-process restart proof (`ORIGIN AND INTEGRITY VERIFIED` after `SIGKILL`); ships `docs/FILE_LEDGER.md` — **published**; `latest`/`next` aligned |
66
68
  | `0.5.0` | **durable witness identity + authorized rotation continuity** (Durable Witness PR 3, #35): encrypted witness keystore (`MAGENTA_WITNESS_KEYFILE`/`MAGENTA_WITNESS_PASSPHRASE`; scrypt N=2^16 with bounded params → AES-256-GCM with header-bound AAD, `O_EXCL` create, atomic replace); **rotation records committed into the evidence ledger** (`magenta-rotation/1`: old-key authorization + new-key countersignature + `effective_tree_size` epoch boundary + chain hash) — *key material alone never grants authority; a validated, durably committed rotation record does*; epoch-aware replay in server **and** standalone verifier (historical STHs validate against their epoch's key; rollback/substitution/forked chains refused); deterministic crash reconciliation at boot (recover committed fact / abandon orphan / fail closed — never silent activation); single-authority config matrix (keystore × env-keys × `MAGENTA_STATE_FILE` exclusions); ships `docs/WITNESS_IDENTITY.md`. **Limitation (documented):** witness identity only — founder/receipt-issuer custody is a separate forthcoming lane (issuer key still regenerates on restart in file-ledger mode; pre-restart evidence keeps verifying). **Migration:** none required — 0.4.0 env-key and memory modes are unchanged; the keystore is opt-in and mutually exclusive with `MAGENTA_WITNESS_SECRET`/`PUBLIC` and `MAGENTA_STATE_FILE`. **Also in this release (repo hygiene, #37/#38):** the legacy RealityOS heartbeat adapter is **deprecated and removed** — excluded from the supported product architecture, never required for Magenta verification or distribution (`docs/LEGACY_REALITYOS_HEARTBEAT.md`); a tracked legacy dev identity was removed with a secret-hygiene CI gate added (`reports/INCIDENT_TRACKED_DEV_IDENTITY.md`) — **published**; `latest`/`next` aligned |
67
- | `0.6.0` | the **executable Sentinel Mesh foundation** (PRs #40/#41): versioned **Invariant Registry** (`magenta-invariants/1`, `shared/invariants.ts` — 37 ratified invariants across repository/artifact/dependency/release/authority/witness domains, permanent IDs, reserved IDs for future lanes); deterministic **Repository Sentinel** (tracked private-key material incl. escaped-JSON forms, identity/keystore/state paths, heartbeat-revival surfaces, `.gitignore` protections); deterministic **Artifact Sentinel** (tarball / extracted dir / `npm pack`: secrets, forbidden file classes, compiled entrypoints, `MANIFEST.json` sha256 equality, version consistency, approved-dependency set, install scripts, native binaries — hardened in-memory tar reader, traversal-inert, bomb/malformation refusal); canonical **violation records** (`magenta-sentinel-violation/1`, deterministic `record_hash`, volatile fields excluded, assurance honestly `unsigned-local-diagnostic`); **scoped release-promotion eligibility** (`eligible / blocked / requires-independent-review / diagnostic-only`; authority-class violations always escalate to human review); and the **Witness & Rotation Sentinel** (MC-WIT-001…011: pinned-anchor continuity — honestly `not-evaluated` without an independently supplied anchor; rotation-chain completeness; old-key authorization + new-key countersignature validation; exact epoch boundaries; key-version & fork detection; epoch-correct STH validation; **substitution / resurrection / rollback / pre-activation** distinguished in evidence; keystore↔ledger authority agreement) — independently re-derived from the published wire formats, never wrapping server code. New CLI: `magenta-canon sentinel repository | artifact | pack | witness | invariants`. **Not included:** production runtime kill switch, hosted Sentinel service, Postgres Ledger Sentinel, independent hosted mirror, founder/root custody persistence, multi-tenant hosted control plane, automated third-party threat-intel intake, complete Sentinel Mesh coverage. **Migration:** none required — no behavior changes to gateway/verifier/ledger/witness paths; the sentinel surface is additive — **not yet published** |
69
+ | `0.6.0` | the **executable Sentinel Mesh foundation** (PRs #40/#41): versioned **Invariant Registry** (`magenta-invariants/1`, `shared/invariants.ts` — 37 ratified invariants across repository/artifact/dependency/release/authority/witness domains, permanent IDs, reserved IDs for future lanes); deterministic **Repository Sentinel** (tracked private-key material incl. escaped-JSON forms, identity/keystore/state paths, heartbeat-revival surfaces, `.gitignore` protections); deterministic **Artifact Sentinel** (tarball / extracted dir / `npm pack`: secrets, forbidden file classes, compiled entrypoints, `MANIFEST.json` sha256 equality, version consistency, approved-dependency set, install scripts, native binaries — hardened in-memory tar reader, traversal-inert, bomb/malformation refusal); canonical **violation records** (`magenta-sentinel-violation/1`, deterministic `record_hash`, volatile fields excluded, assurance honestly `unsigned-local-diagnostic`); **scoped release-promotion eligibility** (`eligible / blocked / requires-independent-review / diagnostic-only`; authority-class violations always escalate to human review); and the **Witness & Rotation Sentinel** (MC-WIT-001…011: pinned-anchor continuity — honestly `not-evaluated` without an independently supplied anchor; rotation-chain completeness; old-key authorization + new-key countersignature validation; exact epoch boundaries; key-version & fork detection; epoch-correct STH validation; **substitution / resurrection / rollback / pre-activation** distinguished in evidence; keystore↔ledger authority agreement) — independently re-derived from the published wire formats, never wrapping server code. New CLI: `magenta-canon sentinel repository | artifact | pack | witness | invariants`. **Not included:** production runtime kill switch, hosted Sentinel service, Postgres Ledger Sentinel, independent hosted mirror, founder/root custody persistence, multi-tenant hosted control plane, automated third-party threat-intel intake, complete Sentinel Mesh coverage. **Migration:** none required — no behavior changes to gateway/verifier/ledger/witness paths; the sentinel surface is additive — **published**; `latest`/`next` aligned |
70
+ | `0.7.0` | the **Ledger Sentinel** (PR #43): MC-LEDGER-RSV-001 ratified into **MC-LEDGER-001…021** — the ledger-truth contract every storage implementation must satisfy (File Ledger today; PostgreSQL/hosted/mirrors later; *the implementation is never its own judge*). Independent evaluation (`magenta-canon sentinel ledger <ledger.jsonl>`): contiguous receipt sequence; duplicate-identity refusal; framing integrity; previous-hash continuity from genesis; receipt/execution-hash re-derivation; issuer-signature validity (recomputed hashes never bypass signatures); **own RFC 6962 Merkle re-derivation**; equivocation-at-rest freeze; exact tree-size/boundary accounting; checkpoint monotonicity; deterministic replay (store↔verifier↔sentinel agreement); corruption fails closed; **bounded crash-tail quarantine** (only a provably incomplete final append); commit-before-authority; single-writer/lock inspection (**never auto-stolen**); single-ledger-authority + **writer-fencing contract** for future stores; witness cross-run agreement; authority-record typing; mirror consistency incl. checkpoint-deletion exposure. Five distinct assurance outcomes (integrity / origin / operational exclusivity / not-evaluated / violation). Ships `docs/LEDGER_SENTINEL.md`. **Not included:** continuous hosted monitoring, Postgres storage, hosted independent mirror, trust-root/issuer persistence, production kill switches, multi-tenancy, automatic multi-model threat ingestion. **Migration:** none — additive CLI surface — **not yet published** |
68
71
 
69
72
 
70
73
  **Verified on macOS / Linux / Windows (current release `0.1.11`; `0.1.12` re-proven via installed-tarball smoke):**
@@ -263,26 +266,27 @@ MCP gateway, and a **local-file external STH mirror foundation** with an
263
266
  operator runbook. **Not** included: hosted mirror service, automated network
264
267
  publishing, production multi-tenant SaaS, production durability guarantees.
265
268
 
266
- ## Verifying the release artifact (checklist — current target `0.6.0`)
269
+ ## Verifying the release artifact (checklist — current target `0.7.0`)
267
270
 
268
271
  From a clean checkout of the release commit:
269
272
 
270
273
  ```bash
271
274
  npm ci && npm run check && npm test # 0 tsc errors; full suite green
272
275
  npm run build:lean && npm run build:lean # deterministic: dist/MANIFEST.json identical
273
- npm pack # prepack rebuilds dist/; expect magenta-canon-0.6.0.tgz
274
- tar -tzf magenta-canon-0.6.0.tgz # 64 files; no .ts, no .map, no tests, no server/routes.ts, no ledger/lock/keystore files
276
+ npm pack # prepack rebuilds dist/; expect magenta-canon-0.7.0.tgz
277
+ tar -tzf magenta-canon-0.7.0.tgz # 66 files; no .ts, no .map, no tests, no server/routes.ts, no ledger/lock/keystore files
275
278
  ```
276
279
 
277
280
  Then in an empty directory:
278
281
 
279
282
  ```bash
280
- npm init -y && npm i ../path/to/magenta-canon-0.6.0.tgz
283
+ npm init -y && npm i ../path/to/magenta-canon-0.7.0.tgz
281
284
  ls node_modules # exactly: magenta-canon tweetnacl zod
282
285
  npx magenta-canon verify --self-test # all verdict levels incl. tamper VERIFICATION FAILED
283
286
  npx magenta-canon mirror --self-test # [PASS] x3
284
287
  npx magenta-canon demo # allow / block / VERIFIED / tamper FAILED
285
288
  npx magenta-canon sentinel artifact node_modules/magenta-canon # PROMOTION: ELIGIBLE
289
+ npx magenta-canon sentinel ledger <your-ledger.jsonl> # ledger truth (new in 0.7.0)
286
290
  npx magenta-canon sentinel invariants # the live invariant registry
287
291
  ```
288
292
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magenta-canon",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "A verifiable MCP accountability gateway for AI-agent tool calls: allows authorized calls, blocks unauthorized calls, records both, and produces cryptographic evidence anyone can verify.",
@@ -45,6 +45,7 @@
45
45
  "docs/NPM_PACKAGING.md",
46
46
  "docs/FILE_LEDGER.md",
47
47
  "docs/WITNESS_IDENTITY.md",
48
+ "docs/LEDGER_SENTINEL.md",
48
49
  "public/canon/schemas/constitutional-spine.schema.json",
49
50
  "public/canon/spine/constitutional-spine.v1.json",
50
51
  "README.md",