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.
package/dist/index.d.ts CHANGED
@@ -154,6 +154,27 @@ interface ProtectConfig {
154
154
  signing?: SigningConfig;
155
155
  /** Credential vault: maps credential labels to injection config */
156
156
  credentials?: Record<string, CredentialConfig>;
157
+ /** Multi-agent mode: identify calling agents and apply per-agent policy */
158
+ multiAgent?: MultiAgentConfig;
159
+ }
160
+ /**
161
+ * Multi-agent mode configuration.
162
+ *
163
+ * When enabled, protect-mcp resolves the calling agent's passport kid
164
+ * from request metadata (x-passport-kid header or _passport_kid param)
165
+ * and applies agent-specific policy overrides.
166
+ */
167
+ interface MultiAgentConfig {
168
+ /** Enable multi-agent mode */
169
+ enabled: boolean;
170
+ /** Registry endpoint for agent manifest lookup */
171
+ registryUrl?: string;
172
+ /** Per-agent policy overrides: maps kid → tool policy overrides */
173
+ agentPolicies?: Record<string, Record<string, ToolPolicy>>;
174
+ /** Default policy for unrecognized agents (default: use base policy) */
175
+ unknownAgentPolicy?: 'base' | 'deny' | 'shadow-only';
176
+ /** Cache TTL for agent manifests in ms (default: 300000 = 5 min) */
177
+ cacheTtlMs?: number;
157
178
  }
158
179
 
159
180
  /**
@@ -570,6 +591,132 @@ declare function createAuditBundle(opts: AuditBundleOptions): AuditBundle;
570
591
  */
571
592
  declare function collectSignedReceipts(logs: DecisionLog[]): Record<string, unknown>[];
572
593
 
594
+ /**
595
+ * protect-mcp simulate — dry-run policy evaluation
596
+ *
597
+ * Reads a recorded log file (.protect-mcp-log.jsonl) and evaluates
598
+ * each tool call against a policy file. Shows what would have been
599
+ * blocked, rate-limited, or approved — without wrapping a live server.
600
+ *
601
+ * Usage:
602
+ * npx protect-mcp simulate --policy strict.json [--log .protect-mcp-log.jsonl] [--json]
603
+ */
604
+
605
+ interface LogEntry {
606
+ v: number;
607
+ tool: string;
608
+ decision: string;
609
+ reason_code: string;
610
+ mode: string;
611
+ timestamp: number;
612
+ tier?: string;
613
+ rate_limit_remaining?: number;
614
+ [key: string]: unknown;
615
+ }
616
+ interface SimulationResult {
617
+ tool: string;
618
+ calls: number;
619
+ results: {
620
+ allow: number;
621
+ block: number;
622
+ rate_limited: number;
623
+ require_approval: number;
624
+ tier_insufficient: number;
625
+ };
626
+ original: {
627
+ allow: number;
628
+ deny: number;
629
+ };
630
+ }
631
+ interface SimulationSummary {
632
+ policy_file: string;
633
+ log_file: string;
634
+ total_calls: number;
635
+ results: {
636
+ allow: number;
637
+ block: number;
638
+ rate_limited: number;
639
+ require_approval: number;
640
+ tier_insufficient: number;
641
+ };
642
+ original: {
643
+ allow: number;
644
+ deny: number;
645
+ };
646
+ tool_breakdown: SimulationResult[];
647
+ changes: string[];
648
+ }
649
+ /**
650
+ * Parse a JSONL log file into log entries.
651
+ */
652
+ declare function parseLogFile(path: string): LogEntry[];
653
+ /**
654
+ * Simulate a policy against a set of log entries.
655
+ * Evaluates each entry against the policy's per-tool rules,
656
+ * including block, rate_limit, min_tier, and require_approval.
657
+ */
658
+ declare function simulate(entries: LogEntry[], policy: ProtectPolicy, tier?: TrustTier): SimulationSummary;
659
+ /**
660
+ * Format simulation results for terminal output.
661
+ */
662
+ declare function formatSimulation(summary: SimulationSummary): string;
663
+
664
+ /**
665
+ * protect-mcp report — compliance report generation
666
+ *
667
+ * Generates structured compliance reports from local log and receipt files.
668
+ * Output as JSON (machine-readable) or Markdown (human-readable, PDF-convertible).
669
+ *
670
+ * Usage:
671
+ * npx protect-mcp report --period 30d --output report.json
672
+ * npx protect-mcp report --period 30d --format md --output report.md
673
+ */
674
+ interface ComplianceReport {
675
+ generated_at: string;
676
+ period: {
677
+ from: string;
678
+ to: string;
679
+ };
680
+ signing_identity: {
681
+ kid: string;
682
+ issuer: string;
683
+ } | null;
684
+ summary: {
685
+ total_decisions: number;
686
+ allowed: number;
687
+ blocked: number;
688
+ rate_limited: number;
689
+ approval_required: number;
690
+ unique_tools: number;
691
+ unique_tiers: number;
692
+ };
693
+ tool_breakdown: Array<{
694
+ tool: string;
695
+ total: number;
696
+ allowed: number;
697
+ blocked: number;
698
+ rate_limited: number;
699
+ approval_required: number;
700
+ }>;
701
+ policy_changes: Array<{
702
+ at: string;
703
+ policy_digest: string;
704
+ }>;
705
+ verification: {
706
+ receipts_signed: number;
707
+ receipts_unsigned: number;
708
+ verify_command: string;
709
+ };
710
+ }
711
+ /**
712
+ * Generate a compliance report from local log and receipt files.
713
+ */
714
+ declare function generateReport(logPath: string, receiptPath: string, periodDays: number): ComplianceReport;
715
+ /**
716
+ * Format a compliance report as Markdown.
717
+ */
718
+ declare function formatReportMarkdown(report: ComplianceReport): string;
719
+
573
720
  /**
574
721
  * Agent identity format: sb:agent:{first 32 hex chars of SHA-256(public key bytes)}
575
722
  * Example: "sb:agent:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
@@ -851,4 +998,4 @@ declare function validateManifest(manifest: unknown): string[];
851
998
  */
852
999
  declare function validateEvidenceReceipt(receipt: unknown): string[];
853
1000
 
854
- export { type AdmissionResult, type AgentId, type AgentManifest, type ArenaPayload, type ArenaReceipt, type AttestationPayload, type AttestationReceipt, type AuditBundle, type AuditBundleOptions, type BenchmarkPayload, type BenchmarkReceipt, type BuilderId, type CredentialConfig, type DecisionContext, type DecisionLog, type DisclosureMode, type Ed25519PublicKey, type EvidenceIssuer, type EvidenceReceipt, type EvidenceReceiptBase, type EvidenceSummary, type EvidenceSummaryEntry, type EvidenceType, type ExternalDecision, type ExternalPDPConfig, type IssuerType, type JsonRpcRequest, type JsonRpcResponse, type LeaseCompatibility, type ManifestBuilder, type ManifestCapabilities, type ManifestConfig, type ManifestIdentity, type ManifestPresentation, type ManifestSignature, type ManifestStatus, type PolicyEngineMode, type ProtectConfig, ProtectGateway, type ProtectPolicy, type RateLimit, type RestraintPayload, type RestraintReceipt, type SHA256Hash, type SigningConfig, type TierOverrides, type ToolPolicy, type TrustTier, type WorkPayload, type WorkReceipt, buildDecisionContext, checkRateLimit, collectSignedReceipts, createAuditBundle, evaluateTier, getSignerInfo, getToolPolicy, initSigning, isAgentId, isDisclosureMode, isEvidenceType, isManifestStatus, isSigningEnabled, listCredentialLabels, loadPolicy, meetsMinTier, parseRateLimit, queryExternalPDP, resolveCredential, signDecision, validateCredentials, validateEvidenceReceipt, validateManifest };
1001
+ export { type AdmissionResult, type AgentId, type AgentManifest, type ArenaPayload, type ArenaReceipt, type AttestationPayload, type AttestationReceipt, type AuditBundle, type AuditBundleOptions, type BenchmarkPayload, type BenchmarkReceipt, type BuilderId, type ComplianceReport, type CredentialConfig, type DecisionContext, type DecisionLog, type DisclosureMode, type Ed25519PublicKey, type EvidenceIssuer, type EvidenceReceipt, type EvidenceReceiptBase, type EvidenceSummary, type EvidenceSummaryEntry, type EvidenceType, type ExternalDecision, type ExternalPDPConfig, type IssuerType, type JsonRpcRequest, type JsonRpcResponse, type LeaseCompatibility, type ManifestBuilder, type ManifestCapabilities, type ManifestConfig, type ManifestIdentity, type ManifestPresentation, type ManifestSignature, type ManifestStatus, type PolicyEngineMode, type ProtectConfig, ProtectGateway, type ProtectPolicy, type RateLimit, type RestraintPayload, type RestraintReceipt, type SHA256Hash, type SigningConfig, type SimulationResult, type SimulationSummary, type TierOverrides, type ToolPolicy, type TrustTier, type WorkPayload, type WorkReceipt, buildDecisionContext, checkRateLimit, collectSignedReceipts, createAuditBundle, evaluateTier, formatReportMarkdown, formatSimulation, generateReport, getSignerInfo, getToolPolicy, initSigning, isAgentId, isDisclosureMode, isEvidenceType, isManifestStatus, isSigningEnabled, listCredentialLabels, loadPolicy, meetsMinTier, parseLogFile, parseRateLimit, queryExternalPDP, resolveCredential, signDecision, simulate, validateCredentials, validateEvidenceReceipt, validateManifest };
package/dist/index.js CHANGED
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
 
30
20
  // src/index.ts
@@ -36,6 +26,9 @@ __export(index_exports, {
36
26
  collectSignedReceipts: () => collectSignedReceipts,
37
27
  createAuditBundle: () => createAuditBundle,
38
28
  evaluateTier: () => evaluateTier,
29
+ formatReportMarkdown: () => formatReportMarkdown,
30
+ formatSimulation: () => formatSimulation,
31
+ generateReport: () => generateReport,
39
32
  getSignerInfo: () => getSignerInfo,
40
33
  getToolPolicy: () => getToolPolicy,
41
34
  initSigning: () => initSigning,
@@ -47,10 +40,12 @@ __export(index_exports, {
47
40
  listCredentialLabels: () => listCredentialLabels,
48
41
  loadPolicy: () => loadPolicy,
49
42
  meetsMinTier: () => meetsMinTier,
43
+ parseLogFile: () => parseLogFile,
50
44
  parseRateLimit: () => parseRateLimit,
51
45
  queryExternalPDP: () => queryExternalPDP,
52
46
  resolveCredential: () => resolveCredential,
53
47
  signDecision: () => signDecision,
48
+ simulate: () => simulate,
54
49
  validateCredentials: () => validateCredentials,
55
50
  validateEvidenceReceipt: () => validateEvidenceReceipt,
56
51
  validateManifest: () => validateManifest
@@ -383,7 +378,11 @@ async function initSigning(config) {
383
378
  return warnings;
384
379
  }
385
380
  try {
386
- artifactsModule = await import("@veritasacta/artifacts");
381
+ const moduleName = "@veritasacta/artifacts";
382
+ artifactsModule = await import(
383
+ /* @vite-ignore */
384
+ moduleName
385
+ );
387
386
  } catch {
388
387
  warnings.push("signing: @veritasacta/artifacts not available \u2014 receipts will be unsigned");
389
388
  return warnings;
@@ -968,8 +967,31 @@ var ProtectGateway = class {
968
967
  async interceptToolCall(request) {
969
968
  const toolName = request.params?.name || "unknown";
970
969
  const requestId = (0, import_node_crypto2.randomUUID)().slice(0, 12);
971
- const toolPolicy = getToolPolicy(toolName, this.config.policy);
972
970
  const mode = this.config.enforce ? "enforce" : "shadow";
971
+ let resolvedAgentKid = this.admissionResult?.agent_id;
972
+ let effectiveToolPolicy;
973
+ if (this.config.multiAgent?.enabled) {
974
+ const paramKid = request.params?._passport_kid;
975
+ if (paramKid) resolvedAgentKid = paramKid;
976
+ const agentOverrides = resolvedAgentKid ? this.config.multiAgent.agentPolicies?.[resolvedAgentKid] : void 0;
977
+ if (agentOverrides && agentOverrides[toolName]) {
978
+ effectiveToolPolicy = { ...getToolPolicy(toolName, this.config.policy), ...agentOverrides[toolName] };
979
+ } else if (!resolvedAgentKid && this.config.multiAgent.unknownAgentPolicy === "deny") {
980
+ this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "unknown_agent_denied", request_id: requestId, tier: this.currentTier });
981
+ if (this.config.enforce) {
982
+ return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied: unidentified agent`);
983
+ }
984
+ return null;
985
+ } else {
986
+ effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
987
+ }
988
+ if (this.config.verbose && resolvedAgentKid) {
989
+ this.log(`Multi-agent: resolved kid=${resolvedAgentKid} for tool=${toolName}`);
990
+ }
991
+ } else {
992
+ effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
993
+ }
994
+ const toolPolicy = effectiveToolPolicy;
973
995
  let credentialRef;
974
996
  if (this.config.credentials) {
975
997
  const cred = resolveCredential(toolName, this.config.credentials);
@@ -1193,6 +1215,298 @@ function collectSignedReceipts(logs) {
1193
1215
  }).filter((r) => typeof r.signature === "string");
1194
1216
  }
1195
1217
 
1218
+ // src/simulate.ts
1219
+ var import_node_fs6 = require("fs");
1220
+ function parseLogFile(path) {
1221
+ const raw = (0, import_node_fs6.readFileSync)(path, "utf-8");
1222
+ const entries = [];
1223
+ for (const line of raw.split("\n")) {
1224
+ const trimmed = line.trim();
1225
+ if (!trimmed) continue;
1226
+ const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
1227
+ try {
1228
+ const parsed = JSON.parse(jsonStr);
1229
+ if (parsed.tool && parsed.decision) {
1230
+ entries.push(parsed);
1231
+ }
1232
+ } catch {
1233
+ }
1234
+ }
1235
+ return entries;
1236
+ }
1237
+ function simulate(entries, policy, tier = "unknown") {
1238
+ const rateLimitStore = /* @__PURE__ */ new Map();
1239
+ const toolResults = /* @__PURE__ */ new Map();
1240
+ const totals = {
1241
+ allow: 0,
1242
+ block: 0,
1243
+ rate_limited: 0,
1244
+ require_approval: 0,
1245
+ tier_insufficient: 0
1246
+ };
1247
+ const originalTotals = { allow: 0, deny: 0 };
1248
+ const changes = [];
1249
+ for (const entry of entries) {
1250
+ const toolName = entry.tool;
1251
+ const toolPolicy = getToolPolicy(toolName, policy);
1252
+ if (entry.decision === "allow") {
1253
+ originalTotals.allow++;
1254
+ } else {
1255
+ originalTotals.deny++;
1256
+ }
1257
+ let newDecision;
1258
+ if (toolPolicy.block) {
1259
+ newDecision = "block";
1260
+ } else if (toolPolicy.min_tier && !meetsMinTier(tier, toolPolicy.min_tier)) {
1261
+ newDecision = "tier_insufficient";
1262
+ } else if (toolPolicy.require_approval) {
1263
+ newDecision = "require_approval";
1264
+ } else if (toolPolicy.rate_limit) {
1265
+ const limit = parseRateLimit(toolPolicy.rate_limit);
1266
+ const result = checkRateLimit(toolName, limit, rateLimitStore);
1267
+ newDecision = result.allowed ? "allow" : "rate_limited";
1268
+ } else {
1269
+ newDecision = "allow";
1270
+ }
1271
+ totals[newDecision]++;
1272
+ if (!toolResults.has(toolName)) {
1273
+ toolResults.set(toolName, {
1274
+ tool: toolName,
1275
+ calls: 0,
1276
+ results: { allow: 0, block: 0, rate_limited: 0, require_approval: 0, tier_insufficient: 0 },
1277
+ original: { allow: 0, deny: 0 }
1278
+ });
1279
+ }
1280
+ const tr = toolResults.get(toolName);
1281
+ tr.calls++;
1282
+ tr.results[newDecision]++;
1283
+ if (entry.decision === "allow") {
1284
+ tr.original.allow++;
1285
+ } else {
1286
+ tr.original.deny++;
1287
+ }
1288
+ }
1289
+ for (const [tool, result] of toolResults) {
1290
+ const wasAllBlocked = result.original.allow === 0;
1291
+ const nowAllBlocked = result.results.allow === 0;
1292
+ const wasAllAllowed = result.original.deny === 0;
1293
+ if (wasAllAllowed && result.results.block > 0) {
1294
+ changes.push(`${tool}: ${result.results.block} calls would be blocked (was: all allowed)`);
1295
+ }
1296
+ if (wasAllAllowed && result.results.rate_limited > 0) {
1297
+ changes.push(`${tool}: ${result.results.rate_limited} calls would be rate-limited (was: all allowed)`);
1298
+ }
1299
+ if (wasAllAllowed && result.results.require_approval > 0) {
1300
+ changes.push(`${tool}: ${result.results.require_approval} calls would require approval (was: all allowed)`);
1301
+ }
1302
+ if (wasAllAllowed && result.results.tier_insufficient > 0) {
1303
+ changes.push(`${tool}: ${result.results.tier_insufficient} calls would fail tier check (was: all allowed)`);
1304
+ }
1305
+ if (wasAllBlocked && result.results.allow > 0 && !nowAllBlocked) {
1306
+ changes.push(`${tool}: ${result.results.allow} calls would now be allowed (was: all blocked)`);
1307
+ }
1308
+ }
1309
+ return {
1310
+ policy_file: "",
1311
+ log_file: "",
1312
+ total_calls: entries.length,
1313
+ results: totals,
1314
+ original: originalTotals,
1315
+ tool_breakdown: Array.from(toolResults.values()).sort((a, b) => b.calls - a.calls),
1316
+ changes
1317
+ };
1318
+ }
1319
+ function formatSimulation(summary) {
1320
+ const lines = [];
1321
+ lines.push(`Simulating ${summary.policy_file} against ${summary.total_calls} recorded tool calls:
1322
+ `);
1323
+ const maxToolLen = Math.max(...summary.tool_breakdown.map((t) => t.tool.length), 4);
1324
+ for (const tr of summary.tool_breakdown) {
1325
+ const parts = [];
1326
+ if (tr.results.allow > 0) parts.push(`${tr.results.allow} allow`);
1327
+ if (tr.results.block > 0) parts.push(`\x1B[31m${tr.results.block} blocked\x1B[0m`);
1328
+ if (tr.results.rate_limited > 0) parts.push(`\x1B[33m${tr.results.rate_limited} rate_limited\x1B[0m`);
1329
+ if (tr.results.require_approval > 0) parts.push(`\x1B[36m${tr.results.require_approval} require_approval\x1B[0m`);
1330
+ if (tr.results.tier_insufficient > 0) parts.push(`\x1B[35m${tr.results.tier_insufficient} tier_insufficient\x1B[0m`);
1331
+ const originalParts = [];
1332
+ if (tr.original.allow > 0) originalParts.push(`${tr.original.allow} allow`);
1333
+ if (tr.original.deny > 0) originalParts.push(`${tr.original.deny} deny`);
1334
+ lines.push(` ${tr.tool.padEnd(maxToolLen)} \xD7 ${String(tr.calls).padStart(3)} \u2192 ${parts.join(", ")} (was: ${originalParts.join(", ")})`);
1335
+ }
1336
+ lines.push("");
1337
+ 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`);
1338
+ lines.push(` vs original: ${summary.original.allow} allow, ${summary.original.deny} deny`);
1339
+ if (summary.changes.length > 0) {
1340
+ lines.push("");
1341
+ lines.push("Changes:");
1342
+ for (const change of summary.changes) {
1343
+ lines.push(` \u2022 ${change}`);
1344
+ }
1345
+ }
1346
+ return lines.join("\n");
1347
+ }
1348
+
1349
+ // src/report.ts
1350
+ var import_node_fs7 = require("fs");
1351
+ function generateReport(logPath, receiptPath, periodDays) {
1352
+ const now = /* @__PURE__ */ new Date();
1353
+ const from = new Date(now.getTime() - periodDays * 864e5);
1354
+ const entries = [];
1355
+ if ((0, import_node_fs7.existsSync)(logPath)) {
1356
+ const raw = (0, import_node_fs7.readFileSync)(logPath, "utf-8");
1357
+ for (const line of raw.split("\n")) {
1358
+ const trimmed = line.trim();
1359
+ if (!trimmed) continue;
1360
+ const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
1361
+ try {
1362
+ const parsed = JSON.parse(jsonStr);
1363
+ if (parsed.tool && parsed.decision && parsed.timestamp) {
1364
+ const entryTime = typeof parsed.timestamp === "number" && parsed.timestamp > 1e12 ? parsed.timestamp : parsed.timestamp * 1e3;
1365
+ if (entryTime >= from.getTime()) {
1366
+ entries.push(parsed);
1367
+ }
1368
+ }
1369
+ } catch {
1370
+ }
1371
+ }
1372
+ }
1373
+ let receiptsSigned = 0;
1374
+ let signerKid = "";
1375
+ let signerIssuer = "";
1376
+ if ((0, import_node_fs7.existsSync)(receiptPath)) {
1377
+ const raw = (0, import_node_fs7.readFileSync)(receiptPath, "utf-8");
1378
+ for (const line of raw.split("\n")) {
1379
+ const trimmed = line.trim();
1380
+ if (!trimmed) continue;
1381
+ try {
1382
+ const parsed = JSON.parse(trimmed);
1383
+ if (parsed.signature) {
1384
+ receiptsSigned++;
1385
+ if (parsed.kid && !signerKid) signerKid = parsed.kid;
1386
+ if (parsed.issuer && !signerIssuer) signerIssuer = parsed.issuer;
1387
+ }
1388
+ } catch {
1389
+ }
1390
+ }
1391
+ }
1392
+ const toolMap = /* @__PURE__ */ new Map();
1393
+ const tiers = /* @__PURE__ */ new Set();
1394
+ const policyDigests = /* @__PURE__ */ new Map();
1395
+ let allowed = 0;
1396
+ let blocked = 0;
1397
+ let rateLimited = 0;
1398
+ let approvalRequired = 0;
1399
+ for (const entry of entries) {
1400
+ const tool = entry.tool;
1401
+ if (!toolMap.has(tool)) {
1402
+ toolMap.set(tool, { total: 0, allowed: 0, blocked: 0, rate_limited: 0, approval_required: 0 });
1403
+ }
1404
+ const tm = toolMap.get(tool);
1405
+ tm.total++;
1406
+ if (entry.decision === "allow") {
1407
+ allowed++;
1408
+ tm.allowed++;
1409
+ } else if (entry.decision === "deny" && entry.reason_code === "rate_limit_exceeded") {
1410
+ rateLimited++;
1411
+ tm.rate_limited++;
1412
+ } else if (entry.decision === "deny" && entry.reason_code === "require_approval") {
1413
+ approvalRequired++;
1414
+ tm.approval_required++;
1415
+ } else {
1416
+ blocked++;
1417
+ tm.blocked++;
1418
+ }
1419
+ if (entry.tier) tiers.add(entry.tier);
1420
+ if (entry.policy_digest && !policyDigests.has(entry.policy_digest)) {
1421
+ policyDigests.set(entry.policy_digest, new Date(entry.timestamp).toISOString());
1422
+ }
1423
+ }
1424
+ const policyChanges = Array.from(policyDigests.entries()).map(([digest, at]) => ({
1425
+ at,
1426
+ policy_digest: digest
1427
+ })).sort((a, b) => a.at.localeCompare(b.at));
1428
+ return {
1429
+ generated_at: now.toISOString(),
1430
+ period: { from: from.toISOString(), to: now.toISOString() },
1431
+ signing_identity: signerKid ? { kid: signerKid, issuer: signerIssuer } : null,
1432
+ summary: {
1433
+ total_decisions: entries.length,
1434
+ allowed,
1435
+ blocked,
1436
+ rate_limited: rateLimited,
1437
+ approval_required: approvalRequired,
1438
+ unique_tools: toolMap.size,
1439
+ unique_tiers: tiers.size
1440
+ },
1441
+ tool_breakdown: Array.from(toolMap.entries()).map(([tool, stats]) => ({ tool, ...stats })).sort((a, b) => b.total - a.total),
1442
+ policy_changes: policyChanges,
1443
+ verification: {
1444
+ receipts_signed: receiptsSigned,
1445
+ receipts_unsigned: entries.length - receiptsSigned,
1446
+ verify_command: "npx @veritasacta/verify audit-bundle.json --bundle"
1447
+ }
1448
+ };
1449
+ }
1450
+ function formatReportMarkdown(report) {
1451
+ const lines = [];
1452
+ lines.push("# ScopeBlind Compliance Report");
1453
+ lines.push("");
1454
+ lines.push(`**Generated:** ${report.generated_at}`);
1455
+ lines.push(`**Period:** ${report.period.from.split("T")[0]} to ${report.period.to.split("T")[0]}`);
1456
+ if (report.signing_identity) {
1457
+ lines.push(`**Signing identity:** kid \`${report.signing_identity.kid}\`, issuer \`${report.signing_identity.issuer}\``);
1458
+ }
1459
+ lines.push("");
1460
+ lines.push("## Summary");
1461
+ lines.push("");
1462
+ lines.push(`| Metric | Value |`);
1463
+ lines.push(`|--------|-------|`);
1464
+ lines.push(`| Total decisions | ${report.summary.total_decisions} |`);
1465
+ lines.push(`| Allowed | ${report.summary.allowed} |`);
1466
+ lines.push(`| Blocked | ${report.summary.blocked} |`);
1467
+ lines.push(`| Rate-limited | ${report.summary.rate_limited} |`);
1468
+ lines.push(`| Approval required | ${report.summary.approval_required} |`);
1469
+ lines.push(`| Unique tools | ${report.summary.unique_tools} |`);
1470
+ lines.push(`| Unique tiers | ${report.summary.unique_tiers} |`);
1471
+ lines.push("");
1472
+ if (report.tool_breakdown.length > 0) {
1473
+ lines.push("## Tool Breakdown");
1474
+ lines.push("");
1475
+ lines.push("| Tool | Total | Allowed | Blocked | Rate-limited | Approval |");
1476
+ lines.push("|------|-------|---------|---------|--------------|----------|");
1477
+ for (const t of report.tool_breakdown) {
1478
+ lines.push(`| \`${t.tool}\` | ${t.total} | ${t.allowed} | ${t.blocked} | ${t.rate_limited} | ${t.approval_required} |`);
1479
+ }
1480
+ lines.push("");
1481
+ }
1482
+ if (report.policy_changes.length > 0) {
1483
+ lines.push("## Policy History");
1484
+ lines.push("");
1485
+ lines.push("| Timestamp | Policy Digest |");
1486
+ lines.push("|-----------|--------------|");
1487
+ for (const pc of report.policy_changes) {
1488
+ lines.push(`| ${pc.at} | \`${pc.policy_digest}\` |`);
1489
+ }
1490
+ lines.push("");
1491
+ }
1492
+ lines.push("## Verification");
1493
+ lines.push("");
1494
+ lines.push(`- Receipts signed: **${report.verification.receipts_signed}**`);
1495
+ lines.push(`- Receipts unsigned: **${report.verification.receipts_unsigned}**`);
1496
+ lines.push("");
1497
+ lines.push("Verify the audit bundle:");
1498
+ lines.push("");
1499
+ lines.push("```bash");
1500
+ lines.push(report.verification.verify_command);
1501
+ lines.push("```");
1502
+ lines.push("");
1503
+ lines.push("The verifier is MIT-licensed and works offline. No ScopeBlind account required.");
1504
+ lines.push("");
1505
+ lines.push("---");
1506
+ lines.push("*Generated by protect-mcp \xB7 scopeblind.com*");
1507
+ return lines.join("\n");
1508
+ }
1509
+
1196
1510
  // src/manifest.ts
1197
1511
  function isAgentId(s) {
1198
1512
  return /^sb:agent:[a-f0-9]{32}$/.test(s);
@@ -1355,6 +1669,9 @@ function validateEvidenceReceipt(receipt) {
1355
1669
  collectSignedReceipts,
1356
1670
  createAuditBundle,
1357
1671
  evaluateTier,
1672
+ formatReportMarkdown,
1673
+ formatSimulation,
1674
+ generateReport,
1358
1675
  getSignerInfo,
1359
1676
  getToolPolicy,
1360
1677
  initSigning,
@@ -1366,10 +1683,12 @@ function validateEvidenceReceipt(receipt) {
1366
1683
  listCredentialLabels,
1367
1684
  loadPolicy,
1368
1685
  meetsMinTier,
1686
+ parseLogFile,
1369
1687
  parseRateLimit,
1370
1688
  queryExternalPDP,
1371
1689
  resolveCredential,
1372
1690
  signDecision,
1691
+ simulate,
1373
1692
  validateCredentials,
1374
1693
  validateEvidenceReceipt,
1375
1694
  validateManifest
package/dist/index.mjs CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  buildDecisionContext,
4
4
  checkRateLimit,
5
5
  evaluateTier,
6
+ formatSimulation,
6
7
  getSignerInfo,
7
8
  getToolPolicy,
8
9
  initSigning,
@@ -10,16 +11,22 @@ import {
10
11
  listCredentialLabels,
11
12
  loadPolicy,
12
13
  meetsMinTier,
14
+ parseLogFile,
13
15
  parseRateLimit,
14
16
  queryExternalPDP,
15
17
  resolveCredential,
16
18
  signDecision,
19
+ simulate,
17
20
  validateCredentials
18
- } from "./chunk-U7TMVD3E.mjs";
21
+ } from "./chunk-GV7N53QE.mjs";
19
22
  import {
20
23
  collectSignedReceipts,
21
24
  createAuditBundle
22
25
  } from "./chunk-5JXFV37Y.mjs";
26
+ import {
27
+ formatReportMarkdown,
28
+ generateReport
29
+ } from "./chunk-JQDVKZBN.mjs";
23
30
 
24
31
  // src/manifest.ts
25
32
  function isAgentId(s) {
@@ -182,6 +189,9 @@ export {
182
189
  collectSignedReceipts,
183
190
  createAuditBundle,
184
191
  evaluateTier,
192
+ formatReportMarkdown,
193
+ formatSimulation,
194
+ generateReport,
185
195
  getSignerInfo,
186
196
  getToolPolicy,
187
197
  initSigning,
@@ -193,10 +203,12 @@ export {
193
203
  listCredentialLabels,
194
204
  loadPolicy,
195
205
  meetsMinTier,
206
+ parseLogFile,
196
207
  parseRateLimit,
197
208
  queryExternalPDP,
198
209
  resolveCredential,
199
210
  signDecision,
211
+ simulate,
200
212
  validateCredentials,
201
213
  validateEvidenceReceipt,
202
214
  validateManifest
@@ -0,0 +1,8 @@
1
+ import {
2
+ formatReportMarkdown,
3
+ generateReport
4
+ } from "./chunk-JQDVKZBN.mjs";
5
+ export {
6
+ formatReportMarkdown,
7
+ generateReport
8
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "protect-mcp",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Security gateway for MCP servers. Shadow-mode logs, per-tool policies, optional local Ed25519-signed receipts. Programmatic hooks for trust tiers, credential config, and external policy engines.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -49,7 +49,7 @@
49
49
  "url": "https://github.com/tomjwxf/scopeblind-gateway/issues"
50
50
  },
51
51
  "dependencies": {
52
- "@veritasacta/artifacts": "^0.2.0"
52
+ "@veritasacta/protocol": "^0.1.0"
53
53
  },
54
54
  "optionalDependencies": {
55
55
  "@noble/curves": "^1.8.0",