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.
- package/README.md +34 -4
- package/bin/magenta-canon.mjs +12 -0
- package/dist/MANIFEST.json +6 -3
- package/dist/scripts/magenta-sentinel.js +63 -0
- package/dist/server/sentinel/configuration-sentinel.js +244 -0
- package/dist/server/sentinel/execution-sentinel.js +326 -0
- package/dist/server/sentinel/mirror-sentinel.js +261 -0
- package/dist/server/sentinel/promotion.js +1 -1
- package/dist/shared/invariants.js +852 -5
- package/docs/NPM_PACKAGING.md +35 -29
- package/examples/verticals/README.md +54 -0
- package/examples/verticals/devops-deploy/README.md +44 -0
- package/examples/verticals/devops-deploy/gateway.config.json +23 -0
- package/examples/verticals/devops-deploy/scenario.json +30 -0
- package/examples/verticals/ecommerce-ops/README.md +45 -0
- package/examples/verticals/ecommerce-ops/gateway.config.json +26 -0
- package/examples/verticals/ecommerce-ops/scenario.json +38 -0
- package/examples/verticals/fintech-refund/README.md +44 -0
- package/examples/verticals/fintech-refund/gateway.config.json +31 -0
- package/examples/verticals/fintech-refund/scenario.json +30 -0
- package/examples/verticals/healthcare-scribe/README.md +46 -0
- package/examples/verticals/healthcare-scribe/gateway.config.json +26 -0
- package/examples/verticals/healthcare-scribe/scenario.json +48 -0
- package/examples/verticals/insurance-claims/README.md +45 -0
- package/examples/verticals/insurance-claims/gateway.config.json +26 -0
- package/examples/verticals/insurance-claims/scenario.json +38 -0
- package/examples/verticals/legal-paralegal/README.md +45 -0
- package/examples/verticals/legal-paralegal/gateway.config.json +26 -0
- package/examples/verticals/legal-paralegal/scenario.json +38 -0
- package/examples/verticals/lib/tool-server.mjs +66 -0
- package/examples/verticals/saas-support-data/README.md +45 -0
- package/examples/verticals/saas-support-data/gateway.config.json +25 -0
- package/examples/verticals/saas-support-data/scenario.json +42 -0
- package/examples/verticals/security-soc/README.md +45 -0
- package/examples/verticals/security-soc/gateway.config.json +26 -0
- package/examples/verticals/security-soc/scenario.json +36 -0
- package/package.json +3 -1
- 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,
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
package/bin/magenta-canon.mjs
CHANGED
|
@@ -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
|
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,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/
|
|
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": "
|
|
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;
|