protect-mcp 0.9.4 → 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 +43 -0
- package/README.md +22 -0
- package/dist/{chunk-H332ZNJ6.mjs → chunk-WCZJGEBO.mjs} +45 -4
- package/dist/cli.js +636 -300
- package/dist/cli.mjs +203 -3
- 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/dist/sample-6LRP73ZD.mjs +99 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
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
|
+
|
|
24
|
+
## 0.9.5: replayable from scratch
|
|
25
|
+
|
|
26
|
+
The public demo film (legate.scopeblind.com/record) is now reproducible by
|
|
27
|
+
anyone in 60 seconds, and the CLI answers the first two commands every new
|
|
28
|
+
user types.
|
|
29
|
+
|
|
30
|
+
- **`sample`.** Seeds a clearly-labeled sample record (fresh Ed25519 keypair,
|
|
31
|
+
kid `sample-demo`): 8 decisions including one BLOCKED network call and two
|
|
32
|
+
payments ($0.02 x402-style per-call, $12.50 invoice), plus
|
|
33
|
+
`demo-tampered.jsonl` with one decision flipped after signing. The receipts
|
|
34
|
+
are real signed artifacts (same envelope and canonical-JSON preimage as the
|
|
35
|
+
gate's signer), so `record`, `claim --payment-under`, `verify-claim`, and
|
|
36
|
+
`anchor-record` work on the folder immediately. Refuses to touch an existing
|
|
37
|
+
record or signing key; `--force` overrides.
|
|
38
|
+
- **`--version` / `version` / `-V`.** Prints the installed version. Previously
|
|
39
|
+
these fell through to wrap-mode parsing and errored demanding a `--`
|
|
40
|
+
separator, which was the first thing many new users saw.
|
|
41
|
+
- **`help` word.** `npx protect-mcp help` prints usage (previously errored).
|
|
42
|
+
Help and version flags are now only recognized BEFORE the `--` separator, so
|
|
43
|
+
`protect-mcp -- node server.js --version` wraps that command as intended
|
|
44
|
+
instead of printing our help.
|
|
45
|
+
|
|
3
46
|
## 0.9.4: agent payments get receipts, records get heartbeats, anchors get names
|
|
4
47
|
|
|
5
48
|
The provenance layer reaches the agentic economy's payment rails (x402), the
|
package/README.md
CHANGED
|
@@ -185,6 +185,26 @@ npx protect-mcp claim --no net.egress --anchor
|
|
|
185
185
|
This is an accountable, position-blind attestation, not full zero-knowledge: it
|
|
186
186
|
reveals the shape, not the content.
|
|
187
187
|
|
|
188
|
+
## Try it in 60 seconds (no agent required)
|
|
189
|
+
|
|
190
|
+
[](https://legate.scopeblind.com/record)
|
|
191
|
+
|
|
192
|
+
Watch the two-minute film at [legate.scopeblind.com/record](https://legate.scopeblind.com/record), then replay it against your own copy:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npx protect-mcp sample # seed a labeled sample record (8 decisions: 1 blocked, 2 payments)
|
|
196
|
+
npx protect-mcp record # open it: signatures verified in your browser
|
|
197
|
+
|
|
198
|
+
npx protect-mcp claim --payment-under 100 --anchor --output payments-under-100.json
|
|
199
|
+
npx protect-mcp verify-claim payments-under-100.json
|
|
200
|
+
npx protect-mcp anchor-record
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Drop the generated `demo-tampered.jsonl` into the record page to watch a
|
|
204
|
+
post-signing edit get caught. `sample` refuses to touch an existing record, so
|
|
205
|
+
run it in an empty folder. When you are ready for the real thing, wire the gate
|
|
206
|
+
below and the same commands run against your agent's own record.
|
|
207
|
+
|
|
188
208
|
## Claude Code hook quickstart
|
|
189
209
|
|
|
190
210
|
```bash
|
|
@@ -368,6 +388,8 @@ To report a vulnerability, see [SECURITY.md](./SECURITY.md).
|
|
|
368
388
|
|---------|-------------|
|
|
369
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. |
|
|
370
390
|
| `init` | Generate an Ed25519 keypair (`keys/gateway.json`), a config template, and a sample policy. |
|
|
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. |
|
|
371
393
|
| `wrap` | Print a protected MCP command or patch Claude Desktop MCP servers. Dry-run by default; use `--write` to update Claude Desktop config. |
|
|
372
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. |
|
|
373
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 {
|