protect-mcp 0.9.5 → 0.9.7

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.
@@ -2,13 +2,15 @@ import {
2
2
  ReceiptBuffer,
3
3
  buildActionReadback,
4
4
  checkRateLimit,
5
- evaluateCedar,
6
5
  getToolPolicy,
7
6
  isSigningEnabled,
8
7
  parseRateLimit,
9
8
  signDecision,
10
9
  startStatusServer
11
- } from "./chunk-XLJUZ4WO.mjs";
10
+ } from "./chunk-G6X763MH.mjs";
11
+ import {
12
+ evaluateCedar
13
+ } from "./chunk-MWXDXYWH.mjs";
12
14
 
13
15
  // src/evidence-store.ts
14
16
  import { readFileSync, writeFileSync, existsSync } from "fs";
package/dist/cli.js CHANGED
@@ -6414,6 +6414,277 @@ var init_sample = __esm({
6414
6414
  }
6415
6415
  });
6416
6416
 
6417
+ // src/mcp-server.ts
6418
+ var mcp_server_exports = {};
6419
+ __export(mcp_server_exports, {
6420
+ runMcpServer: () => runMcpServer
6421
+ });
6422
+ async function loadArtifacts() {
6423
+ if (artifacts) return artifacts;
6424
+ const moduleName = "@veritasacta/artifacts";
6425
+ const mod2 = await import(
6426
+ /* @vite-ignore */
6427
+ moduleName
6428
+ );
6429
+ artifacts = mod2;
6430
+ return mod2;
6431
+ }
6432
+ function buildReceiptPayload(args) {
6433
+ return {
6434
+ tool: args.tool,
6435
+ decision: args.decision,
6436
+ reason_code: args.reason_code ?? (args.decision === "deny" ? "policy_denied" : "post_execution_receipt"),
6437
+ policy_digest: args.policy_digest ?? "none",
6438
+ scope: args.request_id,
6439
+ mode: "enforce",
6440
+ request_id: args.request_id,
6441
+ spec: "draft-farley-acta-signed-receipts-01",
6442
+ issuer_certification: "self-signed",
6443
+ public_key: args.public_key
6444
+ };
6445
+ }
6446
+ function newRequestId() {
6447
+ return `mcp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
6448
+ }
6449
+ async function callEvaluate(args) {
6450
+ const tool = typeof args.tool === "string" ? args.tool : "";
6451
+ const policySource = typeof args.policy === "string" ? args.policy : "";
6452
+ const input = args.input && typeof args.input === "object" ? args.input : {};
6453
+ if (!tool) return { error: "missing_tool" };
6454
+ if (!policySource.trim()) return { allowed: false, decision: "deny", reason: "no policy provided (fail-closed)", policy_digest: "none" };
6455
+ const policySet = policySetFromSource(policySource);
6456
+ const decision = await evaluateCedar(
6457
+ policySet,
6458
+ { tool, tier: "unknown", context: { ...input, input }, toolInput: input },
6459
+ void 0,
6460
+ { failClosed: true }
6461
+ );
6462
+ return {
6463
+ allowed: decision.allowed,
6464
+ decision: decision.allowed ? "allow" : "deny",
6465
+ reason: decision.reason || (decision.allowed ? "allowed" : "denied by policy"),
6466
+ policy_digest: policySet.digest
6467
+ };
6468
+ }
6469
+ async function callSign(args) {
6470
+ const tool = typeof args.tool === "string" ? args.tool : "";
6471
+ const decision = args.decision === "deny" ? "deny" : args.decision === "allow" ? "allow" : null;
6472
+ if (!tool) return { error: "missing_tool" };
6473
+ if (!decision) return { error: 'decision must be "allow" or "deny"' };
6474
+ const a = await loadArtifacts();
6475
+ let privateKey = typeof args.private_key_hex === "string" ? args.private_key_hex : "";
6476
+ let publicKey;
6477
+ let ephemeral = false;
6478
+ if (privateKey) {
6479
+ publicKey = a.getPublicKey(privateKey);
6480
+ } else {
6481
+ const kp = a.generateKeypair();
6482
+ privateKey = kp.privateKey;
6483
+ publicKey = kp.publicKey;
6484
+ ephemeral = true;
6485
+ }
6486
+ const requestId = newRequestId();
6487
+ const payload = buildReceiptPayload({
6488
+ tool,
6489
+ decision,
6490
+ reason_code: typeof args.reason_code === "string" ? args.reason_code : void 0,
6491
+ policy_digest: typeof args.policy_digest === "string" ? args.policy_digest : void 0,
6492
+ request_id: requestId,
6493
+ public_key: publicKey
6494
+ });
6495
+ const artifactType = decision === "deny" ? "gateway_restraint" : "decision_receipt";
6496
+ const { artifact } = a.createSignedArtifact(artifactType, payload, privateKey, { kid: "mcp", issuer: "protect-mcp" });
6497
+ return { receipt: artifact, artifact_type: artifactType, public_key: publicKey, ephemeral };
6498
+ }
6499
+ async function callVerify(args) {
6500
+ const receipt = args.receipt;
6501
+ if (!receipt || typeof receipt !== "object") return { valid: false, error: "missing_receipt" };
6502
+ const a = await loadArtifacts();
6503
+ const embedded = receipt.payload?.public_key;
6504
+ const key = typeof args.public_key_hex === "string" && args.public_key_hex ? args.public_key_hex : typeof embedded === "string" ? embedded : null;
6505
+ if (!key) {
6506
+ return { valid: false, error: "no_public_key", type: receipt.type ?? "unknown", kid: null, issuer: null };
6507
+ }
6508
+ const result = a.verifyArtifact(receipt, key);
6509
+ return {
6510
+ valid: !!result.valid,
6511
+ error: result.valid ? null : result.error || "invalid_signature",
6512
+ type: receipt.type ?? "unknown",
6513
+ kid: receipt.kid ?? null,
6514
+ issuer: receipt.issuer ?? null
6515
+ };
6516
+ }
6517
+ async function callSelfTest() {
6518
+ const details = {};
6519
+ const denied = await callEvaluate({ tool: "Bash", input: { command: "rm -rf /" }, policy: SELF_TEST_POLICY });
6520
+ const gateDeniesForbidden = denied.allowed === false;
6521
+ details.forbidden_action = { tool: "Bash", command: "rm -rf /", allowed: denied.allowed, reason: denied.reason };
6522
+ const allowed = await callEvaluate({ tool: "Read", input: { path: "./notes.txt" }, policy: SELF_TEST_POLICY });
6523
+ details.safe_action = { tool: "Read", allowed: allowed.allowed };
6524
+ let signVerifyRoundtrip = false;
6525
+ try {
6526
+ const signed = await callSign({ tool: "Bash", decision: "deny", reason_code: "self_test" });
6527
+ if (signed.receipt && signed.public_key) {
6528
+ const ok = await callVerify({ receipt: signed.receipt, public_key_hex: signed.public_key });
6529
+ const tampered = JSON.parse(JSON.stringify(signed.receipt));
6530
+ if (tampered.payload) tampered.payload.tool = "tampered";
6531
+ const bad = await callVerify({ receipt: tampered, public_key_hex: signed.public_key });
6532
+ signVerifyRoundtrip = ok.valid === true && bad.valid === false;
6533
+ details.sign_verify = { valid_verifies: ok.valid, tampered_rejected: bad.valid === false };
6534
+ }
6535
+ } catch (err) {
6536
+ details.sign_verify_error = err instanceof Error ? err.message : String(err);
6537
+ }
6538
+ return {
6539
+ ok: gateDeniesForbidden && allowed.allowed === true && signVerifyRoundtrip,
6540
+ gate_denies_forbidden: gateDeniesForbidden,
6541
+ sign_verify_roundtrip: signVerifyRoundtrip,
6542
+ details,
6543
+ note: "No network was contacted."
6544
+ };
6545
+ }
6546
+ function textResult(id, value) {
6547
+ return JSON.stringify({ jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] } });
6548
+ }
6549
+ async function handleRequest(request) {
6550
+ if (request.method === "initialize") {
6551
+ return JSON.stringify({
6552
+ jsonrpc: "2.0",
6553
+ id: request.id,
6554
+ result: {
6555
+ protocolVersion: "2024-11-05",
6556
+ serverInfo: { name: "protect-mcp", version: process.env.PROTECT_MCP_VERSION || "0.9.7" },
6557
+ capabilities: { tools: {} }
6558
+ }
6559
+ });
6560
+ }
6561
+ if (request.method === "notifications/initialized") return "";
6562
+ if (request.method === "tools/list") {
6563
+ return JSON.stringify({ jsonrpc: "2.0", id: request.id, result: { tools: TOOLS } });
6564
+ }
6565
+ if (request.method === "tools/call") {
6566
+ const name = request.params?.name || "";
6567
+ const args = request.params?.arguments || {};
6568
+ try {
6569
+ let value;
6570
+ switch (name) {
6571
+ case "evaluate_action":
6572
+ value = await callEvaluate(args);
6573
+ break;
6574
+ case "sign_decision":
6575
+ value = await callSign(args);
6576
+ break;
6577
+ case "verify_receipt":
6578
+ value = await callVerify(args);
6579
+ break;
6580
+ case "self_test":
6581
+ value = await callSelfTest();
6582
+ break;
6583
+ default:
6584
+ value = { error: `unknown tool: ${name}` };
6585
+ }
6586
+ return textResult(request.id, value);
6587
+ } catch (err) {
6588
+ return textResult(request.id, { error: err instanceof Error ? err.message : String(err) });
6589
+ }
6590
+ }
6591
+ if (request.id !== void 0) {
6592
+ return JSON.stringify({ jsonrpc: "2.0", id: request.id, error: { code: -32601, message: `Method not found: ${request.method}` } });
6593
+ }
6594
+ return "";
6595
+ }
6596
+ async function runMcpServer() {
6597
+ const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, crlfDelay: Infinity });
6598
+ let chain = Promise.resolve();
6599
+ rl.on("line", (line) => {
6600
+ const trimmed = line.trim();
6601
+ if (!trimmed) return;
6602
+ chain = chain.then(async () => {
6603
+ try {
6604
+ const request = JSON.parse(trimmed);
6605
+ const response = await handleRequest(request);
6606
+ if (response) process.stdout.write(response + "\n");
6607
+ } catch {
6608
+ }
6609
+ });
6610
+ });
6611
+ process.stderr.write("[PROTECT_MCP] gate MCP server started \u2014 4 tools: evaluate_action, sign_decision, verify_receipt, self_test\n");
6612
+ await new Promise((resolve) => rl.on("close", () => resolve()));
6613
+ }
6614
+ var import_node_readline2, artifacts, RO, TOOLS, SELF_TEST_POLICY;
6615
+ var init_mcp_server = __esm({
6616
+ "src/mcp-server.ts"() {
6617
+ "use strict";
6618
+ import_node_readline2 = require("readline");
6619
+ init_cedar_evaluator();
6620
+ artifacts = null;
6621
+ RO = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
6622
+ TOOLS = [
6623
+ {
6624
+ name: "evaluate_action",
6625
+ description: `Decide whether a proposed agent tool call is allowed by a Cedar policy, fail-closed. Evaluates the call against the policy the same way the protect-mcp gate does at runtime, and on any policy error the decision is DENY (never a silent allow). Inputs: tool (the tool name, e.g. "Bash" or "send_email"), input (the tool's arguments object; the policy sees it at context.input.*), and policy (inline Cedar source text). Returns JSON { allowed: boolean, decision: "allow" | "deny", reason: string, policy_digest: string (sha256 prefix of the policy) }. A missing or unparseable policy denies. Use this before an agent acts; pair with sign_decision to make the decision auditable.`,
6626
+ inputSchema: {
6627
+ type: "object",
6628
+ properties: {
6629
+ tool: { type: "string", description: 'The tool name being evaluated, e.g. "Bash", "Write", "send_email".' },
6630
+ input: { type: "object", description: "The tool's arguments. Reachable in the policy at context.input.* (e.g. context.input.command)." },
6631
+ policy: { type: "string", description: 'Inline Cedar policy source. Use MCP::Tool::call as the action and Tool::"<name>" as the resource.' }
6632
+ },
6633
+ required: ["tool", "policy"]
6634
+ },
6635
+ annotations: { title: "Evaluate an action against a Cedar policy", idempotentHint: true, ...RO }
6636
+ },
6637
+ {
6638
+ name: "sign_decision",
6639
+ description: `Turn a gate decision into an Ed25519 signed receipt (Veritas Acta format, JCS-canonical). A denial signs a gateway_restraint artifact; an allow signs a decision_receipt. This is what makes 'what the agent was blocked from doing' provable after the fact. Inputs: tool (required), decision ("allow" | "deny", required), reason_code (optional), policy_digest (optional), and private_key_hex (optional 64-hex Ed25519 secret; if omitted, an ephemeral keypair is generated and its public key is returned so the receipt still verifies). Returns JSON { receipt: object (the signed artifact), artifact_type: "decision_receipt" | "gateway_restraint", public_key: string (hex, pass this to verify_receipt or pin it), ephemeral: boolean }. Writes nothing to disk and contacts no network.`,
6640
+ inputSchema: {
6641
+ type: "object",
6642
+ properties: {
6643
+ tool: { type: "string", description: "The tool the decision is about." },
6644
+ decision: { type: "string", enum: ["allow", "deny"], description: "The gate decision to receipt." },
6645
+ reason_code: { type: "string", description: 'Optional machine reason, e.g. "restricted_list" or "post_execution_receipt".' },
6646
+ policy_digest: { type: "string", description: "Optional digest of the policy that produced the decision (e.g. from evaluate_action)." },
6647
+ private_key_hex: { type: "string", description: "Optional 64-character hex Ed25519 secret key. If omitted, an ephemeral key is generated and its public key is returned." }
6648
+ },
6649
+ required: ["tool", "decision"]
6650
+ },
6651
+ // Not idempotent: an ephemeral key and a fresh request_id are minted per call.
6652
+ annotations: { title: "Sign a decision into a receipt", idempotentHint: false, ...RO }
6653
+ },
6654
+ {
6655
+ name: "verify_receipt",
6656
+ description: 'Verify a signed receipt offline against a public key. No network, no accounts: the Ed25519 signature is checked over the canonical bytes. Inputs: receipt (the signed artifact object, required) and public_key_hex (optional; falls back to a public_key embedded in the receipt payload). Returns JSON { valid: boolean, error: string | null (e.g. "invalid_signature", "no_public_key"), type: string, kid: string | null, issuer: string | null }. For authenticity you should verify against a key you pinned out of band, not only the one carried inside the receipt.',
6657
+ inputSchema: {
6658
+ type: "object",
6659
+ properties: {
6660
+ receipt: { type: "object", description: "The signed receipt/artifact to verify." },
6661
+ public_key_hex: { type: "string", description: "Optional Ed25519 public key hex to verify against. Defaults to a key embedded in the receipt payload." }
6662
+ },
6663
+ required: ["receipt"]
6664
+ },
6665
+ annotations: { title: "Verify a receipt offline", idempotentHint: true, ...RO }
6666
+ },
6667
+ {
6668
+ name: "self_test",
6669
+ description: "Prove the gate works, end to end, with no inputs. Runs a known-forbidden action (rm -rf) against a sample deny policy and asserts it is DENIED, then signs a decision and verifies the receipt round-trips. Returns JSON { ok: boolean, gate_denies_forbidden: boolean, sign_verify_roundtrip: boolean, details: object }. This is the 'a gate that cannot prove it denies does not start' check, exposed as a callable tool. Contacts no network.",
6670
+ inputSchema: { type: "object", properties: {} },
6671
+ annotations: { title: "Prove the gate denies and receipts verify", idempotentHint: true, ...RO }
6672
+ }
6673
+ ];
6674
+ SELF_TEST_POLICY = `
6675
+ permit(principal, action == Action::"MCP::Tool::call", resource);
6676
+ forbid(principal, action == Action::"MCP::Tool::call", resource == Tool::"Bash")
6677
+ when {
6678
+ context has input && context.input has command &&
6679
+ (context.input.command like "*rm -rf*" || context.input.command like "*mkfs*")
6680
+ };
6681
+ `;
6682
+ if (process.argv[1] && /mcp-server\.(js|mjs|cjs|ts)$/.test(process.argv[1])) {
6683
+ runMcpServer();
6684
+ }
6685
+ }
6686
+ });
6687
+
6417
6688
  // src/scopeblind-bridge.ts
6418
6689
  function getScopeBlindBridge() {
6419
6690
  if (!singleton) singleton = new ScopeBlindBridge();
@@ -6650,6 +6921,7 @@ async function handlePreToolUse(input, state) {
6650
6921
  ...input.teamName && { team_name: input.teamName },
6651
6922
  ...input.agentType && { agent_type: input.agentType }
6652
6923
  };
6924
+ maybeReloadCedar(state);
6653
6925
  if (state.cedarPolicies) {
6654
6926
  try {
6655
6927
  const cedarDecision = await evaluateCedar(state.cedarPolicies, {
@@ -6687,10 +6959,13 @@ async function handlePreToolUse(input, state) {
6687
6959
  sandbox_state: detectSandboxState(),
6688
6960
  plan_receipt_id: state.activePlanReceiptId || void 0
6689
6961
  });
6962
+ const isDefaultDeny = !reason || reason === "cedar_deny" || /reason":\[\]/.test(reason);
6963
+ const policyRef = state.cedarDir ? ` Policy: ${state.cedarDir}.` : "";
6964
+ 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
6965
  if (denyCount === 1) {
6691
6966
  process.stderr.write(
6692
- `[PROTECT_MCP] No Cedar permit for "${toolName}" \u2014 suggest:
6693
- ${suggestion}
6967
+ `[PROTECT_MCP] Denied "${toolName}" (${isDefaultDeny ? "default-deny" : "forbid"}).
6968
+ Allow with: npx protect-mcp policy allow ${toolName}
6694
6969
  `
6695
6970
  );
6696
6971
  }
@@ -6698,7 +6973,7 @@ async function handlePreToolUse(input, state) {
6698
6973
  hookSpecificOutput: {
6699
6974
  hookEventName: "PreToolUse",
6700
6975
  permissionDecision: "deny",
6701
- permissionDecisionReason: `[ScopeBlind] Denied by Cedar policy. ${reason}. Forbidden: "${toolName}" is not permitted. Try a read-only alternative.` + (denyCount > 1 ? ` (attempt ${denyCount})` : "")
6976
+ permissionDecisionReason: `[ScopeBlind] Denied "${toolName}".${howTo}` + (denyCount > 1 ? ` (attempt ${denyCount})` : "")
6702
6977
  }
6703
6978
  };
6704
6979
  }
@@ -7197,6 +7472,9 @@ async function startHookServer(options = {}) {
7197
7472
  }
7198
7473
  const state = {
7199
7474
  cedarPolicies,
7475
+ cedarDir: cedarDir || null,
7476
+ cedarMtimeMs: cedarDir ? newestCedarMtime(cedarDir) : 0,
7477
+ cedarCheckedMs: 0,
7200
7478
  jsonPolicy,
7201
7479
  rateLimitStore: /* @__PURE__ */ new Map(),
7202
7480
  receiptBuffer: new ReceiptBuffer(),
@@ -7387,6 +7665,39 @@ async function startHookServer(options = {}) {
7387
7665
  process.on("SIGTERM", shutdown);
7388
7666
  return server;
7389
7667
  }
7668
+ function newestCedarMtime(dir) {
7669
+ try {
7670
+ let newest = 0;
7671
+ for (const f of (0, import_node_fs11.readdirSync)(dir)) {
7672
+ if (!f.endsWith(".cedar")) continue;
7673
+ const m = (0, import_node_fs11.statSync)((0, import_node_path8.join)(dir, f)).mtimeMs;
7674
+ if (m > newest) newest = m;
7675
+ }
7676
+ return newest;
7677
+ } catch {
7678
+ return 0;
7679
+ }
7680
+ }
7681
+ function maybeReloadCedar(state) {
7682
+ if (!state.cedarDir) return;
7683
+ const nowMs = Date.now();
7684
+ if (nowMs - state.cedarCheckedMs < CEDAR_CHECK_THROTTLE_MS) return;
7685
+ state.cedarCheckedMs = nowMs;
7686
+ const m = newestCedarMtime(state.cedarDir);
7687
+ if (m <= state.cedarMtimeMs) return;
7688
+ try {
7689
+ const reloaded = loadCedarPolicies(state.cedarDir);
7690
+ state.cedarPolicies = reloaded;
7691
+ state.policyDigest = reloaded.digest;
7692
+ state.cedarMtimeMs = m;
7693
+ process.stderr.write(`[PROTECT_MCP] Cedar policy reloaded (digest: ${reloaded.digest}) after on-disk change.
7694
+ `);
7695
+ } catch (err) {
7696
+ process.stderr.write(`[PROTECT_MCP] Cedar reload failed, keeping the previous policy: ${err instanceof Error ? err.message : err}
7697
+ `);
7698
+ state.cedarMtimeMs = m;
7699
+ }
7700
+ }
7390
7701
  function findCedarDir() {
7391
7702
  for (const candidate of ["cedar", "policies", "."]) {
7392
7703
  try {
@@ -7412,7 +7723,7 @@ function normalizeHookInput(raw) {
7412
7723
  }
7413
7724
  return result;
7414
7725
  }
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;
7726
+ 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
7727
  var init_hook_server = __esm({
7417
7728
  "src/hook-server.ts"() {
7418
7729
  "use strict";
@@ -7431,6 +7742,7 @@ var init_hook_server = __esm({
7431
7742
  LOG_FILE3 = ".protect-mcp-log.jsonl";
7432
7743
  RECEIPTS_FILE2 = ".protect-mcp-receipts.jsonl";
7433
7744
  PAYLOAD_HASH_THRESHOLD = 1024;
7745
+ CEDAR_CHECK_THROTTLE_MS = 2e3;
7434
7746
  SNAKE_TO_CAMEL_MAP = {
7435
7747
  hook_event_name: "hookEventName",
7436
7748
  session_id: "sessionId",
@@ -8857,6 +9169,7 @@ protect-mcp: Enterprise security gateway for MCP servers & Claude Code hooks
8857
9169
  Usage:
8858
9170
  protect-mcp [options] -- <command> [args...]
8859
9171
  protect-mcp serve [--port <port>] [--enforce] [--policy <path>] [--cedar <dir>]
9172
+ protect-mcp mcp # the gate as an MCP server (evaluate/sign/verify/self_test tools)
8860
9173
  protect-mcp init-hooks [--dir <path>] [--port <port>]
8861
9174
  protect-mcp quickstart [--connect]
8862
9175
  protect-mcp wrap [--write] [--claude-desktop] [-- <command>]
@@ -8871,6 +9184,7 @@ Usage:
8871
9184
  protect-mcp connect
8872
9185
  protect-mcp init [--dir <path>]
8873
9186
  protect-mcp sample [--dir <path>] [--force]
9187
+ protect-mcp policy list|show|allow <tool>|deny <tool>|path
8874
9188
  protect-mcp demo
8875
9189
  protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]
8876
9190
  protect-mcp status [--dir <path>]
@@ -11872,7 +12186,7 @@ async function handleKillerDemo(argv) {
11872
12186
  const { ed25519: ed255192 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
11873
12187
  const { bytesToHex: bytesToHex2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
11874
12188
  const { randomBytes: randomBytes4 } = await import("crypto");
11875
- const artifacts = await import("@veritasacta/artifacts");
12189
+ const artifacts2 = await import("@veritasacta/artifacts");
11876
12190
  const {
11877
12191
  createSelectiveDisclosurePackage: createSelectiveDisclosurePackage2,
11878
12192
  signCommittedDecision: signCommittedDecision2,
@@ -11999,8 +12313,8 @@ async function handleKillerDemo(argv) {
11999
12313
  } else {
12000
12314
  tamperedArtifact.tool = "send_email";
12001
12315
  }
12002
- const validOriginal = artifacts.verifyArtifact(receiptArtifact, keypair.publicKey);
12003
- const validTampered = artifacts.verifyArtifact(tamperedArtifact, keypair.publicKey);
12316
+ const validOriginal = artifacts2.verifyArtifact(receiptArtifact, keypair.publicKey);
12317
+ const validTampered = artifacts2.verifyArtifact(tamperedArtifact, keypair.publicKey);
12004
12318
  (0, import_node_fs13.writeFileSync)((0, import_node_path9.join)(dir, "receipts", "tampered.receipt.json"), JSON.stringify(tamperedArtifact, null, 2) + "\n");
12005
12319
  const committed = signCommittedDecision2(
12006
12320
  executedEntry,
@@ -12977,6 +13291,151 @@ ${bold("\u{1F6E1} Sample record seeded")} \xB7 8 decisions (1 blocked, 2 payment
12977
13291
  `);
12978
13292
  process.exit(0);
12979
13293
  }
13294
+ async function handlePolicy(argv) {
13295
+ const { readFileSync: rf, writeFileSync: wf, existsSync: ex, readdirSync: rd, appendFileSync: af } = await import("fs");
13296
+ const { join: pj } = await import("path");
13297
+ const { createHash: createHash6 } = await import("crypto");
13298
+ const sub = argv[0] || "list";
13299
+ const findDir = () => {
13300
+ for (const c of ["cedar", "policies", "."]) {
13301
+ try {
13302
+ if (ex(c) && rd(c).some((f) => f.endsWith(".cedar"))) return c;
13303
+ } catch {
13304
+ }
13305
+ }
13306
+ return null;
13307
+ };
13308
+ const dir = findDir();
13309
+ const cedarFiles = dir ? rd(dir).filter((f) => f.endsWith(".cedar")).sort() : [];
13310
+ const readAll = () => cedarFiles.map((f) => rf(pj(dir, f), "utf-8")).join("\n\n");
13311
+ const digestOf = (src) => createHash6("sha256").update(src).digest("hex").slice(0, 16);
13312
+ const stripComments = (src) => src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
13313
+ const targetFile = cedarFiles.includes("agent.cedar") ? "agent.cedar" : cedarFiles[0];
13314
+ if (sub === "path") {
13315
+ if (!dir) {
13316
+ process.stdout.write("No Cedar policy directory found (looked in ./cedar, ./policies, .).\n");
13317
+ process.exit(0);
13318
+ }
13319
+ process.stdout.write(`${pj(dir, targetFile)}
13320
+ `);
13321
+ process.exit(0);
13322
+ }
13323
+ if (sub === "show") {
13324
+ if (!dir) {
13325
+ process.stderr.write("No Cedar policy found. Run: npx protect-mcp init-hooks\n");
13326
+ process.exit(1);
13327
+ }
13328
+ const src = readAll();
13329
+ process.stdout.write(`${bold("Cedar policy")} ${dim(`(${cedarFiles.length} file${cedarFiles.length === 1 ? "" : "s"} in ${dir}, digest ${digestOf(src)})`)}
13330
+
13331
+ `);
13332
+ process.stdout.write(src.endsWith("\n") ? src : src + "\n");
13333
+ process.exit(0);
13334
+ }
13335
+ if (sub === "allow" || sub === "deny") {
13336
+ const tool = argv[1];
13337
+ if (!tool) {
13338
+ process.stderr.write(`Usage: npx protect-mcp policy ${sub} <ToolName>
13339
+ `);
13340
+ process.exit(1);
13341
+ }
13342
+ if (!/^[A-Za-z0-9_.:-]+$/.test(tool)) {
13343
+ process.stderr.write(`Refusing: "${tool}" is not a valid tool name.
13344
+ `);
13345
+ process.exit(1);
13346
+ }
13347
+ if (!dir) {
13348
+ process.stderr.write("No Cedar policy found. Run: npx protect-mcp init-hooks\n");
13349
+ process.exit(1);
13350
+ }
13351
+ const effect = sub === "allow" ? "permit" : "forbid";
13352
+ const before = readAll();
13353
+ const ruleRe = new RegExp(`${effect}\\s*\\([^)]*resource\\s*==\\s*Tool::"${tool.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}"`, "s");
13354
+ if (ruleRe.test(stripComments(before))) {
13355
+ process.stdout.write(`${dim("No change:")} a ${effect} rule for ${bold(tool)} already exists in ${targetFile}.
13356
+ `);
13357
+ process.exit(0);
13358
+ }
13359
+ const block = `
13360
+ // ${effect === "permit" ? "Allow" : "Block"} ${tool} (added by \`protect-mcp policy ${sub}\`)
13361
+ ${effect}(
13362
+ principal,
13363
+ action == Action::"MCP::Tool::call",
13364
+ resource == Tool::"${tool}"
13365
+ );
13366
+ `;
13367
+ af(pj(dir, targetFile), block);
13368
+ const after = readAll();
13369
+ process.stdout.write(`${bold(sub === "allow" ? "\u2713 Allowed" : "\u2713 Denied")} ${tool}
13370
+ `);
13371
+ process.stdout.write(` ${dim(`appended to ${pj(dir, targetFile)}`)}
13372
+ `);
13373
+ process.stdout.write(` ${dim(`policy digest ${digestOf(before)} \u2192 ${digestOf(after)}`)}
13374
+ `);
13375
+ process.stdout.write(`
13376
+ ${dim("A running gate (protect-mcp serve) hot-reloads on this change; no restart needed. The one-shot hook path re-reads per call.")}
13377
+ `);
13378
+ process.exit(0);
13379
+ }
13380
+ if (sub === "list") {
13381
+ if (!dir) {
13382
+ process.stderr.write("No Cedar policy found. Run: npx protect-mcp init-hooks\n");
13383
+ process.exit(1);
13384
+ }
13385
+ const src = readAll();
13386
+ const active = stripComments(src);
13387
+ const named = (effect) => {
13388
+ const set = /* @__PURE__ */ new Set();
13389
+ const re = new RegExp(`${effect}\\s*\\([\\s\\S]*?resource\\s*==\\s*Tool::"([^"]+)"[\\s\\S]*?\\);`, "g");
13390
+ let m;
13391
+ while (m = re.exec(active)) set.add(m[1]);
13392
+ return set;
13393
+ };
13394
+ const permitted = named("permit"), forbidden = named("forbid");
13395
+ const seen = /* @__PURE__ */ new Map();
13396
+ try {
13397
+ const logPath = pj(process.cwd(), ".protect-mcp-log.jsonl");
13398
+ if (ex(logPath)) {
13399
+ for (const line of rf(logPath, "utf-8").split("\n")) {
13400
+ if (!line.trim()) continue;
13401
+ try {
13402
+ const e = JSON.parse(line);
13403
+ if (!e.tool) continue;
13404
+ const rec = seen.get(e.tool) || { allow: 0, deny: 0 };
13405
+ if (e.decision === "deny") rec.deny++;
13406
+ else rec.allow++;
13407
+ seen.set(e.tool, rec);
13408
+ } catch {
13409
+ }
13410
+ }
13411
+ }
13412
+ } catch {
13413
+ }
13414
+ process.stdout.write(`${bold("Cedar policy")} ${dim(`(${dir}, digest ${digestOf(src)}) \xB7 default-deny, fail-closed`)}
13415
+
13416
+ `);
13417
+ const allTools = /* @__PURE__ */ new Set([...permitted, ...forbidden, ...seen.keys()]);
13418
+ if (allTools.size === 0) {
13419
+ process.stdout.write(dim(" No rules and no decisions logged yet.\n"));
13420
+ process.exit(0);
13421
+ }
13422
+ const rows = [...allTools].sort().map((t) => {
13423
+ const label = forbidden.has(t) ? "forbid" : permitted.has(t) ? "permit" : "default-deny";
13424
+ const colored = forbidden.has(t) ? red(label) : permitted.has(t) ? green(label) : yellow(label);
13425
+ const pad = " ".repeat(Math.max(1, 13 - label.length));
13426
+ const s = seen.get(t);
13427
+ const hits = s ? dim(` ${s.allow} allowed, ${s.deny} denied`) : "";
13428
+ return ` ${colored}${pad}${bold(t)}${hits}`;
13429
+ });
13430
+ process.stdout.write(rows.join("\n") + "\n");
13431
+ process.stdout.write(`
13432
+ ${dim("Allow a tool: npx protect-mcp policy allow <ToolName> \xB7 Block one: policy deny <ToolName>")}
13433
+ `);
13434
+ process.exit(0);
13435
+ }
13436
+ process.stderr.write("Usage: npx protect-mcp policy <list|show|allow <tool>|deny <tool>|path>\n");
13437
+ process.exit(1);
13438
+ }
12980
13439
  async function main() {
12981
13440
  sendInstallTelemetry().catch(() => {
12982
13441
  });
@@ -13000,6 +13459,10 @@ async function main() {
13000
13459
  await handleSign(args.slice(1));
13001
13460
  return;
13002
13461
  }
13462
+ if (args[0] === "mcp") {
13463
+ await (await Promise.resolve().then(() => (init_mcp_server(), mcp_server_exports))).runMcpServer();
13464
+ return;
13465
+ }
13003
13466
  if (args[0] === "serve") {
13004
13467
  const { startHookServer: startHookServer2 } = await Promise.resolve().then(() => (init_hook_server(), hook_server_exports));
13005
13468
  const portIdx = args.indexOf("--port");
@@ -13046,6 +13509,10 @@ async function main() {
13046
13509
  await handleSample(args.slice(1));
13047
13510
  return;
13048
13511
  }
13512
+ if (args[0] === "policy") {
13513
+ await handlePolicy(args.slice(1));
13514
+ return;
13515
+ }
13049
13516
  if (args[0] === "init-hooks") {
13050
13517
  await handleInitHooks(args.slice(1));
13051
13518
  process.exit(0);