protect-mcp 0.3.0 → 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/cli.js CHANGED
@@ -67,7 +67,7 @@ __export(utils_exports, {
67
67
  isLE: () => isLE,
68
68
  kdfInputToBytes: () => kdfInputToBytes,
69
69
  nextTick: () => nextTick,
70
- randomBytes: () => randomBytes,
70
+ randomBytes: () => randomBytes2,
71
71
  rotl: () => rotl,
72
72
  rotr: () => rotr,
73
73
  swap32IfBE: () => swap32IfBE,
@@ -257,7 +257,7 @@ function createXOFer(hashCons) {
257
257
  hashC.create = (opts) => hashCons(opts);
258
258
  return hashC;
259
259
  }
260
- function randomBytes(bytesLength = 32) {
260
+ function randomBytes2(bytesLength = 32) {
261
261
  if (crypto && typeof crypto.getRandomValues === "function") {
262
262
  return crypto.getRandomValues(new Uint8Array(bytesLength));
263
263
  }
@@ -1700,7 +1700,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
1700
1700
  });
1701
1701
  const { prehash } = eddsaOpts;
1702
1702
  const { BASE, Fp: Fp2, Fn: Fn2 } = Point;
1703
- const randomBytes2 = eddsaOpts.randomBytes || randomBytes;
1703
+ const randomBytes3 = eddsaOpts.randomBytes || randomBytes2;
1704
1704
  const adjustScalarBytes2 = eddsaOpts.adjustScalarBytes || ((bytes) => bytes);
1705
1705
  const domain = eddsaOpts.domain || ((data, ctx, phflag) => {
1706
1706
  _abool2(phflag, "phflag");
@@ -1782,7 +1782,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
1782
1782
  signature: 2 * _size,
1783
1783
  seed: _size
1784
1784
  };
1785
- function randomSecretKey(seed = randomBytes2(lengths.seed)) {
1785
+ function randomSecretKey(seed = randomBytes3(lengths.seed)) {
1786
1786
  return _abytes2(seed, lengths.seed, "seed");
1787
1787
  }
1788
1788
  function keygen(seed) {
@@ -2137,7 +2137,7 @@ function montgomery(curveDef) {
2137
2137
  const is25519 = type === "x25519";
2138
2138
  if (!is25519 && type !== "x448")
2139
2139
  throw new Error("invalid type");
2140
- const randomBytes_ = rand || randomBytes;
2140
+ const randomBytes_ = rand || randomBytes2;
2141
2141
  const montgomeryBits = is25519 ? 255 : 448;
2142
2142
  const fieldLen = is25519 ? 32 : 56;
2143
2143
  const Gu = is25519 ? BigInt(9) : BigInt(5);
@@ -2638,6 +2638,237 @@ var init_ed25519 = __esm({
2638
2638
  }
2639
2639
  });
2640
2640
 
2641
+ // src/bundle.ts
2642
+ var bundle_exports = {};
2643
+ __export(bundle_exports, {
2644
+ collectSignedReceipts: () => collectSignedReceipts,
2645
+ createAuditBundle: () => createAuditBundle
2646
+ });
2647
+ function createAuditBundle(opts) {
2648
+ const receipts = opts.receipts.filter(
2649
+ (r) => r && typeof r === "object" && typeof r.signature === "string"
2650
+ );
2651
+ if (receipts.length === 0) {
2652
+ throw new Error("Audit bundle requires at least one signed receipt");
2653
+ }
2654
+ const keyMap = /* @__PURE__ */ new Map();
2655
+ for (const key of opts.signingKeys) {
2656
+ if (!keyMap.has(key.kid)) {
2657
+ keyMap.set(key.kid, key);
2658
+ }
2659
+ }
2660
+ let timeRange = opts.timeRange || null;
2661
+ if (!timeRange) {
2662
+ const timestamps = receipts.map((r) => r.issued_at || r.timestamp).filter(Boolean).sort();
2663
+ if (timestamps.length > 0) {
2664
+ timeRange = {
2665
+ from: timestamps[0],
2666
+ to: timestamps[timestamps.length - 1]
2667
+ };
2668
+ }
2669
+ }
2670
+ return {
2671
+ format: "scopeblind:audit-bundle",
2672
+ version: 1,
2673
+ exported_at: (/* @__PURE__ */ new Date()).toISOString(),
2674
+ tenant: opts.tenant,
2675
+ time_range: timeRange,
2676
+ receipts,
2677
+ anchors: opts.anchors || [],
2678
+ verification: {
2679
+ algorithm: "ed25519",
2680
+ signing_keys: Array.from(keyMap.values()),
2681
+ 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`
2682
+ }
2683
+ };
2684
+ }
2685
+ function collectSignedReceipts(logs) {
2686
+ return logs.filter((log) => log.v === 2).map((log) => {
2687
+ const logRecord = log;
2688
+ if (logRecord.receipt) {
2689
+ return logRecord.receipt;
2690
+ }
2691
+ return logRecord;
2692
+ }).filter((r) => typeof r.signature === "string");
2693
+ }
2694
+ var init_bundle = __esm({
2695
+ "src/bundle.ts"() {
2696
+ "use strict";
2697
+ }
2698
+ });
2699
+
2700
+ // src/report.ts
2701
+ var report_exports = {};
2702
+ __export(report_exports, {
2703
+ formatReportMarkdown: () => formatReportMarkdown,
2704
+ generateReport: () => generateReport
2705
+ });
2706
+ function generateReport(logPath, receiptPath, periodDays) {
2707
+ const now = /* @__PURE__ */ new Date();
2708
+ const from = new Date(now.getTime() - periodDays * 864e5);
2709
+ const entries = [];
2710
+ if ((0, import_node_fs7.existsSync)(logPath)) {
2711
+ const raw = (0, import_node_fs7.readFileSync)(logPath, "utf-8");
2712
+ for (const line of raw.split("\n")) {
2713
+ const trimmed = line.trim();
2714
+ if (!trimmed) continue;
2715
+ const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
2716
+ try {
2717
+ const parsed = JSON.parse(jsonStr);
2718
+ if (parsed.tool && parsed.decision && parsed.timestamp) {
2719
+ const entryTime = typeof parsed.timestamp === "number" && parsed.timestamp > 1e12 ? parsed.timestamp : parsed.timestamp * 1e3;
2720
+ if (entryTime >= from.getTime()) {
2721
+ entries.push(parsed);
2722
+ }
2723
+ }
2724
+ } catch {
2725
+ }
2726
+ }
2727
+ }
2728
+ let receiptsSigned = 0;
2729
+ let signerKid = "";
2730
+ let signerIssuer = "";
2731
+ if ((0, import_node_fs7.existsSync)(receiptPath)) {
2732
+ const raw = (0, import_node_fs7.readFileSync)(receiptPath, "utf-8");
2733
+ for (const line of raw.split("\n")) {
2734
+ const trimmed = line.trim();
2735
+ if (!trimmed) continue;
2736
+ try {
2737
+ const parsed = JSON.parse(trimmed);
2738
+ if (parsed.signature) {
2739
+ receiptsSigned++;
2740
+ if (parsed.kid && !signerKid) signerKid = parsed.kid;
2741
+ if (parsed.issuer && !signerIssuer) signerIssuer = parsed.issuer;
2742
+ }
2743
+ } catch {
2744
+ }
2745
+ }
2746
+ }
2747
+ const toolMap = /* @__PURE__ */ new Map();
2748
+ const tiers = /* @__PURE__ */ new Set();
2749
+ const policyDigests = /* @__PURE__ */ new Map();
2750
+ let allowed = 0;
2751
+ let blocked = 0;
2752
+ let rateLimited = 0;
2753
+ let approvalRequired = 0;
2754
+ for (const entry of entries) {
2755
+ const tool = entry.tool;
2756
+ if (!toolMap.has(tool)) {
2757
+ toolMap.set(tool, { total: 0, allowed: 0, blocked: 0, rate_limited: 0, approval_required: 0 });
2758
+ }
2759
+ const tm = toolMap.get(tool);
2760
+ tm.total++;
2761
+ if (entry.decision === "allow") {
2762
+ allowed++;
2763
+ tm.allowed++;
2764
+ } else if (entry.decision === "deny" && entry.reason_code === "rate_limit_exceeded") {
2765
+ rateLimited++;
2766
+ tm.rate_limited++;
2767
+ } else if (entry.decision === "deny" && entry.reason_code === "require_approval") {
2768
+ approvalRequired++;
2769
+ tm.approval_required++;
2770
+ } else {
2771
+ blocked++;
2772
+ tm.blocked++;
2773
+ }
2774
+ if (entry.tier) tiers.add(entry.tier);
2775
+ if (entry.policy_digest && !policyDigests.has(entry.policy_digest)) {
2776
+ policyDigests.set(entry.policy_digest, new Date(entry.timestamp).toISOString());
2777
+ }
2778
+ }
2779
+ const policyChanges = Array.from(policyDigests.entries()).map(([digest, at]) => ({
2780
+ at,
2781
+ policy_digest: digest
2782
+ })).sort((a, b) => a.at.localeCompare(b.at));
2783
+ return {
2784
+ generated_at: now.toISOString(),
2785
+ period: { from: from.toISOString(), to: now.toISOString() },
2786
+ signing_identity: signerKid ? { kid: signerKid, issuer: signerIssuer } : null,
2787
+ summary: {
2788
+ total_decisions: entries.length,
2789
+ allowed,
2790
+ blocked,
2791
+ rate_limited: rateLimited,
2792
+ approval_required: approvalRequired,
2793
+ unique_tools: toolMap.size,
2794
+ unique_tiers: tiers.size
2795
+ },
2796
+ tool_breakdown: Array.from(toolMap.entries()).map(([tool, stats]) => ({ tool, ...stats })).sort((a, b) => b.total - a.total),
2797
+ policy_changes: policyChanges,
2798
+ verification: {
2799
+ receipts_signed: receiptsSigned,
2800
+ receipts_unsigned: entries.length - receiptsSigned,
2801
+ verify_command: "npx @veritasacta/verify audit-bundle.json --bundle"
2802
+ }
2803
+ };
2804
+ }
2805
+ function formatReportMarkdown(report) {
2806
+ const lines = [];
2807
+ lines.push("# ScopeBlind Compliance Report");
2808
+ lines.push("");
2809
+ lines.push(`**Generated:** ${report.generated_at}`);
2810
+ lines.push(`**Period:** ${report.period.from.split("T")[0]} to ${report.period.to.split("T")[0]}`);
2811
+ if (report.signing_identity) {
2812
+ lines.push(`**Signing identity:** kid \`${report.signing_identity.kid}\`, issuer \`${report.signing_identity.issuer}\``);
2813
+ }
2814
+ lines.push("");
2815
+ lines.push("## Summary");
2816
+ lines.push("");
2817
+ lines.push(`| Metric | Value |`);
2818
+ lines.push(`|--------|-------|`);
2819
+ lines.push(`| Total decisions | ${report.summary.total_decisions} |`);
2820
+ lines.push(`| Allowed | ${report.summary.allowed} |`);
2821
+ lines.push(`| Blocked | ${report.summary.blocked} |`);
2822
+ lines.push(`| Rate-limited | ${report.summary.rate_limited} |`);
2823
+ lines.push(`| Approval required | ${report.summary.approval_required} |`);
2824
+ lines.push(`| Unique tools | ${report.summary.unique_tools} |`);
2825
+ lines.push(`| Unique tiers | ${report.summary.unique_tiers} |`);
2826
+ lines.push("");
2827
+ if (report.tool_breakdown.length > 0) {
2828
+ lines.push("## Tool Breakdown");
2829
+ lines.push("");
2830
+ lines.push("| Tool | Total | Allowed | Blocked | Rate-limited | Approval |");
2831
+ lines.push("|------|-------|---------|---------|--------------|----------|");
2832
+ for (const t of report.tool_breakdown) {
2833
+ lines.push(`| \`${t.tool}\` | ${t.total} | ${t.allowed} | ${t.blocked} | ${t.rate_limited} | ${t.approval_required} |`);
2834
+ }
2835
+ lines.push("");
2836
+ }
2837
+ if (report.policy_changes.length > 0) {
2838
+ lines.push("## Policy History");
2839
+ lines.push("");
2840
+ lines.push("| Timestamp | Policy Digest |");
2841
+ lines.push("|-----------|--------------|");
2842
+ for (const pc of report.policy_changes) {
2843
+ lines.push(`| ${pc.at} | \`${pc.policy_digest}\` |`);
2844
+ }
2845
+ lines.push("");
2846
+ }
2847
+ lines.push("## Verification");
2848
+ lines.push("");
2849
+ lines.push(`- Receipts signed: **${report.verification.receipts_signed}**`);
2850
+ lines.push(`- Receipts unsigned: **${report.verification.receipts_unsigned}**`);
2851
+ lines.push("");
2852
+ lines.push("Verify the audit bundle:");
2853
+ lines.push("");
2854
+ lines.push("```bash");
2855
+ lines.push(report.verification.verify_command);
2856
+ lines.push("```");
2857
+ lines.push("");
2858
+ lines.push("The verifier is MIT-licensed and works offline. No ScopeBlind account required.");
2859
+ lines.push("");
2860
+ lines.push("---");
2861
+ lines.push("*Generated by protect-mcp \xB7 scopeblind.com*");
2862
+ return lines.join("\n");
2863
+ }
2864
+ var import_node_fs7;
2865
+ var init_report = __esm({
2866
+ "src/report.ts"() {
2867
+ "use strict";
2868
+ import_node_fs7 = require("fs");
2869
+ }
2870
+ });
2871
+
2641
2872
  // src/gateway.ts
2642
2873
  var import_node_child_process = require("child_process");
2643
2874
  var import_node_crypto2 = require("crypto");
@@ -2960,7 +3191,11 @@ async function initSigning(config) {
2960
3191
  return warnings;
2961
3192
  }
2962
3193
  try {
2963
- artifactsModule = await import("@veritasacta/artifacts");
3194
+ const moduleName = "@veritasacta/artifacts";
3195
+ artifactsModule = await import(
3196
+ /* @vite-ignore */
3197
+ moduleName
3198
+ );
2964
3199
  } catch {
2965
3200
  warnings.push("signing: @veritasacta/artifacts not available \u2014 receipts will be unsigned");
2966
3201
  return warnings;
@@ -2979,8 +3214,8 @@ async function initSigning(config) {
2979
3214
  signerState = {
2980
3215
  privateKey: keyData.privateKey,
2981
3216
  publicKey: keyData.publicKey,
2982
- kid: artifactsModule.computeKid(keyData.publicKey),
2983
- issuer: config.issuer || "protect-mcp"
3217
+ kid: keyData.kid || artifactsModule.computeKid(keyData.publicKey),
3218
+ issuer: config.issuer || keyData.issuer || "protect-mcp"
2984
3219
  };
2985
3220
  } catch (err) {
2986
3221
  warnings.push(`signing: failed to load key file: ${err instanceof Error ? err.message : err}`);
@@ -3184,13 +3419,16 @@ var ReceiptBuffer = class {
3184
3419
  count() {
3185
3420
  return this.receipts.length;
3186
3421
  }
3422
+ getLatest() {
3423
+ return this.receipts.length > 0 ? this.receipts[this.receipts.length - 1] : void 0;
3424
+ }
3187
3425
  };
3188
- function startStatusServer(config, receiptBuffer) {
3426
+ function startStatusServer(config, receiptBuffer, approvalStore, approvalNonce) {
3189
3427
  const startTime = Date.now();
3190
3428
  const logDir = process.cwd();
3191
3429
  const server = (0, import_node_http.createServer)((req, res) => {
3192
3430
  res.setHeader("Access-Control-Allow-Origin", "*");
3193
- res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
3431
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
3194
3432
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
3195
3433
  res.setHeader("Content-Type", "application/json");
3196
3434
  if (req.method === "OPTIONS") {
@@ -3207,18 +3445,30 @@ function startStatusServer(config, receiptBuffer) {
3207
3445
  handleStatus(res, logDir);
3208
3446
  } else if (path === "/receipts") {
3209
3447
  handleReceipts(res, receiptBuffer, url);
3448
+ } else if (path === "/receipts/latest") {
3449
+ handleReceiptLatest(res, receiptBuffer);
3210
3450
  } else if (path.startsWith("/receipts/")) {
3211
3451
  const id = path.slice("/receipts/".length);
3212
3452
  handleReceiptById(res, receiptBuffer, id);
3453
+ } else if (path === "/approve" && req.method === "POST") {
3454
+ handleApprove(req, res, approvalStore, approvalNonce);
3455
+ } else if (path === "/approvals" && req.method === "GET") {
3456
+ handleListApprovals(res, approvalStore);
3213
3457
  } else {
3214
3458
  res.writeHead(404);
3215
- res.end(JSON.stringify({ error: "not_found", endpoints: ["/health", "/status", "/receipts", "/receipts/:id"] }));
3459
+ res.end(JSON.stringify({ error: "not_found", endpoints: ["/health", "/status", "/receipts", "/receipts/latest", "/receipts/:id", "/approve", "/approvals"] }));
3216
3460
  }
3217
3461
  } catch (err) {
3218
3462
  res.writeHead(500);
3219
3463
  res.end(JSON.stringify({ error: "internal_error" }));
3220
3464
  }
3221
3465
  });
3466
+ server.on("error", (err) => {
3467
+ if (config.verbose) {
3468
+ process.stderr.write(`[PROTECT_MCP] HTTP status server error: ${err.message}
3469
+ `);
3470
+ }
3471
+ });
3222
3472
  server.listen(config.port, "127.0.0.1", () => {
3223
3473
  if (config.verbose) {
3224
3474
  process.stderr.write(`[PROTECT_MCP] HTTP status server listening on http://127.0.0.1:${config.port}
@@ -3234,7 +3484,7 @@ function handleHealth(res, startTime, config) {
3234
3484
  status: "ok",
3235
3485
  uptime_ms: Date.now() - startTime,
3236
3486
  mode: config.mode,
3237
- version: "0.3.0"
3487
+ version: "0.3.1"
3238
3488
  }));
3239
3489
  }
3240
3490
  function handleStatus(res, logDir) {
@@ -3283,6 +3533,16 @@ function handleReceipts(res, buffer, url) {
3283
3533
  receipts
3284
3534
  }));
3285
3535
  }
3536
+ function handleReceiptLatest(res, buffer) {
3537
+ const latest = buffer.getLatest();
3538
+ if (!latest) {
3539
+ res.writeHead(404);
3540
+ res.end(JSON.stringify({ error: "no_receipts", message: "No receipts yet. Make a tool call through protect-mcp first." }));
3541
+ return;
3542
+ }
3543
+ res.writeHead(200);
3544
+ res.end(JSON.stringify(latest));
3545
+ }
3286
3546
  function handleReceiptById(res, buffer, id) {
3287
3547
  const receipt = buffer.getById(id);
3288
3548
  if (!receipt) {
@@ -3293,22 +3553,92 @@ function handleReceiptById(res, buffer, id) {
3293
3553
  res.writeHead(200);
3294
3554
  res.end(JSON.stringify(receipt));
3295
3555
  }
3556
+ function handleApprove(req, res, approvalStore, expectedNonce) {
3557
+ if (!approvalStore) {
3558
+ res.writeHead(503);
3559
+ res.end(JSON.stringify({ error: "approval_store_not_available" }));
3560
+ return;
3561
+ }
3562
+ let body = "";
3563
+ req.on("data", (chunk) => {
3564
+ body += chunk.toString();
3565
+ });
3566
+ req.on("end", () => {
3567
+ try {
3568
+ const { request_id, tool, mode, nonce } = JSON.parse(body);
3569
+ if (expectedNonce && nonce !== expectedNonce) {
3570
+ res.writeHead(403);
3571
+ res.end(JSON.stringify({ error: "invalid_nonce", message: "Approval nonce does not match. Check stderr output for the correct nonce." }));
3572
+ return;
3573
+ }
3574
+ if (!tool || typeof tool !== "string") {
3575
+ res.writeHead(400);
3576
+ res.end(JSON.stringify({ error: "missing_tool", usage: '{"request_id":"abc123","tool":"send_email","mode":"once|always","nonce":"..."}' }));
3577
+ return;
3578
+ }
3579
+ const grantMode = mode === "always" ? "always" : "once";
3580
+ const ttlMs = grantMode === "once" ? 5 * 60 * 1e3 : 24 * 60 * 60 * 1e3;
3581
+ const grantEntry = { tool, mode: grantMode, expires_at: Date.now() + ttlMs };
3582
+ if (grantMode === "always") {
3583
+ approvalStore.set(`always:${tool}`, grantEntry);
3584
+ } else if (request_id) {
3585
+ approvalStore.set(request_id, grantEntry);
3586
+ } else {
3587
+ approvalStore.set(tool, grantEntry);
3588
+ }
3589
+ res.writeHead(200);
3590
+ res.end(JSON.stringify({
3591
+ approved: true,
3592
+ request_id: request_id || null,
3593
+ tool,
3594
+ mode: grantMode,
3595
+ expires_in_seconds: ttlMs / 1e3
3596
+ }));
3597
+ } catch {
3598
+ res.writeHead(400);
3599
+ res.end(JSON.stringify({ error: "invalid_json", usage: '{"request_id":"abc123","tool":"send_email","mode":"once","nonce":"..."}' }));
3600
+ }
3601
+ });
3602
+ }
3603
+ function handleListApprovals(res, approvalStore) {
3604
+ if (!approvalStore) {
3605
+ res.writeHead(200);
3606
+ res.end(JSON.stringify({ grants: [] }));
3607
+ return;
3608
+ }
3609
+ const now = Date.now();
3610
+ const grants = [];
3611
+ for (const [key, grant] of approvalStore) {
3612
+ if (now < grant.expires_at) {
3613
+ grants.push({ key, tool: grant.tool, mode: grant.mode, expires_in_seconds: Math.round((grant.expires_at - now) / 1e3) });
3614
+ }
3615
+ }
3616
+ res.writeHead(200);
3617
+ res.end(JSON.stringify({ grants }));
3618
+ }
3296
3619
 
3297
3620
  // src/gateway.ts
3298
3621
  var LOG_FILE2 = ".protect-mcp-log.jsonl";
3622
+ var RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
3299
3623
  var ProtectGateway = class {
3300
3624
  child = null;
3301
3625
  config;
3302
3626
  rateLimitStore = /* @__PURE__ */ new Map();
3303
3627
  clientReader = null;
3304
3628
  logFilePath;
3629
+ receiptFilePath;
3305
3630
  evidenceStore;
3306
3631
  receiptBuffer;
3632
+ /** Approval grants keyed by request_id (scoped to the specific action that was requested) */
3633
+ approvalStore = /* @__PURE__ */ new Map();
3634
+ /** Random nonce generated at startup — required for approval endpoint authentication */
3635
+ approvalNonce = (0, import_node_crypto2.randomBytes)(16).toString("hex");
3307
3636
  currentTier = "unknown";
3308
3637
  admissionResult = null;
3309
3638
  constructor(config) {
3310
3639
  this.config = config;
3311
3640
  this.logFilePath = (0, import_node_path3.join)(process.cwd(), LOG_FILE2);
3641
+ this.receiptFilePath = (0, import_node_path3.join)(process.cwd(), RECEIPTS_FILE);
3312
3642
  this.evidenceStore = new EvidenceStore();
3313
3643
  this.receiptBuffer = new ReceiptBuffer();
3314
3644
  }
@@ -3332,12 +3662,15 @@ var ProtectGateway = class {
3332
3662
  this.log(`External PDP: ${this.config.policy.external?.endpoint || "not configured"}`);
3333
3663
  }
3334
3664
  }
3665
+ this.log(`Approval nonce: ${this.approvalNonce}`);
3335
3666
  const httpPort = parseInt(process.env.PROTECT_MCP_HTTP_PORT || "9876", 10);
3336
3667
  if (httpPort > 0) {
3337
3668
  try {
3338
3669
  startStatusServer(
3339
3670
  { port: httpPort, mode, verbose },
3340
- this.receiptBuffer
3671
+ this.receiptBuffer,
3672
+ this.approvalStore,
3673
+ this.approvalNonce
3341
3674
  );
3342
3675
  } catch {
3343
3676
  if (verbose) this.log(`HTTP status server could not start on port ${httpPort}`);
@@ -3439,8 +3772,31 @@ var ProtectGateway = class {
3439
3772
  async interceptToolCall(request) {
3440
3773
  const toolName = request.params?.name || "unknown";
3441
3774
  const requestId = (0, import_node_crypto2.randomUUID)().slice(0, 12);
3442
- const toolPolicy = getToolPolicy(toolName, this.config.policy);
3443
3775
  const mode = this.config.enforce ? "enforce" : "shadow";
3776
+ let resolvedAgentKid = this.admissionResult?.agent_id;
3777
+ let effectiveToolPolicy;
3778
+ if (this.config.multiAgent?.enabled) {
3779
+ const paramKid = request.params?._passport_kid;
3780
+ if (paramKid) resolvedAgentKid = paramKid;
3781
+ const agentOverrides = resolvedAgentKid ? this.config.multiAgent.agentPolicies?.[resolvedAgentKid] : void 0;
3782
+ if (agentOverrides && agentOverrides[toolName]) {
3783
+ effectiveToolPolicy = { ...getToolPolicy(toolName, this.config.policy), ...agentOverrides[toolName] };
3784
+ } else if (!resolvedAgentKid && this.config.multiAgent.unknownAgentPolicy === "deny") {
3785
+ this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "unknown_agent_denied", request_id: requestId, tier: this.currentTier });
3786
+ if (this.config.enforce) {
3787
+ return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied: unidentified agent`);
3788
+ }
3789
+ return null;
3790
+ } else {
3791
+ effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
3792
+ }
3793
+ if (this.config.verbose && resolvedAgentKid) {
3794
+ this.log(`Multi-agent: resolved kid=${resolvedAgentKid} for tool=${toolName}`);
3795
+ }
3796
+ } else {
3797
+ effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
3798
+ }
3799
+ const toolPolicy = effectiveToolPolicy;
3444
3800
  let credentialRef;
3445
3801
  if (this.config.credentials) {
3446
3802
  const cred = resolveCredential(toolName, this.config.credentials);
@@ -3491,6 +3847,32 @@ var ProtectGateway = class {
3491
3847
  }
3492
3848
  return null;
3493
3849
  }
3850
+ if (toolPolicy.require_approval) {
3851
+ const grant = this.approvalStore.get(requestId);
3852
+ const alwaysGrant = this.approvalStore.get(`always:${toolName}`);
3853
+ if (grant && Date.now() < grant.expires_at || alwaysGrant && Date.now() < alwaysGrant.expires_at) {
3854
+ if (grant && grant.mode === "once") this.approvalStore.delete(requestId);
3855
+ this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "approval_granted", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
3856
+ return null;
3857
+ }
3858
+ this.emitDecisionLog({ tool: toolName, decision: "require_approval", reason_code: "requires_human_approval", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
3859
+ if (this.config.enforce) {
3860
+ return {
3861
+ jsonrpc: "2.0",
3862
+ id: request.id,
3863
+ result: {
3864
+ content: [
3865
+ {
3866
+ type: "text",
3867
+ 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.`
3868
+ }
3869
+ ],
3870
+ isError: true
3871
+ }
3872
+ };
3873
+ }
3874
+ return null;
3875
+ }
3494
3876
  const rateSpec = this.getTierRateLimit(toolPolicy, this.currentTier);
3495
3877
  if (rateSpec) {
3496
3878
  try {
@@ -3548,6 +3930,10 @@ var ProtectGateway = class {
3548
3930
  if (signed.signed) {
3549
3931
  process.stderr.write(`[PROTECT_MCP_RECEIPT] ${signed.signed}
3550
3932
  `);
3933
+ try {
3934
+ (0, import_node_fs5.appendFileSync)(this.receiptFilePath, signed.signed + "\n");
3935
+ } catch {
3936
+ }
3551
3937
  this.receiptBuffer.add(log.request_id, signed.signed);
3552
3938
  if (this.admissionResult?.agent_id) {
3553
3939
  this.evidenceStore.record(this.admissionResult.agent_id, this.config.signing?.issuer || "protect-mcp");
@@ -3585,17 +3971,154 @@ var ProtectGateway = class {
3585
3971
  }
3586
3972
  };
3587
3973
 
3974
+ // src/simulate.ts
3975
+ var import_node_fs6 = require("fs");
3976
+ function parseLogFile(path) {
3977
+ const raw = (0, import_node_fs6.readFileSync)(path, "utf-8");
3978
+ const entries = [];
3979
+ for (const line of raw.split("\n")) {
3980
+ const trimmed = line.trim();
3981
+ if (!trimmed) continue;
3982
+ const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
3983
+ try {
3984
+ const parsed = JSON.parse(jsonStr);
3985
+ if (parsed.tool && parsed.decision) {
3986
+ entries.push(parsed);
3987
+ }
3988
+ } catch {
3989
+ }
3990
+ }
3991
+ return entries;
3992
+ }
3993
+ function simulate(entries, policy, tier = "unknown") {
3994
+ const rateLimitStore = /* @__PURE__ */ new Map();
3995
+ const toolResults = /* @__PURE__ */ new Map();
3996
+ const totals = {
3997
+ allow: 0,
3998
+ block: 0,
3999
+ rate_limited: 0,
4000
+ require_approval: 0,
4001
+ tier_insufficient: 0
4002
+ };
4003
+ const originalTotals = { allow: 0, deny: 0 };
4004
+ const changes = [];
4005
+ for (const entry of entries) {
4006
+ const toolName = entry.tool;
4007
+ const toolPolicy = getToolPolicy(toolName, policy);
4008
+ if (entry.decision === "allow") {
4009
+ originalTotals.allow++;
4010
+ } else {
4011
+ originalTotals.deny++;
4012
+ }
4013
+ let newDecision;
4014
+ if (toolPolicy.block) {
4015
+ newDecision = "block";
4016
+ } else if (toolPolicy.min_tier && !meetsMinTier(tier, toolPolicy.min_tier)) {
4017
+ newDecision = "tier_insufficient";
4018
+ } else if (toolPolicy.require_approval) {
4019
+ newDecision = "require_approval";
4020
+ } else if (toolPolicy.rate_limit) {
4021
+ const limit = parseRateLimit(toolPolicy.rate_limit);
4022
+ const result = checkRateLimit(toolName, limit, rateLimitStore);
4023
+ newDecision = result.allowed ? "allow" : "rate_limited";
4024
+ } else {
4025
+ newDecision = "allow";
4026
+ }
4027
+ totals[newDecision]++;
4028
+ if (!toolResults.has(toolName)) {
4029
+ toolResults.set(toolName, {
4030
+ tool: toolName,
4031
+ calls: 0,
4032
+ results: { allow: 0, block: 0, rate_limited: 0, require_approval: 0, tier_insufficient: 0 },
4033
+ original: { allow: 0, deny: 0 }
4034
+ });
4035
+ }
4036
+ const tr = toolResults.get(toolName);
4037
+ tr.calls++;
4038
+ tr.results[newDecision]++;
4039
+ if (entry.decision === "allow") {
4040
+ tr.original.allow++;
4041
+ } else {
4042
+ tr.original.deny++;
4043
+ }
4044
+ }
4045
+ for (const [tool, result] of toolResults) {
4046
+ const wasAllBlocked = result.original.allow === 0;
4047
+ const nowAllBlocked = result.results.allow === 0;
4048
+ const wasAllAllowed = result.original.deny === 0;
4049
+ if (wasAllAllowed && result.results.block > 0) {
4050
+ changes.push(`${tool}: ${result.results.block} calls would be blocked (was: all allowed)`);
4051
+ }
4052
+ if (wasAllAllowed && result.results.rate_limited > 0) {
4053
+ changes.push(`${tool}: ${result.results.rate_limited} calls would be rate-limited (was: all allowed)`);
4054
+ }
4055
+ if (wasAllAllowed && result.results.require_approval > 0) {
4056
+ changes.push(`${tool}: ${result.results.require_approval} calls would require approval (was: all allowed)`);
4057
+ }
4058
+ if (wasAllAllowed && result.results.tier_insufficient > 0) {
4059
+ changes.push(`${tool}: ${result.results.tier_insufficient} calls would fail tier check (was: all allowed)`);
4060
+ }
4061
+ if (wasAllBlocked && result.results.allow > 0 && !nowAllBlocked) {
4062
+ changes.push(`${tool}: ${result.results.allow} calls would now be allowed (was: all blocked)`);
4063
+ }
4064
+ }
4065
+ return {
4066
+ policy_file: "",
4067
+ log_file: "",
4068
+ total_calls: entries.length,
4069
+ results: totals,
4070
+ original: originalTotals,
4071
+ tool_breakdown: Array.from(toolResults.values()).sort((a, b) => b.calls - a.calls),
4072
+ changes
4073
+ };
4074
+ }
4075
+ function formatSimulation(summary) {
4076
+ const lines = [];
4077
+ lines.push(`Simulating ${summary.policy_file} against ${summary.total_calls} recorded tool calls:
4078
+ `);
4079
+ const maxToolLen = Math.max(...summary.tool_breakdown.map((t) => t.tool.length), 4);
4080
+ for (const tr of summary.tool_breakdown) {
4081
+ const parts = [];
4082
+ if (tr.results.allow > 0) parts.push(`${tr.results.allow} allow`);
4083
+ if (tr.results.block > 0) parts.push(`\x1B[31m${tr.results.block} blocked\x1B[0m`);
4084
+ if (tr.results.rate_limited > 0) parts.push(`\x1B[33m${tr.results.rate_limited} rate_limited\x1B[0m`);
4085
+ if (tr.results.require_approval > 0) parts.push(`\x1B[36m${tr.results.require_approval} require_approval\x1B[0m`);
4086
+ if (tr.results.tier_insufficient > 0) parts.push(`\x1B[35m${tr.results.tier_insufficient} tier_insufficient\x1B[0m`);
4087
+ const originalParts = [];
4088
+ if (tr.original.allow > 0) originalParts.push(`${tr.original.allow} allow`);
4089
+ if (tr.original.deny > 0) originalParts.push(`${tr.original.deny} deny`);
4090
+ lines.push(` ${tr.tool.padEnd(maxToolLen)} \xD7 ${String(tr.calls).padStart(3)} \u2192 ${parts.join(", ")} (was: ${originalParts.join(", ")})`);
4091
+ }
4092
+ lines.push("");
4093
+ 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`);
4094
+ lines.push(` vs original: ${summary.original.allow} allow, ${summary.original.deny} deny`);
4095
+ if (summary.changes.length > 0) {
4096
+ lines.push("");
4097
+ lines.push("Changes:");
4098
+ for (const change of summary.changes) {
4099
+ lines.push(` \u2022 ${change}`);
4100
+ }
4101
+ }
4102
+ return lines.join("\n");
4103
+ }
4104
+
3588
4105
  // src/cli.ts
3589
- var import_meta = {};
3590
4106
  function printHelp() {
3591
4107
  process.stderr.write(`
3592
4108
  protect-mcp \u2014 Shadow-mode security gateway for MCP servers
3593
4109
 
3594
4110
  Usage:
3595
4111
  protect-mcp [options] -- <command> [args...]
4112
+ protect-mcp quickstart
3596
4113
  protect-mcp init [--dir <path>]
3597
4114
  protect-mcp demo
4115
+ protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]
3598
4116
  protect-mcp status [--dir <path>]
4117
+ protect-mcp digest [--today] [--dir <path>]
4118
+ protect-mcp receipts [--last <n>] [--dir <path>]
4119
+ protect-mcp bundle [--output <path>] [--dir <path>]
4120
+ protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]
4121
+ protect-mcp report [--period <days>d] [--format md|json] [--output <path>] [--dir <path>]
3599
4122
 
3600
4123
  Options:
3601
4124
  --policy <path> Policy/config JSON file (default: allow-all)
@@ -3605,17 +4128,26 @@ Options:
3605
4128
  --help Show this help
3606
4129
 
3607
4130
  Commands:
4131
+ quickstart Zero-config onboarding: init + demo + show receipts in one command
3608
4132
  init Generate config template, Ed25519 keypair, and sample policy
3609
4133
  demo Start a demo server wrapped with protect-mcp (see receipts instantly)
4134
+ trace <id> Visualize the receipt DAG from a given receipt_id (ASCII tree)
3610
4135
  status Show tool call statistics from the local decision log
4136
+ digest Generate a human-readable summary of agent activity
4137
+ receipts Show recent persisted signed receipts
4138
+ bundle Export an offline-verifiable audit bundle
3611
4139
 
3612
4140
  Examples:
4141
+ protect-mcp quickstart
3613
4142
  protect-mcp -- node my-server.js
3614
4143
  protect-mcp --policy protect-mcp.json -- node my-server.js
3615
- protect-mcp --policy protect-mcp.json --enforce -- node my-server.js
3616
4144
  protect-mcp init
3617
4145
  protect-mcp demo
4146
+ protect-mcp trace sha256:abc123 --depth 5
3618
4147
  protect-mcp status
4148
+ protect-mcp digest --today
4149
+ protect-mcp receipts --last 10
4150
+ protect-mcp bundle --output audit.json
3619
4151
 
3620
4152
  `);
3621
4153
  }
@@ -3659,7 +4191,7 @@ function parseArgs(argv) {
3659
4191
  return { policyPath, slug, enforce, verbose, childCommand };
3660
4192
  }
3661
4193
  async function handleInit(argv) {
3662
- const { writeFileSync: writeFileSync2, existsSync: existsSync4, mkdirSync } = await import("fs");
4194
+ const { writeFileSync: writeFileSync2, existsSync: existsSync5, mkdirSync } = await import("fs");
3663
4195
  const { join: join4 } = await import("path");
3664
4196
  let dir = process.cwd();
3665
4197
  const dirIdx = argv.indexOf("--dir");
@@ -3669,21 +4201,18 @@ async function handleInit(argv) {
3669
4201
  const configPath = join4(dir, "protect-mcp.json");
3670
4202
  const keysDir = join4(dir, "keys");
3671
4203
  const keyPath = join4(keysDir, "gateway.json");
3672
- if (existsSync4(configPath)) {
4204
+ if (existsSync5(configPath)) {
3673
4205
  process.stderr.write(`[PROTECT_MCP] Config already exists at ${configPath}
3674
4206
  `);
3675
4207
  process.stderr.write("[PROTECT_MCP] Delete it first if you want to regenerate.\n");
3676
4208
  process.exit(1);
3677
4209
  }
3678
4210
  let keypair;
3679
- try {
3680
- const artifacts = await import("@veritasacta/artifacts");
3681
- keypair = artifacts.generateKeypair();
3682
- } catch {
3683
- const { randomBytes: randomBytes2 } = await import("crypto");
4211
+ {
4212
+ const { randomBytes: randomBytes3 } = await import("crypto");
3684
4213
  const { ed25519: ed255192 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
3685
4214
  const { bytesToHex: bytesToHex2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
3686
- const privateKey = randomBytes2(32);
4215
+ const privateKey = randomBytes3(32);
3687
4216
  const publicKey = ed255192.getPublicKey(privateKey);
3688
4217
  keypair = {
3689
4218
  privateKey: bytesToHex2(privateKey),
@@ -3691,7 +4220,7 @@ async function handleInit(argv) {
3691
4220
  kid: "generated"
3692
4221
  };
3693
4222
  }
3694
- if (!existsSync4(keysDir)) {
4223
+ if (!existsSync5(keysDir)) {
3695
4224
  mkdirSync(keysDir, { recursive: true });
3696
4225
  }
3697
4226
  writeFileSync2(keyPath, JSON.stringify({
@@ -3702,7 +4231,7 @@ async function handleInit(argv) {
3702
4231
  warning: "KEEP THIS FILE SECRET. Never commit to version control."
3703
4232
  }, null, 2) + "\n");
3704
4233
  const gitignorePath = join4(keysDir, ".gitignore");
3705
- if (!existsSync4(gitignorePath)) {
4234
+ if (!existsSync5(gitignorePath)) {
3706
4235
  writeFileSync2(gitignorePath, "# Never commit signing keys\n*.json\n");
3707
4236
  }
3708
4237
  const config = {
@@ -3775,14 +4304,13 @@ Add --enforce when ready to block policy violations.
3775
4304
  `);
3776
4305
  }
3777
4306
  async function handleDemo() {
3778
- const { existsSync: existsSync4 } = await import("fs");
3779
- const { join: join4, dirname } = await import("path");
3780
- const { fileURLToPath } = await import("url");
3781
- const __filename = fileURLToPath(import_meta.url);
3782
- const __dirname = dirname(__filename);
3783
- const demoServerPath = join4(__dirname, "demo-server.js");
4307
+ const { existsSync: existsSync5 } = await import("fs");
4308
+ const { join: join4, dirname, resolve } = await import("path");
4309
+ const cliPath = resolve(process.argv[1] || "dist/cli.js");
4310
+ const cliDir = dirname(cliPath);
4311
+ const demoServerPath = join4(cliDir, "demo-server.js");
3784
4312
  const configPath = join4(process.cwd(), "protect-mcp.json");
3785
- const hasConfig = existsSync4(configPath);
4313
+ const hasConfig = existsSync5(configPath);
3786
4314
  if (!hasConfig) {
3787
4315
  process.stderr.write(`
3788
4316
  ${bold("protect-mcp demo")}
@@ -3856,7 +4384,7 @@ Starting demo server with 5 tools...
3856
4384
  await gateway.start();
3857
4385
  }
3858
4386
  async function handleStatus2(argv) {
3859
- const { readFileSync: readFileSync5, existsSync: existsSync4 } = await import("fs");
4387
+ const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
3860
4388
  const { join: join4 } = await import("path");
3861
4389
  let dir = process.cwd();
3862
4390
  const dirIdx = argv.indexOf("--dir");
@@ -3864,7 +4392,7 @@ async function handleStatus2(argv) {
3864
4392
  dir = argv[dirIdx + 1];
3865
4393
  }
3866
4394
  const logPath = join4(dir, ".protect-mcp-log.jsonl");
3867
- if (!existsSync4(logPath)) {
4395
+ if (!existsSync5(logPath)) {
3868
4396
  process.stderr.write(`${bold("protect-mcp status")}
3869
4397
 
3870
4398
  `);
@@ -3874,7 +4402,7 @@ async function handleStatus2(argv) {
3874
4402
  `);
3875
4403
  process.exit(0);
3876
4404
  }
3877
- const raw = readFileSync5(logPath, "utf-8");
4405
+ const raw = readFileSync7(logPath, "utf-8");
3878
4406
  const lines = raw.trim().split("\n").filter(Boolean);
3879
4407
  if (lines.length === 0) {
3880
4408
  process.stderr.write(`${bold("protect-mcp status")}
@@ -3954,9 +4482,9 @@ ${bold("protect-mcp status")}
3954
4482
  `);
3955
4483
  }
3956
4484
  const evidencePath = join4(dir, ".protect-mcp-evidence.json");
3957
- if (existsSync4(evidencePath)) {
4485
+ if (existsSync5(evidencePath)) {
3958
4486
  try {
3959
- const evidenceRaw = readFileSync5(evidencePath, "utf-8");
4487
+ const evidenceRaw = readFileSync7(evidencePath, "utf-8");
3960
4488
  const evidence = JSON.parse(evidenceRaw);
3961
4489
  const agentCount = Object.keys(evidence.agents || {}).length;
3962
4490
  process.stdout.write(`
@@ -3965,6 +4493,27 @@ ${bold("protect-mcp status")}
3965
4493
  } catch {
3966
4494
  }
3967
4495
  }
4496
+ const keyPath = join4(dir, "keys", "gateway.json");
4497
+ if (existsSync5(keyPath)) {
4498
+ try {
4499
+ const keyData = JSON.parse(readFileSync7(keyPath, "utf-8"));
4500
+ if (keyData.publicKey) {
4501
+ const fingerprint = keyData.publicKey.slice(0, 16) + "...";
4502
+ process.stdout.write(`
4503
+ ${bold("\u{1F6E1}\uFE0F Passport identity:")}
4504
+ `);
4505
+ process.stdout.write(` Public key: ${fingerprint}
4506
+ `);
4507
+ if (keyData.kid) process.stdout.write(` Key ID: ${keyData.kid}
4508
+ `);
4509
+ process.stdout.write(` Issuer: ${keyData.issuer || "protect-mcp"}
4510
+ `);
4511
+ process.stdout.write(` Verify: ${dim("npx @veritasacta/verify <receipt.json>")}
4512
+ `);
4513
+ }
4514
+ } catch {
4515
+ }
4516
+ }
3968
4517
  process.stdout.write(`
3969
4518
  Log file: ${dim(logPath)}
3970
4519
 
@@ -3985,12 +4534,502 @@ function red(s) {
3985
4534
  function yellow(s) {
3986
4535
  return process.env.NO_COLOR ? s : `\x1B[33m${s}\x1B[0m`;
3987
4536
  }
4537
+ async function handleDigest(argv) {
4538
+ const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
4539
+ const { join: join4 } = await import("path");
4540
+ let dir = process.cwd();
4541
+ const dirIdx = argv.indexOf("--dir");
4542
+ if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
4543
+ const today = argv.includes("--today");
4544
+ const logPath = join4(dir, ".protect-mcp-log.jsonl");
4545
+ if (!existsSync5(logPath)) {
4546
+ process.stderr.write(`${bold("protect-mcp digest")}
4547
+
4548
+ No log file found. Run protect-mcp first.
4549
+ `);
4550
+ process.exit(0);
4551
+ }
4552
+ const raw = readFileSync7(logPath, "utf-8");
4553
+ const lines = raw.trim().split("\n").filter(Boolean);
4554
+ let entries = [];
4555
+ for (const line of lines) {
4556
+ try {
4557
+ entries.push(JSON.parse(line));
4558
+ } catch {
4559
+ }
4560
+ }
4561
+ if (today) {
4562
+ const todayStart = /* @__PURE__ */ new Date();
4563
+ todayStart.setHours(0, 0, 0, 0);
4564
+ entries = entries.filter((e) => e.timestamp >= todayStart.getTime());
4565
+ }
4566
+ if (entries.length === 0) {
4567
+ process.stdout.write(`
4568
+ ${bold("\u{1F6E1}\uFE0F Agent Digest")}
4569
+
4570
+ No activity${today ? " today" : ""}.
4571
+
4572
+ `);
4573
+ process.exit(0);
4574
+ }
4575
+ const allowed = entries.filter((e) => e.decision === "allow").length;
4576
+ const denied = entries.filter((e) => e.decision === "deny").length;
4577
+ const approvalRequired = entries.filter((e) => e.decision === "require_approval").length;
4578
+ const toolUsage = /* @__PURE__ */ new Map();
4579
+ for (const e of entries) {
4580
+ toolUsage.set(e.tool, (toolUsage.get(e.tool) || 0) + 1);
4581
+ }
4582
+ const sortedTools = [...toolUsage.entries()].sort((a, b) => b[1] - a[1]);
4583
+ const currentTier = entries[entries.length - 1]?.tier || "unknown";
4584
+ const firstTime = new Date(Math.min(...entries.map((e) => e.timestamp)));
4585
+ const lastTime = new Date(Math.max(...entries.map((e) => e.timestamp)));
4586
+ const durationMs = lastTime.getTime() - firstTime.getTime();
4587
+ const durationStr = durationMs < 6e4 ? `${Math.round(durationMs / 1e3)}s` : durationMs < 36e5 ? `${Math.round(durationMs / 6e4)}m` : `${(durationMs / 36e5).toFixed(1)}h`;
4588
+ process.stdout.write(`
4589
+ ${bold("\u{1F6E1}\uFE0F Agent Daily Digest")}
4590
+
4591
+ `);
4592
+ process.stdout.write(` \u{1F4CA} ${bold(String(entries.length))} actions | `);
4593
+ process.stdout.write(`${green("\u2713 " + allowed)} allowed | `);
4594
+ process.stdout.write(`${red("\u2717 " + denied)} blocked`);
4595
+ if (approvalRequired > 0) process.stdout.write(` | ${yellow("\u23F3 " + approvalRequired)} awaiting approval`);
4596
+ process.stdout.write(`
4597
+ `);
4598
+ process.stdout.write(` \u{1F3C5} Trust tier: ${bold(currentTier)} | \u23F1 Active: ${durationStr}
4599
+
4600
+ `);
4601
+ process.stdout.write(` ${bold("Tools used:")}
4602
+ `);
4603
+ for (const [tool, count] of sortedTools.slice(0, 8)) {
4604
+ process.stdout.write(` ${tool.padEnd(22)} ${count}x
4605
+ `);
4606
+ }
4607
+ if (denied > 0) {
4608
+ const deniedTools = entries.filter((e) => e.decision === "deny");
4609
+ const deniedToolNames = [...new Set(deniedTools.map((e) => e.tool))];
4610
+ process.stdout.write(`
4611
+ ${bold(red("Blocked tools:"))}
4612
+ `);
4613
+ for (const tool of deniedToolNames) {
4614
+ const reason = deniedTools.find((e) => e.tool === tool)?.reason_code || "policy";
4615
+ process.stdout.write(` ${red("\u2717")} ${tool} (${reason})
4616
+ `);
4617
+ }
4618
+ }
4619
+ process.stdout.write(`
4620
+ ${dim("Latest receipt: curl -s http://127.0.0.1:9876/receipts/latest | jq -r .receipt > receipt.json")}
4621
+ `);
4622
+ process.stdout.write(` ${dim("Verify: npx @veritasacta/verify receipt.json --key <public-key-hex>")}
4623
+ `);
4624
+ process.stdout.write(` ${dim("Export: npx protect-mcp bundle --output audit.json")}
4625
+
4626
+ `);
4627
+ }
4628
+ async function handleReceipts2(argv) {
4629
+ const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
4630
+ const { join: join4 } = await import("path");
4631
+ let dir = process.cwd();
4632
+ const dirIdx = argv.indexOf("--dir");
4633
+ if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
4634
+ const lastIdx = argv.indexOf("--last");
4635
+ const count = lastIdx !== -1 && argv[lastIdx + 1] ? parseInt(argv[lastIdx + 1], 10) : 20;
4636
+ const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
4637
+ if (!existsSync5(receiptsPath)) {
4638
+ process.stderr.write(`${bold("protect-mcp receipts")}
4639
+
4640
+ No signed receipt file found. Run protect-mcp with signing enabled first.
4641
+ `);
4642
+ process.exit(0);
4643
+ }
4644
+ const raw = readFileSync7(receiptsPath, "utf-8");
4645
+ const lines = raw.trim().split("\n").filter(Boolean);
4646
+ const recent = lines.slice(-count);
4647
+ process.stdout.write(`
4648
+ ${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
4649
+
4650
+ `);
4651
+ for (const line of recent) {
4652
+ try {
4653
+ const entry = JSON.parse(line);
4654
+ const payload = entry.payload || {};
4655
+ const time = typeof entry.issued_at === "string" ? new Date(entry.issued_at).toLocaleTimeString() : "unknown";
4656
+ const decision = payload.decision || "unknown";
4657
+ const icon = decision === "allow" ? green("\u2713") : decision === "require_approval" ? yellow("\u23F3") : red("\u2717");
4658
+ process.stdout.write(` ${dim(time)} ${icon} ${String(payload.tool || "unknown").padEnd(22)} ${String(entry.type || "receipt").padEnd(18)} ${dim(String(payload.reason_code || "signed"))}
4659
+ `);
4660
+ } catch {
4661
+ }
4662
+ }
4663
+ process.stdout.write(`
4664
+ `);
4665
+ }
4666
+ async function handleBundle(argv) {
4667
+ const { readFileSync: readFileSync7, writeFileSync: writeFileSync2, existsSync: existsSync5 } = await import("fs");
4668
+ const { join: join4 } = await import("path");
4669
+ const { createAuditBundle: createAuditBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
4670
+ let dir = process.cwd();
4671
+ const dirIdx = argv.indexOf("--dir");
4672
+ if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
4673
+ const outputIdx = argv.indexOf("--output");
4674
+ const outputPath = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : join4(dir, "audit-bundle.json");
4675
+ const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
4676
+ const keyPath = join4(dir, "keys", "gateway.json");
4677
+ if (!existsSync5(receiptsPath)) {
4678
+ process.stderr.write(`${bold("protect-mcp bundle")}
4679
+
4680
+ No signed receipt file found. Run protect-mcp with signing enabled first.
4681
+ `);
4682
+ process.exit(0);
4683
+ }
4684
+ if (!existsSync5(keyPath)) {
4685
+ process.stderr.write(`${bold("protect-mcp bundle")}
4686
+
4687
+ No key file found at ${keyPath}
4688
+ `);
4689
+ process.exit(1);
4690
+ }
4691
+ const receipts = readFileSync7(receiptsPath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
4692
+ const keyData = JSON.parse(readFileSync7(keyPath, "utf-8"));
4693
+ const bundle = createAuditBundle2({
4694
+ tenant: keyData.issuer || "protect-mcp",
4695
+ receipts,
4696
+ signingKeys: [{
4697
+ kty: "OKP",
4698
+ crv: "Ed25519",
4699
+ kid: keyData.kid || "unknown",
4700
+ x: Buffer.from(keyData.publicKey, "hex").toString("base64url"),
4701
+ use: "sig"
4702
+ }]
4703
+ });
4704
+ writeFileSync2(outputPath, JSON.stringify(bundle, null, 2) + "\n");
4705
+ process.stdout.write(`
4706
+ ${bold("protect-mcp bundle")}
4707
+
4708
+ `);
4709
+ process.stdout.write(` Receipts: ${receipts.length}
4710
+ `);
4711
+ process.stdout.write(` Output: ${outputPath}
4712
+ `);
4713
+ process.stdout.write(` Verify: npx @veritasacta/verify ${outputPath} --bundle
4714
+
4715
+ `);
4716
+ }
4717
+ async function handleQuickstart() {
4718
+ const { mkdtempSync, writeFileSync: writeFileSync2, existsSync: existsSync5, mkdirSync, readFileSync: readFileSync7 } = await import("fs");
4719
+ const { join: join4 } = await import("path");
4720
+ const { tmpdir } = await import("os");
4721
+ const dir = mkdtempSync(join4(tmpdir(), "protect-mcp-quickstart-"));
4722
+ process.stdout.write(`
4723
+ ${bold("protect-mcp quickstart")}
4724
+ `);
4725
+ process.stdout.write(`${"\u2500".repeat(50)}
4726
+
4727
+ `);
4728
+ process.stdout.write(` This will:
4729
+ `);
4730
+ process.stdout.write(` 1. Generate an Ed25519 signing keypair
4731
+ `);
4732
+ process.stdout.write(` 2. Create a shadow-mode policy
4733
+ `);
4734
+ process.stdout.write(` 3. Start a demo MCP server with protect-mcp wrapping it
4735
+ `);
4736
+ process.stdout.write(` 4. Log signed receipts for every tool call
4737
+
4738
+ `);
4739
+ process.stdout.write(` Working dir: ${dir}
4740
+
4741
+ `);
4742
+ const keysDir = join4(dir, "keys");
4743
+ mkdirSync(keysDir, { recursive: true });
4744
+ const { randomBytes: randomBytes3 } = await import("crypto");
4745
+ let keypair;
4746
+ try {
4747
+ const { ed25519: ed255192 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
4748
+ const { bytesToHex: bytesToHex2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
4749
+ const privateKey = randomBytes3(32);
4750
+ const publicKey = ed255192.getPublicKey(privateKey);
4751
+ keypair = {
4752
+ privateKey: bytesToHex2(privateKey),
4753
+ publicKey: bytesToHex2(publicKey),
4754
+ kid: `quickstart-${Date.now()}`
4755
+ };
4756
+ } catch {
4757
+ keypair = {
4758
+ privateKey: randomBytes3(32).toString("hex"),
4759
+ publicKey: randomBytes3(32).toString("hex"),
4760
+ kid: `quickstart-${Date.now()}`
4761
+ };
4762
+ }
4763
+ writeFileSync2(join4(keysDir, "gateway.json"), JSON.stringify({
4764
+ privateKey: keypair.privateKey,
4765
+ publicKey: keypair.publicKey,
4766
+ kid: keypair.kid,
4767
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
4768
+ }, null, 2) + "\n");
4769
+ const configPath = join4(dir, "protect-mcp.json");
4770
+ const config = {
4771
+ tools: {
4772
+ "*": { rate_limit: "100/hour" },
4773
+ "delete_file": { block: true }
4774
+ },
4775
+ default_tier: "unknown",
4776
+ signing: {
4777
+ key_path: join4(keysDir, "gateway.json"),
4778
+ issuer: "protect-mcp-quickstart",
4779
+ enabled: true
4780
+ }
4781
+ };
4782
+ writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n");
4783
+ process.stdout.write(` \u2713 Keypair generated (kid: ${keypair.kid})
4784
+ `);
4785
+ process.stdout.write(` \u2713 Policy created (shadow mode, all tools logged)
4786
+ `);
4787
+ process.stdout.write(` \u2713 Signing enabled (Ed25519)
4788
+
4789
+ `);
4790
+ process.stdout.write(`${bold("Starting demo server...")}
4791
+
4792
+ `);
4793
+ process.stdout.write(` Every tool call will produce a signed receipt.
4794
+ `);
4795
+ process.stdout.write(` Try it with Claude Desktop or any MCP client.
4796
+
4797
+ `);
4798
+ process.stdout.write(` ${bold("To use in production:")}
4799
+ `);
4800
+ process.stdout.write(` 1. Copy ${configPath} to your project
4801
+ `);
4802
+ process.stdout.write(` 2. Edit tool policies to match your server
4803
+ `);
4804
+ process.stdout.write(` 3. Run: protect-mcp --policy protect-mcp.json -- node your-server.js
4805
+
4806
+ `);
4807
+ process.stdout.write(`${"\u2500".repeat(50)}
4808
+
4809
+ `);
4810
+ process.env.PROTECT_MCP_CONFIG = configPath;
4811
+ await handleDemo();
4812
+ }
4813
+ async function handleTrace(argv) {
4814
+ const receiptId = argv[0];
4815
+ if (!receiptId) {
4816
+ process.stderr.write("[PROTECT_MCP] Usage: protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]\n");
4817
+ process.exit(1);
4818
+ }
4819
+ let endpoint = "https://evidence-indexer.tomjwxf.workers.dev";
4820
+ let depth = 3;
4821
+ for (let i = 1; i < argv.length; i++) {
4822
+ if (argv[i] === "--endpoint" && argv[i + 1]) {
4823
+ endpoint = argv[++i];
4824
+ } else if (argv[i] === "--depth" && argv[i + 1]) {
4825
+ depth = Math.min(10, Math.max(1, parseInt(argv[++i], 10) || 3));
4826
+ }
4827
+ }
4828
+ process.stdout.write(`
4829
+ ${bold("protect-mcp trace")}
4830
+ `);
4831
+ process.stdout.write(`${"\u2500".repeat(60)}
4832
+
4833
+ `);
4834
+ process.stdout.write(` Root: ${receiptId}
4835
+ `);
4836
+ process.stdout.write(` Endpoint: ${endpoint}
4837
+ `);
4838
+ process.stdout.write(` Depth: ${depth}
4839
+
4840
+ `);
4841
+ const url = `${endpoint}/evidence/graph/${encodeURIComponent(receiptId)}?depth=${depth}&direction=both&max=50`;
4842
+ let graphData;
4843
+ try {
4844
+ const resp = await fetch(url);
4845
+ if (!resp.ok) {
4846
+ const body = await resp.text();
4847
+ process.stderr.write(`[PROTECT_MCP] Error fetching graph: ${resp.status} ${body}
4848
+ `);
4849
+ process.exit(1);
4850
+ }
4851
+ graphData = await resp.json();
4852
+ } catch (err) {
4853
+ process.stderr.write(`[PROTECT_MCP] Could not reach evidence indexer at ${endpoint}
4854
+ `);
4855
+ process.stderr.write(`[PROTECT_MCP] Trying local receipts...
4856
+
4857
+ `);
4858
+ await traceLocal(receiptId);
4859
+ return;
4860
+ }
4861
+ if (!graphData.nodes || graphData.nodes.length === 0) {
4862
+ process.stdout.write(` No receipts found for ${receiptId}
4863
+
4864
+ `);
4865
+ return;
4866
+ }
4867
+ process.stdout.write(` ${bold("Evidence DAG")} (${graphData.node_count} nodes, ${graphData.edge_count} edges)
4868
+
4869
+ `);
4870
+ const nodeMap = /* @__PURE__ */ new Map();
4871
+ for (const node of graphData.nodes) {
4872
+ nodeMap.set(node.receipt_id, node);
4873
+ }
4874
+ const childMap = /* @__PURE__ */ new Map();
4875
+ for (const edge of graphData.edges) {
4876
+ if (!childMap.has(edge.from)) childMap.set(edge.from, []);
4877
+ childMap.get(edge.from).push({ to: edge.to, relation: edge.relation });
4878
+ }
4879
+ const rendered = /* @__PURE__ */ new Set();
4880
+ function renderNode(id, prefix, isLast) {
4881
+ const node = nodeMap.get(id);
4882
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
4883
+ const childPrefix = isLast ? " " : "\u2502 ";
4884
+ const typeEmoji = getTypeEmoji(node?.receipt_type || "unknown");
4885
+ const shortId = id.length > 16 ? id.slice(0, 12) + "\u2026" : id;
4886
+ const time = node?.event_time ? new Date(node.event_time).toLocaleTimeString() : "?";
4887
+ const type = node?.receipt_type?.replace("acta:", "") || "unknown";
4888
+ process.stdout.write(`${prefix}${connector}${typeEmoji} ${bold(type)} ${dim(shortId)} ${dim(time)}
4889
+ `);
4890
+ if (rendered.has(id)) {
4891
+ process.stdout.write(`${prefix}${childPrefix}${dim("(cycle \u2014 already rendered)")}
4892
+ `);
4893
+ return;
4894
+ }
4895
+ rendered.add(id);
4896
+ const children = childMap.get(id) || [];
4897
+ for (let i = 0; i < children.length; i++) {
4898
+ const child = children[i];
4899
+ const edgeLabel = dim(`\u2500\u2500[${child.relation}]\u2500\u2500\u25B6`);
4900
+ process.stdout.write(`${prefix}${childPrefix}${edgeLabel}
4901
+ `);
4902
+ renderNode(child.to, prefix + childPrefix, i === children.length - 1);
4903
+ }
4904
+ }
4905
+ const rootNode = nodeMap.get(receiptId);
4906
+ if (rootNode) {
4907
+ const typeEmoji = getTypeEmoji(rootNode.receipt_type);
4908
+ const type = rootNode.receipt_type?.replace("acta:", "") || "unknown";
4909
+ const time = rootNode.event_time ? new Date(rootNode.event_time).toLocaleTimeString() : "?";
4910
+ process.stdout.write(` ${typeEmoji} ${bold(type)} ${dim(receiptId.slice(0, 16) + "\u2026")} ${dim(time)} ${bold("(root)")}
4911
+ `);
4912
+ rendered.add(receiptId);
4913
+ const children = childMap.get(receiptId) || [];
4914
+ for (let i = 0; i < children.length; i++) {
4915
+ const child = children[i];
4916
+ const edgeLabel = dim(`\u2500\u2500[${child.relation}]\u2500\u2500\u25B6`);
4917
+ process.stdout.write(` ${edgeLabel}
4918
+ `);
4919
+ renderNode(child.to, " ", i === children.length - 1);
4920
+ }
4921
+ const incomingEdges = (graphData.edges || []).filter((e) => e.to === receiptId);
4922
+ if (incomingEdges.length > 0) {
4923
+ process.stdout.write(`
4924
+ ${bold("Incoming edges:")}
4925
+ `);
4926
+ for (const edge of incomingEdges) {
4927
+ const fromNode = nodeMap.get(edge.from);
4928
+ const fromType = fromNode?.receipt_type?.replace("acta:", "") || "unknown";
4929
+ process.stdout.write(` \u25C0\u2500\u2500[${edge.relation}]\u2500\u2500 ${getTypeEmoji(fromNode?.receipt_type)} ${fromType} ${dim(edge.from.slice(0, 16) + "\u2026")}
4930
+ `);
4931
+ }
4932
+ }
4933
+ } else {
4934
+ for (const node of graphData.nodes) {
4935
+ const typeEmoji = getTypeEmoji(node.receipt_type);
4936
+ const type = node.receipt_type?.replace("acta:", "") || "unknown";
4937
+ process.stdout.write(` ${typeEmoji} ${bold(type)} ${dim(node.receipt_id.slice(0, 16) + "\u2026")}
4938
+ `);
4939
+ }
4940
+ }
4941
+ process.stdout.write(`
4942
+ ${"\u2500".repeat(60)}
4943
+ `);
4944
+ process.stdout.write(` ${dim(`Fetched from ${endpoint}`)}
4945
+
4946
+ `);
4947
+ }
4948
+ async function traceLocal(receiptId) {
4949
+ const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
4950
+ const { join: join4 } = await import("path");
4951
+ const dir = process.cwd();
4952
+ const receiptsDir = join4(dir, ".protect-mcp", "receipts");
4953
+ if (!existsSync5(receiptsDir)) {
4954
+ process.stdout.write(` No local receipts found in ${receiptsDir}
4955
+
4956
+ `);
4957
+ return;
4958
+ }
4959
+ const { readdirSync } = await import("fs");
4960
+ const files = readdirSync(receiptsDir).filter((f) => f.endsWith(".json"));
4961
+ process.stdout.write(` Scanning ${files.length} local receipts...
4962
+
4963
+ `);
4964
+ const receipts = [];
4965
+ for (const file of files) {
4966
+ try {
4967
+ const content = readFileSync7(join4(receiptsDir, file), "utf-8");
4968
+ const receipt = JSON.parse(content);
4969
+ receipts.push(receipt);
4970
+ } catch {
4971
+ }
4972
+ }
4973
+ const match = receipts.find(
4974
+ (r) => r.signed_claims?.claims?.receipt_id === receiptId || r.receipt_id === receiptId
4975
+ );
4976
+ if (match) {
4977
+ const claims = match.signed_claims?.claims || match;
4978
+ process.stdout.write(` Found: ${getTypeEmoji(claims.receipt_type)} ${bold(claims.receipt_type?.replace("acta:", "") || "unknown")}
4979
+ `);
4980
+ process.stdout.write(` Event: ${claims.event_id || "?"}
4981
+ `);
4982
+ process.stdout.write(` Issuer: ${claims.issuer_id || "?"}
4983
+ `);
4984
+ process.stdout.write(` Time: ${claims.event_time || "?"}
4985
+ `);
4986
+ if (claims.edges && claims.edges.length > 0) {
4987
+ process.stdout.write(`
4988
+ ${bold("Edges:")}
4989
+ `);
4990
+ for (const edge of claims.edges) {
4991
+ process.stdout.write(` \u2500\u2500[${edge.relation}]\u2500\u2500\u25B6 ${dim(edge.receipt_id?.slice(0, 16) + "\u2026")}
4992
+ `);
4993
+ }
4994
+ }
4995
+ } else {
4996
+ process.stdout.write(` Receipt ${receiptId} not found locally.
4997
+ `);
4998
+ }
4999
+ process.stdout.write("\n");
5000
+ }
5001
+ function getTypeEmoji(type) {
5002
+ switch (type) {
5003
+ case "acta:observation":
5004
+ return "\u{1F441} ";
5005
+ case "acta:policy-load":
5006
+ return "\u{1F4CB}";
5007
+ case "acta:approval":
5008
+ return "\u2705";
5009
+ case "acta:decision":
5010
+ return "\u2696\uFE0F ";
5011
+ case "acta:execution":
5012
+ return "\u26A1";
5013
+ case "acta:outcome":
5014
+ return "\u{1F4E6}";
5015
+ case "acta:delegation":
5016
+ return "\u{1F91D}";
5017
+ case "acta:capability-attestation":
5018
+ return "\u{1F3C5}";
5019
+ default:
5020
+ return "\u{1F4C4}";
5021
+ }
5022
+ }
3988
5023
  async function main() {
3989
5024
  const args = process.argv.slice(2);
3990
5025
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
3991
5026
  printHelp();
3992
5027
  process.exit(0);
3993
5028
  }
5029
+ if (args[0] === "quickstart") {
5030
+ await handleQuickstart();
5031
+ return;
5032
+ }
3994
5033
  if (args[0] === "init") {
3995
5034
  await handleInit(args.slice(1));
3996
5035
  process.exit(0);
@@ -4003,6 +5042,30 @@ async function main() {
4003
5042
  await handleStatus2(args.slice(1));
4004
5043
  process.exit(0);
4005
5044
  }
5045
+ if (args[0] === "digest") {
5046
+ await handleDigest(args.slice(1));
5047
+ process.exit(0);
5048
+ }
5049
+ if (args[0] === "receipts") {
5050
+ await handleReceipts2(args.slice(1));
5051
+ process.exit(0);
5052
+ }
5053
+ if (args[0] === "bundle") {
5054
+ await handleBundle(args.slice(1));
5055
+ process.exit(0);
5056
+ }
5057
+ if (args[0] === "trace") {
5058
+ await handleTrace(args.slice(1));
5059
+ process.exit(0);
5060
+ }
5061
+ if (args[0] === "simulate") {
5062
+ await handleSimulate(args.slice(1));
5063
+ process.exit(0);
5064
+ }
5065
+ if (args[0] === "report") {
5066
+ await handleReport(args.slice(1));
5067
+ process.exit(0);
5068
+ }
4006
5069
  const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
4007
5070
  let policy = null;
4008
5071
  let policyDigest = "none";
@@ -4053,6 +5116,85 @@ async function main() {
4053
5116
  const gateway = new ProtectGateway(config);
4054
5117
  await gateway.start();
4055
5118
  }
5119
+ async function handleSimulate(args) {
5120
+ let policyPath = "";
5121
+ let logPath = ".protect-mcp-log.jsonl";
5122
+ let tier = "unknown";
5123
+ let jsonOutput = false;
5124
+ for (let i = 0; i < args.length; i++) {
5125
+ if (args[i] === "--policy" && args[i + 1]) {
5126
+ policyPath = args[++i];
5127
+ } else if (args[i] === "--log" && args[i + 1]) {
5128
+ logPath = args[++i];
5129
+ } else if (args[i] === "--tier" && args[i + 1]) {
5130
+ tier = args[++i];
5131
+ } else if (args[i] === "--json") {
5132
+ jsonOutput = true;
5133
+ }
5134
+ }
5135
+ if (!policyPath) {
5136
+ process.stderr.write("Usage: protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]\n");
5137
+ process.exit(1);
5138
+ }
5139
+ const { existsSync: existsSync5 } = await import("fs");
5140
+ if (!existsSync5(logPath)) {
5141
+ process.stderr.write(`Log file not found: ${logPath}
5142
+ `);
5143
+ process.stderr.write("Run protect-mcp in shadow mode first to generate a log file.\n");
5144
+ process.exit(1);
5145
+ }
5146
+ const { policy } = loadPolicy(policyPath);
5147
+ const entries = parseLogFile(logPath);
5148
+ if (entries.length === 0) {
5149
+ process.stderr.write("No tool call entries found in log file.\n");
5150
+ process.exit(1);
5151
+ }
5152
+ const summary = simulate(entries, policy, tier);
5153
+ summary.policy_file = policyPath;
5154
+ summary.log_file = logPath;
5155
+ if (jsonOutput) {
5156
+ process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
5157
+ } else {
5158
+ process.stdout.write(formatSimulation(summary) + "\n");
5159
+ }
5160
+ }
5161
+ async function handleReport(args) {
5162
+ let period = 30;
5163
+ let format = "json";
5164
+ let outputPath = "";
5165
+ let dir = process.cwd();
5166
+ for (let i = 0; i < args.length; i++) {
5167
+ if (args[i] === "--period" && args[i + 1]) {
5168
+ const match = args[++i].match(/^(\d+)d$/);
5169
+ if (match) period = parseInt(match[1], 10);
5170
+ } else if (args[i] === "--format" && args[i + 1]) {
5171
+ format = args[++i];
5172
+ } else if (args[i] === "--output" && args[i + 1]) {
5173
+ outputPath = args[++i];
5174
+ } else if (args[i] === "--dir" && args[i + 1]) {
5175
+ dir = args[++i];
5176
+ }
5177
+ }
5178
+ const { generateReport: generateReport2, formatReportMarkdown: formatReportMarkdown2 } = await Promise.resolve().then(() => (init_report(), report_exports));
5179
+ const { join: join4 } = await import("path");
5180
+ const logPath = join4(dir, ".protect-mcp-log.jsonl");
5181
+ const receiptPath = join4(dir, ".protect-mcp-receipts.jsonl");
5182
+ const report = generateReport2(logPath, receiptPath, period);
5183
+ let output;
5184
+ if (format === "md") {
5185
+ output = formatReportMarkdown2(report);
5186
+ } else {
5187
+ output = JSON.stringify(report, null, 2);
5188
+ }
5189
+ if (outputPath) {
5190
+ const { writeFileSync: writeFileSync2 } = await import("fs");
5191
+ writeFileSync2(outputPath, output, "utf-8");
5192
+ process.stderr.write(`Report written to ${outputPath}
5193
+ `);
5194
+ } else {
5195
+ process.stdout.write(output + "\n");
5196
+ }
5197
+ }
4056
5198
  main().catch((err) => {
4057
5199
  process.stderr.write(`[PROTECT_MCP] Fatal error: ${err instanceof Error ? err.message : err}
4058
5200
  `);