@fidacy/mcp 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix `npx @fidacy/mcp setup` hanging when it has no terminal. The explicit command forced the prompt open regardless of context, so running it with redirected or piped input waited forever for an answer that could never come. The terminal check now covers that path too and prints how to run it properly instead. Also fixes the postinstall hook failing an install inside a source checkout, where the built file it points at does not exist yet.
8
+
3
9
  ## 0.6.0
4
10
 
5
11
  ### Minor Changes
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.0" : "dev";
491
+ var CLIENT_VERSION = true ? "0.6.2" : "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
@@ -214,7 +214,7 @@ var init_telemetry = __esm({
214
214
  "src/telemetry.ts"() {
215
215
  "use strict";
216
216
  init_config();
217
- CLIENT_VERSION = true ? "0.6.0" : "dev";
217
+ CLIENT_VERSION = true ? "0.6.2" : "dev";
218
218
  currentShell = "mcp";
219
219
  anonIdCache = null;
220
220
  buffer = [];
@@ -291,7 +291,7 @@ var init_register = __esm({
291
291
  "src/register.ts"() {
292
292
  "use strict";
293
293
  init_config();
294
- CLIENT_VERSION3 = true ? "0.6.0" : "dev";
294
+ CLIENT_VERSION3 = true ? "0.6.2" : "dev";
295
295
  }
296
296
  });
297
297
 
@@ -521,7 +521,18 @@ function looksLikeEmail2(s) {
521
521
  return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(s);
522
522
  }
523
523
  async function runSetup(opts = {}) {
524
- if (!opts.force && !isInteractive()) return "skipped_noninteractive";
524
+ if (!isInteractive()) {
525
+ if (opts.force) {
526
+ process.stdout.write(
527
+ `
528
+ ${BRAND}: this needs an interactive terminal.
529
+ Run \`npx @fidacy/mcp setup\` directly in your shell, or set FIDACY_OPERATOR_EMAIL.
530
+
531
+ `
532
+ );
533
+ }
534
+ return "skipped_noninteractive";
535
+ }
525
536
  ensureState();
526
537
  if (!needsOperatorEmail() && !opts.force) return "already";
527
538
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -1256,7 +1267,7 @@ init_telemetry();
1256
1267
 
1257
1268
  // src/provision.ts
1258
1269
  init_config();
1259
- var CLIENT_VERSION2 = true ? "0.6.0" : "dev";
1270
+ var CLIENT_VERSION2 = true ? "0.6.2" : "dev";
1260
1271
  function provisionEnabled() {
1261
1272
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
1262
1273
  return !(v === "1" || v === "true" || v === "yes");
@@ -1373,7 +1384,7 @@ function renderSummary(s) {
1373
1384
  ];
1374
1385
  if (s.denied > 0) {
1375
1386
  const ranked = Object.entries(s.denied_by_rule).sort((a, b) => b[1] - a[1]);
1376
- lines.push(`Blocked: ${money(s.blocked_totals)} in attempted value. By rule: ${ranked.map(([rule, n]) => `${rule} (${n})`).join(", ")}.`);
1387
+ lines.push(`Blocked: ${money(s.blocked_totals)} in attempted value. By reason: ${ranked.map(([rule, n]) => `${ruleLabel(rule)} (${n})`).join(", ")}.`);
1377
1388
  }
1378
1389
  if (s.amount_unknown > 0) {
1379
1390
  lines.push(`${s.amount_unknown} record(s) in this window predate amount logging, so the totals above are a floor, not the exact figure.`);
@@ -1400,32 +1411,82 @@ function renderRows(rows2) {
1400
1411
  return rows2.map((r) => {
1401
1412
  const amount = typeof r.amount === "number" ? `${r.amount.toLocaleString("en-US")} ${r.currency ?? ""}`.trim() : "amount not recorded";
1402
1413
  const who = r.payee ?? "payee not recorded";
1403
- const why = r.status === "DENY" ? ` blocked by ${r.violatedRule ?? "a rule not recorded on this older entry"}.` : "";
1414
+ const why = r.status === "DENY" ? ` ${explainRule(r.violatedRule, r.payee)}` : "";
1404
1415
  const said = r.purpose ? ` Agent's stated reason: "${r.purpose}".` : "";
1405
1416
  return `#${r.seq} ${r.ts} ${r.status} ${amount} to ${who}.${why}${said} (decision ${r.decisionId})`;
1406
1417
  }).join("\n");
1407
1418
  }
1419
+ function ruleLabel(rule) {
1420
+ if (rule.startsWith("payee_lookalike")) return "lookalike payee";
1421
+ if (rule.startsWith("payee_not_in_allowlist")) return "payee not approved";
1422
+ if (rule.startsWith("category_not_allowed")) return "purpose not allowed";
1423
+ if (rule.startsWith("currency_not_allowed")) return "wrong currency";
1424
+ if (rule.startsWith("per_tx") || rule.includes("perTxMax")) return "over the per-payment cap";
1425
+ if (rule.includes("maxTotal") || rule.startsWith("total_cap") || rule.startsWith("budget")) return "over the total budget";
1426
+ if (rule.startsWith("invoice") || rule.includes("duplicate")) return "duplicate invoice";
1427
+ if (rule.startsWith("window") || rule.includes("mandate_window") || rule.includes("expired")) return "outside the mandate window";
1428
+ if (rule.startsWith("revoked") || rule.startsWith("mandate_revoked")) return "mandate revoked";
1429
+ if (rule.startsWith("invalid_mandate")) return "mandate misconfigured";
1430
+ if (rule.startsWith("non_positive_amount")) return "invalid amount";
1431
+ if (rule.startsWith("activation_required")) return "trial used up";
1432
+ if (rule === "not_recorded") return "rule not recorded";
1433
+ return rule;
1434
+ }
1408
1435
  function explainRule(rule, payee) {
1409
1436
  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.";
1437
+ if (rule.startsWith("invalid_mandate")) {
1438
+ const cap = rule.includes("perTxMax") ? "perTxMax" : rule.includes("maxTotal") ? "maxTotal" : null;
1439
+ if (cap)
1440
+ 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.`;
1441
+ 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.";
1442
+ }
1443
+ if (rule.startsWith("payee_lookalike")) {
1444
+ const approved = rule.includes("~") ? rule.split("~")[1] : "an approved payee";
1445
+ 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.`;
1446
+ }
1410
1447
  if (rule.startsWith("payee_not_in_allowlist"))
1411
- 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.`;
1448
+ 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.`;
1412
1449
  if (rule.startsWith("category_not_allowed"))
1413
- return "The stated purpose category is not one the mandate permits for this agent.";
1450
+ 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.";
1414
1451
  if (rule.startsWith("currency_not_allowed"))
1415
- return "The payment currency is not the mandate's currency.";
1452
+ 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.";
1416
1453
  if (rule.startsWith("per_tx") || rule.includes("perTxMax"))
1417
- return "The amount exceeds the per-transaction ceiling in the mandate.";
1418
- if (rule.includes("maxTotal") || rule.startsWith("budget"))
1419
- return "The payment would push cumulative spend past the mandate's total budget for the window.";
1454
+ return "The amount exceeds the per-transaction ceiling in the mandate. Raise `perTxMax` if payments this size are expected, or split the payment.";
1455
+ if (rule.includes("maxTotal") || rule.startsWith("budget") || rule.startsWith("total_cap"))
1456
+ 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.";
1420
1457
  if (rule.startsWith("invoice") || rule.includes("duplicate"))
1421
- 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.";
1422
- if (rule.startsWith("window") || rule.includes("expired"))
1423
- return "The mandate was outside its validity window at the moment of the request.";
1424
- if (rule.startsWith("revoked")) return "The mandate had been revoked.";
1458
+ 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.";
1459
+ if (rule.startsWith("window") || rule.includes("mandate_window") || rule.includes("expired"))
1460
+ 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.";
1461
+ if (rule.startsWith("revoked") || rule.startsWith("mandate_revoked"))
1462
+ return "The mandate had been revoked. A revoked mandate cannot be un-revoked; issue a new one to authorize further payments.";
1463
+ if (rule.startsWith("non_positive_amount"))
1464
+ return "The amount was zero or negative, which is not a payment anyone can authorize. Check what produced that value before retrying.";
1425
1465
  if (rule.startsWith("activation_required"))
1426
1466
  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.";
1427
1467
  return `The mandate rule "${rule}" was violated.`;
1428
1468
  }
1469
+ function explainEngineError(action, status, type, reasons) {
1470
+ const noun = action === "verdict" ? "assess this action" : action === "anchor" ? "anchor this artifact" : "check this artifact";
1471
+ if (status === 401 || status === 403)
1472
+ 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.`;
1473
+ if (status === 402)
1474
+ 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.`;
1475
+ if (status === 429)
1476
+ 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.`;
1477
+ if (status === 404 && action === "check")
1478
+ 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.";
1479
+ if (status === 422 || type === "mandate_violation") {
1480
+ const first = reasons?.[0]?.key;
1481
+ const why = first ? ` ${explainRule(first)}` : "";
1482
+ return `Fidacy denied this action: it falls outside the mandate in force.${why} Nothing was executed.`;
1483
+ }
1484
+ if (status >= 500)
1485
+ 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.`;
1486
+ if (status === 400)
1487
+ 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.`;
1488
+ return `Fidacy could not ${noun} (HTTP ${status}${type ? `, ${type}` : ""}), so the call was refused rather than allowed through. Nothing was executed.`;
1489
+ }
1429
1490
 
1430
1491
  // src/banner.ts
1431
1492
  init_activation();
@@ -1583,7 +1644,7 @@ function renderAlertLine(alerts) {
1583
1644
  var state = ensureState();
1584
1645
  var core = makeCore();
1585
1646
  var subject = process.env.FIDACY_SUBJECT ?? "agent:demo";
1586
- var SERVER_VERSION = true ? "0.6.0" : "dev";
1647
+ var SERVER_VERSION = true ? "0.6.2" : "dev";
1587
1648
  var bootClaim = claimUrl();
1588
1649
  var server = new McpServer(
1589
1650
  // The commercial identity. `name` is the stable slug hosts key on; `title` is
@@ -1848,9 +1909,8 @@ server.registerTool(
1848
1909
  markHostedLapsed();
1849
1910
  return { content: [{ type: "text", text: hostedWallCta("verdict", e.billing) }], isError: true };
1850
1911
  }
1851
- const reasons = e.rejection_reasons?.length ? " (" + e.rejection_reasons.map((x) => x.key).join(",") + ")" : "";
1852
1912
  return {
1853
- content: [{ type: "text", text: `ASSESS ${e.status}: ${e.type}${reasons}` }],
1913
+ content: [{ type: "text", text: explainEngineError("verdict", e.status, e.type, e.rejection_reasons) }],
1854
1914
  isError: true
1855
1915
  };
1856
1916
  }
@@ -1914,7 +1974,7 @@ server.registerTool(
1914
1974
  markHostedLapsed();
1915
1975
  return { content: [{ type: "text", text: hostedWallCta("anchor", e.billing) }], isError: true };
1916
1976
  }
1917
- return { content: [{ type: "text", text: `ANCHOR ${e.status}: ${e.type}` }], isError: true };
1977
+ return { content: [{ type: "text", text: explainEngineError("anchor", e.status, e.type) }], isError: true };
1918
1978
  }
1919
1979
  return { content: [{ type: "text", text: "ANCHOR failed: unexpected error" }], isError: true };
1920
1980
  }
@@ -1967,7 +2027,7 @@ server.registerTool(
1967
2027
  };
1968
2028
  } catch (e) {
1969
2029
  if (e instanceof AssessError) {
1970
- return { content: [{ type: "text", text: `CHECK ${e.status}: ${e.type}` }], isError: true };
2030
+ return { content: [{ type: "text", text: explainEngineError("check", e.status, e.type) }], isError: true };
1971
2031
  }
1972
2032
  return { content: [{ type: "text", text: "CHECK failed: unexpected error" }], isError: true };
1973
2033
  }
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.0" : "dev";
646
+ var CLIENT_VERSION = true ? "0.6.2" : "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.0" : "dev";
834
+ var CLIENT_VERSION2 = true ? "0.6.2" : "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.0" : "dev";
997
+ var CLIENT_VERSION3 = true ? "0.6.2" : "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.0" : "dev";
137
+ CLIENT_VERSION = true ? "0.6.2" : "dev";
138
138
  }
139
139
  });
140
140
 
@@ -192,7 +192,18 @@ function looksLikeEmail2(s) {
192
192
  return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(s);
193
193
  }
194
194
  async function runSetup(opts = {}) {
195
- if (!opts.force && !isInteractive()) return "skipped_noninteractive";
195
+ if (!isInteractive()) {
196
+ if (opts.force) {
197
+ process.stdout.write(
198
+ `
199
+ ${BRAND}: this needs an interactive terminal.
200
+ Run \`npx @fidacy/mcp setup\` directly in your shell, or set FIDACY_OPERATOR_EMAIL.
201
+
202
+ `
203
+ );
204
+ }
205
+ return "skipped_noninteractive";
206
+ }
196
207
  ensureState();
197
208
  if (!needsOperatorEmail() && !opts.force) return "already";
198
209
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -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.0",
3
+ "version": "0.6.2",
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
+ }