protect-mcp 0.7.1 → 0.7.2

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.2: run the gate in other agents (Codex, Cursor, Gemini, Hermes)
4
+
5
+ The `evaluate` and `sign` verbs now accept `--format <host>`
6
+ (claude | codex | gemini | cursor | hermes | grok). With it, the verb reads the
7
+ host's hook payload from stdin (tool name and input) and emits the deny verdict
8
+ in that host's hook contract, so the same fail-closed Cedar gate works as a
9
+ PreToolUse/PostToolUse hook outside Claude Code.
10
+
11
+ The load-bearing detail: **Hermes ignores hook exit codes** and reads the verdict
12
+ from stdout, so `--format hermes` denies via `{"decision":"block"}` on stdout. A
13
+ raw exit-2 (which every other host honors) would have silently failed open there.
14
+ Cursor and Gemini receive their structured stdout deny verdict in addition to
15
+ exit 2. Without `--format`, the verbs behave exactly as before (the
16
+ `--tool`/`--input` flag mode is unchanged).
17
+
3
18
  ## 0.7.1: documentation and security policy
4
19
 
5
20
  No code change from 0.7.0. Rewrote the README to lead with the fail-closed and
package/README.md CHANGED
@@ -93,6 +93,26 @@ session always runs the gate you tested:
93
93
  configured, and if no signer is available it records an honest unsigned line
94
94
  (`"signed": false`) rather than failing the tool.
95
95
 
96
+ ## Use it in other agents (Codex, Cursor, Gemini, Hermes)
97
+
98
+ The same fail-closed gate runs as a tool hook in any agent that supports them. Add
99
+ `--format <host>` so the verb reads that host's hook payload from stdin and denies
100
+ in its contract:
101
+
102
+ ```bash
103
+ # the PreToolUse / before-tool command for each host
104
+ npx -y protect-mcp@latest evaluate --format codex --cedar ./cedar # OpenAI Codex
105
+ npx -y protect-mcp@latest evaluate --format gemini --cedar ./cedar # Gemini CLI BeforeTool
106
+ npx -y protect-mcp@latest evaluate --format cursor --cedar ./cedar # Cursor beforeShellExecution
107
+ npx -y protect-mcp@latest evaluate --format hermes --cedar ./cedar # Hermes pre_tool_call
108
+ ```
109
+
110
+ Pair each with `sign --format <host>` on the post-tool event for receipts. The
111
+ important case is **Hermes**, which ignores hook exit codes and reads the verdict
112
+ from stdout, so `--format hermes` denies via `{"decision":"block"}` rather than
113
+ exit 2 (a raw exit-2 would silently fail open there). Without `--format`, the
114
+ verbs read `--tool`/`--input` flags exactly as in the Claude Code section above.
115
+
96
116
  ## Write a policy
97
117
 
98
118
  Cedar policies live in a directory you point at with `--cedar`. A `forbid` rule
package/dist/cli.js CHANGED
@@ -7622,17 +7622,65 @@ function loadPolicyArg(argv) {
7622
7622
  }
7623
7623
  return null;
7624
7624
  }
7625
+ async function readHookStdin() {
7626
+ if (process.stdin.isTTY) return null;
7627
+ try {
7628
+ const chunks = [];
7629
+ for await (const chunk of process.stdin) chunks.push(chunk);
7630
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
7631
+ return raw ? JSON.parse(raw) : null;
7632
+ } catch {
7633
+ return null;
7634
+ }
7635
+ }
7636
+ function mapHookPayload(j) {
7637
+ const tool = j.tool_name ?? j.toolName;
7638
+ const input = j.tool_input ?? j.toolInput;
7639
+ if (input === void 0 && j.command !== void 0) {
7640
+ return { tool: tool ?? "Bash", input: { command: j.command } };
7641
+ }
7642
+ return { tool, input };
7643
+ }
7644
+ function emitDecision(format, allowed, reason) {
7645
+ if (format === "hermes") {
7646
+ process.stdout.write(JSON.stringify(allowed ? {} : { decision: "block", reason }) + "\n");
7647
+ process.exit(0);
7648
+ }
7649
+ if (allowed) {
7650
+ process.stdout.write(JSON.stringify({ allowed: true, reason }) + "\n");
7651
+ process.exit(0);
7652
+ }
7653
+ if (format === "cursor") {
7654
+ process.stdout.write(JSON.stringify({ permission: "deny", userMessage: reason }) + "\n");
7655
+ } else if (format === "gemini") {
7656
+ process.stdout.write(JSON.stringify({ decision: "deny", reason }) + "\n");
7657
+ }
7658
+ process.stderr.write(`protect-mcp denied: ${reason}
7659
+ `);
7660
+ process.exit(2);
7661
+ }
7625
7662
  async function handleEvaluate(argv) {
7626
- const tool = flagValue(argv, "--tool") || "";
7627
- const inputRaw = flagValue(argv, "--input") || "{}";
7663
+ const format = flagValue(argv, "--format");
7664
+ let tool = flagValue(argv, "--tool") || "";
7665
+ let inputRaw = flagValue(argv, "--input") || "{}";
7628
7666
  const contextRaw = flagValue(argv, "--context");
7629
7667
  const failOnMissing = flagValue(argv, "--fail-on-missing-policy") !== "false";
7668
+ if (format) {
7669
+ const j = await readHookStdin();
7670
+ if (j) {
7671
+ const m = mapHookPayload(j);
7672
+ if (m.tool) tool = m.tool;
7673
+ if (m.input !== void 0) inputRaw = JSON.stringify(m.input);
7674
+ }
7675
+ }
7630
7676
  const policySet = loadPolicyArg(argv);
7631
7677
  if (!policySet) {
7632
7678
  if (failOnMissing) {
7679
+ if (format) emitDecision(format, false, "policy not found (fail-closed)");
7633
7680
  process.stderr.write("protect-mcp evaluate: policy not found; denying (fail-closed). Pass --fail-on-missing-policy false to allow.\n");
7634
7681
  process.exit(2);
7635
7682
  }
7683
+ if (format) emitDecision(format, true, "no_policy_configured");
7636
7684
  process.stdout.write(JSON.stringify({ allowed: true, reason: "no_policy_configured" }) + "\n");
7637
7685
  process.exit(0);
7638
7686
  }
@@ -7653,13 +7701,22 @@ async function handleEvaluate(argv) {
7653
7701
  context.command_pattern = input.command;
7654
7702
  }
7655
7703
  const decision = await evaluateCedar(policySet, { tool, tier: "unknown", context }, void 0, { failClosed: true });
7704
+ if (format) emitDecision(format, decision.allowed, decision.reason || (decision.allowed ? "allowed" : "denied by policy"));
7656
7705
  process.stdout.write(JSON.stringify({ allowed: decision.allowed, reason: decision.reason, policy_digest: policySet.digest }) + "\n");
7657
7706
  process.exit(decision.allowed ? 0 : 2);
7658
7707
  }
7659
7708
  async function handleSign(argv) {
7660
- const tool = flagValue(argv, "--tool") || "";
7709
+ const format = flagValue(argv, "--format");
7710
+ let tool = flagValue(argv, "--tool") || "";
7661
7711
  const receiptsDir = flagValue(argv, "--receipts") || "./receipts/";
7662
7712
  const keyPath = flagValue(argv, "--key");
7713
+ if (format) {
7714
+ const j = await readHookStdin();
7715
+ if (j) {
7716
+ const m = mapHookPayload(j);
7717
+ if (m.tool) tool = m.tool;
7718
+ }
7719
+ }
7663
7720
  if (keyPath && (0, import_node_fs10.existsSync)(keyPath)) {
7664
7721
  try {
7665
7722
  await initSigning({ enabled: true, key_path: keyPath });
@@ -7685,6 +7742,10 @@ async function handleSign(argv) {
7685
7742
  (0, import_node_fs10.appendFileSync)((0, import_node_path6.join)(receiptsDir, "receipts.jsonl"), line + "\n");
7686
7743
  } catch {
7687
7744
  }
7745
+ if (format === "hermes") {
7746
+ process.stdout.write("{}\n");
7747
+ process.exit(0);
7748
+ }
7688
7749
  process.stdout.write(JSON.stringify({ signed: Boolean(signed.signed), artifact_type: signed.artifact_type, request_id: requestId }) + "\n");
7689
7750
  process.exit(0);
7690
7751
  }
package/dist/cli.mjs CHANGED
@@ -1297,17 +1297,65 @@ function loadPolicyArg(argv) {
1297
1297
  }
1298
1298
  return null;
1299
1299
  }
1300
+ async function readHookStdin() {
1301
+ if (process.stdin.isTTY) return null;
1302
+ try {
1303
+ const chunks = [];
1304
+ for await (const chunk of process.stdin) chunks.push(chunk);
1305
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
1306
+ return raw ? JSON.parse(raw) : null;
1307
+ } catch {
1308
+ return null;
1309
+ }
1310
+ }
1311
+ function mapHookPayload(j) {
1312
+ const tool = j.tool_name ?? j.toolName;
1313
+ const input = j.tool_input ?? j.toolInput;
1314
+ if (input === void 0 && j.command !== void 0) {
1315
+ return { tool: tool ?? "Bash", input: { command: j.command } };
1316
+ }
1317
+ return { tool, input };
1318
+ }
1319
+ function emitDecision(format, allowed, reason) {
1320
+ if (format === "hermes") {
1321
+ process.stdout.write(JSON.stringify(allowed ? {} : { decision: "block", reason }) + "\n");
1322
+ process.exit(0);
1323
+ }
1324
+ if (allowed) {
1325
+ process.stdout.write(JSON.stringify({ allowed: true, reason }) + "\n");
1326
+ process.exit(0);
1327
+ }
1328
+ if (format === "cursor") {
1329
+ process.stdout.write(JSON.stringify({ permission: "deny", userMessage: reason }) + "\n");
1330
+ } else if (format === "gemini") {
1331
+ process.stdout.write(JSON.stringify({ decision: "deny", reason }) + "\n");
1332
+ }
1333
+ process.stderr.write(`protect-mcp denied: ${reason}
1334
+ `);
1335
+ process.exit(2);
1336
+ }
1300
1337
  async function handleEvaluate(argv) {
1301
- const tool = flagValue(argv, "--tool") || "";
1302
- const inputRaw = flagValue(argv, "--input") || "{}";
1338
+ const format = flagValue(argv, "--format");
1339
+ let tool = flagValue(argv, "--tool") || "";
1340
+ let inputRaw = flagValue(argv, "--input") || "{}";
1303
1341
  const contextRaw = flagValue(argv, "--context");
1304
1342
  const failOnMissing = flagValue(argv, "--fail-on-missing-policy") !== "false";
1343
+ if (format) {
1344
+ const j = await readHookStdin();
1345
+ if (j) {
1346
+ const m = mapHookPayload(j);
1347
+ if (m.tool) tool = m.tool;
1348
+ if (m.input !== void 0) inputRaw = JSON.stringify(m.input);
1349
+ }
1350
+ }
1305
1351
  const policySet = loadPolicyArg(argv);
1306
1352
  if (!policySet) {
1307
1353
  if (failOnMissing) {
1354
+ if (format) emitDecision(format, false, "policy not found (fail-closed)");
1308
1355
  process.stderr.write("protect-mcp evaluate: policy not found; denying (fail-closed). Pass --fail-on-missing-policy false to allow.\n");
1309
1356
  process.exit(2);
1310
1357
  }
1358
+ if (format) emitDecision(format, true, "no_policy_configured");
1311
1359
  process.stdout.write(JSON.stringify({ allowed: true, reason: "no_policy_configured" }) + "\n");
1312
1360
  process.exit(0);
1313
1361
  }
@@ -1328,13 +1376,22 @@ async function handleEvaluate(argv) {
1328
1376
  context.command_pattern = input.command;
1329
1377
  }
1330
1378
  const decision = await evaluateCedar(policySet, { tool, tier: "unknown", context }, void 0, { failClosed: true });
1379
+ if (format) emitDecision(format, decision.allowed, decision.reason || (decision.allowed ? "allowed" : "denied by policy"));
1331
1380
  process.stdout.write(JSON.stringify({ allowed: decision.allowed, reason: decision.reason, policy_digest: policySet.digest }) + "\n");
1332
1381
  process.exit(decision.allowed ? 0 : 2);
1333
1382
  }
1334
1383
  async function handleSign(argv) {
1335
- const tool = flagValue(argv, "--tool") || "";
1384
+ const format = flagValue(argv, "--format");
1385
+ let tool = flagValue(argv, "--tool") || "";
1336
1386
  const receiptsDir = flagValue(argv, "--receipts") || "./receipts/";
1337
1387
  const keyPath = flagValue(argv, "--key");
1388
+ if (format) {
1389
+ const j = await readHookStdin();
1390
+ if (j) {
1391
+ const m = mapHookPayload(j);
1392
+ if (m.tool) tool = m.tool;
1393
+ }
1394
+ }
1338
1395
  if (keyPath && existsSyncCli(keyPath)) {
1339
1396
  try {
1340
1397
  await initSigning({ enabled: true, key_path: keyPath });
@@ -1360,6 +1417,10 @@ async function handleSign(argv) {
1360
1417
  appendFileSyncCli(joinCli(receiptsDir, "receipts.jsonl"), line + "\n");
1361
1418
  } catch {
1362
1419
  }
1420
+ if (format === "hermes") {
1421
+ process.stdout.write("{}\n");
1422
+ process.exit(0);
1423
+ }
1363
1424
  process.stdout.write(JSON.stringify({ signed: Boolean(signed.signed), artifact_type: signed.artifact_type, request_id: requestId }) + "\n");
1364
1425
  process.exit(0);
1365
1426
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "protect-mcp",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
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",