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.
- package/CHANGELOG.md +69 -0
- package/README.md +157 -9
- package/dist/{bundle-XTR3YMPO.mjs → bundle-YUUHAG7O.mjs} +1 -1
- package/dist/chunk-36UID5WY.mjs +662 -0
- package/dist/{chunk-546U3A7R.mjs → chunk-D2RDY2JR.mjs} +92 -1
- package/dist/chunk-F2FKQ4XN.mjs +410 -0
- package/dist/{chunk-J6L4XCTE.mjs → chunk-KPSICBAJ.mjs} +75 -0
- package/dist/{chunk-X63ELMU4.mjs → chunk-NVJHGXXG.mjs} +37 -7
- package/dist/{chunk-5JXFV37Y.mjs → chunk-PM2ZO57M.mjs} +10 -2
- package/dist/{chunk-OHUTUFTC.mjs → chunk-UBZJ3VI2.mjs} +19 -15
- package/dist/cli.js +4034 -502
- package/dist/cli.mjs +2028 -9
- package/dist/demo-server.d.mts +3 -0
- package/dist/demo-server.d.ts +3 -0
- package/dist/demo-server.js +75 -0
- package/dist/demo-server.mjs +1 -1
- package/dist/hook-server.js +144 -25
- package/dist/hook-server.mjs +2 -2
- package/dist/{http-transport-R5AO7X6D.mjs → http-transport-4GMMRPW7.mjs} +2 -2
- package/dist/index.d.mts +115 -1
- package/dist/index.d.ts +115 -1
- package/dist/index.js +988 -80
- package/dist/index.mjs +48 -249
- package/dist/receipt-registry-6CAOY6RP.mjs +279 -0
- package/dist/signing-committed-MMLJ6OGG.mjs +15 -0
- package/package.json +1 -1
- package/dist/chunk-ZBKJANP7.mjs +0 -145
|
@@ -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-
|
|
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
|
-
|
|
301
|
-
|
|
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]
|
|
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-
|
|
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
|
};
|