protect-mcp 0.7.2 → 0.7.4

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.
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ReceiptBuffer,
3
+ buildActionReadback,
3
4
  checkRateLimit,
4
5
  evaluateCedar,
5
6
  getSignerInfo,
@@ -11,7 +12,7 @@ import {
11
12
  loadPolicy,
12
13
  parseRateLimit,
13
14
  signDecision
14
- } from "./chunk-546U3A7R.mjs";
15
+ } from "./chunk-D2RDY2JR.mjs";
15
16
 
16
17
  // src/hook-server.ts
17
18
  import { createServer } from "http";
@@ -241,6 +242,7 @@ async function handlePreToolUse(input, state) {
241
242
  requestId
242
243
  });
243
244
  const payloadDigest = computePayloadDigest(input.toolInput);
245
+ const actionReadback = buildActionReadback(toolName, input.toolInput || {});
244
246
  const swarm = {
245
247
  ...state.swarmContext,
246
248
  ...input.agentId && { agent_id: input.agentId },
@@ -258,7 +260,10 @@ async function handlePreToolUse(input, state) {
258
260
  context: {
259
261
  hook_event: "PreToolUse",
260
262
  ...input.toolInput || {}
261
- }
263
+ },
264
+ // Also expose the raw tool input under context.input so policies written
265
+ // against the documented nested shape match on the hook path too.
266
+ toolInput: input.toolInput || {}
262
267
  });
263
268
  if (!cedarDecision.allowed) {
264
269
  const reason = cedarDecision.reason || "cedar_deny";
@@ -277,6 +282,7 @@ async function handlePreToolUse(input, state) {
277
282
  swarm: swarm.team_name ? swarm : void 0,
278
283
  timing: { hook_latency_ms: hookLatency2, started_at: hookStart },
279
284
  payload_digest: payloadDigest,
285
+ action_readback: actionReadback,
280
286
  deny_iteration: denyCount,
281
287
  sandbox_state: detectSandboxState(),
282
288
  plan_receipt_id: state.activePlanReceiptId || void 0
@@ -297,10 +303,30 @@ async function handlePreToolUse(input, state) {
297
303
  };
298
304
  }
299
305
  } catch (err) {
300
- if (state.verbose) {
301
- process.stderr.write(`[PROTECT_MCP] Cedar eval error: ${err instanceof Error ? err.message : err}
302
- `);
303
- }
306
+ const hookLatency2 = Date.now() - hookStart;
307
+ process.stderr.write(
308
+ `[PROTECT_MCP] Cedar eval threw for "${toolName}", failing closed: ${err instanceof Error ? err.message : err}
309
+ `
310
+ );
311
+ emitDecisionLog(state, {
312
+ tool: toolName,
313
+ decision: "deny",
314
+ reason_code: "cedar_eval_error",
315
+ request_id: requestId,
316
+ hook_event: "PreToolUse",
317
+ swarm: swarm.team_name ? swarm : void 0,
318
+ timing: { hook_latency_ms: hookLatency2, started_at: hookStart },
319
+ payload_digest: payloadDigest,
320
+ action_readback: actionReadback,
321
+ sandbox_state: detectSandboxState()
322
+ });
323
+ return {
324
+ hookSpecificOutput: {
325
+ hookEventName: "PreToolUse",
326
+ permissionDecision: "deny",
327
+ 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.`
328
+ }
329
+ };
304
330
  }
305
331
  }
306
332
  if (state.jsonPolicy?.policy) {
@@ -316,6 +342,7 @@ async function handlePreToolUse(input, state) {
316
342
  swarm: swarm.team_name ? swarm : void 0,
317
343
  timing: { hook_latency_ms: hookLatency2, started_at: hookStart },
318
344
  payload_digest: payloadDigest,
345
+ action_readback: actionReadback,
319
346
  sandbox_state: detectSandboxState()
320
347
  });
321
348
  return {
@@ -336,13 +363,15 @@ async function handlePreToolUse(input, state) {
336
363
  hook_event: "PreToolUse",
337
364
  swarm: swarm.team_name ? swarm : void 0,
338
365
  timing: { hook_latency_ms: hookLatency2, started_at: hookStart },
366
+ payload_digest: payloadDigest,
367
+ action_readback: actionReadback,
339
368
  sandbox_state: detectSandboxState()
340
369
  });
341
370
  return {
342
371
  hookSpecificOutput: {
343
372
  hookEventName: "PreToolUse",
344
373
  permissionDecision: "ask",
345
- permissionDecisionReason: `[ScopeBlind] "${toolName}" requires human approval. Policy: ${state.policyDigest}`
374
+ permissionDecisionReason: `[ScopeBlind] Approval required for exactly this action: ${actionReadback.summary}. Payload hash: ${actionReadback.payload_hash.slice(0, 16)}\u2026 Policy: ${state.policyDigest}`
346
375
  }
347
376
  };
348
377
  }
@@ -387,6 +416,7 @@ async function handlePreToolUse(input, state) {
387
416
  swarm: swarm.team_name ? swarm : void 0,
388
417
  timing: { hook_latency_ms: hookLatency, started_at: hookStart },
389
418
  payload_digest: payloadDigest,
419
+ action_readback: actionReadback,
390
420
  sandbox_state: detectSandboxState(),
391
421
  plan_receipt_id: state.activePlanReceiptId || void 0
392
422
  });
@@ -1,7 +1,7 @@
1
1
  // src/bundle.ts
2
2
  function createAuditBundle(opts) {
3
3
  const receipts = opts.receipts.filter(
4
- (r) => r && typeof r === "object" && typeof r.signature === "string"
4
+ (r) => r && typeof r === "object" && (typeof r.signature === "string" || r.signature !== null && typeof r.signature === "object")
5
5
  );
6
6
  if (receipts.length === 0) {
7
7
  throw new Error("Audit bundle requires at least one signed receipt");
@@ -30,10 +30,18 @@ function createAuditBundle(opts) {
30
30
  time_range: timeRange,
31
31
  receipts,
32
32
  anchors: opts.anchors || [],
33
+ selective_disclosures: opts.selectiveDisclosures || [],
34
+ privacy: {
35
+ selective_disclosure: {
36
+ supported: true,
37
+ model: "salted_commitments_merkle_v0",
38
+ statement: "Committed receipts may disclose selected fields with salted Merkle openings. Undisclosed committed fields remain hidden while staying bound to the signed commitment root."
39
+ }
40
+ },
33
41
  verification: {
34
42
  algorithm: "ed25519",
35
43
  signing_keys: Array.from(keyMap.values()),
36
- instructions: `Verify each receipt by: (1) remove the "signature" field, (2) canonicalize the remaining object with JCS (sorted keys at every level), (3) encode as UTF-8 bytes, (4) verify the Ed25519 signature using the signing key matching the receipt's "kid" field. CLI: npx @veritasacta/verify bundle.json --bundle`
44
+ instructions: `Verify each receipt by: (1) remove the "signature" field, (2) canonicalize the remaining object with JCS (sorted keys at every level), (3) encode as UTF-8 bytes, (4) verify the Ed25519 signature using the signing key matching the receipt's "kid" field. For scopeblind.selective_disclosure.v0 packages, recompute each disclosed leaf and verify it against the receipt committed_fields_root; fields not disclosed remain hidden. CLI: npx @veritasacta/verify bundle.json --bundle`
37
45
  }
38
46
  };
39
47
  }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ReceiptBuffer,
3
+ buildActionReadback,
3
4
  checkRateLimit,
4
5
  evaluateCedar,
5
6
  getToolPolicy,
@@ -7,7 +8,7 @@ import {
7
8
  parseRateLimit,
8
9
  signDecision,
9
10
  startStatusServer
10
- } from "./chunk-546U3A7R.mjs";
11
+ } from "./chunk-D2RDY2JR.mjs";
11
12
 
12
13
  // src/evidence-store.ts
13
14
  import { readFileSync, writeFileSync, existsSync } from "fs";
@@ -758,6 +759,8 @@ var ProtectGateway = class {
758
759
  const toolName = request.params?.name || "unknown";
759
760
  const requestId = randomUUID().slice(0, 12);
760
761
  const mode = this.config.enforce ? "enforce" : "shadow";
762
+ const toolInput = request.params?.arguments && typeof request.params.arguments === "object" ? request.params.arguments : request.params || {};
763
+ const actionReadback = buildActionReadback(toolName, toolInput);
761
764
  let resolvedAgentKid = this.admissionResult?.agent_id;
762
765
  let effectiveToolPolicy;
763
766
  if (this.config.multiAgent?.enabled) {
@@ -767,7 +770,7 @@ var ProtectGateway = class {
767
770
  if (agentOverrides && agentOverrides[toolName]) {
768
771
  effectiveToolPolicy = { ...getToolPolicy(toolName, this.config.policy), ...agentOverrides[toolName] };
769
772
  } else if (!resolvedAgentKid && this.config.multiAgent.unknownAgentPolicy === "deny") {
770
- this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "unknown_agent_denied", request_id: requestId, tier: this.currentTier });
773
+ this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "unknown_agent_denied", request_id: requestId, tier: this.currentTier, action_readback: actionReadback });
771
774
  if (this.config.enforce) {
772
775
  return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied: unidentified agent`);
773
776
  }
@@ -788,7 +791,7 @@ var ProtectGateway = class {
788
791
  if (cred.resolved) {
789
792
  credentialRef = cred.label;
790
793
  } else if (cred.error && !cred.error.includes("not configured")) {
791
- this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "credential_error", request_id: requestId, tier: this.currentTier, credential_ref: toolName });
794
+ this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "credential_error", request_id: requestId, tier: this.currentTier, credential_ref: toolName, action_readback: actionReadback });
792
795
  if (this.config.enforce) {
793
796
  return this.makeErrorResponse(request.id, -32600, `Credential error for tool "${toolName}"`);
794
797
  }
@@ -803,13 +806,13 @@ var ProtectGateway = class {
803
806
  });
804
807
  if (!cedarDecision.allowed) {
805
808
  const reason = cedarDecision.reason || "cedar_deny";
806
- this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: reason, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
809
+ this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: reason, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
807
810
  if (this.config.enforce) {
808
811
  return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied by Cedar policy`);
809
812
  }
810
813
  return null;
811
814
  }
812
- this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "cedar_allow", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
815
+ this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "cedar_allow", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
813
816
  return null;
814
817
  } catch (err) {
815
818
  if (this.config.verbose) this.log(`Cedar evaluation error: ${err instanceof Error ? err.message : err}`);
@@ -827,7 +830,7 @@ var ProtectGateway = class {
827
830
  const externalDecision = await queryExternalPDP(ctx, this.config.policy.external);
828
831
  if (!externalDecision.allowed) {
829
832
  const reason = `external_pdp_deny${externalDecision.reason ? ": " + externalDecision.reason : ""}`;
830
- this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: reason, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
833
+ this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: reason, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
831
834
  if (this.config.enforce) {
832
835
  return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied by external policy engine`);
833
836
  }
@@ -839,7 +842,7 @@ var ProtectGateway = class {
839
842
  }
840
843
  if (toolPolicy.min_tier) {
841
844
  if (!meetsMinTier(this.currentTier, toolPolicy.min_tier)) {
842
- this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "tier_insufficient", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
845
+ this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "tier_insufficient", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
843
846
  if (this.config.enforce) {
844
847
  return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" requires tier "${toolPolicy.min_tier}"`);
845
848
  }
@@ -847,7 +850,7 @@ var ProtectGateway = class {
847
850
  }
848
851
  }
849
852
  if (toolPolicy.block) {
850
- this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "policy_block", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
853
+ this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "policy_block", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
851
854
  if (this.config.enforce) {
852
855
  return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" is blocked by policy`);
853
856
  }
@@ -858,10 +861,10 @@ var ProtectGateway = class {
858
861
  const alwaysGrant = this.approvalStore.get(`always:${toolName}`);
859
862
  if (grant && Date.now() < grant.expires_at || alwaysGrant && Date.now() < alwaysGrant.expires_at) {
860
863
  if (grant && grant.mode === "once") this.approvalStore.delete(requestId);
861
- this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "approval_granted", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
864
+ this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "approval_granted", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
862
865
  return null;
863
866
  }
864
- this.emitDecisionLog({ tool: toolName, decision: "require_approval", reason_code: "requires_human_approval", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
867
+ this.emitDecisionLog({ tool: toolName, decision: "require_approval", reason_code: "requires_human_approval", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
865
868
  if (this.notificationConfig) {
866
869
  sendApprovalNotification(this.notificationConfig, {
867
870
  requestId,
@@ -884,7 +887,7 @@ var ProtectGateway = class {
884
887
  content: [
885
888
  {
886
889
  type: "text",
887
- text: `REQUIRES_APPROVAL: The tool "${toolName}" requires human approval before execution. Request ID: ${requestId}. Approval nonce: ${this.approvalNonce}. Tell the user you need their approval to use "${toolName}" and will retry when granted. Do NOT retry this tool call until the user explicitly approves it.`
890
+ text: `REQUIRES_APPROVAL: The tool "${toolName}" requires human approval before execution. Exact action: ${actionReadback.summary}. Payload hash: ${actionReadback.payload_hash.slice(0, 16)}\u2026 Request ID: ${requestId}. Approval nonce: ${this.approvalNonce}. Tell the user you need their approval to use "${toolName}" and will retry when granted. Do NOT retry this tool call until the user explicitly approves it.`
888
891
  }
889
892
  ],
890
893
  isError: true
@@ -900,19 +903,19 @@ var ProtectGateway = class {
900
903
  const key = `tool:${toolName}:${this.currentTier}`;
901
904
  const { allowed, remaining } = checkRateLimit(key, limit, this.rateLimitStore);
902
905
  if (!allowed) {
903
- this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "rate_limit_exceeded", request_id: requestId, rate_limit_remaining: 0, tier: this.currentTier, credential_ref: credentialRef });
906
+ this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "rate_limit_exceeded", request_id: requestId, rate_limit_remaining: 0, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
904
907
  if (this.config.enforce) {
905
908
  return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" rate limit exceeded (${rateSpec})`);
906
909
  }
907
910
  return null;
908
911
  }
909
- this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "policy_allow", request_id: requestId, rate_limit_remaining: remaining, tier: this.currentTier, credential_ref: credentialRef });
912
+ this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "policy_allow", request_id: requestId, rate_limit_remaining: remaining, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
910
913
  } catch {
911
- this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "default_allow", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
914
+ this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "default_allow", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
912
915
  }
913
916
  } else {
914
917
  const reasonCode = this.config.enforce ? "policy_allow" : "observe_mode";
915
- this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: reasonCode, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
918
+ this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: reasonCode, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef, action_readback: actionReadback });
916
919
  }
917
920
  return null;
918
921
  }
@@ -949,6 +952,7 @@ var ProtectGateway = class {
949
952
  ...entry.rate_limit_remaining !== void 0 && { rate_limit_remaining: entry.rate_limit_remaining },
950
953
  ...entry.tier && { tier: entry.tier },
951
954
  ...entry.credential_ref && { credential_ref: entry.credential_ref },
955
+ ...entry.action_readback && { action_readback: entry.action_readback },
952
956
  otel_trace_id: otelTraceId,
953
957
  otel_span_id: otelSpanId
954
958
  };