magenta-canon 0.5.0 → 0.6.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
@@ -22,6 +22,7 @@ npx magenta-canon demo # full local proof loop: allow · block
22
22
  npx magenta-canon verify --self-test # verifier self-check: all verdict levels incl. a tamper FAIL
23
23
  npx magenta-canon gateway <config.json> # the stdio MCP capability gateway
24
24
  npx magenta-canon mirror --self-test # external STH mirror: catch operator history rewrites
25
+ npx magenta-canon sentinel pack # Sentinel Mesh: invariant-check the packed artifact
25
26
  ```
26
27
 
27
28
  Magenta Canon is a proven **reference implementation**, not production-hosted infra
@@ -221,6 +222,40 @@ still regenerates on restart; old evidence keeps verifying). Keystore format,
221
222
  rotation protocol, boot-reconciliation table, and operator procedures:
222
223
  [`docs/WITNESS_IDENTITY.md`](docs/WITNESS_IDENTITY.md) (ships in the package).
223
224
 
225
+ ### Sentinel Mesh foundation — new in 0.6.0
226
+
227
+ Magenta Canon 0.6.0 introduces an **executable Sentinel Mesh** that protects
228
+ repository state, npm artifacts, dependency boundaries, release promotion,
229
+ witness identity, and cryptographically authorized witness-key rotation:
230
+ versioned invariants, deterministic repository / artifact / witness
231
+ evaluation, evidence-preserving violations, and scoped release-promotion
232
+ blocking.
233
+
234
+ ```bash
235
+ magenta-canon sentinel repository # invariant-check a git repository
236
+ magenta-canon sentinel artifact <tgz|dir> # invariant-check a packed artifact
237
+ magenta-canon sentinel pack # npm pack, then check the result
238
+ magenta-canon sentinel witness --ledger ledger.jsonl # witness rotation-authority check
239
+ magenta-canon sentinel invariants # list the invariant registry
240
+ ```
241
+
242
+ The **Witness & Rotation Sentinel** (MC-WIT-001…011) watches the 0.5.0
243
+ rotation-authority model itself: pinned-anchor continuity, dual-signed
244
+ rotation validity, exact epoch boundaries, epoch-correct STH signing,
245
+ retired-key resurrection / pre-activation refusal, and keystore↔ledger
246
+ agreement — re-derived independently from the published wire formats, so it
247
+ can disagree with a wrong (or compromised) server.
248
+
249
+ Sentinels are read-only, redacting (paths + fingerprints, never secret
250
+ bodies), deterministic, network-free, and emit `magenta-sentinel-violation/1`
251
+ evidence records; exit codes map to a scoped promotion decision (`eligible` /
252
+ `blocked` / `requires-independent-review`). Records at this layer are
253
+ **unsigned local diagnostics**, and the freeze is a release/build promotion
254
+ gate — **not** a production kill switch, a hosted Sentinel service, or
255
+ complete Mesh coverage (ledger/execution/configuration sentinels are future
256
+ lanes). Honest scope and the full model:
257
+ [`docs/SENTINEL_MESH.md`](docs/SENTINEL_MESH.md).
258
+
224
259
  ### From the repo vs. as a CLI
225
260
 
226
261
  - **From a repo checkout** (above): `npm install` then `npm run demo`.
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * magenta-canon — CLI entry point for the open-source reference implementation.
4
4
  *
@@ -48,6 +48,8 @@ Usage:
48
48
  verify <mirror>, check <mirror> <bundle>, --self-test.
49
49
  magenta-canon mirror-feed ... One feed cycle for a mirror operator:
50
50
  (--bundle <file> | --url <evidence-url>) --mirror <file>.
51
+ magenta-canon sentinel <cmd> Sentinel Mesh: repository | artifact <tgz|dir> |
52
+ pack | invariants (deterministic invariant checks).
51
53
  magenta-canon --help Show this help.
52
54
  magenta-canon --version Print the version.
53
55
 
@@ -110,6 +112,11 @@ switch (sub) {
110
112
  // Plain-Node operator helper; delegates all mirror semantics to `mirror`.
111
113
  run(process.execPath, [path.join(ROOT, "scripts", "mirror-feed.mjs"), ...rest], { cwd: process.cwd() });
112
114
  break;
115
+ case "sentinel":
116
+ // Sentinel Mesh foundation: deterministic invariant evaluation over a
117
+ // repository or a packed artifact (read-only; unsigned local diagnostics).
118
+ runTs("scripts/magenta-sentinel.ts", rest, { cwd: process.cwd() });
119
+ break;
113
120
  case "-v":
114
121
  case "--version":
115
122
  case "version":
@@ -3,6 +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": "0280d7584524201551c61a93beb1f2ee4964e76b0e44eb7c9d15a0f5a95cafdc",
6
7
  "scripts/magenta-verify.js": "0bb65ef6ff1350789eecc4f0434ca94dcf3a89930f8ef1d62cc38100e18094ba",
7
8
  "scripts/mcp-gateway.js": "335923004b73511fe42ea0fc6f2e567afe8018dc95464a79027e4c2537e30de1",
8
9
  "server/agent-policy.js": "20dd9150f8244c33aaf14ecdf3a09f471210960d246cda9ab8d5abe0e8433518",
@@ -24,6 +25,13 @@
24
25
  "server/ledger-store.js": "fc8a46f925908764bac7a076cffa08053af4c1f93237bee408eb0ed1e61eeca2",
25
26
  "server/ledger.js": "9bf8f132e08d636070d2ede568b2eaf75810be245c90ecd5979d2d4de69af934",
26
27
  "server/persistence.js": "2e6b1d0b1c74babbd581780b028c2593256c5ec96c2d60f4c25159e3f54e4e3f",
28
+ "server/sentinel/artifact-sentinel.js": "93bae917af313c9247f8062af148e7c25ccfd16d0507fa85e755c64056715f8d",
29
+ "server/sentinel/promotion.js": "68ac5e54eb8c07d5c75ba0d2325d595b1583557840cc46322b2053dd9924a0be",
30
+ "server/sentinel/repository-sentinel.js": "722e973c8860cf281b29db8c4a22ec3cf473afcb088247916e8278d5e11638cf",
31
+ "server/sentinel/rules.js": "ab24435df4f6f5b10f8b721fde37fe7fa894453648f17f04923cab56020c459f",
32
+ "server/sentinel/tar-reader.js": "ded811eefebf3e4afa332d1bc0a48318cc22ad824222eb15b2dc08eb51048272",
33
+ "server/sentinel/violation.js": "70d44ae5eb43a560fada399b31eac1a2beccb66916340e1f3e9b549731776b38",
34
+ "server/sentinel/witness-sentinel.js": "f024dbe9fb505c7520ec466ed31bf9b3284c3c3066dbd287121d2e4397d3fb0a",
27
35
  "server/storage.js": "7a7556a6d15d736f028698b6094072daa71eed2230427594edac9d29b531081a",
28
36
  "server/transparency-log.js": "7f238edc5cad4edc2c70a7b3e1977197cf2087e77789d181ffae0035f948391b",
29
37
  "server/trust-bootstrap.js": "57a53a3ee9cd630ada2867e8b087a34b069ba26f86a696d3ead74888dd7e8d5f",
@@ -31,6 +39,7 @@
31
39
  "server/witness.js": "b1a8209d4dae4ffafc3ca4a158474b369250472ba80cef03bd92048282be91a4",
32
40
  "shared/canonical.js": "476cee4b5c5702e8a2a198e79c2639cf8ff84000ffb92f68a04433ed2a2f5484",
33
41
  "shared/certificates.js": "7844e71a38e1b1324586c7d054b2f5c15222b86446318d1e6dea9bab504a4a73",
42
+ "shared/invariants.js": "414a2aac034c9b28d0e32fc4afefb9b9ab723ed465b4aba1f8da98dab08363d4",
34
43
  "shared/schema.js": "f89190990e3826e44c722d5e8f5b39fd59b4d3fb9ec047229930a17604e4646c",
35
44
  "shared/terminating-kernel.js": "8e2aa68d47d6780e4a70822482e3e410924e9d1af843f301a487da0af37ed047"
36
45
  }
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || function (mod) {
20
+ if (mod && mod.__esModule) return mod;
21
+ var result = {};
22
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
23
+ __setModuleDefault(result, mod);
24
+ return result;
25
+ };
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ /**
28
+ * magenta-sentinel — Sentinel Mesh CLI (foundation layer).
29
+ *
30
+ * magenta-canon sentinel repository [--root <dir>] [--json]
31
+ * magenta-canon sentinel artifact <tgz|package-dir> [--expected-version <v>] [--json]
32
+ * magenta-canon sentinel pack [--json] (npm pack, then evaluate the tarball)
33
+ *
34
+ * Exit codes: 0 eligible/diagnostic-only · 1 blocked · 2 requires-independent-review
35
+ * 3 usage / subject unreadable.
36
+ *
37
+ * Honesty: all records emitted here are UNSIGNED LOCAL DIAGNOSTICS. They are
38
+ * structured findings, not third-party-verifiable attestations.
39
+ */
40
+ const node_child_process_1 = require("node:child_process");
41
+ const node_fs_1 = require("node:fs");
42
+ const path = __importStar(require("node:path"));
43
+ const repository_sentinel_1 = require("../server/sentinel/repository-sentinel");
44
+ const artifact_sentinel_1 = require("../server/sentinel/artifact-sentinel");
45
+ const witness_sentinel_1 = require("../server/sentinel/witness-sentinel");
46
+ const promotion_1 = require("../server/sentinel/promotion");
47
+ const invariants_1 = require("../shared/invariants");
48
+ const USAGE = `magenta-sentinel — deterministic invariant evaluation (Sentinel Mesh foundation)
49
+
50
+ Usage:
51
+ sentinel repository [--root <dir>] [--json]
52
+ sentinel artifact <tarball.tgz | package-dir> [--expected-version <v>] [--json]
53
+ sentinel pack [--json]
54
+ sentinel witness --ledger <file> [--keystore <file>] [--passphrase-env <VAR>]
55
+ [--anchor <pubkey-hex>] [--json]
56
+ sentinel invariants [--json]
57
+
58
+ Exit: 0 eligible/diagnostic-only · 1 blocked · 2 requires review · 3 usage/unreadable
59
+ `;
60
+ function flag(args, name) {
61
+ const i = args.indexOf(name);
62
+ if (i >= 0) {
63
+ args.splice(i, 1);
64
+ return true;
65
+ }
66
+ return false;
67
+ }
68
+ function option(args, name) {
69
+ const i = args.indexOf(name);
70
+ if (i >= 0 && i + 1 < args.length) {
71
+ const v = args[i + 1];
72
+ args.splice(i, 2);
73
+ return v;
74
+ }
75
+ return undefined;
76
+ }
77
+ function printHuman(kind, subject, result, promo) {
78
+ console.log(`magenta-sentinel ${kind}`);
79
+ console.log(` subject: ${subject}`);
80
+ console.log(` invariants evaluated: ${result.evaluated_invariants.length} — passed ${result.passed.length}, failed ${result.failed.length}`);
81
+ for (const f of result.findings) {
82
+ console.log(` [FAIL] ${f.invariant_id} (${f.severity}, ${f.disposition})`);
83
+ console.log(` observed: ${f.observed}`);
84
+ console.log(` paths: ${f.paths.join(", ") || "-"}`);
85
+ }
86
+ for (const v of result.violations) {
87
+ console.log(` evidence: ${v.invariant_id} record ${v.record_hash.slice(0, 16)}… (${v.assurance})`);
88
+ }
89
+ if (result.findings.length === 0)
90
+ console.log(" all evaluated invariants hold");
91
+ console.log(` PROMOTION: ${promo.decision.toUpperCase()}${promo.evidence_complete ? "" : " (evidence INCOMPLETE)"}`);
92
+ }
93
+ function exitFor(promo) {
94
+ switch (promo.decision) {
95
+ case "eligible":
96
+ case "diagnostic-only": return 0;
97
+ case "blocked": return 1;
98
+ case "requires-independent-review": return 2;
99
+ }
100
+ }
101
+ const argv = process.argv.slice(2);
102
+ const sub = argv.shift();
103
+ const json = flag(argv, "--json");
104
+ try {
105
+ if (sub === "repository") {
106
+ const root = option(argv, "--root") ?? process.cwd();
107
+ const res = (0, repository_sentinel_1.runRepositorySentinel)({ repoRoot: path.resolve(root) });
108
+ const promo = (0, promotion_1.decidePromotion)({ subject: `repository:${res.subject_identity}`, findings: res.findings, violations: res.violations });
109
+ if (json)
110
+ console.log(JSON.stringify({ result: res, promotion: promo }, null, 2));
111
+ else
112
+ printHuman("repository", res.subject_identity, res, promo);
113
+ process.exit(exitFor(promo));
114
+ }
115
+ else if (sub === "artifact") {
116
+ const target = argv.find((a) => !a.startsWith("-"));
117
+ if (!target) {
118
+ console.error("sentinel artifact: missing <tarball|dir>\n\n" + USAGE);
119
+ process.exit(3);
120
+ }
121
+ const expectedVersion = option(argv, "--expected-version");
122
+ const res = (0, artifact_sentinel_1.runArtifactSentinel)(path.resolve(target), { expectedVersion });
123
+ const promo = (0, promotion_1.decidePromotion)({ subject: `artifact:${res.subject_identity}`, findings: res.findings, violations: res.violations });
124
+ if (json)
125
+ console.log(JSON.stringify({ result: res, promotion: promo }, null, 2));
126
+ else
127
+ printHuman("artifact", `${res.artifact_name ?? "?"}@${res.artifact_version ?? "?"} (${res.subject_identity.slice(0, 19)}…, ${res.file_count} files)`, res, promo);
128
+ process.exit(exitFor(promo));
129
+ }
130
+ else if (sub === "pack") {
131
+ const out = (0, node_child_process_1.execFileSync)("npm", ["pack", "--silent"], { cwd: process.cwd(), encoding: "utf8", shell: process.platform === "win32" });
132
+ const tgz = out.trim().split(/\r?\n/).pop();
133
+ if (!(0, node_fs_1.existsSync)(tgz)) {
134
+ console.error(`sentinel pack: npm pack did not produce ${tgz}`);
135
+ process.exit(3);
136
+ }
137
+ const res = (0, artifact_sentinel_1.runArtifactSentinel)(path.resolve(tgz));
138
+ const promo = (0, promotion_1.decidePromotion)({ subject: `artifact:${res.subject_identity}`, findings: res.findings, violations: res.violations });
139
+ if (json)
140
+ console.log(JSON.stringify({ result: res, promotion: promo, tarball: tgz }, null, 2));
141
+ else {
142
+ console.log(` packed: ${tgz}`);
143
+ printHuman("artifact", `${res.artifact_name}@${res.artifact_version}`, res, promo);
144
+ }
145
+ process.exit(exitFor(promo));
146
+ }
147
+ else if (sub === "witness") {
148
+ const ledger = option(argv, "--ledger");
149
+ if (!ledger) {
150
+ console.error("sentinel witness: missing --ledger <file>\n\n" + USAGE);
151
+ process.exit(3);
152
+ }
153
+ const keystore = option(argv, "--keystore");
154
+ const res = (0, witness_sentinel_1.runWitnessSentinel)({
155
+ ledgerPath: path.resolve(ledger),
156
+ keystorePath: keystore ? path.resolve(keystore) : undefined,
157
+ passphraseEnv: option(argv, "--passphrase-env"),
158
+ anchorPubkey: option(argv, "--anchor"),
159
+ });
160
+ const promo = (0, promotion_1.decidePromotion)({ subject: `witness:${res.subject_identity}`, findings: res.findings, violations: res.violations });
161
+ if (json)
162
+ console.log(JSON.stringify({ result: res, promotion: promo }, null, 2));
163
+ else {
164
+ printHuman("witness", `${res.subject_identity.slice(0, 27)}… (${res.ledger_sth_count} STH, ${res.ledger_rotation_count} rotation(s), tip ${res.tip_fingerprint ?? "-"})`, res, promo);
165
+ for (const n of res.not_evaluated)
166
+ console.log(` [NOT EVALUATED] ${n.invariant_id}: ${n.reason}`);
167
+ if (res.incomplete_tail_ignored)
168
+ console.log(" note: incomplete trailing ledger line ignored (crash-tail semantics)");
169
+ }
170
+ process.exit(exitFor(promo));
171
+ }
172
+ else if (sub === "invariants") {
173
+ if (json)
174
+ console.log(JSON.stringify(invariants_1.INVARIANTS, null, 2));
175
+ else
176
+ for (const i of invariants_1.INVARIANTS)
177
+ console.log(`${i.id} v${i.version} ${i.status.padEnd(9)} ${i.severity.padEnd(8)} ${i.disposition.padEnd(26)} ${i.name}`);
178
+ process.exit(0);
179
+ }
180
+ else {
181
+ process.stdout.write(USAGE);
182
+ process.exit(sub === undefined || sub === "--help" || sub === "-h" ? 0 : 3);
183
+ }
184
+ }
185
+ catch (e) {
186
+ if (e instanceof artifact_sentinel_1.TarError) {
187
+ console.error(`sentinel: artifact unreadable (refusing to guess): ${e.message}`);
188
+ process.exit(3);
189
+ }
190
+ console.error(`sentinel: ${e.message}`);
191
+ process.exit(3);
192
+ }
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.TarError = exports.runArtifactSentinel = exports.ARTIFACT_SENTINEL_VERSION = exports.ARTIFACT_SENTINEL_TYPE = void 0;
27
+ /**
28
+ * ARTIFACT SENTINEL — deterministic evaluation of artifact/dependency
29
+ * invariants (MC-ART-*, MC-DEP-*) over a packed npm artifact.
30
+ *
31
+ * Three subject modes:
32
+ * - tarball: a .tgz file (read in-memory via the hardened tar reader);
33
+ * - directory: an extracted package directory (or any package root);
34
+ * - the current `npm pack` output (the CLI wires this; the sentinel itself
35
+ * only ever sees a tarball path or directory path).
36
+ *
37
+ * Read-only (MC-REL-004), no network, deterministic, redacting. It does NOT
38
+ * claim npm-registry origin: subject identity is the sha256 the sentinel
39
+ * computed locally; binding that to a registry artifact requires registry
40
+ * metadata supplied and verified independently.
41
+ */
42
+ const node_crypto_1 = require("node:crypto");
43
+ const node_fs_1 = require("node:fs");
44
+ const path = __importStar(require("node:path"));
45
+ const invariants_1 = require("../../shared/invariants");
46
+ const violation_1 = require("./violation");
47
+ const tar_reader_1 = require("./tar-reader");
48
+ Object.defineProperty(exports, "TarError", { enumerable: true, get: function () { return tar_reader_1.TarError; } });
49
+ const rules_1 = require("./rules");
50
+ exports.ARTIFACT_SENTINEL_TYPE = "artifact-sentinel";
51
+ exports.ARTIFACT_SENTINEL_VERSION = "1.0.0";
52
+ function finding(invId, observed, expected, paths) {
53
+ const inv = (0, invariants_1.getInvariant)(invId);
54
+ return {
55
+ invariant_id: inv.id, invariant_version: inv.version, severity: inv.severity,
56
+ authority_class: inv.authorityClass, disposition: inv.disposition,
57
+ observed, expected, paths: paths.sort(),
58
+ };
59
+ }
60
+ function loadTarball(tgzPath) {
61
+ const buf = (0, node_fs_1.readFileSync)(tgzPath);
62
+ const sha256 = (0, node_crypto_1.createHash)("sha256").update(buf).digest("hex");
63
+ const entries = (0, tar_reader_1.readTgz)(buf);
64
+ const suspicious = entries.filter((e) => e.suspiciousPath).map((e) => e.name);
65
+ const files = entries
66
+ .filter((e) => e.typeflag === "0" || e.typeflag === "\0")
67
+ .map((e) => ({ rel: e.name.replace(/^package\//, ""), content: e.content }));
68
+ return { files, sha256, suspicious };
69
+ }
70
+ function walkDir(root) {
71
+ const out = [];
72
+ const stack = [root];
73
+ while (stack.length) {
74
+ const dir = stack.pop();
75
+ for (const name of (0, node_fs_1.readdirSync)(dir).sort()) {
76
+ const abs = path.join(dir, name);
77
+ const st = (0, node_fs_1.statSync)(abs);
78
+ if (st.isDirectory()) {
79
+ if (name === "node_modules" || name === ".git")
80
+ continue;
81
+ stack.push(abs);
82
+ }
83
+ else if (st.isFile()) {
84
+ out.push(abs);
85
+ }
86
+ }
87
+ }
88
+ return out.sort();
89
+ }
90
+ function loadDirectory(dirPath) {
91
+ const abs = walkDir(dirPath);
92
+ const files = abs.map((a) => ({
93
+ rel: path.relative(dirPath, a).split(path.sep).join("/"),
94
+ content: (0, node_fs_1.readFileSync)(a),
95
+ }));
96
+ // Deterministic directory identity: hash of sorted (path, sha256) pairs.
97
+ const h = (0, node_crypto_1.createHash)("sha256");
98
+ for (const f of files) {
99
+ h.update(f.rel);
100
+ h.update("\0");
101
+ h.update((0, node_crypto_1.createHash)("sha256").update(f.content).digest());
102
+ h.update("\0");
103
+ }
104
+ return { files, sha256: `dir:${h.digest("hex")}` };
105
+ }
106
+ function runArtifactSentinel(subjectPath, opts = {}) {
107
+ if (!(0, node_fs_1.existsSync)(subjectPath)) {
108
+ throw new Error(`artifact-sentinel: subject not found: ${subjectPath}`);
109
+ }
110
+ const isDir = (0, node_fs_1.statSync)(subjectPath).isDirectory();
111
+ const findings = [];
112
+ let files;
113
+ let subjectIdentity;
114
+ const mode = isDir ? "directory" : "tarball";
115
+ if (isDir) {
116
+ ({ files, sha256: subjectIdentity } = loadDirectory(subjectPath));
117
+ }
118
+ else {
119
+ const loaded = loadTarball(subjectPath); // throws TarError on malformed/bomb
120
+ files = loaded.files;
121
+ subjectIdentity = loaded.sha256;
122
+ if (loaded.suspicious.length > 0) {
123
+ // No invariant ID needed to refuse this: a traversal-shaped entry means
124
+ // the archive is not an honest npm artifact. Reported under MC-ART-007
125
+ // (manifest/files integrity) as a structural integrity failure.
126
+ findings.push(finding("MC-ART-007", `archive contains suspicious entry path(s): ${loaded.suspicious.join(", ")}`, "all archive entry paths are relative and traversal-free", loaded.suspicious));
127
+ }
128
+ }
129
+ const fileMap = new Map(files.map((f) => [f.rel, f]));
130
+ const relPaths = files.map((f) => f.rel);
131
+ // package.json metadata
132
+ let pkg = null;
133
+ const pkgFile = fileMap.get("package.json");
134
+ if (pkgFile) {
135
+ try {
136
+ pkg = JSON.parse(pkgFile.content.toString("utf8"));
137
+ }
138
+ catch {
139
+ pkg = null;
140
+ }
141
+ }
142
+ const evaluatedDomains = new Set(["artifact", "dependency"]);
143
+ const evaluated = (0, invariants_1.ratifiedInvariants)().filter((i) => evaluatedDomains.has(i.domain)).map((i) => i.id);
144
+ // ── MC-ART-001 / MC-ART-002: secrets & identities ─────────────────────
145
+ const pemHits = [];
146
+ const prints = [];
147
+ for (const f of files) {
148
+ if (f.content.length > rules_1.MAX_SCAN_BYTES || (0, rules_1.isProbablyBinary)(f.content))
149
+ continue;
150
+ const text = f.content.toString("utf8");
151
+ if (rules_1.PEM_PRIVATE_KEY_WITH_BODY.test(text)) {
152
+ pemHits.push(f.rel);
153
+ prints.push(`${f.rel}=${(0, rules_1.matchFingerprint)(text, rules_1.PEM_PRIVATE_KEY_WITH_BODY)}`);
154
+ }
155
+ }
156
+ if (pemHits.length > 0) {
157
+ findings.push(finding("MC-ART-001", `PEM private-key material in artifact; fingerprints: ${prints.join(", ")}`, "artifact contains no private-key material", pemHits));
158
+ }
159
+ const identityHits = relPaths.filter((p) => rules_1.FORBIDDEN_IDENTITY_PATHS.some((re) => re.test(p)));
160
+ if (identityHits.length > 0) {
161
+ findings.push(finding("MC-ART-002", "generated identity / keystore entry in artifact", "no identity or keystore entries", identityHits));
162
+ }
163
+ // ── MC-ART-003: tests ──────────────────────────────────────────────────
164
+ const testHits = relPaths.filter((p) => rules_1.TEST_FILE_PATHS.some((re) => re.test(p)));
165
+ if (testHits.length > 0) {
166
+ findings.push(finding("MC-ART-003", "test file(s) in artifact", "no test files ship", testHits));
167
+ }
168
+ // ── MC-ART-004: hosted plane (+ legacy heartbeat surfaces) ────────────
169
+ const hostedHits = relPaths.filter((p) => rules_1.HOSTED_PLANE_PATHS.some((re) => re.test(p)) || rules_1.FORBIDDEN_HEARTBEAT_PATHS.some((re) => re.test(p)));
170
+ if (hostedHits.length > 0) {
171
+ findings.push(finding("MC-ART-004", "hosted-only / legacy module(s) in artifact", "no hosted-plane or legacy heartbeat modules ship", hostedHits));
172
+ }
173
+ // ── MC-ART-005: runtime TS / source maps ──────────────────────────────
174
+ const tsHits = relPaths.filter((p) => rules_1.RUNTIME_TS_PATH.test(p) && !rules_1.TYPE_DECLARATION_PATH.test(p));
175
+ const mapHits = relPaths.filter((p) => rules_1.SOURCE_MAP_PATH.test(p));
176
+ if (tsHits.length + mapHits.length > 0) {
177
+ findings.push(finding("MC-ART-005", "runtime TypeScript or source map(s) in artifact", "precompiled JS only; no .ts (non-decl) or .map", [...tsHits, ...mapHits]));
178
+ }
179
+ // ── MC-ART-006: required entrypoints ──────────────────────────────────
180
+ const missingEntry = rules_1.REQUIRED_DIST_ENTRYPOINTS.filter((e) => !fileMap.has(e));
181
+ if (missingEntry.length > 0) {
182
+ findings.push(finding("MC-ART-006", `missing compiled entrypoint(s): ${missingEntry.join(", ")}`, "all required compiled entrypoints present", missingEntry));
183
+ }
184
+ // ── MC-ART-007: manifest equality ─────────────────────────────────────
185
+ const manifestFile = fileMap.get("dist/MANIFEST.json");
186
+ if (!manifestFile) {
187
+ findings.push(finding("MC-ART-007", "dist/MANIFEST.json missing", "manifest present and matching", ["dist/MANIFEST.json"]));
188
+ }
189
+ else {
190
+ try {
191
+ const manifest = JSON.parse(manifestFile.content.toString("utf8"));
192
+ // build-lean writes a flat { "<dist-relative path>": "<sha256>" } map.
193
+ const listed = typeof manifest.files === "object" && manifest.files !== null ? manifest.files : manifest;
194
+ const distFiles = relPaths.filter((p) => p.startsWith("dist/") && p !== "dist/MANIFEST.json");
195
+ const bad = [];
196
+ for (const [rel, hash] of Object.entries(listed)) {
197
+ const f = fileMap.get(`dist/${rel}`);
198
+ if (!f) {
199
+ bad.push(`missing:dist/${rel}`);
200
+ continue;
201
+ }
202
+ const actual = (0, node_crypto_1.createHash)("sha256").update(f.content).digest("hex");
203
+ if (actual !== hash)
204
+ bad.push(`hash-mismatch:dist/${rel}`);
205
+ }
206
+ for (const p of distFiles) {
207
+ const rel = p.replace(/^dist\//, "");
208
+ if (!(rel in listed))
209
+ bad.push(`unlisted:${p}`);
210
+ }
211
+ if (bad.length > 0) {
212
+ findings.push(finding("MC-ART-007", `manifest disagreement: ${bad.join(", ")}`, "dist/MANIFEST.json lists exactly the dist files with matching sha256", bad));
213
+ }
214
+ }
215
+ catch {
216
+ findings.push(finding("MC-ART-007", "dist/MANIFEST.json unparseable", "manifest present and matching", ["dist/MANIFEST.json"]));
217
+ }
218
+ }
219
+ // ── MC-ART-008: version consistency ───────────────────────────────────
220
+ const artifactVersion = pkg?.version ?? null;
221
+ if (!pkg) {
222
+ findings.push(finding("MC-ART-008", "package.json missing or unparseable", "artifact carries valid package metadata", ["package.json"]));
223
+ }
224
+ else if (opts.expectedVersion && artifactVersion !== opts.expectedVersion) {
225
+ findings.push(finding("MC-ART-008", `artifact version ${artifactVersion} != expected ${opts.expectedVersion}`, "artifact version equals the version under evaluation", ["package.json"]));
226
+ }
227
+ // ── MC-DEP-*: dependency posture ──────────────────────────────────────
228
+ if (pkg) {
229
+ const deps = pkg.dependencies ?? {};
230
+ const depNames = Object.keys(deps).sort();
231
+ const unexpected = depNames.filter((d) => !rules_1.APPROVED_RUNTIME_DEPENDENCIES.includes(d));
232
+ if (unexpected.length > 0) {
233
+ findings.push(finding("MC-DEP-001", `unexpected runtime dependencies: ${unexpected.map((d) => `${d}@${deps[d]}`).join(", ")}`, `runtime dependencies exactly: ${rules_1.APPROVED_RUNTIME_DEPENDENCIES.join(", ")}`, ["package.json"]));
234
+ }
235
+ const installScripts = rules_1.INSTALL_SCRIPT_NAMES.filter((s) => pkg.scripts && s in pkg.scripts);
236
+ if (installScripts.length > 0) {
237
+ findings.push(finding("MC-DEP-002", `install lifecycle script(s) declared: ${installScripts.join(", ")}`, "no install lifecycle scripts", ["package.json"]));
238
+ }
239
+ const transpilers = depNames.filter((d) => rules_1.FORBIDDEN_TRANSPILERS.includes(d));
240
+ if (transpilers.length > 0) {
241
+ findings.push(finding("MC-DEP-004", `runtime transpiler dependency: ${transpilers.join(", ")}`, "no runtime transpiler/loader dependencies", ["package.json"]));
242
+ }
243
+ const hostedDeps = depNames.filter((d) => rules_1.HOSTED_DEPENDENCIES.includes(d));
244
+ if (hostedDeps.length > 0) {
245
+ findings.push(finding("MC-DEP-005", `hosted-plane dependency leaked: ${hostedDeps.join(", ")}`, "no hosted-plane runtime dependencies", ["package.json"]));
246
+ }
247
+ }
248
+ const nativeHits = [];
249
+ for (const f of files) {
250
+ const cls = (0, rules_1.nativeBinaryClass)(f.content);
251
+ if (cls || f.rel.endsWith(".node"))
252
+ nativeHits.push(`${f.rel}${cls ? ` (${cls})` : ""}`);
253
+ }
254
+ if (nativeHits.length > 0) {
255
+ findings.push(finding("MC-DEP-003", `native binary entries: ${nativeHits.join(", ")}`, "no native binaries in artifact", nativeHits.map((s) => s.split(" ")[0])));
256
+ }
257
+ const failed = Array.from(new Set(findings.map((f) => f.invariant_id))).sort();
258
+ const passed = evaluated.filter((id) => !failed.includes(id)).sort();
259
+ const violations = findings.map((f) => (0, violation_1.buildViolationRecord)({
260
+ sentinelType: exports.ARTIFACT_SENTINEL_TYPE,
261
+ sentinelVersion: exports.ARTIFACT_SENTINEL_VERSION,
262
+ subjectType: "artifact",
263
+ subjectIdentity,
264
+ finding: f,
265
+ detectedAt: opts.detectedAt,
266
+ detector: opts.detector,
267
+ }));
268
+ const blocking = findings.some((f) => f.disposition !== "report");
269
+ return {
270
+ sentinel_type: exports.ARTIFACT_SENTINEL_TYPE,
271
+ sentinel_version: exports.ARTIFACT_SENTINEL_VERSION,
272
+ subject_type: "artifact",
273
+ subject_identity: subjectIdentity,
274
+ subject_mode: mode,
275
+ artifact_name: pkg?.name ?? null,
276
+ artifact_version: artifactVersion,
277
+ file_count: files.length,
278
+ evaluated_invariants: evaluated.sort(),
279
+ passed,
280
+ failed,
281
+ findings,
282
+ violations,
283
+ release_promotion_eligible: !blocking,
284
+ };
285
+ }
286
+ exports.runArtifactSentinel = runArtifactSentinel;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decidePromotion = void 0;
4
+ const violation_1 = require("./violation");
5
+ const REVIEW_AUTHORITY_CLASSES = new Set(["witness", "issuer", "release", "operator"]);
6
+ function decidePromotion(input) {
7
+ const blocking = [];
8
+ const review = [];
9
+ const diagnostic = [];
10
+ for (const f of input.findings) {
11
+ if (f.disposition === "report") {
12
+ diagnostic.push(f.invariant_id);
13
+ continue;
14
+ }
15
+ if (f.disposition === "require-independent-review" || REVIEW_AUTHORITY_CLASSES.has(f.authority_class)) {
16
+ review.push(f.invariant_id);
17
+ continue;
18
+ }
19
+ blocking.push(f.invariant_id);
20
+ }
21
+ // MC-REL-003: every blocking/review finding must have a verifiable record.
22
+ const recordsByInvariant = new Map();
23
+ for (const v of input.violations) {
24
+ const list = recordsByInvariant.get(v.invariant_id) ?? [];
25
+ list.push(v);
26
+ recordsByInvariant.set(v.invariant_id, list);
27
+ }
28
+ let evidenceComplete = true;
29
+ for (const id of [...blocking, ...review]) {
30
+ const recs = recordsByInvariant.get(id) ?? [];
31
+ if (recs.length === 0 || !recs.every(violation_1.verifyViolationRecord))
32
+ evidenceComplete = false;
33
+ }
34
+ let decision;
35
+ if (review.length > 0)
36
+ decision = "requires-independent-review";
37
+ else if (blocking.length > 0)
38
+ decision = "blocked";
39
+ else if (diagnostic.length > 0)
40
+ decision = "diagnostic-only";
41
+ else
42
+ decision = "eligible";
43
+ // MC-REL-002/003: a blocking state without complete evidence is still a
44
+ // freeze — and escalates, because a silent or unevidenced block is itself
45
+ // a sentinel failure that needs human eyes.
46
+ if (!evidenceComplete && decision !== "eligible")
47
+ decision = "requires-independent-review";
48
+ return {
49
+ subject: input.subject,
50
+ decision,
51
+ blocking_invariants: Array.from(new Set(blocking)).sort(),
52
+ review_invariants: Array.from(new Set(review)).sort(),
53
+ diagnostic_invariants: Array.from(new Set(diagnostic)).sort(),
54
+ evidence_record_hashes: input.violations.map((v) => v.record_hash).sort(),
55
+ evidence_complete: evidenceComplete,
56
+ };
57
+ }
58
+ exports.decidePromotion = decidePromotion;