protect-mcp 0.9.5 → 0.9.6
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 +21 -0
- package/README.md +3 -0
- package/dist/{chunk-H332ZNJ6.mjs → chunk-WCZJGEBO.mjs} +45 -4
- package/dist/cli.js +195 -4
- package/dist/cli.mjs +150 -0
- package/dist/hook-server.js +44 -3
- package/dist/hook-server.mjs +1 -1
- package/dist/index.js +44 -3
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.6: policy you can see and change
|
|
4
|
+
|
|
5
|
+
The gate is default-deny and fail-closed, but a deny used to be a dead end: an
|
|
6
|
+
empty reason and no pointer to the file that decides. This release makes the
|
|
7
|
+
policy legible and editable from the terminal, and lets a running gate pick up
|
|
8
|
+
a policy change without a restart.
|
|
9
|
+
|
|
10
|
+
- **`policy` command.** `policy list` shows every tool named in the policy
|
|
11
|
+
(permit / forbid / default-deny) cross-referenced with how often the gate
|
|
12
|
+
actually allowed or denied it; `policy show` prints the active policy and its
|
|
13
|
+
digest; `policy allow <tool>` / `policy deny <tool>` append a Cedar rule and
|
|
14
|
+
print the digest change; `policy path` prints the file. Idempotent and
|
|
15
|
+
tool-name validated.
|
|
16
|
+
- **Denies that teach.** A blocked call now says whether it was default-deny
|
|
17
|
+
(no permit matched) or an explicit forbid, names the policy directory, and
|
|
18
|
+
gives the exact fix: `npx protect-mcp policy allow <tool>`.
|
|
19
|
+
- **Hot reload.** `protect-mcp serve` watches the .cedar files and reloads the
|
|
20
|
+
policy on an on-disk change, so `policy allow/deny` (or a hand edit) takes
|
|
21
|
+
effect without restarting the gate. Fail-closed: an unparseable edit keeps the
|
|
22
|
+
previous policy rather than opening the gate, and the reload is logged.
|
|
23
|
+
|
|
3
24
|
## 0.9.5: replayable from scratch
|
|
4
25
|
|
|
5
26
|
The public demo film (legate.scopeblind.com/record) is now reproducible by
|
package/README.md
CHANGED
|
@@ -187,6 +187,8 @@ reveals the shape, not the content.
|
|
|
187
187
|
|
|
188
188
|
## Try it in 60 seconds (no agent required)
|
|
189
189
|
|
|
190
|
+
[](https://legate.scopeblind.com/record)
|
|
191
|
+
|
|
190
192
|
Watch the two-minute film at [legate.scopeblind.com/record](https://legate.scopeblind.com/record), then replay it against your own copy:
|
|
191
193
|
|
|
192
194
|
```bash
|
|
@@ -387,6 +389,7 @@ To report a vulnerability, see [SECURITY.md](./SECURITY.md).
|
|
|
387
389
|
| `serve` | Start the HTTP hook server for Claude Code (port 9377). `--enforce` runs the restraint self-test first; `--cedar <dir>` and `--policy <path>` select the policy. |
|
|
388
390
|
| `init` | Generate an Ed25519 keypair (`keys/gateway.json`), a config template, and a sample policy. |
|
|
389
391
|
| `sample` | Seed a clearly-labeled sample record (8 decisions: one blocked call, two payments; kid `sample-demo`) plus a tampered copy, so `record`, `claim`, `verify-claim`, and `anchor-record` are replayable from scratch before wiring an agent. Refuses to touch an existing record; `--force` overrides. |
|
|
392
|
+
| `policy` | See and change the Cedar policy from the terminal: `policy list` (permit / forbid / default-deny per tool, with how often the gate allowed or denied it), `policy show`, `policy allow <tool>`, `policy deny <tool>`, `policy path`. A running `serve` hot-reloads on the change. |
|
|
390
393
|
| `wrap` | Print a protected MCP command or patch Claude Desktop MCP servers. Dry-run by default; use `--write` to update Claude Desktop config. |
|
|
391
394
|
| `dashboard` | Start a local-only dashboard on `127.0.0.1` showing tool inventory, risk, policy coverage, exact-action approvals, receipt chains, and audit export. |
|
|
392
395
|
| `recommend` | Draft a reviewable JSON policy from observed local calls. Dry-run by default; use `--write` to create `protect-mcp.recommended.json`. |
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
// src/hook-server.ts
|
|
21
21
|
import { createServer } from "http";
|
|
22
22
|
import { createHash, randomUUID, randomBytes } from "crypto";
|
|
23
|
-
import { appendFileSync, readFileSync, existsSync, readdirSync } from "fs";
|
|
23
|
+
import { appendFileSync, readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
24
24
|
import { join } from "path";
|
|
25
25
|
|
|
26
26
|
// src/scopeblind-bridge.ts
|
|
@@ -256,6 +256,7 @@ async function handlePreToolUse(input, state) {
|
|
|
256
256
|
...input.teamName && { team_name: input.teamName },
|
|
257
257
|
...input.agentType && { agent_type: input.agentType }
|
|
258
258
|
};
|
|
259
|
+
maybeReloadCedar(state);
|
|
259
260
|
if (state.cedarPolicies) {
|
|
260
261
|
try {
|
|
261
262
|
const cedarDecision = await evaluateCedar(state.cedarPolicies, {
|
|
@@ -293,10 +294,13 @@ async function handlePreToolUse(input, state) {
|
|
|
293
294
|
sandbox_state: detectSandboxState(),
|
|
294
295
|
plan_receipt_id: state.activePlanReceiptId || void 0
|
|
295
296
|
});
|
|
297
|
+
const isDefaultDeny = !reason || reason === "cedar_deny" || /reason":\[\]/.test(reason);
|
|
298
|
+
const policyRef = state.cedarDir ? ` Policy: ${state.cedarDir}.` : "";
|
|
299
|
+
const howTo = isDefaultDeny ? ` No permit matched (default-deny, fail-closed).${policyRef} Allow it: npx protect-mcp policy allow ${toolName}` : ` Blocked by an explicit forbid rule.${policyRef} Review: npx protect-mcp policy show`;
|
|
296
300
|
if (denyCount === 1) {
|
|
297
301
|
process.stderr.write(
|
|
298
|
-
`[PROTECT_MCP]
|
|
299
|
-
${
|
|
302
|
+
`[PROTECT_MCP] Denied "${toolName}" (${isDefaultDeny ? "default-deny" : "forbid"}).
|
|
303
|
+
Allow with: npx protect-mcp policy allow ${toolName}
|
|
300
304
|
`
|
|
301
305
|
);
|
|
302
306
|
}
|
|
@@ -304,7 +308,7 @@ async function handlePreToolUse(input, state) {
|
|
|
304
308
|
hookSpecificOutput: {
|
|
305
309
|
hookEventName: "PreToolUse",
|
|
306
310
|
permissionDecision: "deny",
|
|
307
|
-
permissionDecisionReason: `[ScopeBlind] Denied
|
|
311
|
+
permissionDecisionReason: `[ScopeBlind] Denied "${toolName}".${howTo}` + (denyCount > 1 ? ` (attempt ${denyCount})` : "")
|
|
308
312
|
}
|
|
309
313
|
};
|
|
310
314
|
}
|
|
@@ -803,6 +807,9 @@ async function startHookServer(options = {}) {
|
|
|
803
807
|
}
|
|
804
808
|
const state = {
|
|
805
809
|
cedarPolicies,
|
|
810
|
+
cedarDir: cedarDir || null,
|
|
811
|
+
cedarMtimeMs: cedarDir ? newestCedarMtime(cedarDir) : 0,
|
|
812
|
+
cedarCheckedMs: 0,
|
|
806
813
|
jsonPolicy,
|
|
807
814
|
rateLimitStore: /* @__PURE__ */ new Map(),
|
|
808
815
|
receiptBuffer: new ReceiptBuffer(),
|
|
@@ -993,6 +1000,40 @@ async function startHookServer(options = {}) {
|
|
|
993
1000
|
process.on("SIGTERM", shutdown);
|
|
994
1001
|
return server;
|
|
995
1002
|
}
|
|
1003
|
+
function newestCedarMtime(dir) {
|
|
1004
|
+
try {
|
|
1005
|
+
let newest = 0;
|
|
1006
|
+
for (const f of readdirSync(dir)) {
|
|
1007
|
+
if (!f.endsWith(".cedar")) continue;
|
|
1008
|
+
const m = statSync(join(dir, f)).mtimeMs;
|
|
1009
|
+
if (m > newest) newest = m;
|
|
1010
|
+
}
|
|
1011
|
+
return newest;
|
|
1012
|
+
} catch {
|
|
1013
|
+
return 0;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
var CEDAR_CHECK_THROTTLE_MS = 2e3;
|
|
1017
|
+
function maybeReloadCedar(state) {
|
|
1018
|
+
if (!state.cedarDir) return;
|
|
1019
|
+
const nowMs = Date.now();
|
|
1020
|
+
if (nowMs - state.cedarCheckedMs < CEDAR_CHECK_THROTTLE_MS) return;
|
|
1021
|
+
state.cedarCheckedMs = nowMs;
|
|
1022
|
+
const m = newestCedarMtime(state.cedarDir);
|
|
1023
|
+
if (m <= state.cedarMtimeMs) return;
|
|
1024
|
+
try {
|
|
1025
|
+
const reloaded = loadCedarPolicies(state.cedarDir);
|
|
1026
|
+
state.cedarPolicies = reloaded;
|
|
1027
|
+
state.policyDigest = reloaded.digest;
|
|
1028
|
+
state.cedarMtimeMs = m;
|
|
1029
|
+
process.stderr.write(`[PROTECT_MCP] Cedar policy reloaded (digest: ${reloaded.digest}) after on-disk change.
|
|
1030
|
+
`);
|
|
1031
|
+
} catch (err) {
|
|
1032
|
+
process.stderr.write(`[PROTECT_MCP] Cedar reload failed, keeping the previous policy: ${err instanceof Error ? err.message : err}
|
|
1033
|
+
`);
|
|
1034
|
+
state.cedarMtimeMs = m;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
996
1037
|
function findCedarDir() {
|
|
997
1038
|
for (const candidate of ["cedar", "policies", "."]) {
|
|
998
1039
|
try {
|
package/dist/cli.js
CHANGED
|
@@ -6650,6 +6650,7 @@ async function handlePreToolUse(input, state) {
|
|
|
6650
6650
|
...input.teamName && { team_name: input.teamName },
|
|
6651
6651
|
...input.agentType && { agent_type: input.agentType }
|
|
6652
6652
|
};
|
|
6653
|
+
maybeReloadCedar(state);
|
|
6653
6654
|
if (state.cedarPolicies) {
|
|
6654
6655
|
try {
|
|
6655
6656
|
const cedarDecision = await evaluateCedar(state.cedarPolicies, {
|
|
@@ -6687,10 +6688,13 @@ async function handlePreToolUse(input, state) {
|
|
|
6687
6688
|
sandbox_state: detectSandboxState(),
|
|
6688
6689
|
plan_receipt_id: state.activePlanReceiptId || void 0
|
|
6689
6690
|
});
|
|
6691
|
+
const isDefaultDeny = !reason || reason === "cedar_deny" || /reason":\[\]/.test(reason);
|
|
6692
|
+
const policyRef = state.cedarDir ? ` Policy: ${state.cedarDir}.` : "";
|
|
6693
|
+
const howTo = isDefaultDeny ? ` No permit matched (default-deny, fail-closed).${policyRef} Allow it: npx protect-mcp policy allow ${toolName}` : ` Blocked by an explicit forbid rule.${policyRef} Review: npx protect-mcp policy show`;
|
|
6690
6694
|
if (denyCount === 1) {
|
|
6691
6695
|
process.stderr.write(
|
|
6692
|
-
`[PROTECT_MCP]
|
|
6693
|
-
${
|
|
6696
|
+
`[PROTECT_MCP] Denied "${toolName}" (${isDefaultDeny ? "default-deny" : "forbid"}).
|
|
6697
|
+
Allow with: npx protect-mcp policy allow ${toolName}
|
|
6694
6698
|
`
|
|
6695
6699
|
);
|
|
6696
6700
|
}
|
|
@@ -6698,7 +6702,7 @@ async function handlePreToolUse(input, state) {
|
|
|
6698
6702
|
hookSpecificOutput: {
|
|
6699
6703
|
hookEventName: "PreToolUse",
|
|
6700
6704
|
permissionDecision: "deny",
|
|
6701
|
-
permissionDecisionReason: `[ScopeBlind] Denied
|
|
6705
|
+
permissionDecisionReason: `[ScopeBlind] Denied "${toolName}".${howTo}` + (denyCount > 1 ? ` (attempt ${denyCount})` : "")
|
|
6702
6706
|
}
|
|
6703
6707
|
};
|
|
6704
6708
|
}
|
|
@@ -7197,6 +7201,9 @@ async function startHookServer(options = {}) {
|
|
|
7197
7201
|
}
|
|
7198
7202
|
const state = {
|
|
7199
7203
|
cedarPolicies,
|
|
7204
|
+
cedarDir: cedarDir || null,
|
|
7205
|
+
cedarMtimeMs: cedarDir ? newestCedarMtime(cedarDir) : 0,
|
|
7206
|
+
cedarCheckedMs: 0,
|
|
7200
7207
|
jsonPolicy,
|
|
7201
7208
|
rateLimitStore: /* @__PURE__ */ new Map(),
|
|
7202
7209
|
receiptBuffer: new ReceiptBuffer(),
|
|
@@ -7387,6 +7394,39 @@ async function startHookServer(options = {}) {
|
|
|
7387
7394
|
process.on("SIGTERM", shutdown);
|
|
7388
7395
|
return server;
|
|
7389
7396
|
}
|
|
7397
|
+
function newestCedarMtime(dir) {
|
|
7398
|
+
try {
|
|
7399
|
+
let newest = 0;
|
|
7400
|
+
for (const f of (0, import_node_fs11.readdirSync)(dir)) {
|
|
7401
|
+
if (!f.endsWith(".cedar")) continue;
|
|
7402
|
+
const m = (0, import_node_fs11.statSync)((0, import_node_path8.join)(dir, f)).mtimeMs;
|
|
7403
|
+
if (m > newest) newest = m;
|
|
7404
|
+
}
|
|
7405
|
+
return newest;
|
|
7406
|
+
} catch {
|
|
7407
|
+
return 0;
|
|
7408
|
+
}
|
|
7409
|
+
}
|
|
7410
|
+
function maybeReloadCedar(state) {
|
|
7411
|
+
if (!state.cedarDir) return;
|
|
7412
|
+
const nowMs = Date.now();
|
|
7413
|
+
if (nowMs - state.cedarCheckedMs < CEDAR_CHECK_THROTTLE_MS) return;
|
|
7414
|
+
state.cedarCheckedMs = nowMs;
|
|
7415
|
+
const m = newestCedarMtime(state.cedarDir);
|
|
7416
|
+
if (m <= state.cedarMtimeMs) return;
|
|
7417
|
+
try {
|
|
7418
|
+
const reloaded = loadCedarPolicies(state.cedarDir);
|
|
7419
|
+
state.cedarPolicies = reloaded;
|
|
7420
|
+
state.policyDigest = reloaded.digest;
|
|
7421
|
+
state.cedarMtimeMs = m;
|
|
7422
|
+
process.stderr.write(`[PROTECT_MCP] Cedar policy reloaded (digest: ${reloaded.digest}) after on-disk change.
|
|
7423
|
+
`);
|
|
7424
|
+
} catch (err) {
|
|
7425
|
+
process.stderr.write(`[PROTECT_MCP] Cedar reload failed, keeping the previous policy: ${err instanceof Error ? err.message : err}
|
|
7426
|
+
`);
|
|
7427
|
+
state.cedarMtimeMs = m;
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
7390
7430
|
function findCedarDir() {
|
|
7391
7431
|
for (const candidate of ["cedar", "policies", "."]) {
|
|
7392
7432
|
try {
|
|
@@ -7412,7 +7452,7 @@ function normalizeHookInput(raw) {
|
|
|
7412
7452
|
}
|
|
7413
7453
|
return result;
|
|
7414
7454
|
}
|
|
7415
|
-
var import_node_http2, import_node_crypto6, import_node_fs11, import_node_path8, DEFAULT_PORT, LOG_FILE3, RECEIPTS_FILE2, PAYLOAD_HASH_THRESHOLD, SNAKE_TO_CAMEL_MAP;
|
|
7455
|
+
var import_node_http2, import_node_crypto6, import_node_fs11, import_node_path8, DEFAULT_PORT, LOG_FILE3, RECEIPTS_FILE2, PAYLOAD_HASH_THRESHOLD, CEDAR_CHECK_THROTTLE_MS, SNAKE_TO_CAMEL_MAP;
|
|
7416
7456
|
var init_hook_server = __esm({
|
|
7417
7457
|
"src/hook-server.ts"() {
|
|
7418
7458
|
"use strict";
|
|
@@ -7431,6 +7471,7 @@ var init_hook_server = __esm({
|
|
|
7431
7471
|
LOG_FILE3 = ".protect-mcp-log.jsonl";
|
|
7432
7472
|
RECEIPTS_FILE2 = ".protect-mcp-receipts.jsonl";
|
|
7433
7473
|
PAYLOAD_HASH_THRESHOLD = 1024;
|
|
7474
|
+
CEDAR_CHECK_THROTTLE_MS = 2e3;
|
|
7434
7475
|
SNAKE_TO_CAMEL_MAP = {
|
|
7435
7476
|
hook_event_name: "hookEventName",
|
|
7436
7477
|
session_id: "sessionId",
|
|
@@ -8871,6 +8912,7 @@ Usage:
|
|
|
8871
8912
|
protect-mcp connect
|
|
8872
8913
|
protect-mcp init [--dir <path>]
|
|
8873
8914
|
protect-mcp sample [--dir <path>] [--force]
|
|
8915
|
+
protect-mcp policy list|show|allow <tool>|deny <tool>|path
|
|
8874
8916
|
protect-mcp demo
|
|
8875
8917
|
protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]
|
|
8876
8918
|
protect-mcp status [--dir <path>]
|
|
@@ -12977,6 +13019,151 @@ ${bold("\u{1F6E1} Sample record seeded")} \xB7 8 decisions (1 blocked, 2 payment
|
|
|
12977
13019
|
`);
|
|
12978
13020
|
process.exit(0);
|
|
12979
13021
|
}
|
|
13022
|
+
async function handlePolicy(argv) {
|
|
13023
|
+
const { readFileSync: rf, writeFileSync: wf, existsSync: ex, readdirSync: rd, appendFileSync: af } = await import("fs");
|
|
13024
|
+
const { join: pj } = await import("path");
|
|
13025
|
+
const { createHash: createHash6 } = await import("crypto");
|
|
13026
|
+
const sub = argv[0] || "list";
|
|
13027
|
+
const findDir = () => {
|
|
13028
|
+
for (const c of ["cedar", "policies", "."]) {
|
|
13029
|
+
try {
|
|
13030
|
+
if (ex(c) && rd(c).some((f) => f.endsWith(".cedar"))) return c;
|
|
13031
|
+
} catch {
|
|
13032
|
+
}
|
|
13033
|
+
}
|
|
13034
|
+
return null;
|
|
13035
|
+
};
|
|
13036
|
+
const dir = findDir();
|
|
13037
|
+
const cedarFiles = dir ? rd(dir).filter((f) => f.endsWith(".cedar")).sort() : [];
|
|
13038
|
+
const readAll = () => cedarFiles.map((f) => rf(pj(dir, f), "utf-8")).join("\n\n");
|
|
13039
|
+
const digestOf = (src) => createHash6("sha256").update(src).digest("hex").slice(0, 16);
|
|
13040
|
+
const stripComments = (src) => src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
|
|
13041
|
+
const targetFile = cedarFiles.includes("agent.cedar") ? "agent.cedar" : cedarFiles[0];
|
|
13042
|
+
if (sub === "path") {
|
|
13043
|
+
if (!dir) {
|
|
13044
|
+
process.stdout.write("No Cedar policy directory found (looked in ./cedar, ./policies, .).\n");
|
|
13045
|
+
process.exit(0);
|
|
13046
|
+
}
|
|
13047
|
+
process.stdout.write(`${pj(dir, targetFile)}
|
|
13048
|
+
`);
|
|
13049
|
+
process.exit(0);
|
|
13050
|
+
}
|
|
13051
|
+
if (sub === "show") {
|
|
13052
|
+
if (!dir) {
|
|
13053
|
+
process.stderr.write("No Cedar policy found. Run: npx protect-mcp init-hooks\n");
|
|
13054
|
+
process.exit(1);
|
|
13055
|
+
}
|
|
13056
|
+
const src = readAll();
|
|
13057
|
+
process.stdout.write(`${bold("Cedar policy")} ${dim(`(${cedarFiles.length} file${cedarFiles.length === 1 ? "" : "s"} in ${dir}, digest ${digestOf(src)})`)}
|
|
13058
|
+
|
|
13059
|
+
`);
|
|
13060
|
+
process.stdout.write(src.endsWith("\n") ? src : src + "\n");
|
|
13061
|
+
process.exit(0);
|
|
13062
|
+
}
|
|
13063
|
+
if (sub === "allow" || sub === "deny") {
|
|
13064
|
+
const tool = argv[1];
|
|
13065
|
+
if (!tool) {
|
|
13066
|
+
process.stderr.write(`Usage: npx protect-mcp policy ${sub} <ToolName>
|
|
13067
|
+
`);
|
|
13068
|
+
process.exit(1);
|
|
13069
|
+
}
|
|
13070
|
+
if (!/^[A-Za-z0-9_.:-]+$/.test(tool)) {
|
|
13071
|
+
process.stderr.write(`Refusing: "${tool}" is not a valid tool name.
|
|
13072
|
+
`);
|
|
13073
|
+
process.exit(1);
|
|
13074
|
+
}
|
|
13075
|
+
if (!dir) {
|
|
13076
|
+
process.stderr.write("No Cedar policy found. Run: npx protect-mcp init-hooks\n");
|
|
13077
|
+
process.exit(1);
|
|
13078
|
+
}
|
|
13079
|
+
const effect = sub === "allow" ? "permit" : "forbid";
|
|
13080
|
+
const before = readAll();
|
|
13081
|
+
const ruleRe = new RegExp(`${effect}\\s*\\([^)]*resource\\s*==\\s*Tool::"${tool.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}"`, "s");
|
|
13082
|
+
if (ruleRe.test(stripComments(before))) {
|
|
13083
|
+
process.stdout.write(`${dim("No change:")} a ${effect} rule for ${bold(tool)} already exists in ${targetFile}.
|
|
13084
|
+
`);
|
|
13085
|
+
process.exit(0);
|
|
13086
|
+
}
|
|
13087
|
+
const block = `
|
|
13088
|
+
// ${effect === "permit" ? "Allow" : "Block"} ${tool} (added by \`protect-mcp policy ${sub}\`)
|
|
13089
|
+
${effect}(
|
|
13090
|
+
principal,
|
|
13091
|
+
action == Action::"MCP::Tool::call",
|
|
13092
|
+
resource == Tool::"${tool}"
|
|
13093
|
+
);
|
|
13094
|
+
`;
|
|
13095
|
+
af(pj(dir, targetFile), block);
|
|
13096
|
+
const after = readAll();
|
|
13097
|
+
process.stdout.write(`${bold(sub === "allow" ? "\u2713 Allowed" : "\u2713 Denied")} ${tool}
|
|
13098
|
+
`);
|
|
13099
|
+
process.stdout.write(` ${dim(`appended to ${pj(dir, targetFile)}`)}
|
|
13100
|
+
`);
|
|
13101
|
+
process.stdout.write(` ${dim(`policy digest ${digestOf(before)} \u2192 ${digestOf(after)}`)}
|
|
13102
|
+
`);
|
|
13103
|
+
process.stdout.write(`
|
|
13104
|
+
${dim("A running gate (protect-mcp serve) hot-reloads on this change; no restart needed. The one-shot hook path re-reads per call.")}
|
|
13105
|
+
`);
|
|
13106
|
+
process.exit(0);
|
|
13107
|
+
}
|
|
13108
|
+
if (sub === "list") {
|
|
13109
|
+
if (!dir) {
|
|
13110
|
+
process.stderr.write("No Cedar policy found. Run: npx protect-mcp init-hooks\n");
|
|
13111
|
+
process.exit(1);
|
|
13112
|
+
}
|
|
13113
|
+
const src = readAll();
|
|
13114
|
+
const active = stripComments(src);
|
|
13115
|
+
const named = (effect) => {
|
|
13116
|
+
const set = /* @__PURE__ */ new Set();
|
|
13117
|
+
const re = new RegExp(`${effect}\\s*\\([\\s\\S]*?resource\\s*==\\s*Tool::"([^"]+)"[\\s\\S]*?\\);`, "g");
|
|
13118
|
+
let m;
|
|
13119
|
+
while (m = re.exec(active)) set.add(m[1]);
|
|
13120
|
+
return set;
|
|
13121
|
+
};
|
|
13122
|
+
const permitted = named("permit"), forbidden = named("forbid");
|
|
13123
|
+
const seen = /* @__PURE__ */ new Map();
|
|
13124
|
+
try {
|
|
13125
|
+
const logPath = pj(process.cwd(), ".protect-mcp-log.jsonl");
|
|
13126
|
+
if (ex(logPath)) {
|
|
13127
|
+
for (const line of rf(logPath, "utf-8").split("\n")) {
|
|
13128
|
+
if (!line.trim()) continue;
|
|
13129
|
+
try {
|
|
13130
|
+
const e = JSON.parse(line);
|
|
13131
|
+
if (!e.tool) continue;
|
|
13132
|
+
const rec = seen.get(e.tool) || { allow: 0, deny: 0 };
|
|
13133
|
+
if (e.decision === "deny") rec.deny++;
|
|
13134
|
+
else rec.allow++;
|
|
13135
|
+
seen.set(e.tool, rec);
|
|
13136
|
+
} catch {
|
|
13137
|
+
}
|
|
13138
|
+
}
|
|
13139
|
+
}
|
|
13140
|
+
} catch {
|
|
13141
|
+
}
|
|
13142
|
+
process.stdout.write(`${bold("Cedar policy")} ${dim(`(${dir}, digest ${digestOf(src)}) \xB7 default-deny, fail-closed`)}
|
|
13143
|
+
|
|
13144
|
+
`);
|
|
13145
|
+
const allTools = /* @__PURE__ */ new Set([...permitted, ...forbidden, ...seen.keys()]);
|
|
13146
|
+
if (allTools.size === 0) {
|
|
13147
|
+
process.stdout.write(dim(" No rules and no decisions logged yet.\n"));
|
|
13148
|
+
process.exit(0);
|
|
13149
|
+
}
|
|
13150
|
+
const rows = [...allTools].sort().map((t) => {
|
|
13151
|
+
const label = forbidden.has(t) ? "forbid" : permitted.has(t) ? "permit" : "default-deny";
|
|
13152
|
+
const colored = forbidden.has(t) ? red(label) : permitted.has(t) ? green(label) : yellow(label);
|
|
13153
|
+
const pad = " ".repeat(Math.max(1, 13 - label.length));
|
|
13154
|
+
const s = seen.get(t);
|
|
13155
|
+
const hits = s ? dim(` ${s.allow} allowed, ${s.deny} denied`) : "";
|
|
13156
|
+
return ` ${colored}${pad}${bold(t)}${hits}`;
|
|
13157
|
+
});
|
|
13158
|
+
process.stdout.write(rows.join("\n") + "\n");
|
|
13159
|
+
process.stdout.write(`
|
|
13160
|
+
${dim("Allow a tool: npx protect-mcp policy allow <ToolName> \xB7 Block one: policy deny <ToolName>")}
|
|
13161
|
+
`);
|
|
13162
|
+
process.exit(0);
|
|
13163
|
+
}
|
|
13164
|
+
process.stderr.write("Usage: npx protect-mcp policy <list|show|allow <tool>|deny <tool>|path>\n");
|
|
13165
|
+
process.exit(1);
|
|
13166
|
+
}
|
|
12980
13167
|
async function main() {
|
|
12981
13168
|
sendInstallTelemetry().catch(() => {
|
|
12982
13169
|
});
|
|
@@ -13046,6 +13233,10 @@ async function main() {
|
|
|
13046
13233
|
await handleSample(args.slice(1));
|
|
13047
13234
|
return;
|
|
13048
13235
|
}
|
|
13236
|
+
if (args[0] === "policy") {
|
|
13237
|
+
await handlePolicy(args.slice(1));
|
|
13238
|
+
return;
|
|
13239
|
+
}
|
|
13049
13240
|
if (args[0] === "init-hooks") {
|
|
13050
13241
|
await handleInitHooks(args.slice(1));
|
|
13051
13242
|
process.exit(0);
|
package/dist/cli.mjs
CHANGED
|
@@ -55,6 +55,7 @@ Usage:
|
|
|
55
55
|
protect-mcp connect
|
|
56
56
|
protect-mcp init [--dir <path>]
|
|
57
57
|
protect-mcp sample [--dir <path>] [--force]
|
|
58
|
+
protect-mcp policy list|show|allow <tool>|deny <tool>|path
|
|
58
59
|
protect-mcp demo
|
|
59
60
|
protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]
|
|
60
61
|
protect-mcp status [--dir <path>]
|
|
@@ -4161,6 +4162,151 @@ ${bold("\u{1F6E1} Sample record seeded")} \xB7 8 decisions (1 blocked, 2 payment
|
|
|
4161
4162
|
`);
|
|
4162
4163
|
process.exit(0);
|
|
4163
4164
|
}
|
|
4165
|
+
async function handlePolicy(argv) {
|
|
4166
|
+
const { readFileSync: rf, writeFileSync: wf, existsSync: ex, readdirSync: rd, appendFileSync: af } = await import("fs");
|
|
4167
|
+
const { join: pj } = await import("path");
|
|
4168
|
+
const { createHash } = await import("crypto");
|
|
4169
|
+
const sub = argv[0] || "list";
|
|
4170
|
+
const findDir = () => {
|
|
4171
|
+
for (const c of ["cedar", "policies", "."]) {
|
|
4172
|
+
try {
|
|
4173
|
+
if (ex(c) && rd(c).some((f) => f.endsWith(".cedar"))) return c;
|
|
4174
|
+
} catch {
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
return null;
|
|
4178
|
+
};
|
|
4179
|
+
const dir = findDir();
|
|
4180
|
+
const cedarFiles = dir ? rd(dir).filter((f) => f.endsWith(".cedar")).sort() : [];
|
|
4181
|
+
const readAll = () => cedarFiles.map((f) => rf(pj(dir, f), "utf-8")).join("\n\n");
|
|
4182
|
+
const digestOf = (src) => createHash("sha256").update(src).digest("hex").slice(0, 16);
|
|
4183
|
+
const stripComments = (src) => src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
|
|
4184
|
+
const targetFile = cedarFiles.includes("agent.cedar") ? "agent.cedar" : cedarFiles[0];
|
|
4185
|
+
if (sub === "path") {
|
|
4186
|
+
if (!dir) {
|
|
4187
|
+
process.stdout.write("No Cedar policy directory found (looked in ./cedar, ./policies, .).\n");
|
|
4188
|
+
process.exit(0);
|
|
4189
|
+
}
|
|
4190
|
+
process.stdout.write(`${pj(dir, targetFile)}
|
|
4191
|
+
`);
|
|
4192
|
+
process.exit(0);
|
|
4193
|
+
}
|
|
4194
|
+
if (sub === "show") {
|
|
4195
|
+
if (!dir) {
|
|
4196
|
+
process.stderr.write("No Cedar policy found. Run: npx protect-mcp init-hooks\n");
|
|
4197
|
+
process.exit(1);
|
|
4198
|
+
}
|
|
4199
|
+
const src = readAll();
|
|
4200
|
+
process.stdout.write(`${bold("Cedar policy")} ${dim(`(${cedarFiles.length} file${cedarFiles.length === 1 ? "" : "s"} in ${dir}, digest ${digestOf(src)})`)}
|
|
4201
|
+
|
|
4202
|
+
`);
|
|
4203
|
+
process.stdout.write(src.endsWith("\n") ? src : src + "\n");
|
|
4204
|
+
process.exit(0);
|
|
4205
|
+
}
|
|
4206
|
+
if (sub === "allow" || sub === "deny") {
|
|
4207
|
+
const tool = argv[1];
|
|
4208
|
+
if (!tool) {
|
|
4209
|
+
process.stderr.write(`Usage: npx protect-mcp policy ${sub} <ToolName>
|
|
4210
|
+
`);
|
|
4211
|
+
process.exit(1);
|
|
4212
|
+
}
|
|
4213
|
+
if (!/^[A-Za-z0-9_.:-]+$/.test(tool)) {
|
|
4214
|
+
process.stderr.write(`Refusing: "${tool}" is not a valid tool name.
|
|
4215
|
+
`);
|
|
4216
|
+
process.exit(1);
|
|
4217
|
+
}
|
|
4218
|
+
if (!dir) {
|
|
4219
|
+
process.stderr.write("No Cedar policy found. Run: npx protect-mcp init-hooks\n");
|
|
4220
|
+
process.exit(1);
|
|
4221
|
+
}
|
|
4222
|
+
const effect = sub === "allow" ? "permit" : "forbid";
|
|
4223
|
+
const before = readAll();
|
|
4224
|
+
const ruleRe = new RegExp(`${effect}\\s*\\([^)]*resource\\s*==\\s*Tool::"${tool.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}"`, "s");
|
|
4225
|
+
if (ruleRe.test(stripComments(before))) {
|
|
4226
|
+
process.stdout.write(`${dim("No change:")} a ${effect} rule for ${bold(tool)} already exists in ${targetFile}.
|
|
4227
|
+
`);
|
|
4228
|
+
process.exit(0);
|
|
4229
|
+
}
|
|
4230
|
+
const block = `
|
|
4231
|
+
// ${effect === "permit" ? "Allow" : "Block"} ${tool} (added by \`protect-mcp policy ${sub}\`)
|
|
4232
|
+
${effect}(
|
|
4233
|
+
principal,
|
|
4234
|
+
action == Action::"MCP::Tool::call",
|
|
4235
|
+
resource == Tool::"${tool}"
|
|
4236
|
+
);
|
|
4237
|
+
`;
|
|
4238
|
+
af(pj(dir, targetFile), block);
|
|
4239
|
+
const after = readAll();
|
|
4240
|
+
process.stdout.write(`${bold(sub === "allow" ? "\u2713 Allowed" : "\u2713 Denied")} ${tool}
|
|
4241
|
+
`);
|
|
4242
|
+
process.stdout.write(` ${dim(`appended to ${pj(dir, targetFile)}`)}
|
|
4243
|
+
`);
|
|
4244
|
+
process.stdout.write(` ${dim(`policy digest ${digestOf(before)} \u2192 ${digestOf(after)}`)}
|
|
4245
|
+
`);
|
|
4246
|
+
process.stdout.write(`
|
|
4247
|
+
${dim("A running gate (protect-mcp serve) hot-reloads on this change; no restart needed. The one-shot hook path re-reads per call.")}
|
|
4248
|
+
`);
|
|
4249
|
+
process.exit(0);
|
|
4250
|
+
}
|
|
4251
|
+
if (sub === "list") {
|
|
4252
|
+
if (!dir) {
|
|
4253
|
+
process.stderr.write("No Cedar policy found. Run: npx protect-mcp init-hooks\n");
|
|
4254
|
+
process.exit(1);
|
|
4255
|
+
}
|
|
4256
|
+
const src = readAll();
|
|
4257
|
+
const active = stripComments(src);
|
|
4258
|
+
const named = (effect) => {
|
|
4259
|
+
const set = /* @__PURE__ */ new Set();
|
|
4260
|
+
const re = new RegExp(`${effect}\\s*\\([\\s\\S]*?resource\\s*==\\s*Tool::"([^"]+)"[\\s\\S]*?\\);`, "g");
|
|
4261
|
+
let m;
|
|
4262
|
+
while (m = re.exec(active)) set.add(m[1]);
|
|
4263
|
+
return set;
|
|
4264
|
+
};
|
|
4265
|
+
const permitted = named("permit"), forbidden = named("forbid");
|
|
4266
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4267
|
+
try {
|
|
4268
|
+
const logPath = pj(process.cwd(), ".protect-mcp-log.jsonl");
|
|
4269
|
+
if (ex(logPath)) {
|
|
4270
|
+
for (const line of rf(logPath, "utf-8").split("\n")) {
|
|
4271
|
+
if (!line.trim()) continue;
|
|
4272
|
+
try {
|
|
4273
|
+
const e = JSON.parse(line);
|
|
4274
|
+
if (!e.tool) continue;
|
|
4275
|
+
const rec = seen.get(e.tool) || { allow: 0, deny: 0 };
|
|
4276
|
+
if (e.decision === "deny") rec.deny++;
|
|
4277
|
+
else rec.allow++;
|
|
4278
|
+
seen.set(e.tool, rec);
|
|
4279
|
+
} catch {
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
} catch {
|
|
4284
|
+
}
|
|
4285
|
+
process.stdout.write(`${bold("Cedar policy")} ${dim(`(${dir}, digest ${digestOf(src)}) \xB7 default-deny, fail-closed`)}
|
|
4286
|
+
|
|
4287
|
+
`);
|
|
4288
|
+
const allTools = /* @__PURE__ */ new Set([...permitted, ...forbidden, ...seen.keys()]);
|
|
4289
|
+
if (allTools.size === 0) {
|
|
4290
|
+
process.stdout.write(dim(" No rules and no decisions logged yet.\n"));
|
|
4291
|
+
process.exit(0);
|
|
4292
|
+
}
|
|
4293
|
+
const rows = [...allTools].sort().map((t) => {
|
|
4294
|
+
const label = forbidden.has(t) ? "forbid" : permitted.has(t) ? "permit" : "default-deny";
|
|
4295
|
+
const colored = forbidden.has(t) ? red(label) : permitted.has(t) ? green(label) : yellow(label);
|
|
4296
|
+
const pad = " ".repeat(Math.max(1, 13 - label.length));
|
|
4297
|
+
const s = seen.get(t);
|
|
4298
|
+
const hits = s ? dim(` ${s.allow} allowed, ${s.deny} denied`) : "";
|
|
4299
|
+
return ` ${colored}${pad}${bold(t)}${hits}`;
|
|
4300
|
+
});
|
|
4301
|
+
process.stdout.write(rows.join("\n") + "\n");
|
|
4302
|
+
process.stdout.write(`
|
|
4303
|
+
${dim("Allow a tool: npx protect-mcp policy allow <ToolName> \xB7 Block one: policy deny <ToolName>")}
|
|
4304
|
+
`);
|
|
4305
|
+
process.exit(0);
|
|
4306
|
+
}
|
|
4307
|
+
process.stderr.write("Usage: npx protect-mcp policy <list|show|allow <tool>|deny <tool>|path>\n");
|
|
4308
|
+
process.exit(1);
|
|
4309
|
+
}
|
|
4164
4310
|
async function main() {
|
|
4165
4311
|
sendInstallTelemetry().catch(() => {
|
|
4166
4312
|
});
|
|
@@ -4230,6 +4376,10 @@ async function main() {
|
|
|
4230
4376
|
await handleSample(args.slice(1));
|
|
4231
4377
|
return;
|
|
4232
4378
|
}
|
|
4379
|
+
if (args[0] === "policy") {
|
|
4380
|
+
await handlePolicy(args.slice(1));
|
|
4381
|
+
return;
|
|
4382
|
+
}
|
|
4233
4383
|
if (args[0] === "init-hooks") {
|
|
4234
4384
|
await handleInitHooks(args.slice(1));
|
|
4235
4385
|
process.exit(0);
|
package/dist/hook-server.js
CHANGED
|
@@ -1242,6 +1242,7 @@ async function handlePreToolUse(input, state) {
|
|
|
1242
1242
|
...input.teamName && { team_name: input.teamName },
|
|
1243
1243
|
...input.agentType && { agent_type: input.agentType }
|
|
1244
1244
|
};
|
|
1245
|
+
maybeReloadCedar(state);
|
|
1245
1246
|
if (state.cedarPolicies) {
|
|
1246
1247
|
try {
|
|
1247
1248
|
const cedarDecision = await evaluateCedar(state.cedarPolicies, {
|
|
@@ -1279,10 +1280,13 @@ async function handlePreToolUse(input, state) {
|
|
|
1279
1280
|
sandbox_state: detectSandboxState(),
|
|
1280
1281
|
plan_receipt_id: state.activePlanReceiptId || void 0
|
|
1281
1282
|
});
|
|
1283
|
+
const isDefaultDeny = !reason || reason === "cedar_deny" || /reason":\[\]/.test(reason);
|
|
1284
|
+
const policyRef = state.cedarDir ? ` Policy: ${state.cedarDir}.` : "";
|
|
1285
|
+
const howTo = isDefaultDeny ? ` No permit matched (default-deny, fail-closed).${policyRef} Allow it: npx protect-mcp policy allow ${toolName}` : ` Blocked by an explicit forbid rule.${policyRef} Review: npx protect-mcp policy show`;
|
|
1282
1286
|
if (denyCount === 1) {
|
|
1283
1287
|
process.stderr.write(
|
|
1284
|
-
`[PROTECT_MCP]
|
|
1285
|
-
${
|
|
1288
|
+
`[PROTECT_MCP] Denied "${toolName}" (${isDefaultDeny ? "default-deny" : "forbid"}).
|
|
1289
|
+
Allow with: npx protect-mcp policy allow ${toolName}
|
|
1286
1290
|
`
|
|
1287
1291
|
);
|
|
1288
1292
|
}
|
|
@@ -1290,7 +1294,7 @@ async function handlePreToolUse(input, state) {
|
|
|
1290
1294
|
hookSpecificOutput: {
|
|
1291
1295
|
hookEventName: "PreToolUse",
|
|
1292
1296
|
permissionDecision: "deny",
|
|
1293
|
-
permissionDecisionReason: `[ScopeBlind] Denied
|
|
1297
|
+
permissionDecisionReason: `[ScopeBlind] Denied "${toolName}".${howTo}` + (denyCount > 1 ? ` (attempt ${denyCount})` : "")
|
|
1294
1298
|
}
|
|
1295
1299
|
};
|
|
1296
1300
|
}
|
|
@@ -1789,6 +1793,9 @@ async function startHookServer(options = {}) {
|
|
|
1789
1793
|
}
|
|
1790
1794
|
const state = {
|
|
1791
1795
|
cedarPolicies,
|
|
1796
|
+
cedarDir: cedarDir || null,
|
|
1797
|
+
cedarMtimeMs: cedarDir ? newestCedarMtime(cedarDir) : 0,
|
|
1798
|
+
cedarCheckedMs: 0,
|
|
1792
1799
|
jsonPolicy,
|
|
1793
1800
|
rateLimitStore: /* @__PURE__ */ new Map(),
|
|
1794
1801
|
receiptBuffer: new ReceiptBuffer(),
|
|
@@ -1979,6 +1986,40 @@ async function startHookServer(options = {}) {
|
|
|
1979
1986
|
process.on("SIGTERM", shutdown);
|
|
1980
1987
|
return server;
|
|
1981
1988
|
}
|
|
1989
|
+
function newestCedarMtime(dir) {
|
|
1990
|
+
try {
|
|
1991
|
+
let newest = 0;
|
|
1992
|
+
for (const f of (0, import_node_fs5.readdirSync)(dir)) {
|
|
1993
|
+
if (!f.endsWith(".cedar")) continue;
|
|
1994
|
+
const m = (0, import_node_fs5.statSync)((0, import_node_path3.join)(dir, f)).mtimeMs;
|
|
1995
|
+
if (m > newest) newest = m;
|
|
1996
|
+
}
|
|
1997
|
+
return newest;
|
|
1998
|
+
} catch {
|
|
1999
|
+
return 0;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
var CEDAR_CHECK_THROTTLE_MS = 2e3;
|
|
2003
|
+
function maybeReloadCedar(state) {
|
|
2004
|
+
if (!state.cedarDir) return;
|
|
2005
|
+
const nowMs = Date.now();
|
|
2006
|
+
if (nowMs - state.cedarCheckedMs < CEDAR_CHECK_THROTTLE_MS) return;
|
|
2007
|
+
state.cedarCheckedMs = nowMs;
|
|
2008
|
+
const m = newestCedarMtime(state.cedarDir);
|
|
2009
|
+
if (m <= state.cedarMtimeMs) return;
|
|
2010
|
+
try {
|
|
2011
|
+
const reloaded = loadCedarPolicies(state.cedarDir);
|
|
2012
|
+
state.cedarPolicies = reloaded;
|
|
2013
|
+
state.policyDigest = reloaded.digest;
|
|
2014
|
+
state.cedarMtimeMs = m;
|
|
2015
|
+
process.stderr.write(`[PROTECT_MCP] Cedar policy reloaded (digest: ${reloaded.digest}) after on-disk change.
|
|
2016
|
+
`);
|
|
2017
|
+
} catch (err) {
|
|
2018
|
+
process.stderr.write(`[PROTECT_MCP] Cedar reload failed, keeping the previous policy: ${err instanceof Error ? err.message : err}
|
|
2019
|
+
`);
|
|
2020
|
+
state.cedarMtimeMs = m;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
1982
2023
|
function findCedarDir() {
|
|
1983
2024
|
for (const candidate of ["cedar", "policies", "."]) {
|
|
1984
2025
|
try {
|
package/dist/hook-server.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -40565,6 +40565,7 @@ async function handlePreToolUse(input, state) {
|
|
|
40565
40565
|
...input.teamName && { team_name: input.teamName },
|
|
40566
40566
|
...input.agentType && { agent_type: input.agentType }
|
|
40567
40567
|
};
|
|
40568
|
+
maybeReloadCedar(state);
|
|
40568
40569
|
if (state.cedarPolicies) {
|
|
40569
40570
|
try {
|
|
40570
40571
|
const cedarDecision = await evaluateCedar(state.cedarPolicies, {
|
|
@@ -40602,10 +40603,13 @@ async function handlePreToolUse(input, state) {
|
|
|
40602
40603
|
sandbox_state: detectSandboxState(),
|
|
40603
40604
|
plan_receipt_id: state.activePlanReceiptId || void 0
|
|
40604
40605
|
});
|
|
40606
|
+
const isDefaultDeny = !reason || reason === "cedar_deny" || /reason":\[\]/.test(reason);
|
|
40607
|
+
const policyRef = state.cedarDir ? ` Policy: ${state.cedarDir}.` : "";
|
|
40608
|
+
const howTo = isDefaultDeny ? ` No permit matched (default-deny, fail-closed).${policyRef} Allow it: npx protect-mcp policy allow ${toolName}` : ` Blocked by an explicit forbid rule.${policyRef} Review: npx protect-mcp policy show`;
|
|
40605
40609
|
if (denyCount === 1) {
|
|
40606
40610
|
process.stderr.write(
|
|
40607
|
-
`[PROTECT_MCP]
|
|
40608
|
-
${
|
|
40611
|
+
`[PROTECT_MCP] Denied "${toolName}" (${isDefaultDeny ? "default-deny" : "forbid"}).
|
|
40612
|
+
Allow with: npx protect-mcp policy allow ${toolName}
|
|
40609
40613
|
`
|
|
40610
40614
|
);
|
|
40611
40615
|
}
|
|
@@ -40613,7 +40617,7 @@ async function handlePreToolUse(input, state) {
|
|
|
40613
40617
|
hookSpecificOutput: {
|
|
40614
40618
|
hookEventName: "PreToolUse",
|
|
40615
40619
|
permissionDecision: "deny",
|
|
40616
|
-
permissionDecisionReason: `[ScopeBlind] Denied
|
|
40620
|
+
permissionDecisionReason: `[ScopeBlind] Denied "${toolName}".${howTo}` + (denyCount > 1 ? ` (attempt ${denyCount})` : "")
|
|
40617
40621
|
}
|
|
40618
40622
|
};
|
|
40619
40623
|
}
|
|
@@ -41112,6 +41116,9 @@ async function startHookServer(options = {}) {
|
|
|
41112
41116
|
}
|
|
41113
41117
|
const state = {
|
|
41114
41118
|
cedarPolicies,
|
|
41119
|
+
cedarDir: cedarDir || null,
|
|
41120
|
+
cedarMtimeMs: cedarDir ? newestCedarMtime(cedarDir) : 0,
|
|
41121
|
+
cedarCheckedMs: 0,
|
|
41115
41122
|
jsonPolicy,
|
|
41116
41123
|
rateLimitStore: /* @__PURE__ */ new Map(),
|
|
41117
41124
|
receiptBuffer: new ReceiptBuffer(),
|
|
@@ -41302,6 +41309,40 @@ async function startHookServer(options = {}) {
|
|
|
41302
41309
|
process.on("SIGTERM", shutdown);
|
|
41303
41310
|
return server;
|
|
41304
41311
|
}
|
|
41312
|
+
function newestCedarMtime(dir) {
|
|
41313
|
+
try {
|
|
41314
|
+
let newest = 0;
|
|
41315
|
+
for (const f of (0, import_node_fs9.readdirSync)(dir)) {
|
|
41316
|
+
if (!f.endsWith(".cedar")) continue;
|
|
41317
|
+
const m = (0, import_node_fs9.statSync)((0, import_node_path5.join)(dir, f)).mtimeMs;
|
|
41318
|
+
if (m > newest) newest = m;
|
|
41319
|
+
}
|
|
41320
|
+
return newest;
|
|
41321
|
+
} catch {
|
|
41322
|
+
return 0;
|
|
41323
|
+
}
|
|
41324
|
+
}
|
|
41325
|
+
var CEDAR_CHECK_THROTTLE_MS = 2e3;
|
|
41326
|
+
function maybeReloadCedar(state) {
|
|
41327
|
+
if (!state.cedarDir) return;
|
|
41328
|
+
const nowMs = Date.now();
|
|
41329
|
+
if (nowMs - state.cedarCheckedMs < CEDAR_CHECK_THROTTLE_MS) return;
|
|
41330
|
+
state.cedarCheckedMs = nowMs;
|
|
41331
|
+
const m = newestCedarMtime(state.cedarDir);
|
|
41332
|
+
if (m <= state.cedarMtimeMs) return;
|
|
41333
|
+
try {
|
|
41334
|
+
const reloaded = loadCedarPolicies(state.cedarDir);
|
|
41335
|
+
state.cedarPolicies = reloaded;
|
|
41336
|
+
state.policyDigest = reloaded.digest;
|
|
41337
|
+
state.cedarMtimeMs = m;
|
|
41338
|
+
process.stderr.write(`[PROTECT_MCP] Cedar policy reloaded (digest: ${reloaded.digest}) after on-disk change.
|
|
41339
|
+
`);
|
|
41340
|
+
} catch (err) {
|
|
41341
|
+
process.stderr.write(`[PROTECT_MCP] Cedar reload failed, keeping the previous policy: ${err instanceof Error ? err.message : err}
|
|
41342
|
+
`);
|
|
41343
|
+
state.cedarMtimeMs = m;
|
|
41344
|
+
}
|
|
41345
|
+
}
|
|
41305
41346
|
function findCedarDir() {
|
|
41306
41347
|
for (const candidate of ["cedar", "policies", "."]) {
|
|
41307
41348
|
try {
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "protect-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
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",
|