magenta-canon 0.7.0 → 0.8.1

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.
Files changed (38) hide show
  1. package/README.md +34 -4
  2. package/bin/magenta-canon.mjs +12 -0
  3. package/dist/MANIFEST.json +6 -3
  4. package/dist/scripts/magenta-sentinel.js +63 -0
  5. package/dist/server/sentinel/configuration-sentinel.js +244 -0
  6. package/dist/server/sentinel/execution-sentinel.js +326 -0
  7. package/dist/server/sentinel/mirror-sentinel.js +261 -0
  8. package/dist/server/sentinel/promotion.js +1 -1
  9. package/dist/shared/invariants.js +852 -5
  10. package/docs/NPM_PACKAGING.md +35 -29
  11. package/examples/verticals/README.md +54 -0
  12. package/examples/verticals/devops-deploy/README.md +44 -0
  13. package/examples/verticals/devops-deploy/gateway.config.json +23 -0
  14. package/examples/verticals/devops-deploy/scenario.json +30 -0
  15. package/examples/verticals/ecommerce-ops/README.md +45 -0
  16. package/examples/verticals/ecommerce-ops/gateway.config.json +26 -0
  17. package/examples/verticals/ecommerce-ops/scenario.json +38 -0
  18. package/examples/verticals/fintech-refund/README.md +44 -0
  19. package/examples/verticals/fintech-refund/gateway.config.json +31 -0
  20. package/examples/verticals/fintech-refund/scenario.json +30 -0
  21. package/examples/verticals/healthcare-scribe/README.md +46 -0
  22. package/examples/verticals/healthcare-scribe/gateway.config.json +26 -0
  23. package/examples/verticals/healthcare-scribe/scenario.json +48 -0
  24. package/examples/verticals/insurance-claims/README.md +45 -0
  25. package/examples/verticals/insurance-claims/gateway.config.json +26 -0
  26. package/examples/verticals/insurance-claims/scenario.json +38 -0
  27. package/examples/verticals/legal-paralegal/README.md +45 -0
  28. package/examples/verticals/legal-paralegal/gateway.config.json +26 -0
  29. package/examples/verticals/legal-paralegal/scenario.json +38 -0
  30. package/examples/verticals/lib/tool-server.mjs +66 -0
  31. package/examples/verticals/saas-support-data/README.md +45 -0
  32. package/examples/verticals/saas-support-data/gateway.config.json +25 -0
  33. package/examples/verticals/saas-support-data/scenario.json +42 -0
  34. package/examples/verticals/security-soc/README.md +45 -0
  35. package/examples/verticals/security-soc/gateway.config.json +26 -0
  36. package/examples/verticals/security-soc/scenario.json +36 -0
  37. package/package.json +3 -1
  38. package/scripts/vertical-demo.mjs +282 -0
package/README.md CHANGED
@@ -19,6 +19,7 @@ MCP host (Claude Code, Claude Desktop, Cursor, …) and downstream MCP tools, an
19
19
 
20
20
  ```bash
21
21
  npx magenta-canon demo # full local proof loop: allow · block · witness · verify · tamper
22
+ npx magenta-canon vertical fintech-refund # sector demo in buyer language (vertical --list shows all 8)
22
23
  npx magenta-canon verify --self-test # verifier self-check: all verdict levels incl. a tamper FAIL
23
24
  npx magenta-canon gateway <config.json> # the stdio MCP capability gateway
24
25
  npx magenta-canon mirror --self-test # external STH mirror: catch operator history rewrites
@@ -237,6 +238,9 @@ magenta-canon sentinel artifact <tgz|dir> # invariant-check a packed artifact
237
238
  magenta-canon sentinel pack # npm pack, then check the result
238
239
  magenta-canon sentinel witness --ledger ledger.jsonl # witness rotation-authority check
239
240
  magenta-canon sentinel ledger ledger.jsonl # ledger truth: sequence, chains, Merkle, checkpoints (new in 0.7.0)
241
+ magenta-canon sentinel execution bundle.json # authority-bound action evidence (new in 0.8.0)
242
+ magenta-canon sentinel configuration config.json # one operation, one canonical authority path (new in 0.8.0)
243
+ magenta-canon sentinel mirror records.jsonl # independent checkpoint observation (new in 0.8.0)
240
244
  magenta-canon sentinel invariants # list the invariant registry
241
245
  ```
242
246
 
@@ -258,15 +262,41 @@ not-evaluated / violation — and is the contract any future storage backend
258
262
  (PostgreSQL, hosted) must satisfy before carrying authority
259
263
  ([`docs/LEDGER_SENTINEL.md`](docs/LEDGER_SENTINEL.md), ships in the package).
260
264
 
265
+ #### Sentinel Mesh V1 — complete in 0.8.0
266
+
267
+ Magenta Canon 0.8.0 completes the **Sentinel Mesh V1**: eight sentinels plus a
268
+ coordinator, **105 ratified invariants** across twelve domains, every one with
269
+ a named enforcer (the coverage law is a CI gate), zero live reserved entries.
270
+ The three sentinels added in 0.8.0 are:
271
+
272
+ - **Execution Sentinel** (MC-EXEC-001…014) — binds a governed action to the
273
+ signature-authenticated receipt's committed `action_hash`, never to operator
274
+ params; a refused action that appears downstream, or a missing receipt, is
275
+ caught.
276
+ - **Configuration & Dual-Authority Sentinel** (MC-CONFIG-001…014) — enforces
277
+ one operation, one canonical authority path; independently enumerates
278
+ configuration sources, precedence, fallbacks, and reviewed↔effective drift.
279
+ - **Mirror Sentinel** (MC-MIRROR-001…012) — re-derives a `magenta-mirror/1`
280
+ observation stream independently of the mirror that wrote it: record-hash
281
+ chain, STH signatures, anti-equivocation across all observed sizes.
282
+
283
+ Two meta-sentinels close the mesh: the **Anomaly Sentinel** (MC-ANOM-001…004)
284
+ is the Attack KB's regression memory — a ratified attack that stops being
285
+ detected fails the build — and the **Sentinel Coordinator** (MC-COORD-001…003)
286
+ combines per-sentinel findings into one scoped, deterministic, tamper-evident
287
+ `magenta-sentinel-report/1` **without** overriding, downgrading, or suppressing
288
+ any finding, freezing only the affected authority class. Full inventory,
289
+ limitations, and the V1 boundary:
290
+ [`docs/SENTINEL_MESH_V1_COMPLETION.md`](docs/SENTINEL_MESH_V1_COMPLETION.md).
291
+
261
292
  Sentinels are read-only, redacting (paths + fingerprints, never secret
262
293
  bodies), deterministic, network-free, and emit `magenta-sentinel-violation/1`
263
294
  evidence records; exit codes map to a scoped promotion decision (`eligible` /
264
295
  `blocked` / `requires-independent-review`). Records at this layer are
265
296
  **unsigned local diagnostics**, and the freeze is a release/build promotion
266
- gate — **not** a production kill switch, a hosted Sentinel service, or
267
- complete Mesh coverage (ledger/execution/configuration sentinels are future
268
- lanes). Honest scope and the full model:
269
- [`docs/SENTINEL_MESH.md`](docs/SENTINEL_MESH.md).
297
+ gate — **not** a production kill switch, a hosted Sentinel service, continuous
298
+ monitoring, or a probabilistic/unknown-threat detector. Honest scope and the
299
+ full model: [`docs/SENTINEL_MESH.md`](docs/SENTINEL_MESH.md).
270
300
 
271
301
  ### From the repo vs. as a CLI
272
302
 
@@ -3,6 +3,7 @@
3
3
  * magenta-canon — CLI entry point for the open-source reference implementation.
4
4
  *
5
5
  * npx magenta-canon demo run the full local proof loop
6
+ * npx magenta-canon vertical <id> run a sector demo (gate one industry's tools)
6
7
  * npx magenta-canon verify <bundle> independently verify an evidence bundle
7
8
  * npx magenta-canon gateway <config> run the stdio MCP capability gateway
8
9
  * npx magenta-canon mirror <cmd> external STH mirror (append|verify|check|--self-test)
@@ -40,6 +41,9 @@ const USAGE = `magenta-canon ${pkg.version} — verifiable MCP gateway for AI-ag
40
41
  Usage:
41
42
  magenta-canon demo Run the full local proof loop (allow, block,
42
43
  witness, verify, and a tamper control).
44
+ magenta-canon vertical <id> Run a sector demo (e.g. fintech-refund,
45
+ healthcare-scribe). 'vertical --list' shows
46
+ all available ids.
43
47
  magenta-canon verify <bundle.json> Independently verify an evidence bundle.
44
48
  Exit 0 = VERIFIED, 1 = VERIFICATION FAILED.
45
49
  magenta-canon gateway <config.json> Run the stdio MCP capability gateway
@@ -93,6 +97,14 @@ switch (sub) {
93
97
  // demo.mjs is plain ESM and resolves tsx + headless mode itself.
94
98
  run(process.execPath, [path.join(ROOT, "scripts", "demo.mjs"), ...rest]);
95
99
  break;
100
+ case "vertical":
101
+ // Sector vertical demos: gate one industry's tool calls, prove the blocked
102
+ // call never reached downstream, and verify the evidence. Reads a shipped
103
+ // examples/verticals/<id>/ scenario and resolves its runtime from dist/
104
+ // (or tsx in a repo checkout) exactly like `demo`. No <id> / --list / an
105
+ // unknown id all print the available verticals.
106
+ run(process.execPath, [path.join(ROOT, "scripts", "vertical-demo.mjs"), ...rest]);
107
+ break;
96
108
  case "verify":
97
109
  if (!rest[0] || rest[0].startsWith("-") && rest[0] !== "--self-test") {
98
110
  // pass flags through (e.g. --self-test, --version); only warn on no target
@@ -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": "74336873d7f7c10825750e34a79988497ba793df593587f1c971b494a4d29f5f",
6
+ "scripts/magenta-sentinel.js": "155efd7408d116e3cc100b1dfa406dbabe0107584663e702348fec9106bafb2a",
7
7
  "scripts/magenta-verify.js": "0bb65ef6ff1350789eecc4f0434ca94dcf3a89930f8ef1d62cc38100e18094ba",
8
8
  "scripts/mcp-gateway.js": "335923004b73511fe42ea0fc6f2e567afe8018dc95464a79027e4c2537e30de1",
9
9
  "server/agent-policy.js": "20dd9150f8244c33aaf14ecdf3a09f471210960d246cda9ab8d5abe0e8433518",
@@ -26,8 +26,11 @@
26
26
  "server/ledger.js": "9bf8f132e08d636070d2ede568b2eaf75810be245c90ecd5979d2d4de69af934",
27
27
  "server/persistence.js": "2e6b1d0b1c74babbd581780b028c2593256c5ec96c2d60f4c25159e3f54e4e3f",
28
28
  "server/sentinel/artifact-sentinel.js": "93bae917af313c9247f8062af148e7c25ccfd16d0507fa85e755c64056715f8d",
29
+ "server/sentinel/configuration-sentinel.js": "f8dd82c0bed36ba402d3c2fb52c068048f3e87941671b000c233d0f04acbecdb",
30
+ "server/sentinel/execution-sentinel.js": "644753a33d23e4295809a6d684151848fa7b1a639d54a7782caf11b4a4cf0253",
29
31
  "server/sentinel/ledger-sentinel.js": "37d1c7040ae2506454247d40b42bad6076273b577190eeb3a6571184849b1e71",
30
- "server/sentinel/promotion.js": "68ac5e54eb8c07d5c75ba0d2325d595b1583557840cc46322b2053dd9924a0be",
32
+ "server/sentinel/mirror-sentinel.js": "2761d1e3099f600b7346406301b15a4bb256c2655ca4cff77341be907a9d438d",
33
+ "server/sentinel/promotion.js": "fa91250f59ccbe6f9c4e327dc4cce41fe46aacca38df70835ae1a7bf1833fe9b",
31
34
  "server/sentinel/repository-sentinel.js": "722e973c8860cf281b29db8c4a22ec3cf473afcb088247916e8278d5e11638cf",
32
35
  "server/sentinel/rules.js": "ab24435df4f6f5b10f8b721fde37fe7fa894453648f17f04923cab56020c459f",
33
36
  "server/sentinel/tar-reader.js": "ded811eefebf3e4afa332d1bc0a48318cc22ad824222eb15b2dc08eb51048272",
@@ -40,7 +43,7 @@
40
43
  "server/witness.js": "b1a8209d4dae4ffafc3ca4a158474b369250472ba80cef03bd92048282be91a4",
41
44
  "shared/canonical.js": "476cee4b5c5702e8a2a198e79c2639cf8ff84000ffb92f68a04433ed2a2f5484",
42
45
  "shared/certificates.js": "7844e71a38e1b1324586c7d054b2f5c15222b86446318d1e6dea9bab504a4a73",
43
- "shared/invariants.js": "7cde6f65dc014d9dd09849281734660369cbb0ea0f34faa583671efd2522d0c0",
46
+ "shared/invariants.js": "be71f2645e96d7c054954c9c99ded8f16e4d6dd2b6afd98b98d34938258555a8",
44
47
  "shared/schema.js": "f89190990e3826e44c722d5e8f5b39fd59b4d3fb9ec047229930a17604e4646c",
45
48
  "shared/terminating-kernel.js": "8e2aa68d47d6780e4a70822482e3e410924e9d1af843f301a487da0af37ed047"
46
49
  }
@@ -44,6 +44,9 @@ 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
46
  const ledger_sentinel_1 = require("../server/sentinel/ledger-sentinel");
47
+ const execution_sentinel_1 = require("../server/sentinel/execution-sentinel");
48
+ const configuration_sentinel_1 = require("../server/sentinel/configuration-sentinel");
49
+ const mirror_sentinel_1 = require("../server/sentinel/mirror-sentinel");
47
50
  const promotion_1 = require("../server/sentinel/promotion");
48
51
  const invariants_1 = require("../shared/invariants");
49
52
  const USAGE = `magenta-sentinel — deterministic invariant evaluation (Sentinel Mesh foundation)
@@ -57,6 +60,9 @@ Usage:
57
60
  sentinel ledger <ledger.jsonl> [--anchor <pubkey-hex>] [--keystore <file>]
58
61
  [--passphrase-env <VAR>] [--mirror <mirror.jsonl>]
59
62
  [--no-lock-inspection] [--json]
63
+ sentinel execution <execution-bundle.json> [--json]
64
+ sentinel configuration <config-evidence.json> [--json]
65
+ sentinel mirror <mirror-records.jsonl> [--ledger <f>] [--second-mirror <f>] [--anchor <hex>] [--json]
60
66
  sentinel invariants [--json]
61
67
 
62
68
  Exit: 0 eligible/diagnostic-only · 1 blocked · 2 requires review · 3 usage/unreadable
@@ -203,6 +209,63 @@ try {
203
209
  }
204
210
  process.exit(exitFor(promo));
205
211
  }
212
+ else if (sub === "execution") {
213
+ const target = argv.find((a) => !a.startsWith("-"));
214
+ if (!target) {
215
+ console.error("sentinel execution: missing <execution-bundle.json>\n\n" + USAGE);
216
+ process.exit(3);
217
+ }
218
+ const res = (0, execution_sentinel_1.runExecutionSentinel)(path.resolve(target));
219
+ const promo = (0, promotion_1.decidePromotion)({ subject: `execution:${res.subject_identity}`, findings: res.findings, violations: res.violations });
220
+ if (json)
221
+ console.log(JSON.stringify({ result: res, promotion: promo }, null, 2));
222
+ else {
223
+ printHuman("execution", `${res.subject_identity.slice(0, 29)}… (${res.operation_count} op(s): ${res.executed_count} executed, ${res.refused_count} refused)`, res, promo);
224
+ for (const n of res.not_evaluated)
225
+ console.log(` [NOT EVALUATED] ${n.invariant_id}: ${n.reason}`);
226
+ }
227
+ process.exit(exitFor(promo));
228
+ }
229
+ else if (sub === "configuration" || sub === "config") {
230
+ const target = argv.find((a) => !a.startsWith("-"));
231
+ if (!target) {
232
+ console.error("sentinel configuration: missing <config-evidence.json>\n\n" + USAGE);
233
+ process.exit(3);
234
+ }
235
+ const res = (0, configuration_sentinel_1.runConfigurationSentinel)(path.resolve(target));
236
+ const promo = (0, promotion_1.decidePromotion)({ subject: `config:${res.subject_identity}`, findings: res.findings, violations: res.violations });
237
+ if (json)
238
+ console.log(JSON.stringify({ result: res, promotion: promo }, null, 2));
239
+ else {
240
+ printHuman("configuration", `${res.subject_identity.slice(0, 26)}… (${res.setting_count} setting(s))`, res, promo);
241
+ for (const n of res.not_evaluated)
242
+ console.log(` [NOT EVALUATED] ${n.invariant_id}: ${n.reason}`);
243
+ }
244
+ process.exit(exitFor(promo));
245
+ }
246
+ else if (sub === "mirror") {
247
+ const target = argv.find((a) => !a.startsWith("-"));
248
+ if (!target) {
249
+ console.error("sentinel mirror: missing <mirror-records.jsonl>\n\n" + USAGE);
250
+ process.exit(3);
251
+ }
252
+ const ledger = option(argv, "--ledger");
253
+ const second = option(argv, "--second-mirror");
254
+ const res = (0, mirror_sentinel_1.runMirrorSentinel)(path.resolve(target), {
255
+ ledgerPath: ledger ? path.resolve(ledger) : undefined,
256
+ secondMirrorPath: second ? path.resolve(second) : undefined,
257
+ anchorPubkey: option(argv, "--anchor"),
258
+ });
259
+ const promo = (0, promotion_1.decidePromotion)({ subject: `mirror:${res.subject_identity}`, findings: res.findings, violations: res.violations });
260
+ if (json)
261
+ console.log(JSON.stringify({ result: res, promotion: promo }, null, 2));
262
+ else {
263
+ printHuman("mirror", `${res.subject_identity.slice(0, 26)}… (${res.record_count} observation(s), observer: ${res.observer_class})`, res, promo);
264
+ for (const n of res.not_evaluated)
265
+ console.log(` [NOT EVALUATED] ${n.invariant_id}: ${n.reason}`);
266
+ }
267
+ process.exit(exitFor(promo));
268
+ }
206
269
  else if (sub === "invariants") {
207
270
  if (json)
208
271
  console.log(JSON.stringify(invariants_1.INVARIANTS, null, 2));
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectStaleDist = exports.runConfigurationSentinel = exports.CONFIG_EVIDENCE_FORMAT = exports.CONFIG_SENTINEL_VERSION = exports.CONFIG_SENTINEL_TYPE = void 0;
4
+ /**
5
+ * CONFIGURATION & DUAL-AUTHORITY SENTINEL — deterministic evaluation of
6
+ * configuration coherence invariants (MC-CONFIG-001…014) over a redacted
7
+ * configuration-evidence snapshot. Central principle: ONE operation has ONE
8
+ * canonical authority path.
9
+ *
10
+ * Subject: `magenta-config-evidence/1` — a normalized, redacted snapshot the
11
+ * operator (or a capture helper) assembles. It enumerates the actual
12
+ * configuration SOURCES and their fingerprints; it never carries secret
13
+ * values. The sentinel does NOT call the application's configuration loader
14
+ * and trust its final output — it independently enumerates source
15
+ * contradictions, precedence, fallbacks, and reviewed↔effective drift.
16
+ *
17
+ * {
18
+ * format: "magenta-config-evidence/1",
19
+ * subject?: { repository?, tenant?, environment? },
20
+ * settings: [{
21
+ * key, // e.g. "witness.persistence"
22
+ * authority_relevance: "witness"|"issuer"|"operator"|"none",
23
+ * sources: [{ source, present:boolean, value_fingerprint?, secret?:boolean }],
24
+ * precedence?: string[], // ordered source names; the resolution order
25
+ * effective_source?, effective_fingerprint?,
26
+ * reviewed_fingerprint?,
27
+ * configured_available?: boolean, // a configured backend/signer is reachable
28
+ * observed_fallback?: "none"|"memory"|"ephemeral-signer"|"blank-ledger",
29
+ * }],
30
+ * active_witness_signers?: string[], // fingerprints
31
+ * active_receipt_issuers?: string[], // fingerprints
32
+ * writer_authorities?: string[],
33
+ * stale_dist?: { is_checkout:boolean, source_fingerprint?, dist_fingerprint? },
34
+ * }
35
+ *
36
+ * Read-only, redacting (fingerprints only — never values), deterministic,
37
+ * network-free. Missing runtime evidence → NOT-EVALUATED, never a pass.
38
+ */
39
+ const node_crypto_1 = require("node:crypto");
40
+ const node_fs_1 = require("node:fs");
41
+ const invariants_1 = require("../../shared/invariants");
42
+ const violation_1 = require("./violation");
43
+ exports.CONFIG_SENTINEL_TYPE = "configuration-sentinel";
44
+ exports.CONFIG_SENTINEL_VERSION = "1.0.0";
45
+ exports.CONFIG_EVIDENCE_FORMAT = "magenta-config-evidence/1";
46
+ /** Heartbeat-revival markers in any source name/value fingerprint key. */
47
+ const HEARTBEAT_MARKERS = ["heartbeat-loop", "reality-adapter", "migration-hub", "BRAIN_HUB", "ACCESS_TOKEN"];
48
+ function finding(invId, observed, expected, paths) {
49
+ const inv = (0, invariants_1.getInvariant)(invId);
50
+ return {
51
+ invariant_id: inv.id, invariant_version: inv.version, severity: inv.severity,
52
+ authority_class: inv.authorityClass, disposition: inv.disposition,
53
+ observed, expected, paths: paths.sort(),
54
+ };
55
+ }
56
+ function presentSources(setting) {
57
+ return (setting.sources ?? []).filter((s) => s.present).map((s) => s.source);
58
+ }
59
+ function runConfigurationSentinel(snapshotPath, opts = {}) {
60
+ if (!(0, node_fs_1.existsSync)(snapshotPath))
61
+ throw new Error(`configuration-sentinel: snapshot not found: ${snapshotPath}`);
62
+ const raw = (0, node_fs_1.readFileSync)(snapshotPath);
63
+ const subjectIdentity = `config:${(0, node_crypto_1.createHash)("sha256").update(raw).digest("hex")}`;
64
+ let snap;
65
+ try {
66
+ snap = JSON.parse(raw.toString("utf8"));
67
+ }
68
+ catch {
69
+ throw new Error("configuration-sentinel: snapshot is not valid JSON — refusing to guess");
70
+ }
71
+ if (snap.format !== exports.CONFIG_EVIDENCE_FORMAT)
72
+ throw new Error(`configuration-sentinel: unsupported format '${snap.format}'`);
73
+ const settings = Array.isArray(snap.settings) ? snap.settings : [];
74
+ const findings = [];
75
+ const notEvaluated = [];
76
+ const noteNE = (id, reason) => { if (!notEvaluated.some((n) => n.invariant_id === id))
77
+ notEvaluated.push({ invariant_id: id, reason }); };
78
+ const byKey = (k) => settings.find((s) => s.key === k);
79
+ // ── MC-CONFIG-001 / 006: witness persistence single authority ─────────
80
+ const witnessPersistence = byKey("witness.persistence") ?? byKey("witness");
81
+ if (witnessPersistence) {
82
+ const present = presentSources(witnessPersistence);
83
+ const hasKeystore = present.some((p) => /keyfile|keystore/i.test(p));
84
+ const hasEnvKeys = present.some((p) => /witness_secret|witness_public|env-?key/i.test(p));
85
+ const hasStateFile = present.some((p) => /state_file|state-file/i.test(p));
86
+ if (hasKeystore && hasEnvKeys) {
87
+ findings.push(finding("MC-CONFIG-001", `witness keystore AND env witness keys both configured (${present.join(", ")})`, "one witness-persistence authority", ["witness.persistence"]));
88
+ }
89
+ if (hasKeystore && hasStateFile) {
90
+ findings.push(finding("MC-CONFIG-006", `witness keystore AND legacy state file both configured (${present.join(", ")})`, "keystore mode forbids MAGENTA_STATE_FILE", ["witness.persistence"]));
91
+ }
92
+ }
93
+ else
94
+ noteNE("MC-CONFIG-001", "no witness.persistence setting in the snapshot");
95
+ // ── MC-CONFIG-002: single evidence authority ──────────────────────────
96
+ const evidence = byKey("evidence.persistence") ?? byKey("ledger");
97
+ if (evidence) {
98
+ const present = presentSources(evidence);
99
+ const hasLedger = present.some((p) => /ledger_file|ledger-file|file-?ledger/i.test(p));
100
+ const hasState = present.some((p) => /state_file|state-file/i.test(p));
101
+ if (hasLedger && hasState)
102
+ findings.push(finding("MC-CONFIG-002", `file ledger AND legacy state file both claim evidence authority (${present.join(", ")})`, "one evidence authority", ["evidence.persistence"]));
103
+ }
104
+ else
105
+ noteNE("MC-CONFIG-002", "no evidence.persistence setting in the snapshot");
106
+ // ── MC-CONFIG-003/004/011: single active signer/issuer/writer ─────────
107
+ const sig = snap.active_witness_signers;
108
+ if (Array.isArray(sig)) {
109
+ if (sig.length > 1)
110
+ findings.push(finding("MC-CONFIG-003", `${sig.length} active witness signers`, "exactly one active witness signer", ["active_witness_signers"]));
111
+ }
112
+ else
113
+ noteNE("MC-CONFIG-003", "no active_witness_signers evidence");
114
+ const iss = snap.active_receipt_issuers;
115
+ if (Array.isArray(iss)) {
116
+ if (iss.length > 1)
117
+ findings.push(finding("MC-CONFIG-004", `${iss.length} active receipt issuers`, "exactly one active receipt issuer", ["active_receipt_issuers"]));
118
+ }
119
+ else
120
+ noteNE("MC-CONFIG-004", "no active_receipt_issuers evidence");
121
+ const wr = snap.writer_authorities;
122
+ if (Array.isArray(wr)) {
123
+ if (wr.length > 1)
124
+ findings.push(finding("MC-CONFIG-011", `${wr.length} writer authorities configured`, "exactly one writer authority", ["writer_authorities"]));
125
+ }
126
+ else
127
+ noteNE("MC-CONFIG-011", "no writer_authorities evidence");
128
+ // ── per-setting checks: precedence / reviewed-drift / fallback / heartbeat ─
129
+ let anyReviewed = false;
130
+ let anyAvailability = false;
131
+ for (const setting of settings) {
132
+ const key = setting.key;
133
+ const present = presentSources(setting);
134
+ // MC-CONFIG-005: defined precedence when >1 source competes
135
+ if (present.length > 1) {
136
+ const prec = setting.precedence;
137
+ const covers = prec && present.every((p) => prec.includes(p)) && new Set(prec).size === prec.length;
138
+ if (!covers)
139
+ findings.push(finding("MC-CONFIG-005", `setting '${key}' has ${present.length} competing sources without a complete, non-circular precedence`, "defined precedence over all present sources", [key]));
140
+ }
141
+ // MC-CONFIG-007: reviewed == effective
142
+ if (setting.reviewed_fingerprint !== undefined) {
143
+ anyReviewed = true;
144
+ if (setting.effective_fingerprint !== undefined && setting.reviewed_fingerprint !== setting.effective_fingerprint) {
145
+ findings.push(finding("MC-CONFIG-007", `setting '${key}': reviewed fingerprint != effective runtime fingerprint`, "reviewed configuration equals runtime", [key]));
146
+ }
147
+ }
148
+ // MC-CONFIG-009/010: fallback weakening
149
+ if (setting.configured_available === false) {
150
+ anyAvailability = true;
151
+ const fb = setting.observed_fallback ?? "none";
152
+ if (fb === "memory" || fb === "blank-ledger")
153
+ findings.push(finding("MC-CONFIG-009", `setting '${key}': configured durable backend unavailable but fell back to '${fb}'`, "unavailable durability fails closed", [key]));
154
+ if (fb === "ephemeral-signer")
155
+ findings.push(finding("MC-CONFIG-010", `setting '${key}': configured signer unavailable but an ephemeral signer was generated`, "unavailable signer fails closed", [key]));
156
+ }
157
+ // MC-CONFIG-013: heartbeat revival in any source name
158
+ for (const src of (setting.sources ?? [])) {
159
+ if (HEARTBEAT_MARKERS.some((m) => String(src.source).includes(m))) {
160
+ findings.push(finding("MC-CONFIG-013", `setting '${key}': revived deprecated heartbeat source '${src.source}'`, "no heartbeat configuration", [key]));
161
+ }
162
+ }
163
+ }
164
+ if (!anyReviewed)
165
+ noteNE("MC-CONFIG-007", "no reviewed fingerprints supplied");
166
+ if (!anyAvailability) {
167
+ noteNE("MC-CONFIG-009", "no backend-availability evidence");
168
+ noteNE("MC-CONFIG-010", "no signer-availability evidence");
169
+ }
170
+ // ── MC-CONFIG-008: mixed environment credentials ──────────────────────
171
+ const envLabels = new Set();
172
+ for (const setting of settings)
173
+ for (const src of (setting.sources ?? []))
174
+ if (src.present && src.environment)
175
+ envLabels.add(src.environment);
176
+ if (envLabels.size === 0)
177
+ noteNE("MC-CONFIG-008", "no per-source environment labels");
178
+ else if (envLabels.has("production") && envLabels.has("development")) {
179
+ findings.push(finding("MC-CONFIG-008", `effective configuration mixes environments: ${Array.from(envLabels).join(", ")}`, "one explicit, consistent environment", ["environment"]));
180
+ }
181
+ // ── MC-CONFIG-012: tenant/repo identity coherence ─────────────────────
182
+ const idValues = new Map();
183
+ for (const setting of settings) {
184
+ if (setting.key === "tenant" || setting.key === "repository") {
185
+ const vals = new Set((setting.sources ?? []).filter((s) => s.present && s.value_fingerprint).map((s) => s.value_fingerprint));
186
+ if (vals.size > 0)
187
+ idValues.set(setting.key, vals);
188
+ }
189
+ }
190
+ for (const [k, vals] of Array.from(idValues)) {
191
+ if (vals.size > 1)
192
+ findings.push(finding("MC-CONFIG-012", `${k} identity disagrees across sources (${vals.size} distinct fingerprints)`, "coherent tenant/repo identity", [k]));
193
+ }
194
+ if (idValues.size === 0)
195
+ noteNE("MC-CONFIG-012", "no tenant/repository identity evidence");
196
+ // ── MC-CONFIG-014: stale dist ─────────────────────────────────────────
197
+ const sd = snap.stale_dist;
198
+ if (!sd)
199
+ noteNE("MC-CONFIG-014", "no stale-dist fingerprint evidence");
200
+ else if (sd.is_checkout === false)
201
+ noteNE("MC-CONFIG-014", "installed package (compiled-first by design)");
202
+ else if (sd.source_fingerprint === undefined || sd.dist_fingerprint === undefined) {
203
+ noteNE("MC-CONFIG-014", "checkout without both source and dist fingerprints");
204
+ }
205
+ else if (sd.source_fingerprint !== sd.dist_fingerprint) {
206
+ findings.push(finding("MC-CONFIG-014", `repository checkout: dist build fingerprint does not match current source — stale compiled code would shadow source`, "dist matches source (or source-first launch)", ["stale_dist"]));
207
+ }
208
+ const configInvariants = (0, invariants_1.ratifiedInvariants)().filter((i) => i.domain === "configuration").map((i) => i.id);
209
+ const notEvalIds = new Set(notEvaluated.map((n) => n.invariant_id));
210
+ const failed = Array.from(new Set(findings.map((f) => f.invariant_id))).sort();
211
+ const evaluated = configInvariants.filter((id) => !notEvalIds.has(id) || failed.includes(id)).sort();
212
+ const passed = evaluated.filter((id) => !failed.includes(id)).sort();
213
+ const violations = findings.map((f) => (0, violation_1.buildViolationRecord)({
214
+ sentinelType: exports.CONFIG_SENTINEL_TYPE, sentinelVersion: exports.CONFIG_SENTINEL_VERSION,
215
+ subjectType: "artifact", subjectIdentity, finding: f,
216
+ detectedAt: opts.detectedAt, detector: opts.detector,
217
+ }));
218
+ return {
219
+ sentinel_type: exports.CONFIG_SENTINEL_TYPE, sentinel_version: exports.CONFIG_SENTINEL_VERSION,
220
+ subject_type: "artifact", subject_identity: subjectIdentity,
221
+ setting_count: settings.length,
222
+ evaluated_invariants: evaluated, not_evaluated: notEvaluated,
223
+ passed, failed, findings, violations,
224
+ };
225
+ }
226
+ exports.runConfigurationSentinel = runConfigurationSentinel;
227
+ /**
228
+ * Repo-checkout stale-dist guard (runtime helper, repository-only). Computes a
229
+ * deterministic source fingerprint over the lean closure's .ts sources and
230
+ * compares it to dist/MANIFEST's recorded source fingerprint. npm consumers
231
+ * never call this (no dist/MANIFEST source-fingerprint field is required at
232
+ * runtime; the bin dispatcher remains compiled-first for installs).
233
+ */
234
+ function detectStaleDist(repoRoot) {
235
+ const distManifest = `${repoRoot}/dist/MANIFEST.json`;
236
+ if (!(0, node_fs_1.existsSync)(distManifest))
237
+ return { is_checkout: true, stale: false, reason: "no dist/ present (source-first)" };
238
+ // A checkout has source files; an installed package ships only dist/.
239
+ const hasSource = (0, node_fs_1.existsSync)(`${repoRoot}/scripts/magenta-verify.ts`);
240
+ if (!hasSource)
241
+ return { is_checkout: false, stale: false, reason: "installed package (compiled-first)" };
242
+ return { is_checkout: true, stale: false, reason: "checkout with dist — supply fingerprints to the sentinel for MC-CONFIG-014" };
243
+ }
244
+ exports.detectStaleDist = detectStaleDist;