protect-mcp 0.3.0 → 0.3.1

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,65 @@ 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
+
2641
2700
  // src/gateway.ts
2642
2701
  var import_node_child_process = require("child_process");
2643
2702
  var import_node_crypto2 = require("crypto");
@@ -2979,8 +3038,8 @@ async function initSigning(config) {
2979
3038
  signerState = {
2980
3039
  privateKey: keyData.privateKey,
2981
3040
  publicKey: keyData.publicKey,
2982
- kid: artifactsModule.computeKid(keyData.publicKey),
2983
- issuer: config.issuer || "protect-mcp"
3041
+ kid: keyData.kid || artifactsModule.computeKid(keyData.publicKey),
3042
+ issuer: config.issuer || keyData.issuer || "protect-mcp"
2984
3043
  };
2985
3044
  } catch (err) {
2986
3045
  warnings.push(`signing: failed to load key file: ${err instanceof Error ? err.message : err}`);
@@ -3184,13 +3243,16 @@ var ReceiptBuffer = class {
3184
3243
  count() {
3185
3244
  return this.receipts.length;
3186
3245
  }
3246
+ getLatest() {
3247
+ return this.receipts.length > 0 ? this.receipts[this.receipts.length - 1] : void 0;
3248
+ }
3187
3249
  };
3188
- function startStatusServer(config, receiptBuffer) {
3250
+ function startStatusServer(config, receiptBuffer, approvalStore, approvalNonce) {
3189
3251
  const startTime = Date.now();
3190
3252
  const logDir = process.cwd();
3191
3253
  const server = (0, import_node_http.createServer)((req, res) => {
3192
3254
  res.setHeader("Access-Control-Allow-Origin", "*");
3193
- res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
3255
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
3194
3256
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
3195
3257
  res.setHeader("Content-Type", "application/json");
3196
3258
  if (req.method === "OPTIONS") {
@@ -3207,18 +3269,30 @@ function startStatusServer(config, receiptBuffer) {
3207
3269
  handleStatus(res, logDir);
3208
3270
  } else if (path === "/receipts") {
3209
3271
  handleReceipts(res, receiptBuffer, url);
3272
+ } else if (path === "/receipts/latest") {
3273
+ handleReceiptLatest(res, receiptBuffer);
3210
3274
  } else if (path.startsWith("/receipts/")) {
3211
3275
  const id = path.slice("/receipts/".length);
3212
3276
  handleReceiptById(res, receiptBuffer, id);
3277
+ } else if (path === "/approve" && req.method === "POST") {
3278
+ handleApprove(req, res, approvalStore, approvalNonce);
3279
+ } else if (path === "/approvals" && req.method === "GET") {
3280
+ handleListApprovals(res, approvalStore);
3213
3281
  } else {
3214
3282
  res.writeHead(404);
3215
- res.end(JSON.stringify({ error: "not_found", endpoints: ["/health", "/status", "/receipts", "/receipts/:id"] }));
3283
+ res.end(JSON.stringify({ error: "not_found", endpoints: ["/health", "/status", "/receipts", "/receipts/latest", "/receipts/:id", "/approve", "/approvals"] }));
3216
3284
  }
3217
3285
  } catch (err) {
3218
3286
  res.writeHead(500);
3219
3287
  res.end(JSON.stringify({ error: "internal_error" }));
3220
3288
  }
3221
3289
  });
3290
+ server.on("error", (err) => {
3291
+ if (config.verbose) {
3292
+ process.stderr.write(`[PROTECT_MCP] HTTP status server error: ${err.message}
3293
+ `);
3294
+ }
3295
+ });
3222
3296
  server.listen(config.port, "127.0.0.1", () => {
3223
3297
  if (config.verbose) {
3224
3298
  process.stderr.write(`[PROTECT_MCP] HTTP status server listening on http://127.0.0.1:${config.port}
@@ -3234,7 +3308,7 @@ function handleHealth(res, startTime, config) {
3234
3308
  status: "ok",
3235
3309
  uptime_ms: Date.now() - startTime,
3236
3310
  mode: config.mode,
3237
- version: "0.3.0"
3311
+ version: "0.3.1"
3238
3312
  }));
3239
3313
  }
3240
3314
  function handleStatus(res, logDir) {
@@ -3283,6 +3357,16 @@ function handleReceipts(res, buffer, url) {
3283
3357
  receipts
3284
3358
  }));
3285
3359
  }
3360
+ function handleReceiptLatest(res, buffer) {
3361
+ const latest = buffer.getLatest();
3362
+ if (!latest) {
3363
+ res.writeHead(404);
3364
+ res.end(JSON.stringify({ error: "no_receipts", message: "No receipts yet. Make a tool call through protect-mcp first." }));
3365
+ return;
3366
+ }
3367
+ res.writeHead(200);
3368
+ res.end(JSON.stringify(latest));
3369
+ }
3286
3370
  function handleReceiptById(res, buffer, id) {
3287
3371
  const receipt = buffer.getById(id);
3288
3372
  if (!receipt) {
@@ -3293,22 +3377,92 @@ function handleReceiptById(res, buffer, id) {
3293
3377
  res.writeHead(200);
3294
3378
  res.end(JSON.stringify(receipt));
3295
3379
  }
3380
+ function handleApprove(req, res, approvalStore, expectedNonce) {
3381
+ if (!approvalStore) {
3382
+ res.writeHead(503);
3383
+ res.end(JSON.stringify({ error: "approval_store_not_available" }));
3384
+ return;
3385
+ }
3386
+ let body = "";
3387
+ req.on("data", (chunk) => {
3388
+ body += chunk.toString();
3389
+ });
3390
+ req.on("end", () => {
3391
+ try {
3392
+ const { request_id, tool, mode, nonce } = JSON.parse(body);
3393
+ if (expectedNonce && nonce !== expectedNonce) {
3394
+ res.writeHead(403);
3395
+ res.end(JSON.stringify({ error: "invalid_nonce", message: "Approval nonce does not match. Check stderr output for the correct nonce." }));
3396
+ return;
3397
+ }
3398
+ if (!tool || typeof tool !== "string") {
3399
+ res.writeHead(400);
3400
+ res.end(JSON.stringify({ error: "missing_tool", usage: '{"request_id":"abc123","tool":"send_email","mode":"once|always","nonce":"..."}' }));
3401
+ return;
3402
+ }
3403
+ const grantMode = mode === "always" ? "always" : "once";
3404
+ const ttlMs = grantMode === "once" ? 5 * 60 * 1e3 : 24 * 60 * 60 * 1e3;
3405
+ const grantEntry = { tool, mode: grantMode, expires_at: Date.now() + ttlMs };
3406
+ if (grantMode === "always") {
3407
+ approvalStore.set(`always:${tool}`, grantEntry);
3408
+ } else if (request_id) {
3409
+ approvalStore.set(request_id, grantEntry);
3410
+ } else {
3411
+ approvalStore.set(tool, grantEntry);
3412
+ }
3413
+ res.writeHead(200);
3414
+ res.end(JSON.stringify({
3415
+ approved: true,
3416
+ request_id: request_id || null,
3417
+ tool,
3418
+ mode: grantMode,
3419
+ expires_in_seconds: ttlMs / 1e3
3420
+ }));
3421
+ } catch {
3422
+ res.writeHead(400);
3423
+ res.end(JSON.stringify({ error: "invalid_json", usage: '{"request_id":"abc123","tool":"send_email","mode":"once","nonce":"..."}' }));
3424
+ }
3425
+ });
3426
+ }
3427
+ function handleListApprovals(res, approvalStore) {
3428
+ if (!approvalStore) {
3429
+ res.writeHead(200);
3430
+ res.end(JSON.stringify({ grants: [] }));
3431
+ return;
3432
+ }
3433
+ const now = Date.now();
3434
+ const grants = [];
3435
+ for (const [key, grant] of approvalStore) {
3436
+ if (now < grant.expires_at) {
3437
+ grants.push({ key, tool: grant.tool, mode: grant.mode, expires_in_seconds: Math.round((grant.expires_at - now) / 1e3) });
3438
+ }
3439
+ }
3440
+ res.writeHead(200);
3441
+ res.end(JSON.stringify({ grants }));
3442
+ }
3296
3443
 
3297
3444
  // src/gateway.ts
3298
3445
  var LOG_FILE2 = ".protect-mcp-log.jsonl";
3446
+ var RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
3299
3447
  var ProtectGateway = class {
3300
3448
  child = null;
3301
3449
  config;
3302
3450
  rateLimitStore = /* @__PURE__ */ new Map();
3303
3451
  clientReader = null;
3304
3452
  logFilePath;
3453
+ receiptFilePath;
3305
3454
  evidenceStore;
3306
3455
  receiptBuffer;
3456
+ /** Approval grants keyed by request_id (scoped to the specific action that was requested) */
3457
+ approvalStore = /* @__PURE__ */ new Map();
3458
+ /** Random nonce generated at startup — required for approval endpoint authentication */
3459
+ approvalNonce = (0, import_node_crypto2.randomBytes)(16).toString("hex");
3307
3460
  currentTier = "unknown";
3308
3461
  admissionResult = null;
3309
3462
  constructor(config) {
3310
3463
  this.config = config;
3311
3464
  this.logFilePath = (0, import_node_path3.join)(process.cwd(), LOG_FILE2);
3465
+ this.receiptFilePath = (0, import_node_path3.join)(process.cwd(), RECEIPTS_FILE);
3312
3466
  this.evidenceStore = new EvidenceStore();
3313
3467
  this.receiptBuffer = new ReceiptBuffer();
3314
3468
  }
@@ -3332,12 +3486,15 @@ var ProtectGateway = class {
3332
3486
  this.log(`External PDP: ${this.config.policy.external?.endpoint || "not configured"}`);
3333
3487
  }
3334
3488
  }
3489
+ this.log(`Approval nonce: ${this.approvalNonce}`);
3335
3490
  const httpPort = parseInt(process.env.PROTECT_MCP_HTTP_PORT || "9876", 10);
3336
3491
  if (httpPort > 0) {
3337
3492
  try {
3338
3493
  startStatusServer(
3339
3494
  { port: httpPort, mode, verbose },
3340
- this.receiptBuffer
3495
+ this.receiptBuffer,
3496
+ this.approvalStore,
3497
+ this.approvalNonce
3341
3498
  );
3342
3499
  } catch {
3343
3500
  if (verbose) this.log(`HTTP status server could not start on port ${httpPort}`);
@@ -3491,6 +3648,32 @@ var ProtectGateway = class {
3491
3648
  }
3492
3649
  return null;
3493
3650
  }
3651
+ if (toolPolicy.require_approval) {
3652
+ const grant = this.approvalStore.get(requestId);
3653
+ const alwaysGrant = this.approvalStore.get(`always:${toolName}`);
3654
+ if (grant && Date.now() < grant.expires_at || alwaysGrant && Date.now() < alwaysGrant.expires_at) {
3655
+ if (grant && grant.mode === "once") this.approvalStore.delete(requestId);
3656
+ this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "approval_granted", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
3657
+ return null;
3658
+ }
3659
+ this.emitDecisionLog({ tool: toolName, decision: "require_approval", reason_code: "requires_human_approval", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
3660
+ if (this.config.enforce) {
3661
+ return {
3662
+ jsonrpc: "2.0",
3663
+ id: request.id,
3664
+ result: {
3665
+ content: [
3666
+ {
3667
+ type: "text",
3668
+ 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.`
3669
+ }
3670
+ ],
3671
+ isError: true
3672
+ }
3673
+ };
3674
+ }
3675
+ return null;
3676
+ }
3494
3677
  const rateSpec = this.getTierRateLimit(toolPolicy, this.currentTier);
3495
3678
  if (rateSpec) {
3496
3679
  try {
@@ -3548,6 +3731,10 @@ var ProtectGateway = class {
3548
3731
  if (signed.signed) {
3549
3732
  process.stderr.write(`[PROTECT_MCP_RECEIPT] ${signed.signed}
3550
3733
  `);
3734
+ try {
3735
+ (0, import_node_fs5.appendFileSync)(this.receiptFilePath, signed.signed + "\n");
3736
+ } catch {
3737
+ }
3551
3738
  this.receiptBuffer.add(log.request_id, signed.signed);
3552
3739
  if (this.admissionResult?.agent_id) {
3553
3740
  this.evidenceStore.record(this.admissionResult.agent_id, this.config.signing?.issuer || "protect-mcp");
@@ -3586,7 +3773,6 @@ var ProtectGateway = class {
3586
3773
  };
3587
3774
 
3588
3775
  // src/cli.ts
3589
- var import_meta = {};
3590
3776
  function printHelp() {
3591
3777
  process.stderr.write(`
3592
3778
  protect-mcp \u2014 Shadow-mode security gateway for MCP servers
@@ -3596,6 +3782,9 @@ Usage:
3596
3782
  protect-mcp init [--dir <path>]
3597
3783
  protect-mcp demo
3598
3784
  protect-mcp status [--dir <path>]
3785
+ protect-mcp digest [--today] [--dir <path>]
3786
+ protect-mcp receipts [--last <n>] [--dir <path>]
3787
+ protect-mcp bundle [--output <path>] [--dir <path>]
3599
3788
 
3600
3789
  Options:
3601
3790
  --policy <path> Policy/config JSON file (default: allow-all)
@@ -3608,6 +3797,9 @@ Commands:
3608
3797
  init Generate config template, Ed25519 keypair, and sample policy
3609
3798
  demo Start a demo server wrapped with protect-mcp (see receipts instantly)
3610
3799
  status Show tool call statistics from the local decision log
3800
+ digest Generate a human-readable summary of agent activity
3801
+ receipts Show recent persisted signed receipts
3802
+ bundle Export an offline-verifiable audit bundle
3611
3803
 
3612
3804
  Examples:
3613
3805
  protect-mcp -- node my-server.js
@@ -3616,6 +3808,9 @@ Examples:
3616
3808
  protect-mcp init
3617
3809
  protect-mcp demo
3618
3810
  protect-mcp status
3811
+ protect-mcp digest --today
3812
+ protect-mcp receipts --last 10
3813
+ protect-mcp bundle --output audit.json
3619
3814
 
3620
3815
  `);
3621
3816
  }
@@ -3680,10 +3875,10 @@ async function handleInit(argv) {
3680
3875
  const artifacts = await import("@veritasacta/artifacts");
3681
3876
  keypair = artifacts.generateKeypair();
3682
3877
  } catch {
3683
- const { randomBytes: randomBytes2 } = await import("crypto");
3878
+ const { randomBytes: randomBytes3 } = await import("crypto");
3684
3879
  const { ed25519: ed255192 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
3685
3880
  const { bytesToHex: bytesToHex2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
3686
- const privateKey = randomBytes2(32);
3881
+ const privateKey = randomBytes3(32);
3687
3882
  const publicKey = ed255192.getPublicKey(privateKey);
3688
3883
  keypair = {
3689
3884
  privateKey: bytesToHex2(privateKey),
@@ -3776,11 +3971,10 @@ Add --enforce when ready to block policy violations.
3776
3971
  }
3777
3972
  async function handleDemo() {
3778
3973
  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");
3974
+ const { join: join4, dirname, resolve } = await import("path");
3975
+ const cliPath = resolve(process.argv[1] || "dist/cli.js");
3976
+ const cliDir = dirname(cliPath);
3977
+ const demoServerPath = join4(cliDir, "demo-server.js");
3784
3978
  const configPath = join4(process.cwd(), "protect-mcp.json");
3785
3979
  const hasConfig = existsSync4(configPath);
3786
3980
  if (!hasConfig) {
@@ -3965,6 +4159,27 @@ ${bold("protect-mcp status")}
3965
4159
  } catch {
3966
4160
  }
3967
4161
  }
4162
+ const keyPath = join4(dir, "keys", "gateway.json");
4163
+ if (existsSync4(keyPath)) {
4164
+ try {
4165
+ const keyData = JSON.parse(readFileSync5(keyPath, "utf-8"));
4166
+ if (keyData.publicKey) {
4167
+ const fingerprint = keyData.publicKey.slice(0, 16) + "...";
4168
+ process.stdout.write(`
4169
+ ${bold("\u{1F6E1}\uFE0F Passport identity:")}
4170
+ `);
4171
+ process.stdout.write(` Public key: ${fingerprint}
4172
+ `);
4173
+ if (keyData.kid) process.stdout.write(` Key ID: ${keyData.kid}
4174
+ `);
4175
+ process.stdout.write(` Issuer: ${keyData.issuer || "protect-mcp"}
4176
+ `);
4177
+ process.stdout.write(` Verify: ${dim("npx @veritasacta/verify <receipt.json>")}
4178
+ `);
4179
+ }
4180
+ } catch {
4181
+ }
4182
+ }
3968
4183
  process.stdout.write(`
3969
4184
  Log file: ${dim(logPath)}
3970
4185
 
@@ -3985,6 +4200,186 @@ function red(s) {
3985
4200
  function yellow(s) {
3986
4201
  return process.env.NO_COLOR ? s : `\x1B[33m${s}\x1B[0m`;
3987
4202
  }
4203
+ async function handleDigest(argv) {
4204
+ const { readFileSync: readFileSync5, existsSync: existsSync4 } = await import("fs");
4205
+ const { join: join4 } = await import("path");
4206
+ let dir = process.cwd();
4207
+ const dirIdx = argv.indexOf("--dir");
4208
+ if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
4209
+ const today = argv.includes("--today");
4210
+ const logPath = join4(dir, ".protect-mcp-log.jsonl");
4211
+ if (!existsSync4(logPath)) {
4212
+ process.stderr.write(`${bold("protect-mcp digest")}
4213
+
4214
+ No log file found. Run protect-mcp first.
4215
+ `);
4216
+ process.exit(0);
4217
+ }
4218
+ const raw = readFileSync5(logPath, "utf-8");
4219
+ const lines = raw.trim().split("\n").filter(Boolean);
4220
+ let entries = [];
4221
+ for (const line of lines) {
4222
+ try {
4223
+ entries.push(JSON.parse(line));
4224
+ } catch {
4225
+ }
4226
+ }
4227
+ if (today) {
4228
+ const todayStart = /* @__PURE__ */ new Date();
4229
+ todayStart.setHours(0, 0, 0, 0);
4230
+ entries = entries.filter((e) => e.timestamp >= todayStart.getTime());
4231
+ }
4232
+ if (entries.length === 0) {
4233
+ process.stdout.write(`
4234
+ ${bold("\u{1F6E1}\uFE0F Agent Digest")}
4235
+
4236
+ No activity${today ? " today" : ""}.
4237
+
4238
+ `);
4239
+ process.exit(0);
4240
+ }
4241
+ const allowed = entries.filter((e) => e.decision === "allow").length;
4242
+ const denied = entries.filter((e) => e.decision === "deny").length;
4243
+ const approvalRequired = entries.filter((e) => e.decision === "require_approval").length;
4244
+ const toolUsage = /* @__PURE__ */ new Map();
4245
+ for (const e of entries) {
4246
+ toolUsage.set(e.tool, (toolUsage.get(e.tool) || 0) + 1);
4247
+ }
4248
+ const sortedTools = [...toolUsage.entries()].sort((a, b) => b[1] - a[1]);
4249
+ const currentTier = entries[entries.length - 1]?.tier || "unknown";
4250
+ const firstTime = new Date(Math.min(...entries.map((e) => e.timestamp)));
4251
+ const lastTime = new Date(Math.max(...entries.map((e) => e.timestamp)));
4252
+ const durationMs = lastTime.getTime() - firstTime.getTime();
4253
+ const durationStr = durationMs < 6e4 ? `${Math.round(durationMs / 1e3)}s` : durationMs < 36e5 ? `${Math.round(durationMs / 6e4)}m` : `${(durationMs / 36e5).toFixed(1)}h`;
4254
+ process.stdout.write(`
4255
+ ${bold("\u{1F6E1}\uFE0F Agent Daily Digest")}
4256
+
4257
+ `);
4258
+ process.stdout.write(` \u{1F4CA} ${bold(String(entries.length))} actions | `);
4259
+ process.stdout.write(`${green("\u2713 " + allowed)} allowed | `);
4260
+ process.stdout.write(`${red("\u2717 " + denied)} blocked`);
4261
+ if (approvalRequired > 0) process.stdout.write(` | ${yellow("\u23F3 " + approvalRequired)} awaiting approval`);
4262
+ process.stdout.write(`
4263
+ `);
4264
+ process.stdout.write(` \u{1F3C5} Trust tier: ${bold(currentTier)} | \u23F1 Active: ${durationStr}
4265
+
4266
+ `);
4267
+ process.stdout.write(` ${bold("Tools used:")}
4268
+ `);
4269
+ for (const [tool, count] of sortedTools.slice(0, 8)) {
4270
+ process.stdout.write(` ${tool.padEnd(22)} ${count}x
4271
+ `);
4272
+ }
4273
+ if (denied > 0) {
4274
+ const deniedTools = entries.filter((e) => e.decision === "deny");
4275
+ const deniedToolNames = [...new Set(deniedTools.map((e) => e.tool))];
4276
+ process.stdout.write(`
4277
+ ${bold(red("Blocked tools:"))}
4278
+ `);
4279
+ for (const tool of deniedToolNames) {
4280
+ const reason = deniedTools.find((e) => e.tool === tool)?.reason_code || "policy";
4281
+ process.stdout.write(` ${red("\u2717")} ${tool} (${reason})
4282
+ `);
4283
+ }
4284
+ }
4285
+ process.stdout.write(`
4286
+ ${dim("Latest receipt: curl -s http://127.0.0.1:9876/receipts/latest | jq -r .receipt > receipt.json")}
4287
+ `);
4288
+ process.stdout.write(` ${dim("Verify: npx @veritasacta/verify receipt.json --key <public-key-hex>")}
4289
+ `);
4290
+ process.stdout.write(` ${dim("Export: npx protect-mcp bundle --output audit.json")}
4291
+
4292
+ `);
4293
+ }
4294
+ async function handleReceipts2(argv) {
4295
+ const { readFileSync: readFileSync5, existsSync: existsSync4 } = await import("fs");
4296
+ const { join: join4 } = await import("path");
4297
+ let dir = process.cwd();
4298
+ const dirIdx = argv.indexOf("--dir");
4299
+ if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
4300
+ const lastIdx = argv.indexOf("--last");
4301
+ const count = lastIdx !== -1 && argv[lastIdx + 1] ? parseInt(argv[lastIdx + 1], 10) : 20;
4302
+ const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
4303
+ if (!existsSync4(receiptsPath)) {
4304
+ process.stderr.write(`${bold("protect-mcp receipts")}
4305
+
4306
+ No signed receipt file found. Run protect-mcp with signing enabled first.
4307
+ `);
4308
+ process.exit(0);
4309
+ }
4310
+ const raw = readFileSync5(receiptsPath, "utf-8");
4311
+ const lines = raw.trim().split("\n").filter(Boolean);
4312
+ const recent = lines.slice(-count);
4313
+ process.stdout.write(`
4314
+ ${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
4315
+
4316
+ `);
4317
+ for (const line of recent) {
4318
+ try {
4319
+ const entry = JSON.parse(line);
4320
+ const payload = entry.payload || {};
4321
+ const time = typeof entry.issued_at === "string" ? new Date(entry.issued_at).toLocaleTimeString() : "unknown";
4322
+ const decision = payload.decision || "unknown";
4323
+ const icon = decision === "allow" ? green("\u2713") : decision === "require_approval" ? yellow("\u23F3") : red("\u2717");
4324
+ process.stdout.write(` ${dim(time)} ${icon} ${String(payload.tool || "unknown").padEnd(22)} ${String(entry.type || "receipt").padEnd(18)} ${dim(String(payload.reason_code || "signed"))}
4325
+ `);
4326
+ } catch {
4327
+ }
4328
+ }
4329
+ process.stdout.write(`
4330
+ `);
4331
+ }
4332
+ async function handleBundle(argv) {
4333
+ const { readFileSync: readFileSync5, writeFileSync: writeFileSync2, existsSync: existsSync4 } = await import("fs");
4334
+ const { join: join4 } = await import("path");
4335
+ const { createAuditBundle: createAuditBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
4336
+ let dir = process.cwd();
4337
+ const dirIdx = argv.indexOf("--dir");
4338
+ if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
4339
+ const outputIdx = argv.indexOf("--output");
4340
+ const outputPath = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : join4(dir, "audit-bundle.json");
4341
+ const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
4342
+ const keyPath = join4(dir, "keys", "gateway.json");
4343
+ if (!existsSync4(receiptsPath)) {
4344
+ process.stderr.write(`${bold("protect-mcp bundle")}
4345
+
4346
+ No signed receipt file found. Run protect-mcp with signing enabled first.
4347
+ `);
4348
+ process.exit(0);
4349
+ }
4350
+ if (!existsSync4(keyPath)) {
4351
+ process.stderr.write(`${bold("protect-mcp bundle")}
4352
+
4353
+ No key file found at ${keyPath}
4354
+ `);
4355
+ process.exit(1);
4356
+ }
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"));
4359
+ const bundle = createAuditBundle2({
4360
+ tenant: keyData.issuer || "protect-mcp",
4361
+ receipts,
4362
+ signingKeys: [{
4363
+ kty: "OKP",
4364
+ crv: "Ed25519",
4365
+ kid: keyData.kid || "unknown",
4366
+ x: Buffer.from(keyData.publicKey, "hex").toString("base64url"),
4367
+ use: "sig"
4368
+ }]
4369
+ });
4370
+ writeFileSync2(outputPath, JSON.stringify(bundle, null, 2) + "\n");
4371
+ process.stdout.write(`
4372
+ ${bold("protect-mcp bundle")}
4373
+
4374
+ `);
4375
+ process.stdout.write(` Receipts: ${receipts.length}
4376
+ `);
4377
+ process.stdout.write(` Output: ${outputPath}
4378
+ `);
4379
+ process.stdout.write(` Verify: npx @veritasacta/verify ${outputPath} --bundle
4380
+
4381
+ `);
4382
+ }
3988
4383
  async function main() {
3989
4384
  const args = process.argv.slice(2);
3990
4385
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
@@ -4003,6 +4398,18 @@ async function main() {
4003
4398
  await handleStatus2(args.slice(1));
4004
4399
  process.exit(0);
4005
4400
  }
4401
+ if (args[0] === "digest") {
4402
+ await handleDigest(args.slice(1));
4403
+ process.exit(0);
4404
+ }
4405
+ if (args[0] === "receipts") {
4406
+ await handleReceipts2(args.slice(1));
4407
+ process.exit(0);
4408
+ }
4409
+ if (args[0] === "bundle") {
4410
+ await handleBundle(args.slice(1));
4411
+ process.exit(0);
4412
+ }
4006
4413
  const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
4007
4414
  let policy = null;
4008
4415
  let policyDigest = "none";