protect-mcp 0.7.1 → 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 +36 -0
- package/README.md +22 -2
- package/dist/{chunk-X63ELMU4.mjs → chunk-G67NUBML.mjs} +27 -5
- package/dist/cli.js +92 -9
- package/dist/cli.mjs +65 -4
- package/dist/hook-server.js +27 -5
- package/dist/hook-server.mjs +1 -1
- package/dist/index.js +27 -5
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
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
|
+
|
|
24
|
+
## 0.7.2: run the gate in other agents (Codex, Cursor, Gemini, Hermes)
|
|
25
|
+
|
|
26
|
+
The `evaluate` and `sign` verbs now accept `--format <host>`
|
|
27
|
+
(claude | codex | gemini | cursor | hermes | grok). With it, the verb reads the
|
|
28
|
+
host's hook payload from stdin (tool name and input) and emits the deny verdict
|
|
29
|
+
in that host's hook contract, so the same fail-closed Cedar gate works as a
|
|
30
|
+
PreToolUse/PostToolUse hook outside Claude Code.
|
|
31
|
+
|
|
32
|
+
The load-bearing detail: **Hermes ignores hook exit codes** and reads the verdict
|
|
33
|
+
from stdout, so `--format hermes` denies via `{"decision":"block"}` on stdout. A
|
|
34
|
+
raw exit-2 (which every other host honors) would have silently failed open there.
|
|
35
|
+
Cursor and Gemini receive their structured stdout deny verdict in addition to
|
|
36
|
+
exit 2. Without `--format`, the verbs behave exactly as before (the
|
|
37
|
+
`--tool`/`--input` flag mode is unchanged).
|
|
38
|
+
|
|
3
39
|
## 0.7.1: documentation and security policy
|
|
4
40
|
|
|
5
41
|
No code change from 0.7.0. Rewrote the README to lead with the fail-closed and
|
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.
|
|
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.
|
|
82
|
+
"command": "npx protect-mcp@0.7.3 sign --tool \"$TOOL_NAME\" --receipts ./receipts --key ./keys/gateway.json"
|
|
83
83
|
}
|
|
84
84
|
]
|
|
85
85
|
}
|
|
@@ -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
|
|
@@ -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
|
-
|
|
301
|
-
|
|
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
|
-
|
|
5139
|
-
|
|
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) {
|
|
@@ -7622,17 +7644,65 @@ function loadPolicyArg(argv) {
|
|
|
7622
7644
|
}
|
|
7623
7645
|
return null;
|
|
7624
7646
|
}
|
|
7647
|
+
async function readHookStdin() {
|
|
7648
|
+
if (process.stdin.isTTY) return null;
|
|
7649
|
+
try {
|
|
7650
|
+
const chunks = [];
|
|
7651
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7652
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
7653
|
+
return raw ? JSON.parse(raw) : null;
|
|
7654
|
+
} catch {
|
|
7655
|
+
return null;
|
|
7656
|
+
}
|
|
7657
|
+
}
|
|
7658
|
+
function mapHookPayload(j) {
|
|
7659
|
+
const tool = j.tool_name ?? j.toolName;
|
|
7660
|
+
const input = j.tool_input ?? j.toolInput;
|
|
7661
|
+
if (input === void 0 && j.command !== void 0) {
|
|
7662
|
+
return { tool: tool ?? "Bash", input: { command: j.command } };
|
|
7663
|
+
}
|
|
7664
|
+
return { tool, input };
|
|
7665
|
+
}
|
|
7666
|
+
function emitDecision(format, allowed, reason) {
|
|
7667
|
+
if (format === "hermes") {
|
|
7668
|
+
process.stdout.write(JSON.stringify(allowed ? {} : { decision: "block", reason }) + "\n");
|
|
7669
|
+
process.exit(0);
|
|
7670
|
+
}
|
|
7671
|
+
if (allowed) {
|
|
7672
|
+
process.stdout.write(JSON.stringify({ allowed: true, reason }) + "\n");
|
|
7673
|
+
process.exit(0);
|
|
7674
|
+
}
|
|
7675
|
+
if (format === "cursor") {
|
|
7676
|
+
process.stdout.write(JSON.stringify({ permission: "deny", userMessage: reason }) + "\n");
|
|
7677
|
+
} else if (format === "gemini") {
|
|
7678
|
+
process.stdout.write(JSON.stringify({ decision: "deny", reason }) + "\n");
|
|
7679
|
+
}
|
|
7680
|
+
process.stderr.write(`protect-mcp denied: ${reason}
|
|
7681
|
+
`);
|
|
7682
|
+
process.exit(2);
|
|
7683
|
+
}
|
|
7625
7684
|
async function handleEvaluate(argv) {
|
|
7626
|
-
const
|
|
7627
|
-
|
|
7685
|
+
const format = flagValue(argv, "--format");
|
|
7686
|
+
let tool = flagValue(argv, "--tool") || "";
|
|
7687
|
+
let inputRaw = flagValue(argv, "--input") || "{}";
|
|
7628
7688
|
const contextRaw = flagValue(argv, "--context");
|
|
7629
7689
|
const failOnMissing = flagValue(argv, "--fail-on-missing-policy") !== "false";
|
|
7690
|
+
if (format) {
|
|
7691
|
+
const j = await readHookStdin();
|
|
7692
|
+
if (j) {
|
|
7693
|
+
const m = mapHookPayload(j);
|
|
7694
|
+
if (m.tool) tool = m.tool;
|
|
7695
|
+
if (m.input !== void 0) inputRaw = JSON.stringify(m.input);
|
|
7696
|
+
}
|
|
7697
|
+
}
|
|
7630
7698
|
const policySet = loadPolicyArg(argv);
|
|
7631
7699
|
if (!policySet) {
|
|
7632
7700
|
if (failOnMissing) {
|
|
7701
|
+
if (format) emitDecision(format, false, "policy not found (fail-closed)");
|
|
7633
7702
|
process.stderr.write("protect-mcp evaluate: policy not found; denying (fail-closed). Pass --fail-on-missing-policy false to allow.\n");
|
|
7634
7703
|
process.exit(2);
|
|
7635
7704
|
}
|
|
7705
|
+
if (format) emitDecision(format, true, "no_policy_configured");
|
|
7636
7706
|
process.stdout.write(JSON.stringify({ allowed: true, reason: "no_policy_configured" }) + "\n");
|
|
7637
7707
|
process.exit(0);
|
|
7638
7708
|
}
|
|
@@ -7652,14 +7722,23 @@ async function handleEvaluate(argv) {
|
|
|
7652
7722
|
if (typeof input.command === "string" && context.command_pattern === void 0) {
|
|
7653
7723
|
context.command_pattern = input.command;
|
|
7654
7724
|
}
|
|
7655
|
-
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 });
|
|
7726
|
+
if (format) emitDecision(format, decision.allowed, decision.reason || (decision.allowed ? "allowed" : "denied by policy"));
|
|
7656
7727
|
process.stdout.write(JSON.stringify({ allowed: decision.allowed, reason: decision.reason, policy_digest: policySet.digest }) + "\n");
|
|
7657
7728
|
process.exit(decision.allowed ? 0 : 2);
|
|
7658
7729
|
}
|
|
7659
7730
|
async function handleSign(argv) {
|
|
7660
|
-
const
|
|
7731
|
+
const format = flagValue(argv, "--format");
|
|
7732
|
+
let tool = flagValue(argv, "--tool") || "";
|
|
7661
7733
|
const receiptsDir = flagValue(argv, "--receipts") || "./receipts/";
|
|
7662
7734
|
const keyPath = flagValue(argv, "--key");
|
|
7735
|
+
if (format) {
|
|
7736
|
+
const j = await readHookStdin();
|
|
7737
|
+
if (j) {
|
|
7738
|
+
const m = mapHookPayload(j);
|
|
7739
|
+
if (m.tool) tool = m.tool;
|
|
7740
|
+
}
|
|
7741
|
+
}
|
|
7663
7742
|
if (keyPath && (0, import_node_fs10.existsSync)(keyPath)) {
|
|
7664
7743
|
try {
|
|
7665
7744
|
await initSigning({ enabled: true, key_path: keyPath });
|
|
@@ -7685,6 +7764,10 @@ async function handleSign(argv) {
|
|
|
7685
7764
|
(0, import_node_fs10.appendFileSync)((0, import_node_path6.join)(receiptsDir, "receipts.jsonl"), line + "\n");
|
|
7686
7765
|
} catch {
|
|
7687
7766
|
}
|
|
7767
|
+
if (format === "hermes") {
|
|
7768
|
+
process.stdout.write("{}\n");
|
|
7769
|
+
process.exit(0);
|
|
7770
|
+
}
|
|
7688
7771
|
process.stdout.write(JSON.stringify({ signed: Boolean(signed.signed), artifact_type: signed.artifact_type, request_id: requestId }) + "\n");
|
|
7689
7772
|
process.exit(0);
|
|
7690
7773
|
}
|
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
|
|
1302
|
-
|
|
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
|
}
|
|
@@ -1327,14 +1375,23 @@ async function handleEvaluate(argv) {
|
|
|
1327
1375
|
if (typeof input.command === "string" && context.command_pattern === void 0) {
|
|
1328
1376
|
context.command_pattern = input.command;
|
|
1329
1377
|
}
|
|
1330
|
-
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
|
+
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
|
|
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/dist/hook-server.js
CHANGED
|
@@ -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
|
-
|
|
744
|
-
|
|
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) {
|
package/dist/hook-server.mjs
CHANGED
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
|
-
|
|
40003
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "protect-mcp",
|
|
3
|
-
"version": "0.7.
|
|
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",
|