protect-mcp 0.3.1 → 0.3.2

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.
@@ -317,7 +317,11 @@ async function initSigning(config) {
317
317
  return warnings;
318
318
  }
319
319
  try {
320
- artifactsModule = await import("@veritasacta/artifacts");
320
+ const moduleName = "@veritasacta/artifacts";
321
+ artifactsModule = await import(
322
+ /* @vite-ignore */
323
+ moduleName
324
+ );
321
325
  } catch {
322
326
  warnings.push("signing: @veritasacta/artifacts not available \u2014 receipts will be unsigned");
323
327
  return warnings;
@@ -909,8 +913,31 @@ var ProtectGateway = class {
909
913
  async interceptToolCall(request) {
910
914
  const toolName = request.params?.name || "unknown";
911
915
  const requestId = randomUUID().slice(0, 12);
912
- const toolPolicy = getToolPolicy(toolName, this.config.policy);
913
916
  const mode = this.config.enforce ? "enforce" : "shadow";
917
+ let resolvedAgentKid = this.admissionResult?.agent_id;
918
+ let effectiveToolPolicy;
919
+ if (this.config.multiAgent?.enabled) {
920
+ const paramKid = request.params?._passport_kid;
921
+ if (paramKid) resolvedAgentKid = paramKid;
922
+ const agentOverrides = resolvedAgentKid ? this.config.multiAgent.agentPolicies?.[resolvedAgentKid] : void 0;
923
+ if (agentOverrides && agentOverrides[toolName]) {
924
+ effectiveToolPolicy = { ...getToolPolicy(toolName, this.config.policy), ...agentOverrides[toolName] };
925
+ } else if (!resolvedAgentKid && this.config.multiAgent.unknownAgentPolicy === "deny") {
926
+ this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "unknown_agent_denied", request_id: requestId, tier: this.currentTier });
927
+ if (this.config.enforce) {
928
+ return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied: unidentified agent`);
929
+ }
930
+ return null;
931
+ } else {
932
+ effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
933
+ }
934
+ if (this.config.verbose && resolvedAgentKid) {
935
+ this.log(`Multi-agent: resolved kid=${resolvedAgentKid} for tool=${toolName}`);
936
+ }
937
+ } else {
938
+ effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
939
+ }
940
+ const toolPolicy = effectiveToolPolicy;
914
941
  let credentialRef;
915
942
  if (this.config.credentials) {
916
943
  const cred = resolveCredential(toolName, this.config.credentials);
@@ -1085,6 +1112,137 @@ var ProtectGateway = class {
1085
1112
  }
1086
1113
  };
1087
1114
 
1115
+ // src/simulate.ts
1116
+ import { readFileSync as readFileSync5 } from "fs";
1117
+ function parseLogFile(path) {
1118
+ const raw = readFileSync5(path, "utf-8");
1119
+ const entries = [];
1120
+ for (const line of raw.split("\n")) {
1121
+ const trimmed = line.trim();
1122
+ if (!trimmed) continue;
1123
+ const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
1124
+ try {
1125
+ const parsed = JSON.parse(jsonStr);
1126
+ if (parsed.tool && parsed.decision) {
1127
+ entries.push(parsed);
1128
+ }
1129
+ } catch {
1130
+ }
1131
+ }
1132
+ return entries;
1133
+ }
1134
+ function simulate(entries, policy, tier = "unknown") {
1135
+ const rateLimitStore = /* @__PURE__ */ new Map();
1136
+ const toolResults = /* @__PURE__ */ new Map();
1137
+ const totals = {
1138
+ allow: 0,
1139
+ block: 0,
1140
+ rate_limited: 0,
1141
+ require_approval: 0,
1142
+ tier_insufficient: 0
1143
+ };
1144
+ const originalTotals = { allow: 0, deny: 0 };
1145
+ const changes = [];
1146
+ for (const entry of entries) {
1147
+ const toolName = entry.tool;
1148
+ const toolPolicy = getToolPolicy(toolName, policy);
1149
+ if (entry.decision === "allow") {
1150
+ originalTotals.allow++;
1151
+ } else {
1152
+ originalTotals.deny++;
1153
+ }
1154
+ let newDecision;
1155
+ if (toolPolicy.block) {
1156
+ newDecision = "block";
1157
+ } else if (toolPolicy.min_tier && !meetsMinTier(tier, toolPolicy.min_tier)) {
1158
+ newDecision = "tier_insufficient";
1159
+ } else if (toolPolicy.require_approval) {
1160
+ newDecision = "require_approval";
1161
+ } else if (toolPolicy.rate_limit) {
1162
+ const limit = parseRateLimit(toolPolicy.rate_limit);
1163
+ const result = checkRateLimit(toolName, limit, rateLimitStore);
1164
+ newDecision = result.allowed ? "allow" : "rate_limited";
1165
+ } else {
1166
+ newDecision = "allow";
1167
+ }
1168
+ totals[newDecision]++;
1169
+ if (!toolResults.has(toolName)) {
1170
+ toolResults.set(toolName, {
1171
+ tool: toolName,
1172
+ calls: 0,
1173
+ results: { allow: 0, block: 0, rate_limited: 0, require_approval: 0, tier_insufficient: 0 },
1174
+ original: { allow: 0, deny: 0 }
1175
+ });
1176
+ }
1177
+ const tr = toolResults.get(toolName);
1178
+ tr.calls++;
1179
+ tr.results[newDecision]++;
1180
+ if (entry.decision === "allow") {
1181
+ tr.original.allow++;
1182
+ } else {
1183
+ tr.original.deny++;
1184
+ }
1185
+ }
1186
+ for (const [tool, result] of toolResults) {
1187
+ const wasAllBlocked = result.original.allow === 0;
1188
+ const nowAllBlocked = result.results.allow === 0;
1189
+ const wasAllAllowed = result.original.deny === 0;
1190
+ if (wasAllAllowed && result.results.block > 0) {
1191
+ changes.push(`${tool}: ${result.results.block} calls would be blocked (was: all allowed)`);
1192
+ }
1193
+ if (wasAllAllowed && result.results.rate_limited > 0) {
1194
+ changes.push(`${tool}: ${result.results.rate_limited} calls would be rate-limited (was: all allowed)`);
1195
+ }
1196
+ if (wasAllAllowed && result.results.require_approval > 0) {
1197
+ changes.push(`${tool}: ${result.results.require_approval} calls would require approval (was: all allowed)`);
1198
+ }
1199
+ if (wasAllAllowed && result.results.tier_insufficient > 0) {
1200
+ changes.push(`${tool}: ${result.results.tier_insufficient} calls would fail tier check (was: all allowed)`);
1201
+ }
1202
+ if (wasAllBlocked && result.results.allow > 0 && !nowAllBlocked) {
1203
+ changes.push(`${tool}: ${result.results.allow} calls would now be allowed (was: all blocked)`);
1204
+ }
1205
+ }
1206
+ return {
1207
+ policy_file: "",
1208
+ log_file: "",
1209
+ total_calls: entries.length,
1210
+ results: totals,
1211
+ original: originalTotals,
1212
+ tool_breakdown: Array.from(toolResults.values()).sort((a, b) => b.calls - a.calls),
1213
+ changes
1214
+ };
1215
+ }
1216
+ function formatSimulation(summary) {
1217
+ const lines = [];
1218
+ lines.push(`Simulating ${summary.policy_file} against ${summary.total_calls} recorded tool calls:
1219
+ `);
1220
+ const maxToolLen = Math.max(...summary.tool_breakdown.map((t) => t.tool.length), 4);
1221
+ for (const tr of summary.tool_breakdown) {
1222
+ const parts = [];
1223
+ if (tr.results.allow > 0) parts.push(`${tr.results.allow} allow`);
1224
+ if (tr.results.block > 0) parts.push(`\x1B[31m${tr.results.block} blocked\x1B[0m`);
1225
+ if (tr.results.rate_limited > 0) parts.push(`\x1B[33m${tr.results.rate_limited} rate_limited\x1B[0m`);
1226
+ if (tr.results.require_approval > 0) parts.push(`\x1B[36m${tr.results.require_approval} require_approval\x1B[0m`);
1227
+ if (tr.results.tier_insufficient > 0) parts.push(`\x1B[35m${tr.results.tier_insufficient} tier_insufficient\x1B[0m`);
1228
+ const originalParts = [];
1229
+ if (tr.original.allow > 0) originalParts.push(`${tr.original.allow} allow`);
1230
+ if (tr.original.deny > 0) originalParts.push(`${tr.original.deny} deny`);
1231
+ lines.push(` ${tr.tool.padEnd(maxToolLen)} \xD7 ${String(tr.calls).padStart(3)} \u2192 ${parts.join(", ")} (was: ${originalParts.join(", ")})`);
1232
+ }
1233
+ lines.push("");
1234
+ lines.push(`Summary: ${summary.results.allow} allow, ${summary.results.block} blocked, ${summary.results.rate_limited} rate_limited, ${summary.results.require_approval} require_approval, ${summary.results.tier_insufficient} tier_insufficient`);
1235
+ lines.push(` vs original: ${summary.original.allow} allow, ${summary.original.deny} deny`);
1236
+ if (summary.changes.length > 0) {
1237
+ lines.push("");
1238
+ lines.push("Changes:");
1239
+ for (const change of summary.changes) {
1240
+ lines.push(` \u2022 ${change}`);
1241
+ }
1242
+ }
1243
+ return lines.join("\n");
1244
+ }
1245
+
1088
1246
  export {
1089
1247
  loadPolicy,
1090
1248
  getToolPolicy,
@@ -1101,5 +1259,8 @@ export {
1101
1259
  isSigningEnabled,
1102
1260
  queryExternalPDP,
1103
1261
  buildDecisionContext,
1104
- ProtectGateway
1262
+ ProtectGateway,
1263
+ parseLogFile,
1264
+ simulate,
1265
+ formatSimulation
1105
1266
  };
@@ -0,0 +1,165 @@
1
+ // src/report.ts
2
+ import { readFileSync, existsSync } from "fs";
3
+ function generateReport(logPath, receiptPath, periodDays) {
4
+ const now = /* @__PURE__ */ new Date();
5
+ const from = new Date(now.getTime() - periodDays * 864e5);
6
+ const entries = [];
7
+ if (existsSync(logPath)) {
8
+ const raw = readFileSync(logPath, "utf-8");
9
+ for (const line of raw.split("\n")) {
10
+ const trimmed = line.trim();
11
+ if (!trimmed) continue;
12
+ const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
13
+ try {
14
+ const parsed = JSON.parse(jsonStr);
15
+ if (parsed.tool && parsed.decision && parsed.timestamp) {
16
+ const entryTime = typeof parsed.timestamp === "number" && parsed.timestamp > 1e12 ? parsed.timestamp : parsed.timestamp * 1e3;
17
+ if (entryTime >= from.getTime()) {
18
+ entries.push(parsed);
19
+ }
20
+ }
21
+ } catch {
22
+ }
23
+ }
24
+ }
25
+ let receiptsSigned = 0;
26
+ let signerKid = "";
27
+ let signerIssuer = "";
28
+ if (existsSync(receiptPath)) {
29
+ const raw = readFileSync(receiptPath, "utf-8");
30
+ for (const line of raw.split("\n")) {
31
+ const trimmed = line.trim();
32
+ if (!trimmed) continue;
33
+ try {
34
+ const parsed = JSON.parse(trimmed);
35
+ if (parsed.signature) {
36
+ receiptsSigned++;
37
+ if (parsed.kid && !signerKid) signerKid = parsed.kid;
38
+ if (parsed.issuer && !signerIssuer) signerIssuer = parsed.issuer;
39
+ }
40
+ } catch {
41
+ }
42
+ }
43
+ }
44
+ const toolMap = /* @__PURE__ */ new Map();
45
+ const tiers = /* @__PURE__ */ new Set();
46
+ const policyDigests = /* @__PURE__ */ new Map();
47
+ let allowed = 0;
48
+ let blocked = 0;
49
+ let rateLimited = 0;
50
+ let approvalRequired = 0;
51
+ for (const entry of entries) {
52
+ const tool = entry.tool;
53
+ if (!toolMap.has(tool)) {
54
+ toolMap.set(tool, { total: 0, allowed: 0, blocked: 0, rate_limited: 0, approval_required: 0 });
55
+ }
56
+ const tm = toolMap.get(tool);
57
+ tm.total++;
58
+ if (entry.decision === "allow") {
59
+ allowed++;
60
+ tm.allowed++;
61
+ } else if (entry.decision === "deny" && entry.reason_code === "rate_limit_exceeded") {
62
+ rateLimited++;
63
+ tm.rate_limited++;
64
+ } else if (entry.decision === "deny" && entry.reason_code === "require_approval") {
65
+ approvalRequired++;
66
+ tm.approval_required++;
67
+ } else {
68
+ blocked++;
69
+ tm.blocked++;
70
+ }
71
+ if (entry.tier) tiers.add(entry.tier);
72
+ if (entry.policy_digest && !policyDigests.has(entry.policy_digest)) {
73
+ policyDigests.set(entry.policy_digest, new Date(entry.timestamp).toISOString());
74
+ }
75
+ }
76
+ const policyChanges = Array.from(policyDigests.entries()).map(([digest, at]) => ({
77
+ at,
78
+ policy_digest: digest
79
+ })).sort((a, b) => a.at.localeCompare(b.at));
80
+ return {
81
+ generated_at: now.toISOString(),
82
+ period: { from: from.toISOString(), to: now.toISOString() },
83
+ signing_identity: signerKid ? { kid: signerKid, issuer: signerIssuer } : null,
84
+ summary: {
85
+ total_decisions: entries.length,
86
+ allowed,
87
+ blocked,
88
+ rate_limited: rateLimited,
89
+ approval_required: approvalRequired,
90
+ unique_tools: toolMap.size,
91
+ unique_tiers: tiers.size
92
+ },
93
+ tool_breakdown: Array.from(toolMap.entries()).map(([tool, stats]) => ({ tool, ...stats })).sort((a, b) => b.total - a.total),
94
+ policy_changes: policyChanges,
95
+ verification: {
96
+ receipts_signed: receiptsSigned,
97
+ receipts_unsigned: entries.length - receiptsSigned,
98
+ verify_command: "npx @veritasacta/verify audit-bundle.json --bundle"
99
+ }
100
+ };
101
+ }
102
+ function formatReportMarkdown(report) {
103
+ const lines = [];
104
+ lines.push("# ScopeBlind Compliance Report");
105
+ lines.push("");
106
+ lines.push(`**Generated:** ${report.generated_at}`);
107
+ lines.push(`**Period:** ${report.period.from.split("T")[0]} to ${report.period.to.split("T")[0]}`);
108
+ if (report.signing_identity) {
109
+ lines.push(`**Signing identity:** kid \`${report.signing_identity.kid}\`, issuer \`${report.signing_identity.issuer}\``);
110
+ }
111
+ lines.push("");
112
+ lines.push("## Summary");
113
+ lines.push("");
114
+ lines.push(`| Metric | Value |`);
115
+ lines.push(`|--------|-------|`);
116
+ lines.push(`| Total decisions | ${report.summary.total_decisions} |`);
117
+ lines.push(`| Allowed | ${report.summary.allowed} |`);
118
+ lines.push(`| Blocked | ${report.summary.blocked} |`);
119
+ lines.push(`| Rate-limited | ${report.summary.rate_limited} |`);
120
+ lines.push(`| Approval required | ${report.summary.approval_required} |`);
121
+ lines.push(`| Unique tools | ${report.summary.unique_tools} |`);
122
+ lines.push(`| Unique tiers | ${report.summary.unique_tiers} |`);
123
+ lines.push("");
124
+ if (report.tool_breakdown.length > 0) {
125
+ lines.push("## Tool Breakdown");
126
+ lines.push("");
127
+ lines.push("| Tool | Total | Allowed | Blocked | Rate-limited | Approval |");
128
+ lines.push("|------|-------|---------|---------|--------------|----------|");
129
+ for (const t of report.tool_breakdown) {
130
+ lines.push(`| \`${t.tool}\` | ${t.total} | ${t.allowed} | ${t.blocked} | ${t.rate_limited} | ${t.approval_required} |`);
131
+ }
132
+ lines.push("");
133
+ }
134
+ if (report.policy_changes.length > 0) {
135
+ lines.push("## Policy History");
136
+ lines.push("");
137
+ lines.push("| Timestamp | Policy Digest |");
138
+ lines.push("|-----------|--------------|");
139
+ for (const pc of report.policy_changes) {
140
+ lines.push(`| ${pc.at} | \`${pc.policy_digest}\` |`);
141
+ }
142
+ lines.push("");
143
+ }
144
+ lines.push("## Verification");
145
+ lines.push("");
146
+ lines.push(`- Receipts signed: **${report.verification.receipts_signed}**`);
147
+ lines.push(`- Receipts unsigned: **${report.verification.receipts_unsigned}**`);
148
+ lines.push("");
149
+ lines.push("Verify the audit bundle:");
150
+ lines.push("");
151
+ lines.push("```bash");
152
+ lines.push(report.verification.verify_command);
153
+ lines.push("```");
154
+ lines.push("");
155
+ lines.push("The verifier is MIT-licensed and works offline. No ScopeBlind account required.");
156
+ lines.push("");
157
+ lines.push("---");
158
+ lines.push("*Generated by protect-mcp \xB7 scopeblind.com*");
159
+ return lines.join("\n");
160
+ }
161
+
162
+ export {
163
+ generateReport,
164
+ formatReportMarkdown
165
+ };