@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 +1 -1
- package/dist/index.js +118 -22
- package/dist/lib.js +44 -15
- package/dist/postinstall.js +1 -1
- package/dist/reporting.d.ts +22 -0
- package/package.json +13 -12
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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" ? `
|
|
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")
|
|
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.
|
|
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(
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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" ? `
|
|
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")
|
|
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.`;
|
package/dist/postinstall.js
CHANGED
package/dist/reporting.d.ts
CHANGED
|
@@ -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.
|
|
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
|
+
}
|