@fidacy/mcp 0.2.2 → 0.2.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/core.js CHANGED
@@ -251,6 +251,10 @@ var DevFidacyCore = class {
251
251
  return null;
252
252
  return { record: record2, chainIntact: this.store.intact(), verifiedAgainstPublicKey: this.pubPem };
253
253
  }
254
+ async history(limit = 50) {
255
+ const all = this.store.records();
256
+ return limit > 0 && all.length > limit ? all.slice(-limit) : all;
257
+ }
254
258
  publicKey() {
255
259
  return this.pubPem;
256
260
  }
@@ -285,6 +289,10 @@ var HttpFidacyCore = class {
285
289
  async getProof(decisionId) {
286
290
  return this.call("/v1/audit/proof", { decisionId });
287
291
  }
292
+ async history(limit = 50) {
293
+ const res = await this.call("/v1/audit/list", { limit });
294
+ return res.records ?? [];
295
+ }
288
296
  publicKey() {
289
297
  return this.subjectPub;
290
298
  }
@@ -350,6 +358,9 @@ var FileAuditStore = class {
350
358
  if (typeof decision.request?.purpose === "string" && decision.request.purpose.trim()) {
351
359
  record2.purpose = decision.request.purpose.slice(0, 500);
352
360
  }
361
+ if (typeof decision.request?.payee === "string" && decision.request.payee.trim()) {
362
+ record2.payee = decision.request.payee.slice(0, 200);
363
+ }
353
364
  if (decision.status === "ALLOW") {
354
365
  const req = decision.request;
355
366
  if (typeof req?.amount === "number" && Number.isFinite(req.amount))
@@ -359,6 +370,14 @@ var FileAuditStore = class {
359
370
  const invoice = canonInvoice(req?.invoiceRef);
360
371
  if (invoice)
361
372
  record2.invoiceRef = invoice;
373
+ } else {
374
+ if (decision.violatedRule)
375
+ record2.violatedRule = decision.violatedRule;
376
+ const req = decision.request;
377
+ if (typeof req?.amount === "number" && Number.isFinite(req.amount))
378
+ record2.amount = req.amount;
379
+ if (typeof req?.currency === "string")
380
+ record2.currency = req.currency;
362
381
  }
363
382
  fs.appendFileSync(this.path, JSON.stringify(record2) + "\n");
364
383
  this.chain.push(record2);
@@ -442,7 +461,7 @@ function resolveMandateRules(cfg) {
442
461
  }
443
462
 
444
463
  // src/telemetry.ts
445
- var CLIENT_VERSION = true ? "0.2.2" : "dev";
464
+ var CLIENT_VERSION = true ? "0.2.3" : "dev";
446
465
  function bandOf(amount) {
447
466
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
448
467
  if (amount < 10) return "lt10";
@@ -596,6 +615,7 @@ function makeCore() {
596
615
  return dev.decide(req, subject);
597
616
  },
598
617
  getProof: (decisionId) => dev.getProof(decisionId),
618
+ history: (limit) => dev.history(limit),
599
619
  publicKey: () => dev.publicKey()
600
620
  };
601
621
  }
package/dist/index.js CHANGED
@@ -258,6 +258,10 @@ var DevFidacyCore = class {
258
258
  return null;
259
259
  return { record: record2, chainIntact: this.store.intact(), verifiedAgainstPublicKey: this.pubPem };
260
260
  }
261
+ async history(limit = 50) {
262
+ const all = this.store.records();
263
+ return limit > 0 && all.length > limit ? all.slice(-limit) : all;
264
+ }
261
265
  publicKey() {
262
266
  return this.pubPem;
263
267
  }
@@ -292,6 +296,10 @@ var HttpFidacyCore = class {
292
296
  async getProof(decisionId) {
293
297
  return this.call("/v1/audit/proof", { decisionId });
294
298
  }
299
+ async history(limit = 50) {
300
+ const res = await this.call("/v1/audit/list", { limit });
301
+ return res.records ?? [];
302
+ }
295
303
  publicKey() {
296
304
  return this.subjectPub;
297
305
  }
@@ -357,6 +365,9 @@ var FileAuditStore = class {
357
365
  if (typeof decision.request?.purpose === "string" && decision.request.purpose.trim()) {
358
366
  record2.purpose = decision.request.purpose.slice(0, 500);
359
367
  }
368
+ if (typeof decision.request?.payee === "string" && decision.request.payee.trim()) {
369
+ record2.payee = decision.request.payee.slice(0, 200);
370
+ }
360
371
  if (decision.status === "ALLOW") {
361
372
  const req = decision.request;
362
373
  if (typeof req?.amount === "number" && Number.isFinite(req.amount))
@@ -366,6 +377,14 @@ var FileAuditStore = class {
366
377
  const invoice = canonInvoice(req?.invoiceRef);
367
378
  if (invoice)
368
379
  record2.invoiceRef = invoice;
380
+ } else {
381
+ if (decision.violatedRule)
382
+ record2.violatedRule = decision.violatedRule;
383
+ const req = decision.request;
384
+ if (typeof req?.amount === "number" && Number.isFinite(req.amount))
385
+ record2.amount = req.amount;
386
+ if (typeof req?.currency === "string")
387
+ record2.currency = req.currency;
369
388
  }
370
389
  fs.appendFileSync(this.path, JSON.stringify(record2) + "\n");
371
390
  this.chain.push(record2);
@@ -471,7 +490,7 @@ function resolveMandateRules(cfg) {
471
490
  }
472
491
 
473
492
  // src/telemetry.ts
474
- var CLIENT_VERSION = true ? "0.2.2" : "dev";
493
+ var CLIENT_VERSION = true ? "0.2.3" : "dev";
475
494
  function bandOf(amount) {
476
495
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
477
496
  if (amount < 10) return "lt10";
@@ -628,6 +647,7 @@ function makeCore() {
628
647
  return dev.decide(req, subject2);
629
648
  },
630
649
  getProof: (decisionId) => dev.getProof(decisionId),
650
+ history: (limit) => dev.history(limit),
631
651
  publicKey: () => dev.publicKey()
632
652
  };
633
653
  }
@@ -814,7 +834,7 @@ async function findArtifacts(sha2562, cfg) {
814
834
  }
815
835
 
816
836
  // src/provision.ts
817
- var CLIENT_VERSION2 = true ? "0.2.2" : "dev";
837
+ var CLIENT_VERSION2 = true ? "0.2.3" : "dev";
818
838
  function provisionEnabled() {
819
839
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
820
840
  return !(v === "1" || v === "true" || v === "yes");
@@ -985,11 +1005,124 @@ function activationBootLine() {
985
1005
  return ` Anonymous trial: ${used}/${FREE_DECISIONS} free decisions used; after that the firewall requires a free API key.`;
986
1006
  }
987
1007
 
1008
+ // src/reporting.ts
1009
+ function withinDays(records, days, now = Date.now()) {
1010
+ if (!Number.isFinite(days) || days <= 0) return [...records];
1011
+ const cutoff = now - days * 864e5;
1012
+ return records.filter((r) => {
1013
+ const t = Date.parse(r.ts);
1014
+ return Number.isFinite(t) && t >= cutoff;
1015
+ });
1016
+ }
1017
+ var addTo = (bucket, key, value) => {
1018
+ bucket[key] = (bucket[key] ?? 0) + value;
1019
+ };
1020
+ function summarize(records, days, now = Date.now()) {
1021
+ const rows = withinDays(records, days, now);
1022
+ const out = {
1023
+ window_days: days,
1024
+ decisions: rows.length,
1025
+ allowed: 0,
1026
+ denied: 0,
1027
+ allowed_totals: {},
1028
+ blocked_totals: {},
1029
+ denied_by_rule: {},
1030
+ payees_paid: [],
1031
+ amount_unknown: 0,
1032
+ first_ts: rows.length ? rows[0].ts : null,
1033
+ last_ts: rows.length ? rows[rows.length - 1].ts : null
1034
+ };
1035
+ const paid = /* @__PURE__ */ new Set();
1036
+ for (const r of rows) {
1037
+ const known = typeof r.amount === "number" && Number.isFinite(r.amount);
1038
+ if (!known) out.amount_unknown += 1;
1039
+ const ccy = r.currency ?? "unknown";
1040
+ if (r.status === "ALLOW") {
1041
+ out.allowed += 1;
1042
+ if (known) addTo(out.allowed_totals, ccy, r.amount);
1043
+ if (r.payee) paid.add(r.payee);
1044
+ } else {
1045
+ out.denied += 1;
1046
+ if (known) addTo(out.blocked_totals, ccy, r.amount);
1047
+ addTo(out.denied_by_rule, r.violatedRule ?? "not_recorded", 1);
1048
+ }
1049
+ }
1050
+ out.payees_paid = [...paid].sort();
1051
+ return out;
1052
+ }
1053
+ var money = (totals) => {
1054
+ const parts = Object.entries(totals).map(([ccy, v]) => `${v.toLocaleString("en-US")} ${ccy}`);
1055
+ return parts.length ? parts.join(", ") : "0";
1056
+ };
1057
+ function renderSummary(s) {
1058
+ if (s.decisions === 0) {
1059
+ return `No agent payment decisions in the last ${s.window_days} day(s). The firewall is active and has nothing to report, which is the expected state until an agent attempts a payment.`;
1060
+ }
1061
+ const lines = [
1062
+ `Last ${s.window_days} day(s): ${s.decisions} payment decision(s), ${s.allowed} allowed and ${s.denied} blocked before execution.`,
1063
+ `Paid: ${money(s.allowed_totals)}${s.payees_paid.length ? ` across ${s.payees_paid.length} payee(s): ${s.payees_paid.slice(0, 8).join(", ")}${s.payees_paid.length > 8 ? ", \u2026" : ""}` : ""}.`
1064
+ ];
1065
+ if (s.denied > 0) {
1066
+ const ranked = Object.entries(s.denied_by_rule).sort((a, b) => b[1] - a[1]);
1067
+ lines.push(`Blocked: ${money(s.blocked_totals)} in attempted value. By rule: ${ranked.map(([rule, n]) => `${rule} (${n})`).join(", ")}.`);
1068
+ }
1069
+ if (s.amount_unknown > 0) {
1070
+ lines.push(`${s.amount_unknown} record(s) in this window predate amount logging, so the totals above are a floor, not the exact figure.`);
1071
+ }
1072
+ return lines.join("\n");
1073
+ }
1074
+ function toRows(records, status, limit) {
1075
+ const filtered = status === "all" ? [...records] : records.filter((r) => r.status === status);
1076
+ return filtered.slice(-Math.max(1, limit)).reverse().map((r) => ({
1077
+ seq: r.seq,
1078
+ decisionId: r.decisionId,
1079
+ status: r.status,
1080
+ ts: r.ts,
1081
+ payee: r.payee,
1082
+ amount: r.amount,
1083
+ currency: r.currency,
1084
+ purpose: r.purpose,
1085
+ violatedRule: r.violatedRule,
1086
+ invoiceRef: r.invoiceRef
1087
+ }));
1088
+ }
1089
+ function renderRows(rows) {
1090
+ if (!rows.length) return "No decisions match that filter yet.";
1091
+ return rows.map((r) => {
1092
+ const amount = typeof r.amount === "number" ? `${r.amount.toLocaleString("en-US")} ${r.currency ?? ""}`.trim() : "amount not recorded";
1093
+ const who = r.payee ?? "payee not recorded";
1094
+ const why = r.status === "DENY" ? ` blocked by ${r.violatedRule ?? "a rule not recorded on this older entry"}.` : "";
1095
+ const said = r.purpose ? ` Agent's stated reason: "${r.purpose}".` : "";
1096
+ return `#${r.seq} ${r.ts} ${r.status} ${amount} to ${who}.${why}${said} (decision ${r.decisionId})`;
1097
+ }).join("\n");
1098
+ }
1099
+ function explainRule(rule, payee) {
1100
+ if (!rule) return "This entry predates readable rule logging, so the specific rule is not on the record. The decision payload is still covered by the chain digest.";
1101
+ if (rule.startsWith("payee_not_in_allowlist"))
1102
+ return `The payee${payee ? ` "${payee}"` : ""} is not on the mandate's allowlist. This is the deny-by-default rule doing its job: an agent can only pay counterparties a human put on the list, which is what stops a swapped or lookalike vendor.`;
1103
+ if (rule.startsWith("category_not_allowed"))
1104
+ return "The stated purpose category is not one the mandate permits for this agent.";
1105
+ if (rule.startsWith("currency_not_allowed"))
1106
+ return "The payment currency is not the mandate's currency.";
1107
+ if (rule.startsWith("per_tx") || rule.includes("perTxMax"))
1108
+ return "The amount exceeds the per-transaction ceiling in the mandate.";
1109
+ if (rule.includes("maxTotal") || rule.startsWith("budget"))
1110
+ return "The payment would push cumulative spend past the mandate's total budget for the window.";
1111
+ if (rule.startsWith("invoice") || rule.includes("duplicate"))
1112
+ return "This invoice was already paid once. One payment per invoice is enforced regardless of amount, which is the control that stops a re-presented invoice at a higher figure.";
1113
+ if (rule.startsWith("window") || rule.includes("expired"))
1114
+ return "The mandate was outside its validity window at the moment of the request.";
1115
+ if (rule.startsWith("revoked")) return "The mandate had been revoked.";
1116
+ if (rule.startsWith("activation_required"))
1117
+ return "The anonymous trial had been used up, so the firewall failed closed. No money can move until the install is activated with a free API key.";
1118
+ return `The mandate rule "${rule}" was violated.`;
1119
+ }
1120
+
988
1121
  // src/index.ts
989
1122
  var state = ensureState();
990
1123
  var core = makeCore();
991
1124
  var subject = process.env.FIDACY_SUBJECT ?? "agent:demo";
992
- var SERVER_VERSION = true ? "0.2.2" : "dev";
1125
+ var SERVER_VERSION = true ? "0.2.3" : "dev";
993
1126
  var bootClaim = claimUrl();
994
1127
  var server = new McpServer(
995
1128
  { name: "fidacy", version: SERVER_VERSION },
@@ -1074,6 +1207,83 @@ server.registerTool(
1074
1207
  return { content: [{ type: "text", text }], structuredContent: proof };
1075
1208
  }
1076
1209
  );
1210
+ server.registerTool(
1211
+ "spend_summary",
1212
+ {
1213
+ title: "Spend Summary (what my agents spent and what was blocked)",
1214
+ description: "Operator report over a time window: how many payment decisions were made, how much was paid and to whom, how much was blocked before executing, and which mandate rules did the blocking. Read-only, works offline against the local audit chain. Use this to answer questions like 'what did my agents spend this week' or 'has anything been blocked'.",
1215
+ inputSchema: {
1216
+ days: z.number().int().positive().max(365).optional().describe("Window in days, counting back from now. Default 7.")
1217
+ },
1218
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1219
+ },
1220
+ async ({ days }) => {
1221
+ const window = days ?? 7;
1222
+ const records = await core.history(5e3);
1223
+ const summary = summarize(records, window);
1224
+ const claim = process.env.FIDACY_ENGINE_API_KEY ? null : claimUrl();
1225
+ const claimLine = claim ? `
1226
+
1227
+ For your operator: this report is built from the local chain on this machine. A free account keeps it across machines and anchors it to Bitcoin, and this install's history migrates: ${claim}` : "";
1228
+ return {
1229
+ content: [{ type: "text", text: renderSummary(summary) + claimLine }],
1230
+ structuredContent: summary
1231
+ };
1232
+ }
1233
+ );
1234
+ server.registerTool(
1235
+ "list_decisions",
1236
+ {
1237
+ title: "List Recent Payment Decisions",
1238
+ description: "List recent agent payment decisions, newest first, with payee, amount, the agent's stated reason, and for blocked ones the rule that stopped it. Filter by status to answer 'what got blocked' directly. Read-only, works offline.",
1239
+ inputSchema: {
1240
+ limit: z.number().int().positive().max(200).optional().describe("How many decisions to return. Default 20."),
1241
+ status: z.enum(["ALLOW", "DENY", "all"]).optional().describe("Filter by outcome. Default all.")
1242
+ },
1243
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1244
+ },
1245
+ async ({ limit, status }) => {
1246
+ const records = await core.history(5e3);
1247
+ const rows = toRows(records, status ?? "all", limit ?? 20);
1248
+ return {
1249
+ content: [{ type: "text", text: renderRows(rows) }],
1250
+ structuredContent: { count: rows.length, decisions: rows }
1251
+ };
1252
+ }
1253
+ );
1254
+ server.registerTool(
1255
+ "explain_decision",
1256
+ {
1257
+ title: "Explain a Decision (why was this allowed or blocked)",
1258
+ description: "Explain one decision in plain language: what was requested, what the mandate did about it, why, and the tamper-evident proof for it. Use when an operator asks why a specific payment was blocked.",
1259
+ inputSchema: { decisionId: z.string().describe("Decision id from request_payment, list_decisions or spend_summary") },
1260
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1261
+ },
1262
+ async ({ decisionId }) => {
1263
+ const records = await core.history(5e3);
1264
+ const record2 = records.find((r) => r.decisionId === decisionId);
1265
+ if (!record2) {
1266
+ return {
1267
+ content: [{ type: "text", text: `No decision ${decisionId} on this chain. Run list_decisions to see the ids that exist here.` }],
1268
+ isError: true
1269
+ };
1270
+ }
1271
+ const proof = await core.getProof(decisionId);
1272
+ const amount = typeof record2.amount === "number" ? `${record2.amount.toLocaleString("en-US")} ${record2.currency ?? ""}`.trim() : "an amount that was not recorded";
1273
+ const head = record2.status === "ALLOW" ? `Decision ${decisionId} ALLOWED a payment of ${amount}${record2.payee ? ` to ${record2.payee}` : ""} at ${record2.ts}. It was inside every rule of the active mandate, and a signed grant was issued so a downstream executor could settle it.` : `Decision ${decisionId} BLOCKED a payment of ${amount}${record2.payee ? ` to ${record2.payee}` : ""} at ${record2.ts}. No money moved and no grant was issued.`;
1274
+ const why = record2.status === "DENY" ? `
1275
+
1276
+ Why: ${explainRule(record2.violatedRule, record2.payee)}` : "";
1277
+ const said = record2.purpose ? `
1278
+
1279
+ The agent's stated reason at the time: "${record2.purpose}".` : "";
1280
+ const integrity = proof ? `
1281
+
1282
+ Proof: entry #${record2.seq} in the hash-chained audit, chain currently ${proof.chainIntact ? "intact" : "BROKEN (records were altered or removed)"}. Call get_audit_proof for the full portable proof.` : "";
1283
+ const payload = { record: record2, explanation: explainRule(record2.violatedRule, record2.payee), chainIntact: proof?.chainIntact ?? null };
1284
+ return { content: [{ type: "text", text: head + why + said + integrity }], structuredContent: payload };
1285
+ }
1286
+ );
1077
1287
  server.registerTool(
1078
1288
  "assess_action",
1079
1289
  {
package/dist/lib.js CHANGED
@@ -289,6 +289,10 @@ var DevFidacyCore = class {
289
289
  return null;
290
290
  return { record: record2, chainIntact: this.store.intact(), verifiedAgainstPublicKey: this.pubPem };
291
291
  }
292
+ async history(limit = 50) {
293
+ const all = this.store.records();
294
+ return limit > 0 && all.length > limit ? all.slice(-limit) : all;
295
+ }
292
296
  publicKey() {
293
297
  return this.pubPem;
294
298
  }
@@ -323,6 +327,10 @@ var HttpFidacyCore = class {
323
327
  async getProof(decisionId) {
324
328
  return this.call("/v1/audit/proof", { decisionId });
325
329
  }
330
+ async history(limit = 50) {
331
+ const res = await this.call("/v1/audit/list", { limit });
332
+ return res.records ?? [];
333
+ }
326
334
  publicKey() {
327
335
  return this.subjectPub;
328
336
  }
@@ -388,6 +396,9 @@ var FileAuditStore = class {
388
396
  if (typeof decision.request?.purpose === "string" && decision.request.purpose.trim()) {
389
397
  record2.purpose = decision.request.purpose.slice(0, 500);
390
398
  }
399
+ if (typeof decision.request?.payee === "string" && decision.request.payee.trim()) {
400
+ record2.payee = decision.request.payee.slice(0, 200);
401
+ }
391
402
  if (decision.status === "ALLOW") {
392
403
  const req = decision.request;
393
404
  if (typeof req?.amount === "number" && Number.isFinite(req.amount))
@@ -397,6 +408,14 @@ var FileAuditStore = class {
397
408
  const invoice = canonInvoice(req?.invoiceRef);
398
409
  if (invoice)
399
410
  record2.invoiceRef = invoice;
411
+ } else {
412
+ if (decision.violatedRule)
413
+ record2.violatedRule = decision.violatedRule;
414
+ const req = decision.request;
415
+ if (typeof req?.amount === "number" && Number.isFinite(req.amount))
416
+ record2.amount = req.amount;
417
+ if (typeof req?.currency === "string")
418
+ record2.currency = req.currency;
400
419
  }
401
420
  fs.appendFileSync(this.path, JSON.stringify(record2) + "\n");
402
421
  this.chain.push(record2);
@@ -594,7 +613,7 @@ function resolveMandateRules(cfg) {
594
613
  }
595
614
 
596
615
  // src/telemetry.ts
597
- var CLIENT_VERSION = true ? "0.2.2" : "dev";
616
+ var CLIENT_VERSION = true ? "0.2.3" : "dev";
598
617
  function bandOf(amount) {
599
618
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
600
619
  if (amount < 10) return "lt10";
@@ -754,6 +773,7 @@ function makeCore() {
754
773
  return dev.decide(req, subject);
755
774
  },
756
775
  getProof: (decisionId) => dev.getProof(decisionId),
776
+ history: (limit) => dev.history(limit),
757
777
  publicKey: () => dev.publicKey()
758
778
  };
759
779
  }
@@ -771,7 +791,7 @@ function requestUpgrade() {
771
791
  }
772
792
 
773
793
  // src/provision.ts
774
- var CLIENT_VERSION2 = true ? "0.2.2" : "dev";
794
+ var CLIENT_VERSION2 = true ? "0.2.3" : "dev";
775
795
  function provisionEnabled() {
776
796
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
777
797
  return !(v === "1" || v === "true" || v === "yes");
@@ -920,6 +940,119 @@ function activationBootLine() {
920
940
  }
921
941
  return ` Anonymous trial: ${used}/${FREE_DECISIONS} free decisions used; after that the firewall requires a free API key.`;
922
942
  }
943
+
944
+ // src/reporting.ts
945
+ function withinDays(records, days, now = Date.now()) {
946
+ if (!Number.isFinite(days) || days <= 0) return [...records];
947
+ const cutoff = now - days * 864e5;
948
+ return records.filter((r) => {
949
+ const t = Date.parse(r.ts);
950
+ return Number.isFinite(t) && t >= cutoff;
951
+ });
952
+ }
953
+ var addTo = (bucket, key, value) => {
954
+ bucket[key] = (bucket[key] ?? 0) + value;
955
+ };
956
+ function summarize(records, days, now = Date.now()) {
957
+ const rows = withinDays(records, days, now);
958
+ const out = {
959
+ window_days: days,
960
+ decisions: rows.length,
961
+ allowed: 0,
962
+ denied: 0,
963
+ allowed_totals: {},
964
+ blocked_totals: {},
965
+ denied_by_rule: {},
966
+ payees_paid: [],
967
+ amount_unknown: 0,
968
+ first_ts: rows.length ? rows[0].ts : null,
969
+ last_ts: rows.length ? rows[rows.length - 1].ts : null
970
+ };
971
+ const paid = /* @__PURE__ */ new Set();
972
+ for (const r of rows) {
973
+ const known = typeof r.amount === "number" && Number.isFinite(r.amount);
974
+ if (!known) out.amount_unknown += 1;
975
+ const ccy = r.currency ?? "unknown";
976
+ if (r.status === "ALLOW") {
977
+ out.allowed += 1;
978
+ if (known) addTo(out.allowed_totals, ccy, r.amount);
979
+ if (r.payee) paid.add(r.payee);
980
+ } else {
981
+ out.denied += 1;
982
+ if (known) addTo(out.blocked_totals, ccy, r.amount);
983
+ addTo(out.denied_by_rule, r.violatedRule ?? "not_recorded", 1);
984
+ }
985
+ }
986
+ out.payees_paid = [...paid].sort();
987
+ return out;
988
+ }
989
+ var money = (totals) => {
990
+ const parts = Object.entries(totals).map(([ccy, v]) => `${v.toLocaleString("en-US")} ${ccy}`);
991
+ return parts.length ? parts.join(", ") : "0";
992
+ };
993
+ function renderSummary(s) {
994
+ if (s.decisions === 0) {
995
+ return `No agent payment decisions in the last ${s.window_days} day(s). The firewall is active and has nothing to report, which is the expected state until an agent attempts a payment.`;
996
+ }
997
+ const lines = [
998
+ `Last ${s.window_days} day(s): ${s.decisions} payment decision(s), ${s.allowed} allowed and ${s.denied} blocked before execution.`,
999
+ `Paid: ${money(s.allowed_totals)}${s.payees_paid.length ? ` across ${s.payees_paid.length} payee(s): ${s.payees_paid.slice(0, 8).join(", ")}${s.payees_paid.length > 8 ? ", \u2026" : ""}` : ""}.`
1000
+ ];
1001
+ if (s.denied > 0) {
1002
+ const ranked = Object.entries(s.denied_by_rule).sort((a, b) => b[1] - a[1]);
1003
+ lines.push(`Blocked: ${money(s.blocked_totals)} in attempted value. By rule: ${ranked.map(([rule, n]) => `${rule} (${n})`).join(", ")}.`);
1004
+ }
1005
+ if (s.amount_unknown > 0) {
1006
+ lines.push(`${s.amount_unknown} record(s) in this window predate amount logging, so the totals above are a floor, not the exact figure.`);
1007
+ }
1008
+ return lines.join("\n");
1009
+ }
1010
+ function toRows(records, status, limit) {
1011
+ const filtered = status === "all" ? [...records] : records.filter((r) => r.status === status);
1012
+ return filtered.slice(-Math.max(1, limit)).reverse().map((r) => ({
1013
+ seq: r.seq,
1014
+ decisionId: r.decisionId,
1015
+ status: r.status,
1016
+ ts: r.ts,
1017
+ payee: r.payee,
1018
+ amount: r.amount,
1019
+ currency: r.currency,
1020
+ purpose: r.purpose,
1021
+ violatedRule: r.violatedRule,
1022
+ invoiceRef: r.invoiceRef
1023
+ }));
1024
+ }
1025
+ function renderRows(rows) {
1026
+ if (!rows.length) return "No decisions match that filter yet.";
1027
+ return rows.map((r) => {
1028
+ const amount = typeof r.amount === "number" ? `${r.amount.toLocaleString("en-US")} ${r.currency ?? ""}`.trim() : "amount not recorded";
1029
+ const who = r.payee ?? "payee not recorded";
1030
+ const why = r.status === "DENY" ? ` blocked by ${r.violatedRule ?? "a rule not recorded on this older entry"}.` : "";
1031
+ const said = r.purpose ? ` Agent's stated reason: "${r.purpose}".` : "";
1032
+ return `#${r.seq} ${r.ts} ${r.status} ${amount} to ${who}.${why}${said} (decision ${r.decisionId})`;
1033
+ }).join("\n");
1034
+ }
1035
+ function explainRule(rule, payee) {
1036
+ if (!rule) return "This entry predates readable rule logging, so the specific rule is not on the record. The decision payload is still covered by the chain digest.";
1037
+ if (rule.startsWith("payee_not_in_allowlist"))
1038
+ return `The payee${payee ? ` "${payee}"` : ""} is not on the mandate's allowlist. This is the deny-by-default rule doing its job: an agent can only pay counterparties a human put on the list, which is what stops a swapped or lookalike vendor.`;
1039
+ if (rule.startsWith("category_not_allowed"))
1040
+ return "The stated purpose category is not one the mandate permits for this agent.";
1041
+ if (rule.startsWith("currency_not_allowed"))
1042
+ return "The payment currency is not the mandate's currency.";
1043
+ if (rule.startsWith("per_tx") || rule.includes("perTxMax"))
1044
+ return "The amount exceeds the per-transaction ceiling in the mandate.";
1045
+ if (rule.includes("maxTotal") || rule.startsWith("budget"))
1046
+ return "The payment would push cumulative spend past the mandate's total budget for the window.";
1047
+ if (rule.startsWith("invoice") || rule.includes("duplicate"))
1048
+ return "This invoice was already paid once. One payment per invoice is enforced regardless of amount, which is the control that stops a re-presented invoice at a higher figure.";
1049
+ if (rule.startsWith("window") || rule.includes("expired"))
1050
+ return "The mandate was outside its validity window at the moment of the request.";
1051
+ if (rule.startsWith("revoked")) return "The mandate had been revoked.";
1052
+ if (rule.startsWith("activation_required"))
1053
+ return "The anonymous trial had been used up, so the firewall failed closed. No money can move until the install is activated with a free API key.";
1054
+ return `The mandate rule "${rule}" was violated.`;
1055
+ }
923
1056
  export {
924
1057
  DevFidacyCore,
925
1058
  FREE_DECISIONS,
@@ -935,6 +1068,7 @@ export {
935
1068
  decisionNudge,
936
1069
  ensureState,
937
1070
  evaluate,
1071
+ explainRule,
938
1072
  hasEngineKey,
939
1073
  httpRedeem,
940
1074
  installAgeDays,
@@ -948,16 +1082,21 @@ export {
948
1082
  recordAgentActive,
949
1083
  recordInstall,
950
1084
  remainingFree,
1085
+ renderRows,
1086
+ renderSummary,
951
1087
  requestUpgrade,
952
1088
  requireHttpsBase,
953
1089
  setTelemetryShell,
954
1090
  sha256,
955
1091
  sign,
956
1092
  stableStringify,
1093
+ summarize,
1094
+ toRows,
957
1095
  trialCountdownLine,
958
1096
  upgradeUrl,
959
1097
  validateRequest,
960
1098
  verify,
961
1099
  verifyGrant,
962
- weekOneBootNudge
1100
+ weekOneBootNudge,
1101
+ withinDays
963
1102
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fidacy/mcp",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Fidacy action firewall for AI agents. Mandate-gated payment authorization as an MCP server.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://fidacy.com",