protect-mcp 0.3.1 → 0.3.3

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
@@ -2697,6 +2697,178 @@ var init_bundle = __esm({
2697
2697
  }
2698
2698
  });
2699
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
+
2700
2872
  // src/gateway.ts
2701
2873
  var import_node_child_process = require("child_process");
2702
2874
  var import_node_crypto2 = require("crypto");
@@ -3019,7 +3191,11 @@ async function initSigning(config) {
3019
3191
  return warnings;
3020
3192
  }
3021
3193
  try {
3022
- artifactsModule = await import("@veritasacta/artifacts");
3194
+ const moduleName = "@veritasacta/artifacts";
3195
+ artifactsModule = await import(
3196
+ /* @vite-ignore */
3197
+ moduleName
3198
+ );
3023
3199
  } catch {
3024
3200
  warnings.push("signing: @veritasacta/artifacts not available \u2014 receipts will be unsigned");
3025
3201
  return warnings;
@@ -3150,6 +3326,28 @@ function formatRequest(context, format) {
3150
3326
  },
3151
3327
  actions: [context.action.operation || "call"]
3152
3328
  };
3329
+ case "cedar":
3330
+ return {
3331
+ principal: {
3332
+ type: "Agent",
3333
+ id: context.actor.id || "unknown"
3334
+ },
3335
+ action: {
3336
+ type: "Action",
3337
+ id: `MCP::Tool::${context.action.operation || "call"}`
3338
+ },
3339
+ resource: {
3340
+ type: "Tool",
3341
+ id: context.action.tool
3342
+ },
3343
+ context: {
3344
+ tier: context.actor.tier,
3345
+ manifest_hash: context.actor.manifest_hash || null,
3346
+ service: context.target.service || "default",
3347
+ mode: context.mode,
3348
+ credential_ref: context.credential_ref || null
3349
+ }
3350
+ };
3153
3351
  case "generic":
3154
3352
  default:
3155
3353
  return context;
@@ -3179,6 +3377,22 @@ function parseResponse(result, format) {
3179
3377
  }
3180
3378
  }
3181
3379
  return { allowed: false, reason: "unrecognized Cerbos response" };
3380
+ case "cedar":
3381
+ if (typeof result.decision === "string") {
3382
+ return {
3383
+ allowed: result.decision === "Allow",
3384
+ reason: result.decision === "Deny" ? `cedar_deny${result.diagnostics ? ": " + JSON.stringify(result.diagnostics) : ""}` : void 0,
3385
+ metadata: result.diagnostics
3386
+ };
3387
+ }
3388
+ if (Array.isArray(result.results) && result.results.length > 0) {
3389
+ const first = result.results[0];
3390
+ return {
3391
+ allowed: first.decision === "Allow",
3392
+ reason: first.decision === "Deny" ? "cedar_deny" : void 0
3393
+ };
3394
+ }
3395
+ return { allowed: false, reason: "unrecognized Cedar response" };
3182
3396
  case "generic":
3183
3397
  default:
3184
3398
  return {
@@ -3596,8 +3810,31 @@ var ProtectGateway = class {
3596
3810
  async interceptToolCall(request) {
3597
3811
  const toolName = request.params?.name || "unknown";
3598
3812
  const requestId = (0, import_node_crypto2.randomUUID)().slice(0, 12);
3599
- const toolPolicy = getToolPolicy(toolName, this.config.policy);
3600
3813
  const mode = this.config.enforce ? "enforce" : "shadow";
3814
+ let resolvedAgentKid = this.admissionResult?.agent_id;
3815
+ let effectiveToolPolicy;
3816
+ if (this.config.multiAgent?.enabled) {
3817
+ const paramKid = request.params?._passport_kid;
3818
+ if (paramKid) resolvedAgentKid = paramKid;
3819
+ const agentOverrides = resolvedAgentKid ? this.config.multiAgent.agentPolicies?.[resolvedAgentKid] : void 0;
3820
+ if (agentOverrides && agentOverrides[toolName]) {
3821
+ effectiveToolPolicy = { ...getToolPolicy(toolName, this.config.policy), ...agentOverrides[toolName] };
3822
+ } else if (!resolvedAgentKid && this.config.multiAgent.unknownAgentPolicy === "deny") {
3823
+ this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "unknown_agent_denied", request_id: requestId, tier: this.currentTier });
3824
+ if (this.config.enforce) {
3825
+ return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied: unidentified agent`);
3826
+ }
3827
+ return null;
3828
+ } else {
3829
+ effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
3830
+ }
3831
+ if (this.config.verbose && resolvedAgentKid) {
3832
+ this.log(`Multi-agent: resolved kid=${resolvedAgentKid} for tool=${toolName}`);
3833
+ }
3834
+ } else {
3835
+ effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
3836
+ }
3837
+ const toolPolicy = effectiveToolPolicy;
3601
3838
  let credentialRef;
3602
3839
  if (this.config.credentials) {
3603
3840
  const cred = resolveCredential(toolName, this.config.credentials);
@@ -3772,6 +4009,137 @@ var ProtectGateway = class {
3772
4009
  }
3773
4010
  };
3774
4011
 
4012
+ // src/simulate.ts
4013
+ var import_node_fs6 = require("fs");
4014
+ function parseLogFile(path) {
4015
+ const raw = (0, import_node_fs6.readFileSync)(path, "utf-8");
4016
+ const entries = [];
4017
+ for (const line of raw.split("\n")) {
4018
+ const trimmed = line.trim();
4019
+ if (!trimmed) continue;
4020
+ const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
4021
+ try {
4022
+ const parsed = JSON.parse(jsonStr);
4023
+ if (parsed.tool && parsed.decision) {
4024
+ entries.push(parsed);
4025
+ }
4026
+ } catch {
4027
+ }
4028
+ }
4029
+ return entries;
4030
+ }
4031
+ function simulate(entries, policy, tier = "unknown") {
4032
+ const rateLimitStore = /* @__PURE__ */ new Map();
4033
+ const toolResults = /* @__PURE__ */ new Map();
4034
+ const totals = {
4035
+ allow: 0,
4036
+ block: 0,
4037
+ rate_limited: 0,
4038
+ require_approval: 0,
4039
+ tier_insufficient: 0
4040
+ };
4041
+ const originalTotals = { allow: 0, deny: 0 };
4042
+ const changes = [];
4043
+ for (const entry of entries) {
4044
+ const toolName = entry.tool;
4045
+ const toolPolicy = getToolPolicy(toolName, policy);
4046
+ if (entry.decision === "allow") {
4047
+ originalTotals.allow++;
4048
+ } else {
4049
+ originalTotals.deny++;
4050
+ }
4051
+ let newDecision;
4052
+ if (toolPolicy.block) {
4053
+ newDecision = "block";
4054
+ } else if (toolPolicy.min_tier && !meetsMinTier(tier, toolPolicy.min_tier)) {
4055
+ newDecision = "tier_insufficient";
4056
+ } else if (toolPolicy.require_approval) {
4057
+ newDecision = "require_approval";
4058
+ } else if (toolPolicy.rate_limit) {
4059
+ const limit = parseRateLimit(toolPolicy.rate_limit);
4060
+ const result = checkRateLimit(toolName, limit, rateLimitStore);
4061
+ newDecision = result.allowed ? "allow" : "rate_limited";
4062
+ } else {
4063
+ newDecision = "allow";
4064
+ }
4065
+ totals[newDecision]++;
4066
+ if (!toolResults.has(toolName)) {
4067
+ toolResults.set(toolName, {
4068
+ tool: toolName,
4069
+ calls: 0,
4070
+ results: { allow: 0, block: 0, rate_limited: 0, require_approval: 0, tier_insufficient: 0 },
4071
+ original: { allow: 0, deny: 0 }
4072
+ });
4073
+ }
4074
+ const tr = toolResults.get(toolName);
4075
+ tr.calls++;
4076
+ tr.results[newDecision]++;
4077
+ if (entry.decision === "allow") {
4078
+ tr.original.allow++;
4079
+ } else {
4080
+ tr.original.deny++;
4081
+ }
4082
+ }
4083
+ for (const [tool, result] of toolResults) {
4084
+ const wasAllBlocked = result.original.allow === 0;
4085
+ const nowAllBlocked = result.results.allow === 0;
4086
+ const wasAllAllowed = result.original.deny === 0;
4087
+ if (wasAllAllowed && result.results.block > 0) {
4088
+ changes.push(`${tool}: ${result.results.block} calls would be blocked (was: all allowed)`);
4089
+ }
4090
+ if (wasAllAllowed && result.results.rate_limited > 0) {
4091
+ changes.push(`${tool}: ${result.results.rate_limited} calls would be rate-limited (was: all allowed)`);
4092
+ }
4093
+ if (wasAllAllowed && result.results.require_approval > 0) {
4094
+ changes.push(`${tool}: ${result.results.require_approval} calls would require approval (was: all allowed)`);
4095
+ }
4096
+ if (wasAllAllowed && result.results.tier_insufficient > 0) {
4097
+ changes.push(`${tool}: ${result.results.tier_insufficient} calls would fail tier check (was: all allowed)`);
4098
+ }
4099
+ if (wasAllBlocked && result.results.allow > 0 && !nowAllBlocked) {
4100
+ changes.push(`${tool}: ${result.results.allow} calls would now be allowed (was: all blocked)`);
4101
+ }
4102
+ }
4103
+ return {
4104
+ policy_file: "",
4105
+ log_file: "",
4106
+ total_calls: entries.length,
4107
+ results: totals,
4108
+ original: originalTotals,
4109
+ tool_breakdown: Array.from(toolResults.values()).sort((a, b) => b.calls - a.calls),
4110
+ changes
4111
+ };
4112
+ }
4113
+ function formatSimulation(summary) {
4114
+ const lines = [];
4115
+ lines.push(`Simulating ${summary.policy_file} against ${summary.total_calls} recorded tool calls:
4116
+ `);
4117
+ const maxToolLen = Math.max(...summary.tool_breakdown.map((t) => t.tool.length), 4);
4118
+ for (const tr of summary.tool_breakdown) {
4119
+ const parts = [];
4120
+ if (tr.results.allow > 0) parts.push(`${tr.results.allow} allow`);
4121
+ if (tr.results.block > 0) parts.push(`\x1B[31m${tr.results.block} blocked\x1B[0m`);
4122
+ if (tr.results.rate_limited > 0) parts.push(`\x1B[33m${tr.results.rate_limited} rate_limited\x1B[0m`);
4123
+ if (tr.results.require_approval > 0) parts.push(`\x1B[36m${tr.results.require_approval} require_approval\x1B[0m`);
4124
+ if (tr.results.tier_insufficient > 0) parts.push(`\x1B[35m${tr.results.tier_insufficient} tier_insufficient\x1B[0m`);
4125
+ const originalParts = [];
4126
+ if (tr.original.allow > 0) originalParts.push(`${tr.original.allow} allow`);
4127
+ if (tr.original.deny > 0) originalParts.push(`${tr.original.deny} deny`);
4128
+ lines.push(` ${tr.tool.padEnd(maxToolLen)} \xD7 ${String(tr.calls).padStart(3)} \u2192 ${parts.join(", ")} (was: ${originalParts.join(", ")})`);
4129
+ }
4130
+ lines.push("");
4131
+ 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`);
4132
+ lines.push(` vs original: ${summary.original.allow} allow, ${summary.original.deny} deny`);
4133
+ if (summary.changes.length > 0) {
4134
+ lines.push("");
4135
+ lines.push("Changes:");
4136
+ for (const change of summary.changes) {
4137
+ lines.push(` \u2022 ${change}`);
4138
+ }
4139
+ }
4140
+ return lines.join("\n");
4141
+ }
4142
+
3775
4143
  // src/cli.ts
3776
4144
  function printHelp() {
3777
4145
  process.stderr.write(`
@@ -3779,12 +4147,16 @@ protect-mcp \u2014 Shadow-mode security gateway for MCP servers
3779
4147
 
3780
4148
  Usage:
3781
4149
  protect-mcp [options] -- <command> [args...]
4150
+ protect-mcp quickstart
3782
4151
  protect-mcp init [--dir <path>]
3783
4152
  protect-mcp demo
4153
+ protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]
3784
4154
  protect-mcp status [--dir <path>]
3785
4155
  protect-mcp digest [--today] [--dir <path>]
3786
4156
  protect-mcp receipts [--last <n>] [--dir <path>]
3787
4157
  protect-mcp bundle [--output <path>] [--dir <path>]
4158
+ protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]
4159
+ protect-mcp report [--period <days>d] [--format md|json] [--output <path>] [--dir <path>]
3788
4160
 
3789
4161
  Options:
3790
4162
  --policy <path> Policy/config JSON file (default: allow-all)
@@ -3794,19 +4166,22 @@ Options:
3794
4166
  --help Show this help
3795
4167
 
3796
4168
  Commands:
4169
+ quickstart Zero-config onboarding: init + demo + show receipts in one command
3797
4170
  init Generate config template, Ed25519 keypair, and sample policy
3798
4171
  demo Start a demo server wrapped with protect-mcp (see receipts instantly)
4172
+ trace <id> Visualize the receipt DAG from a given receipt_id (ASCII tree)
3799
4173
  status Show tool call statistics from the local decision log
3800
4174
  digest Generate a human-readable summary of agent activity
3801
4175
  receipts Show recent persisted signed receipts
3802
4176
  bundle Export an offline-verifiable audit bundle
3803
4177
 
3804
4178
  Examples:
4179
+ protect-mcp quickstart
3805
4180
  protect-mcp -- node my-server.js
3806
4181
  protect-mcp --policy protect-mcp.json -- node my-server.js
3807
- protect-mcp --policy protect-mcp.json --enforce -- node my-server.js
3808
4182
  protect-mcp init
3809
4183
  protect-mcp demo
4184
+ protect-mcp trace sha256:abc123 --depth 5
3810
4185
  protect-mcp status
3811
4186
  protect-mcp digest --today
3812
4187
  protect-mcp receipts --last 10
@@ -3854,7 +4229,7 @@ function parseArgs(argv) {
3854
4229
  return { policyPath, slug, enforce, verbose, childCommand };
3855
4230
  }
3856
4231
  async function handleInit(argv) {
3857
- const { writeFileSync: writeFileSync2, existsSync: existsSync4, mkdirSync } = await import("fs");
4232
+ const { writeFileSync: writeFileSync2, existsSync: existsSync5, mkdirSync } = await import("fs");
3858
4233
  const { join: join4 } = await import("path");
3859
4234
  let dir = process.cwd();
3860
4235
  const dirIdx = argv.indexOf("--dir");
@@ -3864,17 +4239,14 @@ async function handleInit(argv) {
3864
4239
  const configPath = join4(dir, "protect-mcp.json");
3865
4240
  const keysDir = join4(dir, "keys");
3866
4241
  const keyPath = join4(keysDir, "gateway.json");
3867
- if (existsSync4(configPath)) {
4242
+ if (existsSync5(configPath)) {
3868
4243
  process.stderr.write(`[PROTECT_MCP] Config already exists at ${configPath}
3869
4244
  `);
3870
4245
  process.stderr.write("[PROTECT_MCP] Delete it first if you want to regenerate.\n");
3871
4246
  process.exit(1);
3872
4247
  }
3873
4248
  let keypair;
3874
- try {
3875
- const artifacts = await import("@veritasacta/artifacts");
3876
- keypair = artifacts.generateKeypair();
3877
- } catch {
4249
+ {
3878
4250
  const { randomBytes: randomBytes3 } = await import("crypto");
3879
4251
  const { ed25519: ed255192 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
3880
4252
  const { bytesToHex: bytesToHex2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
@@ -3886,7 +4258,7 @@ async function handleInit(argv) {
3886
4258
  kid: "generated"
3887
4259
  };
3888
4260
  }
3889
- if (!existsSync4(keysDir)) {
4261
+ if (!existsSync5(keysDir)) {
3890
4262
  mkdirSync(keysDir, { recursive: true });
3891
4263
  }
3892
4264
  writeFileSync2(keyPath, JSON.stringify({
@@ -3897,7 +4269,7 @@ async function handleInit(argv) {
3897
4269
  warning: "KEEP THIS FILE SECRET. Never commit to version control."
3898
4270
  }, null, 2) + "\n");
3899
4271
  const gitignorePath = join4(keysDir, ".gitignore");
3900
- if (!existsSync4(gitignorePath)) {
4272
+ if (!existsSync5(gitignorePath)) {
3901
4273
  writeFileSync2(gitignorePath, "# Never commit signing keys\n*.json\n");
3902
4274
  }
3903
4275
  const config = {
@@ -3970,13 +4342,13 @@ Add --enforce when ready to block policy violations.
3970
4342
  `);
3971
4343
  }
3972
4344
  async function handleDemo() {
3973
- const { existsSync: existsSync4 } = await import("fs");
4345
+ const { existsSync: existsSync5 } = await import("fs");
3974
4346
  const { join: join4, dirname, resolve } = await import("path");
3975
4347
  const cliPath = resolve(process.argv[1] || "dist/cli.js");
3976
4348
  const cliDir = dirname(cliPath);
3977
4349
  const demoServerPath = join4(cliDir, "demo-server.js");
3978
4350
  const configPath = join4(process.cwd(), "protect-mcp.json");
3979
- const hasConfig = existsSync4(configPath);
4351
+ const hasConfig = existsSync5(configPath);
3980
4352
  if (!hasConfig) {
3981
4353
  process.stderr.write(`
3982
4354
  ${bold("protect-mcp demo")}
@@ -4050,7 +4422,7 @@ Starting demo server with 5 tools...
4050
4422
  await gateway.start();
4051
4423
  }
4052
4424
  async function handleStatus2(argv) {
4053
- const { readFileSync: readFileSync5, existsSync: existsSync4 } = await import("fs");
4425
+ const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
4054
4426
  const { join: join4 } = await import("path");
4055
4427
  let dir = process.cwd();
4056
4428
  const dirIdx = argv.indexOf("--dir");
@@ -4058,7 +4430,7 @@ async function handleStatus2(argv) {
4058
4430
  dir = argv[dirIdx + 1];
4059
4431
  }
4060
4432
  const logPath = join4(dir, ".protect-mcp-log.jsonl");
4061
- if (!existsSync4(logPath)) {
4433
+ if (!existsSync5(logPath)) {
4062
4434
  process.stderr.write(`${bold("protect-mcp status")}
4063
4435
 
4064
4436
  `);
@@ -4068,7 +4440,7 @@ async function handleStatus2(argv) {
4068
4440
  `);
4069
4441
  process.exit(0);
4070
4442
  }
4071
- const raw = readFileSync5(logPath, "utf-8");
4443
+ const raw = readFileSync7(logPath, "utf-8");
4072
4444
  const lines = raw.trim().split("\n").filter(Boolean);
4073
4445
  if (lines.length === 0) {
4074
4446
  process.stderr.write(`${bold("protect-mcp status")}
@@ -4148,9 +4520,9 @@ ${bold("protect-mcp status")}
4148
4520
  `);
4149
4521
  }
4150
4522
  const evidencePath = join4(dir, ".protect-mcp-evidence.json");
4151
- if (existsSync4(evidencePath)) {
4523
+ if (existsSync5(evidencePath)) {
4152
4524
  try {
4153
- const evidenceRaw = readFileSync5(evidencePath, "utf-8");
4525
+ const evidenceRaw = readFileSync7(evidencePath, "utf-8");
4154
4526
  const evidence = JSON.parse(evidenceRaw);
4155
4527
  const agentCount = Object.keys(evidence.agents || {}).length;
4156
4528
  process.stdout.write(`
@@ -4160,9 +4532,9 @@ ${bold("protect-mcp status")}
4160
4532
  }
4161
4533
  }
4162
4534
  const keyPath = join4(dir, "keys", "gateway.json");
4163
- if (existsSync4(keyPath)) {
4535
+ if (existsSync5(keyPath)) {
4164
4536
  try {
4165
- const keyData = JSON.parse(readFileSync5(keyPath, "utf-8"));
4537
+ const keyData = JSON.parse(readFileSync7(keyPath, "utf-8"));
4166
4538
  if (keyData.publicKey) {
4167
4539
  const fingerprint = keyData.publicKey.slice(0, 16) + "...";
4168
4540
  process.stdout.write(`
@@ -4201,21 +4573,21 @@ function yellow(s) {
4201
4573
  return process.env.NO_COLOR ? s : `\x1B[33m${s}\x1B[0m`;
4202
4574
  }
4203
4575
  async function handleDigest(argv) {
4204
- const { readFileSync: readFileSync5, existsSync: existsSync4 } = await import("fs");
4576
+ const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
4205
4577
  const { join: join4 } = await import("path");
4206
4578
  let dir = process.cwd();
4207
4579
  const dirIdx = argv.indexOf("--dir");
4208
4580
  if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
4209
4581
  const today = argv.includes("--today");
4210
4582
  const logPath = join4(dir, ".protect-mcp-log.jsonl");
4211
- if (!existsSync4(logPath)) {
4583
+ if (!existsSync5(logPath)) {
4212
4584
  process.stderr.write(`${bold("protect-mcp digest")}
4213
4585
 
4214
4586
  No log file found. Run protect-mcp first.
4215
4587
  `);
4216
4588
  process.exit(0);
4217
4589
  }
4218
- const raw = readFileSync5(logPath, "utf-8");
4590
+ const raw = readFileSync7(logPath, "utf-8");
4219
4591
  const lines = raw.trim().split("\n").filter(Boolean);
4220
4592
  let entries = [];
4221
4593
  for (const line of lines) {
@@ -4292,7 +4664,7 @@ ${bold("\u{1F6E1}\uFE0F Agent Daily Digest")}
4292
4664
  `);
4293
4665
  }
4294
4666
  async function handleReceipts2(argv) {
4295
- const { readFileSync: readFileSync5, existsSync: existsSync4 } = await import("fs");
4667
+ const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
4296
4668
  const { join: join4 } = await import("path");
4297
4669
  let dir = process.cwd();
4298
4670
  const dirIdx = argv.indexOf("--dir");
@@ -4300,14 +4672,14 @@ async function handleReceipts2(argv) {
4300
4672
  const lastIdx = argv.indexOf("--last");
4301
4673
  const count = lastIdx !== -1 && argv[lastIdx + 1] ? parseInt(argv[lastIdx + 1], 10) : 20;
4302
4674
  const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
4303
- if (!existsSync4(receiptsPath)) {
4675
+ if (!existsSync5(receiptsPath)) {
4304
4676
  process.stderr.write(`${bold("protect-mcp receipts")}
4305
4677
 
4306
4678
  No signed receipt file found. Run protect-mcp with signing enabled first.
4307
4679
  `);
4308
4680
  process.exit(0);
4309
4681
  }
4310
- const raw = readFileSync5(receiptsPath, "utf-8");
4682
+ const raw = readFileSync7(receiptsPath, "utf-8");
4311
4683
  const lines = raw.trim().split("\n").filter(Boolean);
4312
4684
  const recent = lines.slice(-count);
4313
4685
  process.stdout.write(`
@@ -4330,7 +4702,7 @@ ${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
4330
4702
  `);
4331
4703
  }
4332
4704
  async function handleBundle(argv) {
4333
- const { readFileSync: readFileSync5, writeFileSync: writeFileSync2, existsSync: existsSync4 } = await import("fs");
4705
+ const { readFileSync: readFileSync7, writeFileSync: writeFileSync2, existsSync: existsSync5 } = await import("fs");
4334
4706
  const { join: join4 } = await import("path");
4335
4707
  const { createAuditBundle: createAuditBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
4336
4708
  let dir = process.cwd();
@@ -4340,22 +4712,22 @@ async function handleBundle(argv) {
4340
4712
  const outputPath = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : join4(dir, "audit-bundle.json");
4341
4713
  const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
4342
4714
  const keyPath = join4(dir, "keys", "gateway.json");
4343
- if (!existsSync4(receiptsPath)) {
4715
+ if (!existsSync5(receiptsPath)) {
4344
4716
  process.stderr.write(`${bold("protect-mcp bundle")}
4345
4717
 
4346
4718
  No signed receipt file found. Run protect-mcp with signing enabled first.
4347
4719
  `);
4348
4720
  process.exit(0);
4349
4721
  }
4350
- if (!existsSync4(keyPath)) {
4722
+ if (!existsSync5(keyPath)) {
4351
4723
  process.stderr.write(`${bold("protect-mcp bundle")}
4352
4724
 
4353
4725
  No key file found at ${keyPath}
4354
4726
  `);
4355
4727
  process.exit(1);
4356
4728
  }
4357
- const receipts = readFileSync5(receiptsPath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
4358
- const keyData = JSON.parse(readFileSync5(keyPath, "utf-8"));
4729
+ const receipts = readFileSync7(receiptsPath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
4730
+ const keyData = JSON.parse(readFileSync7(keyPath, "utf-8"));
4359
4731
  const bundle = createAuditBundle2({
4360
4732
  tenant: keyData.issuer || "protect-mcp",
4361
4733
  receipts,
@@ -4380,12 +4752,322 @@ ${bold("protect-mcp bundle")}
4380
4752
 
4381
4753
  `);
4382
4754
  }
4755
+ async function handleQuickstart() {
4756
+ const { mkdtempSync, writeFileSync: writeFileSync2, existsSync: existsSync5, mkdirSync, readFileSync: readFileSync7 } = await import("fs");
4757
+ const { join: join4 } = await import("path");
4758
+ const { tmpdir } = await import("os");
4759
+ const dir = mkdtempSync(join4(tmpdir(), "protect-mcp-quickstart-"));
4760
+ process.stdout.write(`
4761
+ ${bold("protect-mcp quickstart")}
4762
+ `);
4763
+ process.stdout.write(`${"\u2500".repeat(50)}
4764
+
4765
+ `);
4766
+ process.stdout.write(` This will:
4767
+ `);
4768
+ process.stdout.write(` 1. Generate an Ed25519 signing keypair
4769
+ `);
4770
+ process.stdout.write(` 2. Create a shadow-mode policy
4771
+ `);
4772
+ process.stdout.write(` 3. Start a demo MCP server with protect-mcp wrapping it
4773
+ `);
4774
+ process.stdout.write(` 4. Log signed receipts for every tool call
4775
+
4776
+ `);
4777
+ process.stdout.write(` Working dir: ${dir}
4778
+
4779
+ `);
4780
+ const keysDir = join4(dir, "keys");
4781
+ mkdirSync(keysDir, { recursive: true });
4782
+ const { randomBytes: randomBytes3 } = await import("crypto");
4783
+ let keypair;
4784
+ try {
4785
+ const { ed25519: ed255192 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
4786
+ const { bytesToHex: bytesToHex2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
4787
+ const privateKey = randomBytes3(32);
4788
+ const publicKey = ed255192.getPublicKey(privateKey);
4789
+ keypair = {
4790
+ privateKey: bytesToHex2(privateKey),
4791
+ publicKey: bytesToHex2(publicKey),
4792
+ kid: `quickstart-${Date.now()}`
4793
+ };
4794
+ } catch {
4795
+ keypair = {
4796
+ privateKey: randomBytes3(32).toString("hex"),
4797
+ publicKey: randomBytes3(32).toString("hex"),
4798
+ kid: `quickstart-${Date.now()}`
4799
+ };
4800
+ }
4801
+ writeFileSync2(join4(keysDir, "gateway.json"), JSON.stringify({
4802
+ privateKey: keypair.privateKey,
4803
+ publicKey: keypair.publicKey,
4804
+ kid: keypair.kid,
4805
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
4806
+ }, null, 2) + "\n");
4807
+ const configPath = join4(dir, "protect-mcp.json");
4808
+ const config = {
4809
+ tools: {
4810
+ "*": { rate_limit: "100/hour" },
4811
+ "delete_file": { block: true }
4812
+ },
4813
+ default_tier: "unknown",
4814
+ signing: {
4815
+ key_path: join4(keysDir, "gateway.json"),
4816
+ issuer: "protect-mcp-quickstart",
4817
+ enabled: true
4818
+ }
4819
+ };
4820
+ writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n");
4821
+ process.stdout.write(` \u2713 Keypair generated (kid: ${keypair.kid})
4822
+ `);
4823
+ process.stdout.write(` \u2713 Policy created (shadow mode, all tools logged)
4824
+ `);
4825
+ process.stdout.write(` \u2713 Signing enabled (Ed25519)
4826
+
4827
+ `);
4828
+ process.stdout.write(`${bold("Starting demo server...")}
4829
+
4830
+ `);
4831
+ process.stdout.write(` Every tool call will produce a signed receipt.
4832
+ `);
4833
+ process.stdout.write(` Try it with Claude Desktop or any MCP client.
4834
+
4835
+ `);
4836
+ process.stdout.write(` ${bold("To use in production:")}
4837
+ `);
4838
+ process.stdout.write(` 1. Copy ${configPath} to your project
4839
+ `);
4840
+ process.stdout.write(` 2. Edit tool policies to match your server
4841
+ `);
4842
+ process.stdout.write(` 3. Run: protect-mcp --policy protect-mcp.json -- node your-server.js
4843
+
4844
+ `);
4845
+ process.stdout.write(`${"\u2500".repeat(50)}
4846
+
4847
+ `);
4848
+ process.env.PROTECT_MCP_CONFIG = configPath;
4849
+ await handleDemo();
4850
+ }
4851
+ async function handleTrace(argv) {
4852
+ const receiptId = argv[0];
4853
+ if (!receiptId) {
4854
+ process.stderr.write("[PROTECT_MCP] Usage: protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]\n");
4855
+ process.exit(1);
4856
+ }
4857
+ let endpoint = "https://evidence-indexer.tomjwxf.workers.dev";
4858
+ let depth = 3;
4859
+ for (let i = 1; i < argv.length; i++) {
4860
+ if (argv[i] === "--endpoint" && argv[i + 1]) {
4861
+ endpoint = argv[++i];
4862
+ } else if (argv[i] === "--depth" && argv[i + 1]) {
4863
+ depth = Math.min(10, Math.max(1, parseInt(argv[++i], 10) || 3));
4864
+ }
4865
+ }
4866
+ process.stdout.write(`
4867
+ ${bold("protect-mcp trace")}
4868
+ `);
4869
+ process.stdout.write(`${"\u2500".repeat(60)}
4870
+
4871
+ `);
4872
+ process.stdout.write(` Root: ${receiptId}
4873
+ `);
4874
+ process.stdout.write(` Endpoint: ${endpoint}
4875
+ `);
4876
+ process.stdout.write(` Depth: ${depth}
4877
+
4878
+ `);
4879
+ const url = `${endpoint}/evidence/graph/${encodeURIComponent(receiptId)}?depth=${depth}&direction=both&max=50`;
4880
+ let graphData;
4881
+ try {
4882
+ const resp = await fetch(url);
4883
+ if (!resp.ok) {
4884
+ const body = await resp.text();
4885
+ process.stderr.write(`[PROTECT_MCP] Error fetching graph: ${resp.status} ${body}
4886
+ `);
4887
+ process.exit(1);
4888
+ }
4889
+ graphData = await resp.json();
4890
+ } catch (err) {
4891
+ process.stderr.write(`[PROTECT_MCP] Could not reach evidence indexer at ${endpoint}
4892
+ `);
4893
+ process.stderr.write(`[PROTECT_MCP] Trying local receipts...
4894
+
4895
+ `);
4896
+ await traceLocal(receiptId);
4897
+ return;
4898
+ }
4899
+ if (!graphData.nodes || graphData.nodes.length === 0) {
4900
+ process.stdout.write(` No receipts found for ${receiptId}
4901
+
4902
+ `);
4903
+ return;
4904
+ }
4905
+ process.stdout.write(` ${bold("Evidence DAG")} (${graphData.node_count} nodes, ${graphData.edge_count} edges)
4906
+
4907
+ `);
4908
+ const nodeMap = /* @__PURE__ */ new Map();
4909
+ for (const node of graphData.nodes) {
4910
+ nodeMap.set(node.receipt_id, node);
4911
+ }
4912
+ const childMap = /* @__PURE__ */ new Map();
4913
+ for (const edge of graphData.edges) {
4914
+ if (!childMap.has(edge.from)) childMap.set(edge.from, []);
4915
+ childMap.get(edge.from).push({ to: edge.to, relation: edge.relation });
4916
+ }
4917
+ const rendered = /* @__PURE__ */ new Set();
4918
+ function renderNode(id, prefix, isLast) {
4919
+ const node = nodeMap.get(id);
4920
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
4921
+ const childPrefix = isLast ? " " : "\u2502 ";
4922
+ const typeEmoji = getTypeEmoji(node?.receipt_type || "unknown");
4923
+ const shortId = id.length > 16 ? id.slice(0, 12) + "\u2026" : id;
4924
+ const time = node?.event_time ? new Date(node.event_time).toLocaleTimeString() : "?";
4925
+ const type = node?.receipt_type?.replace("acta:", "") || "unknown";
4926
+ process.stdout.write(`${prefix}${connector}${typeEmoji} ${bold(type)} ${dim(shortId)} ${dim(time)}
4927
+ `);
4928
+ if (rendered.has(id)) {
4929
+ process.stdout.write(`${prefix}${childPrefix}${dim("(cycle \u2014 already rendered)")}
4930
+ `);
4931
+ return;
4932
+ }
4933
+ rendered.add(id);
4934
+ const children = childMap.get(id) || [];
4935
+ for (let i = 0; i < children.length; i++) {
4936
+ const child = children[i];
4937
+ const edgeLabel = dim(`\u2500\u2500[${child.relation}]\u2500\u2500\u25B6`);
4938
+ process.stdout.write(`${prefix}${childPrefix}${edgeLabel}
4939
+ `);
4940
+ renderNode(child.to, prefix + childPrefix, i === children.length - 1);
4941
+ }
4942
+ }
4943
+ const rootNode = nodeMap.get(receiptId);
4944
+ if (rootNode) {
4945
+ const typeEmoji = getTypeEmoji(rootNode.receipt_type);
4946
+ const type = rootNode.receipt_type?.replace("acta:", "") || "unknown";
4947
+ const time = rootNode.event_time ? new Date(rootNode.event_time).toLocaleTimeString() : "?";
4948
+ process.stdout.write(` ${typeEmoji} ${bold(type)} ${dim(receiptId.slice(0, 16) + "\u2026")} ${dim(time)} ${bold("(root)")}
4949
+ `);
4950
+ rendered.add(receiptId);
4951
+ const children = childMap.get(receiptId) || [];
4952
+ for (let i = 0; i < children.length; i++) {
4953
+ const child = children[i];
4954
+ const edgeLabel = dim(`\u2500\u2500[${child.relation}]\u2500\u2500\u25B6`);
4955
+ process.stdout.write(` ${edgeLabel}
4956
+ `);
4957
+ renderNode(child.to, " ", i === children.length - 1);
4958
+ }
4959
+ const incomingEdges = (graphData.edges || []).filter((e) => e.to === receiptId);
4960
+ if (incomingEdges.length > 0) {
4961
+ process.stdout.write(`
4962
+ ${bold("Incoming edges:")}
4963
+ `);
4964
+ for (const edge of incomingEdges) {
4965
+ const fromNode = nodeMap.get(edge.from);
4966
+ const fromType = fromNode?.receipt_type?.replace("acta:", "") || "unknown";
4967
+ process.stdout.write(` \u25C0\u2500\u2500[${edge.relation}]\u2500\u2500 ${getTypeEmoji(fromNode?.receipt_type)} ${fromType} ${dim(edge.from.slice(0, 16) + "\u2026")}
4968
+ `);
4969
+ }
4970
+ }
4971
+ } else {
4972
+ for (const node of graphData.nodes) {
4973
+ const typeEmoji = getTypeEmoji(node.receipt_type);
4974
+ const type = node.receipt_type?.replace("acta:", "") || "unknown";
4975
+ process.stdout.write(` ${typeEmoji} ${bold(type)} ${dim(node.receipt_id.slice(0, 16) + "\u2026")}
4976
+ `);
4977
+ }
4978
+ }
4979
+ process.stdout.write(`
4980
+ ${"\u2500".repeat(60)}
4981
+ `);
4982
+ process.stdout.write(` ${dim(`Fetched from ${endpoint}`)}
4983
+
4984
+ `);
4985
+ }
4986
+ async function traceLocal(receiptId) {
4987
+ const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
4988
+ const { join: join4 } = await import("path");
4989
+ const dir = process.cwd();
4990
+ const receiptsDir = join4(dir, ".protect-mcp", "receipts");
4991
+ if (!existsSync5(receiptsDir)) {
4992
+ process.stdout.write(` No local receipts found in ${receiptsDir}
4993
+
4994
+ `);
4995
+ return;
4996
+ }
4997
+ const { readdirSync } = await import("fs");
4998
+ const files = readdirSync(receiptsDir).filter((f) => f.endsWith(".json"));
4999
+ process.stdout.write(` Scanning ${files.length} local receipts...
5000
+
5001
+ `);
5002
+ const receipts = [];
5003
+ for (const file of files) {
5004
+ try {
5005
+ const content = readFileSync7(join4(receiptsDir, file), "utf-8");
5006
+ const receipt = JSON.parse(content);
5007
+ receipts.push(receipt);
5008
+ } catch {
5009
+ }
5010
+ }
5011
+ const match = receipts.find(
5012
+ (r) => r.signed_claims?.claims?.receipt_id === receiptId || r.receipt_id === receiptId
5013
+ );
5014
+ if (match) {
5015
+ const claims = match.signed_claims?.claims || match;
5016
+ process.stdout.write(` Found: ${getTypeEmoji(claims.receipt_type)} ${bold(claims.receipt_type?.replace("acta:", "") || "unknown")}
5017
+ `);
5018
+ process.stdout.write(` Event: ${claims.event_id || "?"}
5019
+ `);
5020
+ process.stdout.write(` Issuer: ${claims.issuer_id || "?"}
5021
+ `);
5022
+ process.stdout.write(` Time: ${claims.event_time || "?"}
5023
+ `);
5024
+ if (claims.edges && claims.edges.length > 0) {
5025
+ process.stdout.write(`
5026
+ ${bold("Edges:")}
5027
+ `);
5028
+ for (const edge of claims.edges) {
5029
+ process.stdout.write(` \u2500\u2500[${edge.relation}]\u2500\u2500\u25B6 ${dim(edge.receipt_id?.slice(0, 16) + "\u2026")}
5030
+ `);
5031
+ }
5032
+ }
5033
+ } else {
5034
+ process.stdout.write(` Receipt ${receiptId} not found locally.
5035
+ `);
5036
+ }
5037
+ process.stdout.write("\n");
5038
+ }
5039
+ function getTypeEmoji(type) {
5040
+ switch (type) {
5041
+ case "acta:observation":
5042
+ return "\u{1F441} ";
5043
+ case "acta:policy-load":
5044
+ return "\u{1F4CB}";
5045
+ case "acta:approval":
5046
+ return "\u2705";
5047
+ case "acta:decision":
5048
+ return "\u2696\uFE0F ";
5049
+ case "acta:execution":
5050
+ return "\u26A1";
5051
+ case "acta:outcome":
5052
+ return "\u{1F4E6}";
5053
+ case "acta:delegation":
5054
+ return "\u{1F91D}";
5055
+ case "acta:capability-attestation":
5056
+ return "\u{1F3C5}";
5057
+ default:
5058
+ return "\u{1F4C4}";
5059
+ }
5060
+ }
4383
5061
  async function main() {
4384
5062
  const args = process.argv.slice(2);
4385
5063
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
4386
5064
  printHelp();
4387
5065
  process.exit(0);
4388
5066
  }
5067
+ if (args[0] === "quickstart") {
5068
+ await handleQuickstart();
5069
+ return;
5070
+ }
4389
5071
  if (args[0] === "init") {
4390
5072
  await handleInit(args.slice(1));
4391
5073
  process.exit(0);
@@ -4410,6 +5092,18 @@ async function main() {
4410
5092
  await handleBundle(args.slice(1));
4411
5093
  process.exit(0);
4412
5094
  }
5095
+ if (args[0] === "trace") {
5096
+ await handleTrace(args.slice(1));
5097
+ process.exit(0);
5098
+ }
5099
+ if (args[0] === "simulate") {
5100
+ await handleSimulate(args.slice(1));
5101
+ process.exit(0);
5102
+ }
5103
+ if (args[0] === "report") {
5104
+ await handleReport(args.slice(1));
5105
+ process.exit(0);
5106
+ }
4413
5107
  const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
4414
5108
  let policy = null;
4415
5109
  let policyDigest = "none";
@@ -4460,6 +5154,85 @@ async function main() {
4460
5154
  const gateway = new ProtectGateway(config);
4461
5155
  await gateway.start();
4462
5156
  }
5157
+ async function handleSimulate(args) {
5158
+ let policyPath = "";
5159
+ let logPath = ".protect-mcp-log.jsonl";
5160
+ let tier = "unknown";
5161
+ let jsonOutput = false;
5162
+ for (let i = 0; i < args.length; i++) {
5163
+ if (args[i] === "--policy" && args[i + 1]) {
5164
+ policyPath = args[++i];
5165
+ } else if (args[i] === "--log" && args[i + 1]) {
5166
+ logPath = args[++i];
5167
+ } else if (args[i] === "--tier" && args[i + 1]) {
5168
+ tier = args[++i];
5169
+ } else if (args[i] === "--json") {
5170
+ jsonOutput = true;
5171
+ }
5172
+ }
5173
+ if (!policyPath) {
5174
+ process.stderr.write("Usage: protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]\n");
5175
+ process.exit(1);
5176
+ }
5177
+ const { existsSync: existsSync5 } = await import("fs");
5178
+ if (!existsSync5(logPath)) {
5179
+ process.stderr.write(`Log file not found: ${logPath}
5180
+ `);
5181
+ process.stderr.write("Run protect-mcp in shadow mode first to generate a log file.\n");
5182
+ process.exit(1);
5183
+ }
5184
+ const { policy } = loadPolicy(policyPath);
5185
+ const entries = parseLogFile(logPath);
5186
+ if (entries.length === 0) {
5187
+ process.stderr.write("No tool call entries found in log file.\n");
5188
+ process.exit(1);
5189
+ }
5190
+ const summary = simulate(entries, policy, tier);
5191
+ summary.policy_file = policyPath;
5192
+ summary.log_file = logPath;
5193
+ if (jsonOutput) {
5194
+ process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
5195
+ } else {
5196
+ process.stdout.write(formatSimulation(summary) + "\n");
5197
+ }
5198
+ }
5199
+ async function handleReport(args) {
5200
+ let period = 30;
5201
+ let format = "json";
5202
+ let outputPath = "";
5203
+ let dir = process.cwd();
5204
+ for (let i = 0; i < args.length; i++) {
5205
+ if (args[i] === "--period" && args[i + 1]) {
5206
+ const match = args[++i].match(/^(\d+)d$/);
5207
+ if (match) period = parseInt(match[1], 10);
5208
+ } else if (args[i] === "--format" && args[i + 1]) {
5209
+ format = args[++i];
5210
+ } else if (args[i] === "--output" && args[i + 1]) {
5211
+ outputPath = args[++i];
5212
+ } else if (args[i] === "--dir" && args[i + 1]) {
5213
+ dir = args[++i];
5214
+ }
5215
+ }
5216
+ const { generateReport: generateReport2, formatReportMarkdown: formatReportMarkdown2 } = await Promise.resolve().then(() => (init_report(), report_exports));
5217
+ const { join: join4 } = await import("path");
5218
+ const logPath = join4(dir, ".protect-mcp-log.jsonl");
5219
+ const receiptPath = join4(dir, ".protect-mcp-receipts.jsonl");
5220
+ const report = generateReport2(logPath, receiptPath, period);
5221
+ let output;
5222
+ if (format === "md") {
5223
+ output = formatReportMarkdown2(report);
5224
+ } else {
5225
+ output = JSON.stringify(report, null, 2);
5226
+ }
5227
+ if (outputPath) {
5228
+ const { writeFileSync: writeFileSync2 } = await import("fs");
5229
+ writeFileSync2(outputPath, output, "utf-8");
5230
+ process.stderr.write(`Report written to ${outputPath}
5231
+ `);
5232
+ } else {
5233
+ process.stdout.write(output + "\n");
5234
+ }
5235
+ }
4463
5236
  main().catch((err) => {
4464
5237
  process.stderr.write(`[PROTECT_MCP] Fatal error: ${err instanceof Error ? err.message : err}
4465
5238
  `);