protect-mcp 0.7.2 → 0.7.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.3: tool input reaches `context.input`, and the hook path fails closed
4
+
5
+ Two correctness fixes for policy authors who rely on the documented Cedar shape.
6
+
7
+ The evaluator maps tool input to Cedar `context.input`, and the bundled policy
8
+ examples are written against `context.input.*`, but the `evaluate` CLI path and
9
+ the HTTP hook server only flattened tool input into top-level context fields. So
10
+ a policy keyed on `context.input.path` silently saw nothing on those paths: a
11
+ `forbid` that should have denied never fired, and the call was allowed. Both
12
+ paths now pass the tool input through, so nested-shape policies match. The
13
+ existing flattened fields are kept for back-compat. (Thanks to @koriyoshi2041,
14
+ scopeblind-gateway#8, for the report and repro.)
15
+
16
+ Separately, the hook server's Cedar evaluation fell through to **allow** on an
17
+ unexpected evaluator throw, which contradicted the "denies on any error"
18
+ guarantee from 0.7.0 (the CLI path already failed closed). The hook path now
19
+ denies on an unexpected eval error while a policy is configured.
20
+
21
+ Regression coverage added on all three paths; `npm ci` from a clean clone also
22
+ works again (the lockfile was out of sync).
23
+
3
24
  ## 0.7.2: run the gate in other agents (Codex, Cursor, Gemini, Hermes)
4
25
 
5
26
  The `evaluate` and `sign` verbs now accept `--format <host>`
package/README.md CHANGED
@@ -68,7 +68,7 @@ session always runs the gate you tested:
68
68
  "hooks": [
69
69
  {
70
70
  "type": "command",
71
- "command": "npx protect-mcp@0.7.0 evaluate --cedar ./cedar --tool \"$TOOL_NAME\" --input \"$TOOL_INPUT\""
71
+ "command": "npx protect-mcp@0.7.3 evaluate --cedar ./cedar --tool \"$TOOL_NAME\" --input \"$TOOL_INPUT\""
72
72
  }
73
73
  ]
74
74
  }
@@ -79,7 +79,7 @@ session always runs the gate you tested:
79
79
  "hooks": [
80
80
  {
81
81
  "type": "command",
82
- "command": "npx protect-mcp@0.7.0 sign --tool \"$TOOL_NAME\" --receipts ./receipts --key ./keys/gateway.json"
82
+ "command": "npx protect-mcp@0.7.3 sign --tool \"$TOOL_NAME\" --receipts ./receipts --key ./keys/gateway.json"
83
83
  }
84
84
  ]
85
85
  }
@@ -258,7 +258,10 @@ async function handlePreToolUse(input, state) {
258
258
  context: {
259
259
  hook_event: "PreToolUse",
260
260
  ...input.toolInput || {}
261
- }
261
+ },
262
+ // Also expose the raw tool input under context.input so policies written
263
+ // against the documented nested shape match on the hook path too.
264
+ toolInput: input.toolInput || {}
262
265
  });
263
266
  if (!cedarDecision.allowed) {
264
267
  const reason = cedarDecision.reason || "cedar_deny";
@@ -297,10 +300,29 @@ async function handlePreToolUse(input, state) {
297
300
  };
298
301
  }
299
302
  } catch (err) {
300
- if (state.verbose) {
301
- process.stderr.write(`[PROTECT_MCP] Cedar eval error: ${err instanceof Error ? err.message : err}
302
- `);
303
- }
303
+ const hookLatency2 = Date.now() - hookStart;
304
+ process.stderr.write(
305
+ `[PROTECT_MCP] Cedar eval threw for "${toolName}", failing closed: ${err instanceof Error ? err.message : err}
306
+ `
307
+ );
308
+ emitDecisionLog(state, {
309
+ tool: toolName,
310
+ decision: "deny",
311
+ reason_code: "cedar_eval_error",
312
+ request_id: requestId,
313
+ hook_event: "PreToolUse",
314
+ swarm: swarm.team_name ? swarm : void 0,
315
+ timing: { hook_latency_ms: hookLatency2, started_at: hookStart },
316
+ payload_digest: payloadDigest,
317
+ sandbox_state: detectSandboxState()
318
+ });
319
+ return {
320
+ hookSpecificOutput: {
321
+ hookEventName: "PreToolUse",
322
+ permissionDecision: "deny",
323
+ permissionDecisionReason: `[ScopeBlind] Denied: the policy engine errored while evaluating "${toolName}", so the gate fails closed rather than allow an unverified call. Check the policy set and server logs.`
324
+ }
325
+ };
304
326
  }
305
327
  }
306
328
  if (state.jsonPolicy?.policy) {
package/dist/cli.js CHANGED
@@ -5096,7 +5096,10 @@ async function handlePreToolUse(input, state) {
5096
5096
  context: {
5097
5097
  hook_event: "PreToolUse",
5098
5098
  ...input.toolInput || {}
5099
- }
5099
+ },
5100
+ // Also expose the raw tool input under context.input so policies written
5101
+ // against the documented nested shape match on the hook path too.
5102
+ toolInput: input.toolInput || {}
5100
5103
  });
5101
5104
  if (!cedarDecision.allowed) {
5102
5105
  const reason = cedarDecision.reason || "cedar_deny";
@@ -5135,10 +5138,29 @@ async function handlePreToolUse(input, state) {
5135
5138
  };
5136
5139
  }
5137
5140
  } catch (err) {
5138
- if (state.verbose) {
5139
- process.stderr.write(`[PROTECT_MCP] Cedar eval error: ${err instanceof Error ? err.message : err}
5140
- `);
5141
- }
5141
+ const hookLatency2 = Date.now() - hookStart;
5142
+ process.stderr.write(
5143
+ `[PROTECT_MCP] Cedar eval threw for "${toolName}", failing closed: ${err instanceof Error ? err.message : err}
5144
+ `
5145
+ );
5146
+ emitDecisionLog(state, {
5147
+ tool: toolName,
5148
+ decision: "deny",
5149
+ reason_code: "cedar_eval_error",
5150
+ request_id: requestId,
5151
+ hook_event: "PreToolUse",
5152
+ swarm: swarm.team_name ? swarm : void 0,
5153
+ timing: { hook_latency_ms: hookLatency2, started_at: hookStart },
5154
+ payload_digest: payloadDigest,
5155
+ sandbox_state: detectSandboxState()
5156
+ });
5157
+ return {
5158
+ hookSpecificOutput: {
5159
+ hookEventName: "PreToolUse",
5160
+ permissionDecision: "deny",
5161
+ permissionDecisionReason: `[ScopeBlind] Denied: the policy engine errored while evaluating "${toolName}", so the gate fails closed rather than allow an unverified call. Check the policy set and server logs.`
5162
+ }
5163
+ };
5142
5164
  }
5143
5165
  }
5144
5166
  if (state.jsonPolicy?.policy) {
@@ -7700,7 +7722,7 @@ async function handleEvaluate(argv) {
7700
7722
  if (typeof input.command === "string" && context.command_pattern === void 0) {
7701
7723
  context.command_pattern = input.command;
7702
7724
  }
7703
- const decision = await evaluateCedar(policySet, { tool, tier: "unknown", context }, void 0, { failClosed: true });
7725
+ const decision = await evaluateCedar(policySet, { tool, tier: "unknown", context, toolInput: input }, void 0, { failClosed: true });
7704
7726
  if (format) emitDecision(format, decision.allowed, decision.reason || (decision.allowed ? "allowed" : "denied by policy"));
7705
7727
  process.stdout.write(JSON.stringify({ allowed: decision.allowed, reason: decision.reason, policy_digest: policySet.digest }) + "\n");
7706
7728
  process.exit(decision.allowed ? 0 : 2);
package/dist/cli.mjs CHANGED
@@ -1375,7 +1375,7 @@ async function handleEvaluate(argv) {
1375
1375
  if (typeof input.command === "string" && context.command_pattern === void 0) {
1376
1376
  context.command_pattern = input.command;
1377
1377
  }
1378
- const decision = await evaluateCedar(policySet, { tool, tier: "unknown", context }, void 0, { failClosed: true });
1378
+ const decision = await evaluateCedar(policySet, { tool, tier: "unknown", context, toolInput: input }, void 0, { failClosed: true });
1379
1379
  if (format) emitDecision(format, decision.allowed, decision.reason || (decision.allowed ? "allowed" : "denied by policy"));
1380
1380
  process.stdout.write(JSON.stringify({ allowed: decision.allowed, reason: decision.reason, policy_digest: policySet.digest }) + "\n");
1381
1381
  process.exit(decision.allowed ? 0 : 2);
@@ -701,7 +701,10 @@ async function handlePreToolUse(input, state) {
701
701
  context: {
702
702
  hook_event: "PreToolUse",
703
703
  ...input.toolInput || {}
704
- }
704
+ },
705
+ // Also expose the raw tool input under context.input so policies written
706
+ // against the documented nested shape match on the hook path too.
707
+ toolInput: input.toolInput || {}
705
708
  });
706
709
  if (!cedarDecision.allowed) {
707
710
  const reason = cedarDecision.reason || "cedar_deny";
@@ -740,10 +743,29 @@ async function handlePreToolUse(input, state) {
740
743
  };
741
744
  }
742
745
  } catch (err) {
743
- if (state.verbose) {
744
- process.stderr.write(`[PROTECT_MCP] Cedar eval error: ${err instanceof Error ? err.message : err}
745
- `);
746
- }
746
+ const hookLatency2 = Date.now() - hookStart;
747
+ process.stderr.write(
748
+ `[PROTECT_MCP] Cedar eval threw for "${toolName}", failing closed: ${err instanceof Error ? err.message : err}
749
+ `
750
+ );
751
+ emitDecisionLog(state, {
752
+ tool: toolName,
753
+ decision: "deny",
754
+ reason_code: "cedar_eval_error",
755
+ request_id: requestId,
756
+ hook_event: "PreToolUse",
757
+ swarm: swarm.team_name ? swarm : void 0,
758
+ timing: { hook_latency_ms: hookLatency2, started_at: hookStart },
759
+ payload_digest: payloadDigest,
760
+ sandbox_state: detectSandboxState()
761
+ });
762
+ return {
763
+ hookSpecificOutput: {
764
+ hookEventName: "PreToolUse",
765
+ permissionDecision: "deny",
766
+ permissionDecisionReason: `[ScopeBlind] Denied: the policy engine errored while evaluating "${toolName}", so the gate fails closed rather than allow an unverified call. Check the policy set and server logs.`
767
+ }
768
+ };
747
769
  }
748
770
  }
749
771
  if (state.jsonPolicy?.policy) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  startHookServer
3
- } from "./chunk-X63ELMU4.mjs";
3
+ } from "./chunk-G67NUBML.mjs";
4
4
  import "./chunk-546U3A7R.mjs";
5
5
  import "./chunk-PQJP2ZCI.mjs";
6
6
  export {
package/dist/index.js CHANGED
@@ -39960,7 +39960,10 @@ async function handlePreToolUse(input, state) {
39960
39960
  context: {
39961
39961
  hook_event: "PreToolUse",
39962
39962
  ...input.toolInput || {}
39963
- }
39963
+ },
39964
+ // Also expose the raw tool input under context.input so policies written
39965
+ // against the documented nested shape match on the hook path too.
39966
+ toolInput: input.toolInput || {}
39964
39967
  });
39965
39968
  if (!cedarDecision.allowed) {
39966
39969
  const reason = cedarDecision.reason || "cedar_deny";
@@ -39999,10 +40002,29 @@ async function handlePreToolUse(input, state) {
39999
40002
  };
40000
40003
  }
40001
40004
  } catch (err) {
40002
- if (state.verbose) {
40003
- process.stderr.write(`[PROTECT_MCP] Cedar eval error: ${err instanceof Error ? err.message : err}
40004
- `);
40005
- }
40005
+ const hookLatency2 = Date.now() - hookStart;
40006
+ process.stderr.write(
40007
+ `[PROTECT_MCP] Cedar eval threw for "${toolName}", failing closed: ${err instanceof Error ? err.message : err}
40008
+ `
40009
+ );
40010
+ emitDecisionLog(state, {
40011
+ tool: toolName,
40012
+ decision: "deny",
40013
+ reason_code: "cedar_eval_error",
40014
+ request_id: requestId,
40015
+ hook_event: "PreToolUse",
40016
+ swarm: swarm.team_name ? swarm : void 0,
40017
+ timing: { hook_latency_ms: hookLatency2, started_at: hookStart },
40018
+ payload_digest: payloadDigest,
40019
+ sandbox_state: detectSandboxState()
40020
+ });
40021
+ return {
40022
+ hookSpecificOutput: {
40023
+ hookEventName: "PreToolUse",
40024
+ permissionDecision: "deny",
40025
+ permissionDecisionReason: `[ScopeBlind] Denied: the policy engine errored while evaluating "${toolName}", so the gate fails closed rather than allow an unverified call. Check the policy set and server logs.`
40026
+ }
40027
+ };
40006
40028
  }
40007
40029
  }
40008
40030
  if (state.jsonPolicy?.policy) {
package/dist/index.mjs CHANGED
@@ -33,7 +33,7 @@ import {
33
33
  forwardReceipt,
34
34
  getScopeBlindBridge,
35
35
  startHookServer
36
- } from "./chunk-X63ELMU4.mjs";
36
+ } from "./chunk-G67NUBML.mjs";
37
37
  import {
38
38
  checkRateLimit,
39
39
  evaluateCedar,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "protect-mcp",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "mcpName": "com.scopeblind/protect-mcp",
5
5
  "description": "Fail-closed Cedar policy gate + signed receipts for AI agent tool calls. Blocks what breaks the rules before it runs, denies on any policy error, and proves the gate is live with a startup self-test.",
6
6
  "main": "dist/index.js",