magenta-canon 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -237,6 +237,9 @@ 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
239
  magenta-canon sentinel ledger ledger.jsonl # ledger truth: sequence, chains, Merkle, checkpoints (new in 0.7.0)
240
+ magenta-canon sentinel execution bundle.json # authority-bound action evidence (new in 0.8.0)
241
+ magenta-canon sentinel configuration config.json # one operation, one canonical authority path (new in 0.8.0)
242
+ magenta-canon sentinel mirror records.jsonl # independent checkpoint observation (new in 0.8.0)
240
243
  magenta-canon sentinel invariants # list the invariant registry
241
244
  ```
242
245
 
@@ -258,15 +261,41 @@ not-evaluated / violation — and is the contract any future storage backend
258
261
  (PostgreSQL, hosted) must satisfy before carrying authority
259
262
  ([`docs/LEDGER_SENTINEL.md`](docs/LEDGER_SENTINEL.md), ships in the package).
260
263
 
264
+ #### Sentinel Mesh V1 — complete in 0.8.0
265
+
266
+ Magenta Canon 0.8.0 completes the **Sentinel Mesh V1**: eight sentinels plus a
267
+ coordinator, **105 ratified invariants** across twelve domains, every one with
268
+ a named enforcer (the coverage law is a CI gate), zero live reserved entries.
269
+ The three sentinels added in 0.8.0 are:
270
+
271
+ - **Execution Sentinel** (MC-EXEC-001…014) — binds a governed action to the
272
+ signature-authenticated receipt's committed `action_hash`, never to operator
273
+ params; a refused action that appears downstream, or a missing receipt, is
274
+ caught.
275
+ - **Configuration & Dual-Authority Sentinel** (MC-CONFIG-001…014) — enforces
276
+ one operation, one canonical authority path; independently enumerates
277
+ configuration sources, precedence, fallbacks, and reviewed↔effective drift.
278
+ - **Mirror Sentinel** (MC-MIRROR-001…012) — re-derives a `magenta-mirror/1`
279
+ observation stream independently of the mirror that wrote it: record-hash
280
+ chain, STH signatures, anti-equivocation across all observed sizes.
281
+
282
+ Two meta-sentinels close the mesh: the **Anomaly Sentinel** (MC-ANOM-001…004)
283
+ is the Attack KB's regression memory — a ratified attack that stops being
284
+ detected fails the build — and the **Sentinel Coordinator** (MC-COORD-001…003)
285
+ combines per-sentinel findings into one scoped, deterministic, tamper-evident
286
+ `magenta-sentinel-report/1` **without** overriding, downgrading, or suppressing
287
+ any finding, freezing only the affected authority class. Full inventory,
288
+ limitations, and the V1 boundary:
289
+ [`docs/SENTINEL_MESH_V1_COMPLETION.md`](docs/SENTINEL_MESH_V1_COMPLETION.md).
290
+
261
291
  Sentinels are read-only, redacting (paths + fingerprints, never secret
262
292
  bodies), deterministic, network-free, and emit `magenta-sentinel-violation/1`
263
293
  evidence records; exit codes map to a scoped promotion decision (`eligible` /
264
294
  `blocked` / `requires-independent-review`). Records at this layer are
265
295
  **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).
296
+ gate — **not** a production kill switch, a hosted Sentinel service, continuous
297
+ monitoring, or a probabilistic/unknown-threat detector. Honest scope and the
298
+ full model: [`docs/SENTINEL_MESH.md`](docs/SENTINEL_MESH.md).
270
299
 
271
300
  ### From the repo vs. as a CLI
272
301
 
@@ -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;