protect-mcp 0.11.0 → 0.11.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.
@@ -18,7 +18,8 @@ import {
18
18
  import {
19
19
  evaluateCedar,
20
20
  isCedarAvailable,
21
- loadCedarPolicies
21
+ loadCedarPolicies,
22
+ runEvaluatorSelfTest
22
23
  } from "./chunk-FGCNKEEW.mjs";
23
24
  import {
24
25
  receiptHash
@@ -826,6 +827,41 @@ async function startHookServer(options = {}) {
826
827
  }
827
828
  }
828
829
  }
830
+ if (enforce) {
831
+ const selfTest = await runEvaluatorSelfTest();
832
+ for (const c of selfTest.cases) {
833
+ if (!c.pass) {
834
+ process.stderr.write(
835
+ `[PROTECT_MCP] SELF-TEST FAIL: ${c.name} (expected ${c.expected}, got ${c.actual})
836
+ `
837
+ );
838
+ }
839
+ }
840
+ if (!selfTest.passed) {
841
+ throw new Error(
842
+ "enforce mode refused to start: the restraint self-test failed. A gate that cannot prove it denies must not arm."
843
+ );
844
+ }
845
+ const receiptProbe = signDecision({
846
+ v: 2,
847
+ tool: "__protect_mcp_startup_selftest__",
848
+ decision: "deny",
849
+ reason_code: "startup_selftest",
850
+ policy_digest: policyDigest,
851
+ request_id: `selftest-${Date.now()}`,
852
+ mode: "enforce",
853
+ timestamp: Date.now()
854
+ });
855
+ if (receiptProbe.error) {
856
+ throw new Error(
857
+ `enforce mode refused to start: signing is configured but a denial receipt could not be produced (${receiptProbe.error}). An enforcing gate that cannot evidence a denial must not arm.`
858
+ );
859
+ }
860
+ process.stderr.write(
861
+ `[PROTECT_MCP] Restraint self-test passed (${selfTest.cases.length} vectors${selfTest.wasmAvailable ? "" : "; Cedar WASM absent, fail-closed invariant verified"})${receiptProbe.ok ? "; denial receipt signing verified" : ""}. Arming enforce mode.
862
+ `
863
+ );
864
+ }
829
865
  const state = {
830
866
  cedarPolicies,
831
867
  cedarDir: cedarDir || null,
package/dist/cli.js CHANGED
@@ -4930,6 +4930,41 @@ async function startHookServer(options = {}) {
4930
4930
  }
4931
4931
  }
4932
4932
  }
4933
+ if (enforce) {
4934
+ const selfTest = await runEvaluatorSelfTest();
4935
+ for (const c of selfTest.cases) {
4936
+ if (!c.pass) {
4937
+ process.stderr.write(
4938
+ `[PROTECT_MCP] SELF-TEST FAIL: ${c.name} (expected ${c.expected}, got ${c.actual})
4939
+ `
4940
+ );
4941
+ }
4942
+ }
4943
+ if (!selfTest.passed) {
4944
+ throw new Error(
4945
+ "enforce mode refused to start: the restraint self-test failed. A gate that cannot prove it denies must not arm."
4946
+ );
4947
+ }
4948
+ const receiptProbe = signDecision({
4949
+ v: 2,
4950
+ tool: "__protect_mcp_startup_selftest__",
4951
+ decision: "deny",
4952
+ reason_code: "startup_selftest",
4953
+ policy_digest: policyDigest,
4954
+ request_id: `selftest-${Date.now()}`,
4955
+ mode: "enforce",
4956
+ timestamp: Date.now()
4957
+ });
4958
+ if (receiptProbe.error) {
4959
+ throw new Error(
4960
+ `enforce mode refused to start: signing is configured but a denial receipt could not be produced (${receiptProbe.error}). An enforcing gate that cannot evidence a denial must not arm.`
4961
+ );
4962
+ }
4963
+ process.stderr.write(
4964
+ `[PROTECT_MCP] Restraint self-test passed (${selfTest.cases.length} vectors${selfTest.wasmAvailable ? "" : "; Cedar WASM absent, fail-closed invariant verified"})${receiptProbe.ok ? "; denial receipt signing verified" : ""}. Arming enforce mode.
4965
+ `
4966
+ );
4967
+ }
4933
4968
  const state = {
4934
4969
  cedarPolicies,
4935
4970
  cedarDir: cedarDir || null,
@@ -5281,6 +5316,8 @@ function buildCoverageStatement(input) {
5281
5316
  not_governed: [...DEFAULT_NOT_GOVERNED, ...extras],
5282
5317
  residual_bypass_routes: RESIDUAL_BYPASS_ROUTES,
5283
5318
  self_test: input.probe,
5319
+ ...input.evaluatorSelfTest !== void 0 ? { evaluator_self_test: input.evaluatorSelfTest } : {},
5320
+ basis: "configuration-at-generation-time",
5284
5321
  attestation_note: ATTESTATION_NOTE
5285
5322
  };
5286
5323
  }
@@ -5337,6 +5374,7 @@ async function handleCoverage(args) {
5337
5374
  return;
5338
5375
  }
5339
5376
  const version = process.env.PROTECT_MCP_VERSION || "unknown";
5377
+ const evaluatorSelfTest = await runEvaluatorSelfTest();
5340
5378
  let statement;
5341
5379
  let signingConfig;
5342
5380
  if (opts.cedarDir) {
@@ -5357,6 +5395,7 @@ async function handleCoverage(args) {
5357
5395
  reason: "cedar policies are evaluated per-request; a static probe would not exercise the WASM evaluation path. Run the gate with --enforce to exercise live evaluation."
5358
5396
  },
5359
5397
  extraNotGoverned: opts.extraNotGoverned,
5398
+ evaluatorSelfTest,
5360
5399
  packageVersion: version
5361
5400
  });
5362
5401
  } else if (opts.policyPath) {
@@ -5374,6 +5413,7 @@ async function handleCoverage(args) {
5374
5413
  wildcardRule: analyzed.wildcardRule,
5375
5414
  probe: analyzed.probe,
5376
5415
  extraNotGoverned: opts.extraNotGoverned,
5416
+ evaluatorSelfTest,
5377
5417
  packageVersion: version
5378
5418
  });
5379
5419
  } else {
@@ -5387,6 +5427,7 @@ async function handleCoverage(args) {
5387
5427
  blockedTools: [],
5388
5428
  probe: { method: "none", reason: "no policy loaded; the gate would allow all tool calls" },
5389
5429
  extraNotGoverned: opts.extraNotGoverned,
5430
+ evaluatorSelfTest,
5390
5431
  packageVersion: version
5391
5432
  });
5392
5433
  }
@@ -5428,6 +5469,7 @@ async function handleCoverage(args) {
5428
5469
  governed : ${statement.governed.tools_with_explicit_rules.length} tools with explicit rules${statement.governed.explicitly_blocked_tools.length ? `, ${statement.governed.explicitly_blocked_tools.length} explicitly blocked` : ""}
5429
5470
  not governed: ${statement.not_governed.length} declared items (defaults cannot be removed)
5430
5471
  self test : ${statement.self_test.method}${statement.self_test.would_deny !== void 0 ? ` (would_deny=${statement.self_test.would_deny})` : ""}
5472
+ evaluator : live self-test ${evaluatorSelfTest.passed ? "passed" : "FAILED"} (${evaluatorSelfTest.cases.length} vectors)
5431
5473
  signed : ${signer ? `yes (kid ${signer.kid})` : "no (unsigned declaration)"}
5432
5474
  `
5433
5475
  );
@@ -5473,7 +5515,7 @@ var init_coverage = __esm({
5473
5515
  residual_risk: "downstream system behavior (not this layer)"
5474
5516
  }
5475
5517
  ];
5476
- ATTESTATION_NOTE = "This statement enumerates what the gate is configured to govern and declares what it does not see. It is a scope declaration, not a proof of the absence of other action paths: making the gate the only path is a deployment property (single-endpoint configuration, or egress lockdown for a hard guarantee). Shadow enforcement observes and receipts but does not block.";
5518
+ ATTESTATION_NOTE = "This statement enumerates what the gate is configured to govern and declares what it does not see. It is a scope declaration, not a proof of the absence of other action paths: making the gate the only path is a deployment property (single-endpoint configuration, or egress lockdown for a hard guarantee). Shadow enforcement observes and receipts but does not block. The statement reflects configuration at generation time; a statement emitted by the running gateway (observed coverage, with intended-versus-observed mismatch flagging) is on the roadmap. In enforce mode the gate refuses to arm unless the live restraint self-test passes.";
5477
5519
  CHANNEL_DESCRIPTION = {
5478
5520
  hook: "every tool call the agent harness routes through the pre-tool-call hook",
5479
5521
  proxy: "every call made to the gate HTTP endpoint wrapping the tool server"
package/dist/cli.mjs CHANGED
@@ -4529,7 +4529,7 @@ async function main() {
4529
4529
  return;
4530
4530
  }
4531
4531
  if (args[0] === "coverage") {
4532
- const { handleCoverage } = await import("./coverage-NB55FTYL.mjs");
4532
+ const { handleCoverage } = await import("./coverage-IHN7MUBH.mjs");
4533
4533
  await handleCoverage(args.slice(1));
4534
4534
  process.exit(process.exitCode || 0);
4535
4535
  }
@@ -7,7 +7,8 @@ import {
7
7
  signGenericArtifact
8
8
  } from "./chunk-WMIWQ6TS.mjs";
9
9
  import {
10
- loadCedarPolicies
10
+ loadCedarPolicies,
11
+ runEvaluatorSelfTest
11
12
  } from "./chunk-FGCNKEEW.mjs";
12
13
  import "./chunk-XOP3PEBM.mjs";
13
14
  import "./chunk-PQJP2ZCI.mjs";
@@ -47,7 +48,7 @@ var RESIDUAL_BYPASS_ROUTES = [
47
48
  residual_risk: "downstream system behavior (not this layer)"
48
49
  }
49
50
  ];
50
- var ATTESTATION_NOTE = "This statement enumerates what the gate is configured to govern and declares what it does not see. It is a scope declaration, not a proof of the absence of other action paths: making the gate the only path is a deployment property (single-endpoint configuration, or egress lockdown for a hard guarantee). Shadow enforcement observes and receipts but does not block.";
51
+ var ATTESTATION_NOTE = "This statement enumerates what the gate is configured to govern and declares what it does not see. It is a scope declaration, not a proof of the absence of other action paths: making the gate the only path is a deployment property (single-endpoint configuration, or egress lockdown for a hard guarantee). Shadow enforcement observes and receipts but does not block. The statement reflects configuration at generation time; a statement emitted by the running gateway (observed coverage, with intended-versus-observed mismatch flagging) is on the roadmap. In enforce mode the gate refuses to arm unless the live restraint self-test passes.";
51
52
  var CHANNEL_DESCRIPTION = {
52
53
  hook: "every tool call the agent harness routes through the pre-tool-call hook",
53
54
  proxy: "every call made to the gate HTTP endpoint wrapping the tool server"
@@ -77,6 +78,8 @@ function buildCoverageStatement(input) {
77
78
  not_governed: [...DEFAULT_NOT_GOVERNED, ...extras],
78
79
  residual_bypass_routes: RESIDUAL_BYPASS_ROUTES,
79
80
  self_test: input.probe,
81
+ ...input.evaluatorSelfTest !== void 0 ? { evaluator_self_test: input.evaluatorSelfTest } : {},
82
+ basis: "configuration-at-generation-time",
80
83
  attestation_note: ATTESTATION_NOTE
81
84
  };
82
85
  }
@@ -133,6 +136,7 @@ async function handleCoverage(args) {
133
136
  return;
134
137
  }
135
138
  const version = process.env.PROTECT_MCP_VERSION || "unknown";
139
+ const evaluatorSelfTest = await runEvaluatorSelfTest();
136
140
  let statement;
137
141
  let signingConfig;
138
142
  if (opts.cedarDir) {
@@ -153,6 +157,7 @@ async function handleCoverage(args) {
153
157
  reason: "cedar policies are evaluated per-request; a static probe would not exercise the WASM evaluation path. Run the gate with --enforce to exercise live evaluation."
154
158
  },
155
159
  extraNotGoverned: opts.extraNotGoverned,
160
+ evaluatorSelfTest,
156
161
  packageVersion: version
157
162
  });
158
163
  } else if (opts.policyPath) {
@@ -170,6 +175,7 @@ async function handleCoverage(args) {
170
175
  wildcardRule: analyzed.wildcardRule,
171
176
  probe: analyzed.probe,
172
177
  extraNotGoverned: opts.extraNotGoverned,
178
+ evaluatorSelfTest,
173
179
  packageVersion: version
174
180
  });
175
181
  } else {
@@ -183,6 +189,7 @@ async function handleCoverage(args) {
183
189
  blockedTools: [],
184
190
  probe: { method: "none", reason: "no policy loaded; the gate would allow all tool calls" },
185
191
  extraNotGoverned: opts.extraNotGoverned,
192
+ evaluatorSelfTest,
186
193
  packageVersion: version
187
194
  });
188
195
  }
@@ -224,6 +231,7 @@ async function handleCoverage(args) {
224
231
  governed : ${statement.governed.tools_with_explicit_rules.length} tools with explicit rules${statement.governed.explicitly_blocked_tools.length ? `, ${statement.governed.explicitly_blocked_tools.length} explicitly blocked` : ""}
225
232
  not governed: ${statement.not_governed.length} declared items (defaults cannot be removed)
226
233
  self test : ${statement.self_test.method}${statement.self_test.would_deny !== void 0 ? ` (would_deny=${statement.self_test.would_deny})` : ""}
234
+ evaluator : live self-test ${evaluatorSelfTest.passed ? "passed" : "FAILED"} (${evaluatorSelfTest.cases.length} vectors)
227
235
  signed : ${signer ? `yes (kid ${signer.kid})` : "no (unsigned declaration)"}
228
236
  `
229
237
  );
@@ -109,6 +109,9 @@ function digestPolicyFiles(engine, files) {
109
109
  files: entries
110
110
  };
111
111
  }
112
+ function digestCedarSource(source) {
113
+ return digestPolicyFiles("cedar", [{ name: "policy.cedar", content: source }]);
114
+ }
112
115
  function digestBuiltinPolicy(policy) {
113
116
  return digestPolicyFiles("builtin", [{ name: "policy.json", content: canonicalize(policy) }]);
114
117
  }
@@ -284,6 +287,33 @@ function extractPolicyErrors(result) {
284
287
  async function isCedarAvailable() {
285
288
  return ensureCedarWasm();
286
289
  }
290
+ function policySetFromSource(source, name = "inline") {
291
+ const digest = digestCedarSource(source).policy_digest;
292
+ return { source, digest, fileCount: 1, files: [name] };
293
+ }
294
+ async function runEvaluatorSelfTest() {
295
+ const wasmAvailable = await isCedarAvailable();
296
+ const cases = [];
297
+ const run = async (name, expected, policy, context) => {
298
+ const d = await evaluateCedar(policy, { tool: "Bash", tier: "unknown", context }, void 0, { failClosed: true });
299
+ const actual = d.allowed ? "ALLOW" : "DENY";
300
+ cases.push({ name, expected, actual, pass: actual === expected, reason: d.reason });
301
+ };
302
+ if (!wasmAvailable) {
303
+ await run("engine unavailable denies", "DENY", policySetFromSource("permit(principal, action, resource);"), {});
304
+ return { wasmAvailable, passed: cases.every((c) => c.pass), cases };
305
+ }
306
+ const correct = policySetFromSource(
307
+ 'forbid(principal, action, resource) when { ["rm", "dd", "mkfs"].contains(context.command) };\npermit(principal, action, resource);'
308
+ );
309
+ await run("forbid denies rm", "DENY", correct, { command: "rm" });
310
+ await run("permit allows ls", "ALLOW", correct, { command: "ls" });
311
+ const broken = policySetFromSource(
312
+ 'forbid(principal, action, resource) when { context.command in ["rm", "dd"] };\npermit(principal, action, resource);'
313
+ );
314
+ await run("in-on-String forbid does not permit-all", "DENY", broken, { command: "rm" });
315
+ return { wasmAvailable, passed: cases.every((c) => c.pass), cases };
316
+ }
287
317
 
288
318
  // src/signing.ts
289
319
  var import_node_fs3 = require("fs");
@@ -1536,6 +1566,41 @@ async function startHookServer(options = {}) {
1536
1566
  }
1537
1567
  }
1538
1568
  }
1569
+ if (enforce) {
1570
+ const selfTest = await runEvaluatorSelfTest();
1571
+ for (const c of selfTest.cases) {
1572
+ if (!c.pass) {
1573
+ process.stderr.write(
1574
+ `[PROTECT_MCP] SELF-TEST FAIL: ${c.name} (expected ${c.expected}, got ${c.actual})
1575
+ `
1576
+ );
1577
+ }
1578
+ }
1579
+ if (!selfTest.passed) {
1580
+ throw new Error(
1581
+ "enforce mode refused to start: the restraint self-test failed. A gate that cannot prove it denies must not arm."
1582
+ );
1583
+ }
1584
+ const receiptProbe = signDecision({
1585
+ v: 2,
1586
+ tool: "__protect_mcp_startup_selftest__",
1587
+ decision: "deny",
1588
+ reason_code: "startup_selftest",
1589
+ policy_digest: policyDigest,
1590
+ request_id: `selftest-${Date.now()}`,
1591
+ mode: "enforce",
1592
+ timestamp: Date.now()
1593
+ });
1594
+ if (receiptProbe.error) {
1595
+ throw new Error(
1596
+ `enforce mode refused to start: signing is configured but a denial receipt could not be produced (${receiptProbe.error}). An enforcing gate that cannot evidence a denial must not arm.`
1597
+ );
1598
+ }
1599
+ process.stderr.write(
1600
+ `[PROTECT_MCP] Restraint self-test passed (${selfTest.cases.length} vectors${selfTest.wasmAvailable ? "" : "; Cedar WASM absent, fail-closed invariant verified"})${receiptProbe.ok ? "; denial receipt signing verified" : ""}. Arming enforce mode.
1601
+ `
1602
+ );
1603
+ }
1539
1604
  const state = {
1540
1605
  cedarPolicies,
1541
1606
  cedarDir: cedarDir || null,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  startHookServer
3
- } from "./chunk-3FSKOGGK.mjs";
3
+ } from "./chunk-UBTX3A3D.mjs";
4
4
  import "./chunk-KRKZ2YX7.mjs";
5
5
  import "./chunk-CIWIK6BT.mjs";
6
6
  import "./chunk-WMIWQ6TS.mjs";
package/dist/index.js CHANGED
@@ -38947,6 +38947,41 @@ async function startHookServer(options = {}) {
38947
38947
  }
38948
38948
  }
38949
38949
  }
38950
+ if (enforce) {
38951
+ const selfTest = await runEvaluatorSelfTest();
38952
+ for (const c of selfTest.cases) {
38953
+ if (!c.pass) {
38954
+ process.stderr.write(
38955
+ `[PROTECT_MCP] SELF-TEST FAIL: ${c.name} (expected ${c.expected}, got ${c.actual})
38956
+ `
38957
+ );
38958
+ }
38959
+ }
38960
+ if (!selfTest.passed) {
38961
+ throw new Error(
38962
+ "enforce mode refused to start: the restraint self-test failed. A gate that cannot prove it denies must not arm."
38963
+ );
38964
+ }
38965
+ const receiptProbe = signDecision({
38966
+ v: 2,
38967
+ tool: "__protect_mcp_startup_selftest__",
38968
+ decision: "deny",
38969
+ reason_code: "startup_selftest",
38970
+ policy_digest: policyDigest,
38971
+ request_id: `selftest-${Date.now()}`,
38972
+ mode: "enforce",
38973
+ timestamp: Date.now()
38974
+ });
38975
+ if (receiptProbe.error) {
38976
+ throw new Error(
38977
+ `enforce mode refused to start: signing is configured but a denial receipt could not be produced (${receiptProbe.error}). An enforcing gate that cannot evidence a denial must not arm.`
38978
+ );
38979
+ }
38980
+ process.stderr.write(
38981
+ `[PROTECT_MCP] Restraint self-test passed (${selfTest.cases.length} vectors${selfTest.wasmAvailable ? "" : "; Cedar WASM absent, fail-closed invariant verified"})${receiptProbe.ok ? "; denial receipt signing verified" : ""}. Arming enforce mode.
38982
+ `
38983
+ );
38984
+ }
38950
38985
  const state = {
38951
38986
  cedarPolicies,
38952
38987
  cedarDir: cedarDir || null,
package/dist/index.mjs CHANGED
@@ -49,7 +49,7 @@ import {
49
49
  forwardReceipt,
50
50
  getScopeBlindBridge,
51
51
  startHookServer
52
- } from "./chunk-3FSKOGGK.mjs";
52
+ } from "./chunk-UBTX3A3D.mjs";
53
53
  import "./chunk-KRKZ2YX7.mjs";
54
54
  import "./chunk-CIWIK6BT.mjs";
55
55
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "protect-mcp",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "mcpName": "com.scopeblind/protect-mcp",
5
5
  "description": "Fail-closed Cedar policy gate + Ed25519 signed receipts for AI agent tool calls. Denies on any policy error, proves the gate is live with a startup self-test, and turns every decision into a local searchable record you own. The open gate behind Legate by ScopeBlind. scopeblind.com",
6
6
  "main": "dist/index.js",