@fidacy/mcp 0.6.1 → 0.6.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
@@ -488,7 +488,7 @@ function resolveMandateRules(cfg) {
488
488
  }
489
489
 
490
490
  // src/telemetry.ts
491
- var CLIENT_VERSION = true ? "0.6.1" : "dev";
491
+ var CLIENT_VERSION = true ? "0.6.3" : "dev";
492
492
  function bandOf(amount) {
493
493
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
494
494
  if (amount < 10) return "lt10";
package/dist/index.js CHANGED
@@ -15,6 +15,17 @@ var __export = (target, all) => {
15
15
  };
16
16
 
17
17
  // src/config.ts
18
+ var config_exports = {};
19
+ __export(config_exports, {
20
+ auditLogPath: () => auditLogPath,
21
+ configDir: () => configDir,
22
+ configPath: () => configPath,
23
+ ensureState: () => ensureState,
24
+ hasEngineKey: () => hasEngineKey,
25
+ readConfig: () => readConfig,
26
+ resolveMandateRules: () => resolveMandateRules,
27
+ writeConfig: () => writeConfig
28
+ });
18
29
  import { randomUUID as randomUUID2 } from "node:crypto";
19
30
  import { homedir } from "node:os";
20
31
  import { join } from "node:path";
@@ -214,7 +225,7 @@ var init_telemetry = __esm({
214
225
  "src/telemetry.ts"() {
215
226
  "use strict";
216
227
  init_config();
217
- CLIENT_VERSION = true ? "0.6.1" : "dev";
228
+ CLIENT_VERSION = true ? "0.6.3" : "dev";
218
229
  currentShell = "mcp";
219
230
  anonIdCache = null;
220
231
  buffer = [];
@@ -291,7 +302,7 @@ var init_register = __esm({
291
302
  "src/register.ts"() {
292
303
  "use strict";
293
304
  init_config();
294
- CLIENT_VERSION3 = true ? "0.6.1" : "dev";
305
+ CLIENT_VERSION3 = true ? "0.6.3" : "dev";
295
306
  }
296
307
  });
297
308
 
@@ -583,6 +594,7 @@ var init_setup = __esm({
583
594
  // src/index.ts
584
595
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
585
596
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
597
+ import { createHash as createHash2 } from "node:crypto";
586
598
  import { z } from "zod";
587
599
 
588
600
  // src/core.ts
@@ -1267,7 +1279,7 @@ init_telemetry();
1267
1279
 
1268
1280
  // src/provision.ts
1269
1281
  init_config();
1270
- var CLIENT_VERSION2 = true ? "0.6.1" : "dev";
1282
+ var CLIENT_VERSION2 = true ? "0.6.3" : "dev";
1271
1283
  function provisionEnabled() {
1272
1284
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
1273
1285
  return !(v === "1" || v === "true" || v === "yes");
@@ -1384,7 +1396,7 @@ function renderSummary(s) {
1384
1396
  ];
1385
1397
  if (s.denied > 0) {
1386
1398
  const ranked = Object.entries(s.denied_by_rule).sort((a, b) => b[1] - a[1]);
1387
- lines.push(`Blocked: ${money(s.blocked_totals)} in attempted value. By rule: ${ranked.map(([rule, n]) => `${rule} (${n})`).join(", ")}.`);
1399
+ lines.push(`Blocked: ${money(s.blocked_totals)} in attempted value. By reason: ${ranked.map(([rule, n]) => `${ruleLabel(rule)} (${n})`).join(", ")}.`);
1388
1400
  }
1389
1401
  if (s.amount_unknown > 0) {
1390
1402
  lines.push(`${s.amount_unknown} record(s) in this window predate amount logging, so the totals above are a floor, not the exact figure.`);
@@ -1411,32 +1423,82 @@ function renderRows(rows2) {
1411
1423
  return rows2.map((r) => {
1412
1424
  const amount = typeof r.amount === "number" ? `${r.amount.toLocaleString("en-US")} ${r.currency ?? ""}`.trim() : "amount not recorded";
1413
1425
  const who = r.payee ?? "payee not recorded";
1414
- const why = r.status === "DENY" ? ` blocked by ${r.violatedRule ?? "a rule not recorded on this older entry"}.` : "";
1426
+ const why = r.status === "DENY" ? ` ${explainRule(r.violatedRule, r.payee)}` : "";
1415
1427
  const said = r.purpose ? ` Agent's stated reason: "${r.purpose}".` : "";
1416
1428
  return `#${r.seq} ${r.ts} ${r.status} ${amount} to ${who}.${why}${said} (decision ${r.decisionId})`;
1417
1429
  }).join("\n");
1418
1430
  }
1431
+ function ruleLabel(rule) {
1432
+ if (rule.startsWith("payee_lookalike")) return "lookalike payee";
1433
+ if (rule.startsWith("payee_not_in_allowlist")) return "payee not approved";
1434
+ if (rule.startsWith("category_not_allowed")) return "purpose not allowed";
1435
+ if (rule.startsWith("currency_not_allowed")) return "wrong currency";
1436
+ if (rule.startsWith("per_tx") || rule.includes("perTxMax")) return "over the per-payment cap";
1437
+ if (rule.includes("maxTotal") || rule.startsWith("total_cap") || rule.startsWith("budget")) return "over the total budget";
1438
+ if (rule.startsWith("invoice") || rule.includes("duplicate")) return "duplicate invoice";
1439
+ if (rule.startsWith("window") || rule.includes("mandate_window") || rule.includes("expired")) return "outside the mandate window";
1440
+ if (rule.startsWith("revoked") || rule.startsWith("mandate_revoked")) return "mandate revoked";
1441
+ if (rule.startsWith("invalid_mandate")) return "mandate misconfigured";
1442
+ if (rule.startsWith("non_positive_amount")) return "invalid amount";
1443
+ if (rule.startsWith("activation_required")) return "trial used up";
1444
+ if (rule === "not_recorded") return "rule not recorded";
1445
+ return rule;
1446
+ }
1419
1447
  function explainRule(rule, payee) {
1420
1448
  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.";
1449
+ if (rule.startsWith("invalid_mandate")) {
1450
+ const cap = rule.includes("perTxMax") ? "perTxMax" : rule.includes("maxTotal") ? "maxTotal" : null;
1451
+ if (cap)
1452
+ return `The mandate's \`${cap}\` is not a usable number, so the firewall denied rather than run without that cap. Values like "2,500", "$2500" or "2500 USD" all look right to a human and become NaN, and a cap that cannot be compared would silently let everything through. Write it as a bare number: ${cap}: 2500.`;
1453
+ return "The mandate is missing its `allow` block, so there is nothing to enforce and the firewall denied. Check ~/.fidacy/config.json or FIDACY_MANDATE_JSON.";
1454
+ }
1455
+ if (rule.startsWith("payee_lookalike")) {
1456
+ const approved = rule.includes("~") ? rule.split("~")[1] : "an approved payee";
1457
+ return `The payee${payee ? ` "${payee}"` : ""} is a lookalike of your approved payee "${approved}": same name to a human glance, different string. That is the signature of a payee swap, whether from a prompt injection or a spoofed invoice. If it really is a separate vendor, add it to \`payees\` explicitly; if it is not, this attempt is exactly what the firewall exists to stop.`;
1458
+ }
1421
1459
  if (rule.startsWith("payee_not_in_allowlist"))
1422
- 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.`;
1460
+ 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. To authorize this one, add it to \`payees\` in ~/.fidacy/config.json, or pass a full mandate through FIDACY_MANDATE_JSON.`;
1423
1461
  if (rule.startsWith("category_not_allowed"))
1424
- return "The stated purpose category is not one the mandate permits for this agent.";
1462
+ return "The stated purpose category is not one the mandate permits for this agent. Add it to `categories` in the mandate, or send the payment under a purpose the mandate already allows.";
1425
1463
  if (rule.startsWith("currency_not_allowed"))
1426
- return "The payment currency is not the mandate's currency.";
1464
+ return "The payment currency is not the mandate's currency. Set `currency` to the one you intend to pay in. A mandate authorizes one currency at a time on purpose, so a swapped currency cannot slip past a cap set in another.";
1427
1465
  if (rule.startsWith("per_tx") || rule.includes("perTxMax"))
1428
- return "The amount exceeds the per-transaction ceiling in the mandate.";
1429
- if (rule.includes("maxTotal") || rule.startsWith("budget"))
1430
- return "The payment would push cumulative spend past the mandate's total budget for the window.";
1466
+ return "The amount exceeds the per-transaction ceiling in the mandate. Raise `perTxMax` if payments this size are expected, or split the payment.";
1467
+ if (rule.includes("maxTotal") || rule.startsWith("budget") || rule.startsWith("total_cap"))
1468
+ return "The payment would push cumulative spend past the mandate's total budget for the window. Raise `maxTotal`, or wait for the window to roll over. Blocked attempts do not consume the budget; only payments that went through do.";
1431
1469
  if (rule.startsWith("invoice") || rule.includes("duplicate"))
1432
- 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.";
1433
- if (rule.startsWith("window") || rule.includes("expired"))
1434
- return "The mandate was outside its validity window at the moment of the request.";
1435
- if (rule.startsWith("revoked")) return "The mandate had been revoked.";
1470
+ 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. If this genuinely is a second, separate payment, give it its own invoiceRef.";
1471
+ if (rule.startsWith("window") || rule.includes("mandate_window") || rule.includes("expired"))
1472
+ return "The mandate was outside its validity window at the moment of the request. Check `notBefore` and `notAfter`, and issue a mandate whose window covers now.";
1473
+ if (rule.startsWith("revoked") || rule.startsWith("mandate_revoked"))
1474
+ return "The mandate had been revoked. A revoked mandate cannot be un-revoked; issue a new one to authorize further payments.";
1475
+ if (rule.startsWith("non_positive_amount"))
1476
+ return "The amount was zero or negative, which is not a payment anyone can authorize. Check what produced that value before retrying.";
1436
1477
  if (rule.startsWith("activation_required"))
1437
1478
  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.";
1438
1479
  return `The mandate rule "${rule}" was violated.`;
1439
1480
  }
1481
+ function explainEngineError(action, status, type, reasons) {
1482
+ const noun = action === "verdict" ? "assess this action" : action === "anchor" ? "anchor this artifact" : "check this artifact";
1483
+ if (status === 401 || status === 403)
1484
+ return `Fidacy could not ${noun}: the engine rejected the credential (HTTP ${status}). This is not an outage. The key is missing, wrong, or lacks the scope this call needs \u2014 an anonymous provisioned key has no scopes at all. Get an account key at https://fidacy.com/claim and set FIDACY_ENGINE_API_KEY. Nothing was executed.`;
1485
+ if (status === 402)
1486
+ return `Fidacy could not ${noun}: the account is past a spending or quota limit (HTTP 402). The integration is fine; the limit needs settling at https://fidacy.com/claim. Nothing was executed.`;
1487
+ if (status === 429)
1488
+ return `Fidacy could not ${noun}: too many requests in a short window (HTTP 429). Retry in a few seconds. Nothing was executed, and no decision was recorded.`;
1489
+ if (status === 404 && action === "check")
1490
+ return "No anchor exists for that hash on this account. Either the artifact was never anchored, or its contents changed since it was \u2014 if you expected a match, the file is not the one that was anchored.";
1491
+ if (status === 422 || type === "mandate_violation") {
1492
+ const first = reasons?.[0]?.key;
1493
+ const why = first ? ` ${explainRule(first)}` : "";
1494
+ return `Fidacy denied this action: it falls outside the mandate in force.${why} Nothing was executed.`;
1495
+ }
1496
+ if (status >= 500)
1497
+ return `Fidacy could not ${noun}: the engine returned an error (HTTP ${status}). Nothing was executed, and the call was refused rather than allowed through. Retrying is safe.`;
1498
+ if (status === 400)
1499
+ return `Fidacy could not ${noun}: the request was malformed (HTTP 400${type ? `, ${type}` : ""}). This is a bug in how the call was built, not a policy decision. Nothing was executed.`;
1500
+ return `Fidacy could not ${noun} (HTTP ${status}${type ? `, ${type}` : ""}), so the call was refused rather than allowed through. Nothing was executed.`;
1501
+ }
1440
1502
 
1441
1503
  // src/banner.ts
1442
1504
  init_activation();
@@ -1594,8 +1656,12 @@ function renderAlertLine(alerts) {
1594
1656
  var state = ensureState();
1595
1657
  var core = makeCore();
1596
1658
  var subject = process.env.FIDACY_SUBJECT ?? "agent:demo";
1597
- var SERVER_VERSION = true ? "0.6.1" : "dev";
1659
+ var SERVER_VERSION = true ? "0.6.3" : "dev";
1598
1660
  var bootClaim = claimUrl();
1661
+ function derivedIdempotencyKey(req) {
1662
+ const material = [req.payee, req.amount, req.currency, req.invoiceRef ?? "", req.category ?? ""].join("|");
1663
+ return "auto-" + createHash2("sha256").update(material).digest("hex").slice(0, 24);
1664
+ }
1599
1665
  var server = new McpServer(
1600
1666
  // The commercial identity. `name` is the stable slug hosts key on; `title` is
1601
1667
  // what a human reads in the client's server list. The npm package stays
@@ -1629,7 +1695,9 @@ server.registerTool(
1629
1695
  "Your stated reason for this payment, in your own words. It is persisted readably on the tamper-evident audit record (ALLOW and DENY alike) and becomes part of the after-the-fact proof, so write the real reason."
1630
1696
  ),
1631
1697
  category: z.string().describe("Purpose category (must be allowed by the mandate)"),
1632
- idempotencyKey: z.string().describe("Caller-supplied idempotency key"),
1698
+ idempotencyKey: z.string().describe(
1699
+ "Optional. Your own key for this payment, if you have one (an order id, a job id). Leave it out and Fidacy derives one from the payment itself, which is the safer default: a retry of the SAME payment produces the SAME key, where an invented one would not."
1700
+ ).optional(),
1633
1701
  invoiceRef: z.string().describe(
1634
1702
  "Optional invoice identity. When set, Fidacy enforces one payment per invoice: a second request for the same invoiceRef is DENIED, at any amount."
1635
1703
  ).optional()
@@ -1664,7 +1732,10 @@ server.registerTool(
1664
1732
  };
1665
1733
  return { content: [{ type: "text", text: halted }], structuredContent: out2 };
1666
1734
  }
1667
- const req = args;
1735
+ const req = {
1736
+ ...args,
1737
+ idempotencyKey: args.idempotencyKey || derivedIdempotencyKey(args)
1738
+ };
1668
1739
  let sentinelAlerts = [];
1669
1740
  try {
1670
1741
  const [before, mandate] = await Promise.all([core.history(5e3), core.getMandate(subject)]);
@@ -1859,9 +1930,8 @@ server.registerTool(
1859
1930
  markHostedLapsed();
1860
1931
  return { content: [{ type: "text", text: hostedWallCta("verdict", e.billing) }], isError: true };
1861
1932
  }
1862
- const reasons = e.rejection_reasons?.length ? " (" + e.rejection_reasons.map((x) => x.key).join(",") + ")" : "";
1863
1933
  return {
1864
- content: [{ type: "text", text: `ASSESS ${e.status}: ${e.type}${reasons}` }],
1934
+ content: [{ type: "text", text: explainEngineError("verdict", e.status, e.type, e.rejection_reasons) }],
1865
1935
  isError: true
1866
1936
  };
1867
1937
  }
@@ -1925,7 +1995,7 @@ server.registerTool(
1925
1995
  markHostedLapsed();
1926
1996
  return { content: [{ type: "text", text: hostedWallCta("anchor", e.billing) }], isError: true };
1927
1997
  }
1928
- return { content: [{ type: "text", text: `ANCHOR ${e.status}: ${e.type}` }], isError: true };
1998
+ return { content: [{ type: "text", text: explainEngineError("anchor", e.status, e.type) }], isError: true };
1929
1999
  }
1930
2000
  return { content: [{ type: "text", text: "ANCHOR failed: unexpected error" }], isError: true };
1931
2001
  }
@@ -1978,7 +2048,7 @@ server.registerTool(
1978
2048
  };
1979
2049
  } catch (e) {
1980
2050
  if (e instanceof AssessError) {
1981
- return { content: [{ type: "text", text: `CHECK ${e.status}: ${e.type}` }], isError: true };
2051
+ return { content: [{ type: "text", text: explainEngineError("check", e.status, e.type) }], isError: true };
1982
2052
  }
1983
2053
  return { content: [{ type: "text", text: "CHECK failed: unexpected error" }], isError: true };
1984
2054
  }
@@ -2025,6 +2095,32 @@ async function main() {
2025
2095
  await runSetup2({ force: true });
2026
2096
  return;
2027
2097
  }
2098
+ const allowAt = process.argv.indexOf("allow");
2099
+ if (allowAt !== -1) {
2100
+ const { readConfig: readConfig2, writeConfig: writeConfig2, configPath: configPath2 } = await Promise.resolve().then(() => (init_config(), config_exports));
2101
+ const payees = process.argv.slice(allowAt + 1).filter((a) => !a.startsWith("-"));
2102
+ if (!payees.length) {
2103
+ console.error("Usage: npx @fidacy/mcp allow <payee> [more payees\u2026]\nApproves counterparties this agent may pay. Everything else stays denied.");
2104
+ process.exitCode = 1;
2105
+ return;
2106
+ }
2107
+ const cfg = readConfig2();
2108
+ if (!cfg) {
2109
+ console.error("No Fidacy config yet. Run the agent once (or `npx @fidacy/mcp setup`) so the install exists, then approve a payee.");
2110
+ process.exitCode = 1;
2111
+ return;
2112
+ }
2113
+ const mandate = { ...cfg.mandate ?? {} };
2114
+ const current = Array.isArray(mandate.payees) ? mandate.payees : [];
2115
+ const added = payees.filter((p) => !current.includes(p));
2116
+ mandate.payees = [...current, ...added];
2117
+ writeConfig2({ ...cfg, mandate });
2118
+ console.error(
2119
+ added.length ? `Approved ${added.map((p) => `"${p}"`).join(", ")}. This agent may now pay ${added.length === 1 ? "that counterparty" : "those counterparties"} within the mandate's caps; everything else is still denied.
2120
+ Written to ${configPath2()}. The firewall picks it up on the next call, no restart needed.` : `Already approved: ${payees.map((p) => `"${p}"`).join(", ")}. Nothing changed.`
2121
+ );
2122
+ return;
2123
+ }
2028
2124
  const transport = new StdioServerTransport();
2029
2125
  await server.connect(transport);
2030
2126
  if (state.firstRun) recordInstall();
package/dist/lib.js CHANGED
@@ -643,7 +643,7 @@ function resolveMandateRules(cfg) {
643
643
  }
644
644
 
645
645
  // src/telemetry.ts
646
- var CLIENT_VERSION = true ? "0.6.1" : "dev";
646
+ var CLIENT_VERSION = true ? "0.6.3" : "dev";
647
647
  function bandOf(amount) {
648
648
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
649
649
  if (amount < 10) return "lt10";
@@ -831,7 +831,7 @@ function requestUpgrade() {
831
831
  }
832
832
 
833
833
  // src/provision.ts
834
- var CLIENT_VERSION2 = true ? "0.6.1" : "dev";
834
+ var CLIENT_VERSION2 = true ? "0.6.3" : "dev";
835
835
  function provisionEnabled() {
836
836
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
837
837
  return !(v === "1" || v === "true" || v === "yes");
@@ -994,7 +994,7 @@ function activationBootLine() {
994
994
  }
995
995
 
996
996
  // src/register.ts
997
- var CLIENT_VERSION3 = true ? "0.6.1" : "dev";
997
+ var CLIENT_VERSION3 = true ? "0.6.3" : "dev";
998
998
  function endpoint3() {
999
999
  const base = (process.env.FIDACY_ENGINE_URL ?? "https://api.fidacy.com").replace(/\/$/, "");
1000
1000
  return `${base}/v1/register`;
@@ -1160,7 +1160,7 @@ function renderSummary(s) {
1160
1160
  ];
1161
1161
  if (s.denied > 0) {
1162
1162
  const ranked = Object.entries(s.denied_by_rule).sort((a, b) => b[1] - a[1]);
1163
- lines.push(`Blocked: ${money(s.blocked_totals)} in attempted value. By rule: ${ranked.map(([rule, n]) => `${rule} (${n})`).join(", ")}.`);
1163
+ lines.push(`Blocked: ${money(s.blocked_totals)} in attempted value. By reason: ${ranked.map(([rule, n]) => `${ruleLabel(rule)} (${n})`).join(", ")}.`);
1164
1164
  }
1165
1165
  if (s.amount_unknown > 0) {
1166
1166
  lines.push(`${s.amount_unknown} record(s) in this window predate amount logging, so the totals above are a floor, not the exact figure.`);
@@ -1187,28 +1187,57 @@ function renderRows(rows) {
1187
1187
  return rows.map((r) => {
1188
1188
  const amount = typeof r.amount === "number" ? `${r.amount.toLocaleString("en-US")} ${r.currency ?? ""}`.trim() : "amount not recorded";
1189
1189
  const who = r.payee ?? "payee not recorded";
1190
- const why = r.status === "DENY" ? ` blocked by ${r.violatedRule ?? "a rule not recorded on this older entry"}.` : "";
1190
+ const why = r.status === "DENY" ? ` ${explainRule(r.violatedRule, r.payee)}` : "";
1191
1191
  const said = r.purpose ? ` Agent's stated reason: "${r.purpose}".` : "";
1192
1192
  return `#${r.seq} ${r.ts} ${r.status} ${amount} to ${who}.${why}${said} (decision ${r.decisionId})`;
1193
1193
  }).join("\n");
1194
1194
  }
1195
+ function ruleLabel(rule) {
1196
+ if (rule.startsWith("payee_lookalike")) return "lookalike payee";
1197
+ if (rule.startsWith("payee_not_in_allowlist")) return "payee not approved";
1198
+ if (rule.startsWith("category_not_allowed")) return "purpose not allowed";
1199
+ if (rule.startsWith("currency_not_allowed")) return "wrong currency";
1200
+ if (rule.startsWith("per_tx") || rule.includes("perTxMax")) return "over the per-payment cap";
1201
+ if (rule.includes("maxTotal") || rule.startsWith("total_cap") || rule.startsWith("budget")) return "over the total budget";
1202
+ if (rule.startsWith("invoice") || rule.includes("duplicate")) return "duplicate invoice";
1203
+ if (rule.startsWith("window") || rule.includes("mandate_window") || rule.includes("expired")) return "outside the mandate window";
1204
+ if (rule.startsWith("revoked") || rule.startsWith("mandate_revoked")) return "mandate revoked";
1205
+ if (rule.startsWith("invalid_mandate")) return "mandate misconfigured";
1206
+ if (rule.startsWith("non_positive_amount")) return "invalid amount";
1207
+ if (rule.startsWith("activation_required")) return "trial used up";
1208
+ if (rule === "not_recorded") return "rule not recorded";
1209
+ return rule;
1210
+ }
1195
1211
  function explainRule(rule, payee) {
1196
1212
  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.";
1213
+ if (rule.startsWith("invalid_mandate")) {
1214
+ const cap = rule.includes("perTxMax") ? "perTxMax" : rule.includes("maxTotal") ? "maxTotal" : null;
1215
+ if (cap)
1216
+ return `The mandate's \`${cap}\` is not a usable number, so the firewall denied rather than run without that cap. Values like "2,500", "$2500" or "2500 USD" all look right to a human and become NaN, and a cap that cannot be compared would silently let everything through. Write it as a bare number: ${cap}: 2500.`;
1217
+ return "The mandate is missing its `allow` block, so there is nothing to enforce and the firewall denied. Check ~/.fidacy/config.json or FIDACY_MANDATE_JSON.";
1218
+ }
1219
+ if (rule.startsWith("payee_lookalike")) {
1220
+ const approved = rule.includes("~") ? rule.split("~")[1] : "an approved payee";
1221
+ return `The payee${payee ? ` "${payee}"` : ""} is a lookalike of your approved payee "${approved}": same name to a human glance, different string. That is the signature of a payee swap, whether from a prompt injection or a spoofed invoice. If it really is a separate vendor, add it to \`payees\` explicitly; if it is not, this attempt is exactly what the firewall exists to stop.`;
1222
+ }
1197
1223
  if (rule.startsWith("payee_not_in_allowlist"))
1198
- 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.`;
1224
+ 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. To authorize this one, add it to \`payees\` in ~/.fidacy/config.json, or pass a full mandate through FIDACY_MANDATE_JSON.`;
1199
1225
  if (rule.startsWith("category_not_allowed"))
1200
- return "The stated purpose category is not one the mandate permits for this agent.";
1226
+ return "The stated purpose category is not one the mandate permits for this agent. Add it to `categories` in the mandate, or send the payment under a purpose the mandate already allows.";
1201
1227
  if (rule.startsWith("currency_not_allowed"))
1202
- return "The payment currency is not the mandate's currency.";
1228
+ return "The payment currency is not the mandate's currency. Set `currency` to the one you intend to pay in. A mandate authorizes one currency at a time on purpose, so a swapped currency cannot slip past a cap set in another.";
1203
1229
  if (rule.startsWith("per_tx") || rule.includes("perTxMax"))
1204
- return "The amount exceeds the per-transaction ceiling in the mandate.";
1205
- if (rule.includes("maxTotal") || rule.startsWith("budget"))
1206
- return "The payment would push cumulative spend past the mandate's total budget for the window.";
1230
+ return "The amount exceeds the per-transaction ceiling in the mandate. Raise `perTxMax` if payments this size are expected, or split the payment.";
1231
+ if (rule.includes("maxTotal") || rule.startsWith("budget") || rule.startsWith("total_cap"))
1232
+ return "The payment would push cumulative spend past the mandate's total budget for the window. Raise `maxTotal`, or wait for the window to roll over. Blocked attempts do not consume the budget; only payments that went through do.";
1207
1233
  if (rule.startsWith("invoice") || rule.includes("duplicate"))
1208
- 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.";
1209
- if (rule.startsWith("window") || rule.includes("expired"))
1210
- return "The mandate was outside its validity window at the moment of the request.";
1211
- if (rule.startsWith("revoked")) return "The mandate had been revoked.";
1234
+ 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. If this genuinely is a second, separate payment, give it its own invoiceRef.";
1235
+ if (rule.startsWith("window") || rule.includes("mandate_window") || rule.includes("expired"))
1236
+ return "The mandate was outside its validity window at the moment of the request. Check `notBefore` and `notAfter`, and issue a mandate whose window covers now.";
1237
+ if (rule.startsWith("revoked") || rule.startsWith("mandate_revoked"))
1238
+ return "The mandate had been revoked. A revoked mandate cannot be un-revoked; issue a new one to authorize further payments.";
1239
+ if (rule.startsWith("non_positive_amount"))
1240
+ return "The amount was zero or negative, which is not a payment anyone can authorize. Check what produced that value before retrying.";
1212
1241
  if (rule.startsWith("activation_required"))
1213
1242
  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.";
1214
1243
  return `The mandate rule "${rule}" was violated.`;
@@ -134,7 +134,7 @@ var init_register = __esm({
134
134
  "src/register.ts"() {
135
135
  "use strict";
136
136
  init_config();
137
- CLIENT_VERSION = true ? "0.6.1" : "dev";
137
+ CLIENT_VERSION = true ? "0.6.3" : "dev";
138
138
  }
139
139
  });
140
140
 
@@ -56,4 +56,26 @@ export declare function renderRows(rows: DecisionRow[]): string;
56
56
  * "why" without reading a rule name: a violated rule is a policy sentence, and
57
57
  * the mandate that produced it is the thing they can actually change.
58
58
  */
59
+ /**
60
+ * Two-to-four word name for a rule, for places that RANK rules rather than
61
+ * explain one. The ranked list in a summary needs a label a human can scan, not
62
+ * a paragraph and not `per_tx_cap_exceeded:4200>1000`.
63
+ */
64
+ export declare function ruleLabel(rule: string): string;
59
65
  export declare function explainRule(rule: string | undefined, payee?: string): string;
66
+ /**
67
+ * Plain-language reading of an ENGINE failure, for the moment a call is refused.
68
+ *
69
+ * The hosted path used to surface `ASSESS 422: mandate_violation (payee_not_in_allowlist)`
70
+ * verbatim. That is a log line: it names a status code and an internal key, and
71
+ * leaves the operator to guess whether Fidacy is down, their key is wrong, or
72
+ * their own policy just did its job. Those three have completely different fixes,
73
+ * and only one of them is a problem with Fidacy.
74
+ *
75
+ * `reasons` are the engine's rejection keys, which map onto the same vocabulary
76
+ * explainRule already speaks, so a policy rejection reads the same whether it was
77
+ * decided locally or hosted.
78
+ */
79
+ export declare function explainEngineError(action: "verdict" | "anchor" | "check", status: number, type: string, reasons?: {
80
+ key: string;
81
+ }[]): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fidacy/mcp",
3
- "version": "0.6.1",
3
+ "version": "0.6.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
  "author": "Fidacy (ZeepCode Group Technology LLC) <hello@fidacy.com> (https://fidacy.com)",
@@ -50,6 +50,15 @@
50
50
  "publishConfig": {
51
51
  "access": "public"
52
52
  },
53
+ "scripts": {
54
+ "build": "node scripts/bundle.mjs",
55
+ "dev": "tsx watch src/index.ts",
56
+ "start": "node dist/index.js",
57
+ "prepublishOnly": "npm run build",
58
+ "typecheck": "tsc --noEmit",
59
+ "test": "vitest run",
60
+ "postinstall": "node -e \"try{require('fs').accessSync('dist/postinstall.js')}catch{process.exit(0)};import('./dist/postinstall.js')\" || exit 0"
61
+ },
53
62
  "engines": {
54
63
  "node": ">=18"
55
64
  },
@@ -58,23 +67,15 @@
58
67
  "zod": "^3.25.0"
59
68
  },
60
69
  "devDependencies": {
70
+ "@fidacy/firewall": "workspace:*",
61
71
  "@types/node": "^22.10.0",
62
72
  "tsx": "^4.19.2",
63
73
  "typescript": "^5.7.2",
64
- "vitest": "^3.2.4",
65
- "@fidacy/firewall": "0.1.1"
74
+ "vitest": "^3.2.4"
66
75
  },
67
76
  "mcpName": "com.fidacy/ai-agent-firewall",
68
77
  "repository": {
69
78
  "type": "git",
70
79
  "url": "git+https://github.com/lucaslubi/fidacy-mcp.git"
71
- },
72
- "scripts": {
73
- "build": "node scripts/bundle.mjs",
74
- "dev": "tsx watch src/index.ts",
75
- "start": "node dist/index.js",
76
- "typecheck": "tsc --noEmit",
77
- "test": "vitest run",
78
- "postinstall": "node -e \"try{require('fs').accessSync('dist/postinstall.js')}catch{process.exit(0)};import('./dist/postinstall.js')\" || exit 0"
79
80
  }
80
- }
81
+ }