magenta-canon 0.6.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 +45 -4
- package/bin/magenta-canon.mjs +1 -1
- package/dist/MANIFEST.json +7 -3
- package/dist/scripts/magenta-sentinel.js +97 -0
- package/dist/server/sentinel/configuration-sentinel.js +244 -0
- package/dist/server/sentinel/execution-sentinel.js +326 -0
- package/dist/server/sentinel/ledger-sentinel.js +408 -0
- package/dist/server/sentinel/mirror-sentinel.js +261 -0
- package/dist/server/sentinel/promotion.js +1 -1
- package/dist/shared/invariants.js +1248 -11
- package/docs/FILE_LEDGER.md +9 -0
- package/docs/LEDGER_SENTINEL.md +96 -0
- package/docs/NPM_PACKAGING.md +39 -26
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -236,6 +236,10 @@ 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)
|
|
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)
|
|
239
243
|
magenta-canon sentinel invariants # list the invariant registry
|
|
240
244
|
```
|
|
241
245
|
|
|
@@ -246,15 +250,52 @@ retired-key resurrection / pre-activation refusal, and keystore↔ledger
|
|
|
246
250
|
agreement — re-derived independently from the published wire formats, so it
|
|
247
251
|
can disagree with a wrong (or compromised) server.
|
|
248
252
|
|
|
253
|
+
The **Ledger Sentinel** (MC-LEDGER-001…021, new in 0.7.0) is the independent
|
|
254
|
+
judge of ledger truth: contiguous receipt sequence, hash-chain and issuer
|
|
255
|
+
signatures, RFC 6962 Merkle re-derivation, checkpoint monotonicity and
|
|
256
|
+
equivocation refusal, corruption fail-closed vs bounded crash-tail
|
|
257
|
+
quarantine, writer/lock authority (never auto-stolen), witness agreement,
|
|
258
|
+
and mirror consistency. It reports five distinct outcomes — integrity /
|
|
259
|
+
origin (pinned anchor only) / operational exclusivity (lock metadata only) /
|
|
260
|
+
not-evaluated / violation — and is the contract any future storage backend
|
|
261
|
+
(PostgreSQL, hosted) must satisfy before carrying authority
|
|
262
|
+
([`docs/LEDGER_SENTINEL.md`](docs/LEDGER_SENTINEL.md), ships in the package).
|
|
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
|
+
|
|
249
291
|
Sentinels are read-only, redacting (paths + fingerprints, never secret
|
|
250
292
|
bodies), deterministic, network-free, and emit `magenta-sentinel-violation/1`
|
|
251
293
|
evidence records; exit codes map to a scoped promotion decision (`eligible` /
|
|
252
294
|
`blocked` / `requires-independent-review`). Records at this layer are
|
|
253
295
|
**unsigned local diagnostics**, and the freeze is a release/build promotion
|
|
254
|
-
gate — **not** a production kill switch, a hosted Sentinel service,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
[`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).
|
|
258
299
|
|
|
259
300
|
### From the repo vs. as a CLI
|
|
260
301
|
|
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": "155efd7408d116e3cc100b1dfa406dbabe0107584663e702348fec9106bafb2a",
|
|
7
7
|
"scripts/magenta-verify.js": "0bb65ef6ff1350789eecc4f0434ca94dcf3a89930f8ef1d62cc38100e18094ba",
|
|
8
8
|
"scripts/mcp-gateway.js": "335923004b73511fe42ea0fc6f2e567afe8018dc95464a79027e4c2537e30de1",
|
|
9
9
|
"server/agent-policy.js": "20dd9150f8244c33aaf14ecdf3a09f471210960d246cda9ab8d5abe0e8433518",
|
|
@@ -26,7 +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/
|
|
29
|
+
"server/sentinel/configuration-sentinel.js": "f8dd82c0bed36ba402d3c2fb52c068048f3e87941671b000c233d0f04acbecdb",
|
|
30
|
+
"server/sentinel/execution-sentinel.js": "644753a33d23e4295809a6d684151848fa7b1a639d54a7782caf11b4a4cf0253",
|
|
31
|
+
"server/sentinel/ledger-sentinel.js": "37d1c7040ae2506454247d40b42bad6076273b577190eeb3a6571184849b1e71",
|
|
32
|
+
"server/sentinel/mirror-sentinel.js": "2761d1e3099f600b7346406301b15a4bb256c2655ca4cff77341be907a9d438d",
|
|
33
|
+
"server/sentinel/promotion.js": "fa91250f59ccbe6f9c4e327dc4cce41fe46aacca38df70835ae1a7bf1833fe9b",
|
|
30
34
|
"server/sentinel/repository-sentinel.js": "722e973c8860cf281b29db8c4a22ec3cf473afcb088247916e8278d5e11638cf",
|
|
31
35
|
"server/sentinel/rules.js": "ab24435df4f6f5b10f8b721fde37fe7fa894453648f17f04923cab56020c459f",
|
|
32
36
|
"server/sentinel/tar-reader.js": "ded811eefebf3e4afa332d1bc0a48318cc22ad824222eb15b2dc08eb51048272",
|
|
@@ -39,7 +43,7 @@
|
|
|
39
43
|
"server/witness.js": "b1a8209d4dae4ffafc3ca4a158474b369250472ba80cef03bd92048282be91a4",
|
|
40
44
|
"shared/canonical.js": "476cee4b5c5702e8a2a198e79c2639cf8ff84000ffb92f68a04433ed2a2f5484",
|
|
41
45
|
"shared/certificates.js": "7844e71a38e1b1324586c7d054b2f5c15222b86446318d1e6dea9bab504a4a73",
|
|
42
|
-
"shared/invariants.js": "
|
|
46
|
+
"shared/invariants.js": "be71f2645e96d7c054954c9c99ded8f16e4d6dd2b6afd98b98d34938258555a8",
|
|
43
47
|
"shared/schema.js": "f89190990e3826e44c722d5e8f5b39fd59b4d3fb9ec047229930a17604e4646c",
|
|
44
48
|
"shared/terminating-kernel.js": "8e2aa68d47d6780e4a70822482e3e410924e9d1af843f301a487da0af37ed047"
|
|
45
49
|
}
|
|
@@ -43,6 +43,10 @@ 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");
|
|
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");
|
|
46
50
|
const promotion_1 = require("../server/sentinel/promotion");
|
|
47
51
|
const invariants_1 = require("../shared/invariants");
|
|
48
52
|
const USAGE = `magenta-sentinel — deterministic invariant evaluation (Sentinel Mesh foundation)
|
|
@@ -53,6 +57,12 @@ Usage:
|
|
|
53
57
|
sentinel pack [--json]
|
|
54
58
|
sentinel witness --ledger <file> [--keystore <file>] [--passphrase-env <VAR>]
|
|
55
59
|
[--anchor <pubkey-hex>] [--json]
|
|
60
|
+
sentinel ledger <ledger.jsonl> [--anchor <pubkey-hex>] [--keystore <file>]
|
|
61
|
+
[--passphrase-env <VAR>] [--mirror <mirror.jsonl>]
|
|
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]
|
|
56
66
|
sentinel invariants [--json]
|
|
57
67
|
|
|
58
68
|
Exit: 0 eligible/diagnostic-only · 1 blocked · 2 requires review · 3 usage/unreadable
|
|
@@ -169,6 +179,93 @@ try {
|
|
|
169
179
|
}
|
|
170
180
|
process.exit(exitFor(promo));
|
|
171
181
|
}
|
|
182
|
+
else if (sub === "ledger") {
|
|
183
|
+
const target = argv.find((a) => !a.startsWith("-"));
|
|
184
|
+
if (!target) {
|
|
185
|
+
console.error("sentinel ledger: missing <ledger.jsonl>\n\n" + USAGE);
|
|
186
|
+
process.exit(3);
|
|
187
|
+
}
|
|
188
|
+
const keystore = option(argv, "--keystore");
|
|
189
|
+
const mirror = option(argv, "--mirror");
|
|
190
|
+
const res = (0, ledger_sentinel_1.runLedgerSentinel)({
|
|
191
|
+
ledgerPath: path.resolve(target),
|
|
192
|
+
anchorPubkey: option(argv, "--anchor") ?? option(argv, "--expected-witness-key"),
|
|
193
|
+
keystorePath: keystore ? path.resolve(keystore) : undefined,
|
|
194
|
+
passphraseEnv: option(argv, "--passphrase-env"),
|
|
195
|
+
mirrorPath: mirror ? path.resolve(mirror) : undefined,
|
|
196
|
+
inspectLock: !flag(argv, "--no-lock-inspection"),
|
|
197
|
+
});
|
|
198
|
+
const promo = (0, promotion_1.decidePromotion)({ subject: `ledger:${res.subject_identity}`, findings: res.findings, violations: res.violations });
|
|
199
|
+
if (json)
|
|
200
|
+
console.log(JSON.stringify({ result: res, promotion: promo }, null, 2));
|
|
201
|
+
else {
|
|
202
|
+
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);
|
|
203
|
+
for (const n of res.not_evaluated)
|
|
204
|
+
console.log(` [NOT EVALUATED] ${n.invariant_id}: ${n.reason}`);
|
|
205
|
+
if (res.incomplete_tail_quarantined)
|
|
206
|
+
console.log(" note: incomplete trailing append quarantined (carries no authority)");
|
|
207
|
+
if (res.witness_cross_run)
|
|
208
|
+
console.log(` witness cross-run: ${res.witness_cross_run.failed.length === 0 ? "agrees" : "DISAGREES"} (tip ${res.witness_cross_run.tip_fingerprint ?? "-"})`);
|
|
209
|
+
}
|
|
210
|
+
process.exit(exitFor(promo));
|
|
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
|
+
}
|
|
172
269
|
else if (sub === "invariants") {
|
|
173
270
|
if (json)
|
|
174
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;
|