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 +12 -0
- package/bin/magenta-canon.mjs +1 -1
- package/dist/MANIFEST.json +3 -2
- package/dist/scripts/magenta-sentinel.js +34 -0
- package/dist/server/sentinel/ledger-sentinel.js +408 -0
- package/dist/shared/invariants.js +396 -6
- package/docs/FILE_LEDGER.md +9 -0
- package/docs/LEDGER_SENTINEL.md +96 -0
- package/docs/NPM_PACKAGING.md +25 -21
- package/package.json +2 -1
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` /
|
package/bin/magenta-canon.mjs
CHANGED
package/dist/MANIFEST.json
CHANGED
|
@@ -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": "
|
|
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": "
|
|
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:
|
|
490
|
-
"
|
|
491
|
-
"
|
|
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 (
|
|
498
|
-
owner: "
|
|
499
|
-
status: "
|
|
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",
|
package/docs/FILE_LEDGER.md
CHANGED
|
@@ -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.
|
package/docs/NPM_PACKAGING.md
CHANGED
|
@@ -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.
|
|
7
|
-
> (
|
|
8
|
-
>
|
|
9
|
-
>
|
|
10
|
-
> prepares **`0.
|
|
11
|
-
> Magenta Canon 0.
|
|
12
|
-
>
|
|
13
|
-
>
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
>
|
|
17
|
-
>
|
|
18
|
-
>
|
|
19
|
-
>
|
|
20
|
-
>
|
|
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.
|
|
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 — **
|
|
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.
|
|
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.
|
|
274
|
-
tar -tzf magenta-canon-0.
|
|
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.
|
|
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.
|
|
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",
|