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 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
+ [![Watch the two-minute demo film](https://legate.scopeblind.com/media/scopeblind-demo-poster.jpg)](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] No Cedar permit for "${toolName}" \u2014 suggest:
299
- ${suggestion}
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 by Cedar policy. ${reason}. Forbidden: "${toolName}" is not permitted. Try a read-only alternative.` + (denyCount > 1 ? ` (attempt ${denyCount})` : "")
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 {