@fidacy/mcp 0.3.3 → 0.4.0

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Every block is now credited, and every gap is now felt. A denied payment answers with `🛡 Fidacy AI Agent Firewall blocked this payment. Your agent was protected.` — the product's most valuable moment, said in the operator's language, with no upsell attached to it. Around that, the install now reports one honest protection state at boot and on the responses that matter: covered, about to halt, halted, or blocking-but-no-longer-provable. Each state names the brand, states the consequence, and gives one route forward: the free rung (free key, 300 signed verdicts a month) before the paid one (pay-as-you-go from $0.04).
8
+
9
+ What it deliberately does NOT do is tell you your agent is unprotected while the local firewall is still denying payments. This product blocks or halts; it never quietly stops defending. When the anonymous trial runs out the copy says the true and more urgent thing — your agent cannot pay, it is frozen, not exposed — and when a paid allowance runs out it says that your blocks continue but stop being provable to an auditor, insurer or counterparty. A security tool that inflates its own alarm is the one thing we cannot ship.
10
+
11
+ An install that lapses and stays wired in keeps being told, across restarts, that its blocks are no longer provable and how to restore them.
12
+
13
+ ## 0.3.4
14
+
15
+ ### Patch Changes
16
+
17
+ - Declare the MCP Registry identity that matches the product name: `mcpName` is now `com.fidacy/ai-agent-firewall`. The registry validates server ownership against this field, so the listing could not carry the commercial name until the package declared it. Nothing about installing or running the server changes.
18
+
3
19
  ## 0.3.3
4
20
 
5
21
  ### Minor Changes
package/dist/config.d.ts CHANGED
@@ -36,6 +36,13 @@ export interface FidacyConfig {
36
36
  nudges?: Partial<Record<string, boolean>>;
37
37
  /** Payment-gate decisions taken on this install (milestone nudges). */
38
38
  decisions_count?: number;
39
+ /**
40
+ * ISO timestamp of when the hosted allowance last ran out. Persisted so the
41
+ * "your blocks are no longer provable" warning survives restarts: an install
42
+ * that lapsed but is still wired in is the highest-intent audience we have.
43
+ * Cleared as soon as a hosted call succeeds again.
44
+ */
45
+ hosted_lapsed_at?: string;
39
46
  }
40
47
  /**
41
48
  * True when the install carries an operator-set engine key (env, or the plugin's
package/dist/core.js CHANGED
@@ -454,7 +454,8 @@ function readConfig() {
454
454
  // reset every once-per-install nudge into an every-time nag.
455
455
  created_at: typeof raw.created_at === "string" ? raw.created_at : void 0,
456
456
  nudges: raw.nudges && typeof raw.nudges === "object" ? raw.nudges : void 0,
457
- decisions_count: typeof raw.decisions_count === "number" ? raw.decisions_count : void 0
457
+ decisions_count: typeof raw.decisions_count === "number" ? raw.decisions_count : void 0,
458
+ hosted_lapsed_at: typeof raw.hosted_lapsed_at === "string" ? raw.hosted_lapsed_at : void 0
458
459
  };
459
460
  } catch {
460
461
  return null;
@@ -485,7 +486,7 @@ function resolveMandateRules(cfg) {
485
486
  }
486
487
 
487
488
  // src/telemetry.ts
488
- var CLIENT_VERSION = true ? "0.3.3" : "dev";
489
+ var CLIENT_VERSION = true ? "0.4.0" : "dev";
489
490
  function bandOf(amount) {
490
491
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
491
492
  if (amount < 10) return "lt10";
package/dist/index.js CHANGED
@@ -465,7 +465,8 @@ function readConfig() {
465
465
  // reset every once-per-install nudge into an every-time nag.
466
466
  created_at: typeof raw.created_at === "string" ? raw.created_at : void 0,
467
467
  nudges: raw.nudges && typeof raw.nudges === "object" ? raw.nudges : void 0,
468
- decisions_count: typeof raw.decisions_count === "number" ? raw.decisions_count : void 0
468
+ decisions_count: typeof raw.decisions_count === "number" ? raw.decisions_count : void 0,
469
+ hosted_lapsed_at: typeof raw.hosted_lapsed_at === "string" ? raw.hosted_lapsed_at : void 0
469
470
  };
470
471
  } catch {
471
472
  return null;
@@ -517,7 +518,7 @@ function resolveMandateRules(cfg) {
517
518
  }
518
519
 
519
520
  // src/telemetry.ts
520
- var CLIENT_VERSION = true ? "0.3.3" : "dev";
521
+ var CLIENT_VERSION = true ? "0.4.0" : "dev";
521
522
  function bandOf(amount) {
522
523
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
523
524
  if (amount < 10) return "lt10";
@@ -879,7 +880,7 @@ async function findArtifacts(sha2562, cfg) {
879
880
  }
880
881
 
881
882
  // src/provision.ts
882
- var CLIENT_VERSION2 = true ? "0.3.3" : "dev";
883
+ var CLIENT_VERSION2 = true ? "0.4.0" : "dev";
883
884
  function provisionEnabled() {
884
885
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
885
886
  return !(v === "1" || v === "true" || v === "yes");
@@ -989,9 +990,9 @@ function decisionNudge(status, violatedRule, upgradeToolName) {
989
990
  function hostedWallCta(feature, billing) {
990
991
  const noun = feature === "anchor" ? "Bitcoin-anchored proofs" : "server-signed verdicts";
991
992
  if (hasEngineKey()) {
992
- const where = billing?.billingUrl ?? "https://app.fidacy.com/billing";
993
+ const where2 = billing?.billingUrl ?? "https://app.fidacy.com/billing";
993
994
  const what = billing?.reason === "spending_cap_reached" ? `This account hit the monthly spending cap you set, so ${noun} are paused.` : billing?.detail ?? `This account's included ${noun} for the month are used up.`;
994
- return `${what} The local firewall keeps deciding and blocking as normal \u2014 this only pauses ${noun}. For your operator: resolve it at ${where}. Pay-as-you-go needs no plan and starts at $0.04 per verdict, dropping to $0.025 past 50k and $0.015 past 250k; committed plans price below that curve.`;
995
+ return `${what} The local firewall keeps deciding and blocking as normal \u2014 this only pauses ${noun}. For your operator: resolve it at ${where2}. Pay-as-you-go needs no plan and starts at $0.04 per verdict, dropping to $0.025 past 50k and $0.015 past 250k; committed plans price below that curve.`;
995
996
  }
996
997
  const url = claimUrl();
997
998
  const cta = url ? `open ${url} \u2014 free account, this install's history migrates` : `call the upgrade tool \u2014 free account, your history migrates`;
@@ -1044,6 +1045,74 @@ function trialCountdownLine(keyOverride) {
1044
1045
  return ` [fidacy] ${left} free anonymous decision${left === 1 ? "" : "s"} left on this install before the firewall requires activation. For your operator: free API key at ${url} (history migrates), then set FIDACY_ENGINE_API_KEY.`;
1045
1046
  }
1046
1047
 
1048
+ // src/protection.ts
1049
+ var BRAND = "Fidacy AI Agent Firewall";
1050
+ var FREE_MONTHLY_VERDICTS = 300;
1051
+ function markHostedLapsed() {
1052
+ const cfg = readConfig();
1053
+ if (!cfg || cfg.hosted_lapsed_at) return;
1054
+ cfg.hosted_lapsed_at = (/* @__PURE__ */ new Date()).toISOString();
1055
+ try {
1056
+ writeConfig(cfg);
1057
+ } catch {
1058
+ }
1059
+ }
1060
+ function clearHostedLapsed() {
1061
+ const cfg = readConfig();
1062
+ if (!cfg?.hosted_lapsed_at) return;
1063
+ delete cfg.hosted_lapsed_at;
1064
+ try {
1065
+ writeConfig(cfg);
1066
+ } catch {
1067
+ }
1068
+ }
1069
+ function isHostedLapsed() {
1070
+ return Boolean(readConfig()?.hosted_lapsed_at);
1071
+ }
1072
+ function protectionState(decisionsUsed2, keyOverride) {
1073
+ if (hasEngineKey(keyOverride)) return isHostedLapsed() ? "unproven" : "protected_keyed";
1074
+ if (decisionsUsed2 >= FREE_DECISIONS) return "halted";
1075
+ return FREE_DECISIONS - decisionsUsed2 <= 5 ? "protected_low" : "protected_trial";
1076
+ }
1077
+ function where() {
1078
+ return claimUrl() ?? "https://fidacy.com/claim";
1079
+ }
1080
+ function protectionBadge() {
1081
+ return `\u{1F6E1} ${BRAND} blocked this payment. Your agent was protected.`;
1082
+ }
1083
+ function protectionWarning(decisionsUsed2, keyOverride) {
1084
+ const state2 = protectionState(decisionsUsed2, keyOverride);
1085
+ const left = Math.max(0, FREE_DECISIONS - decisionsUsed2);
1086
+ switch (state2) {
1087
+ case "protected_trial":
1088
+ return null;
1089
+ // working and nothing is expiring soon: silence is the respect.
1090
+ case "protected_low":
1091
+ return `\u26A0 ${BRAND}: ${left} free decision${left === 1 ? "" : "s"} left on this install. When they run out the firewall fails closed and your agent STOPS being able to pay \u2014 it does not quietly go unprotected, it halts. Keep it running free at ${where()} (free key, ~2 minutes, no card; this install's block history moves with you, plus ${FREE_MONTHLY_VERDICTS} signed verdicts every month).`;
1092
+ case "halted":
1093
+ return `\u26D4 ${BRAND} is NOT ACTIVATED and your agent can no longer pay. The firewall failed closed after ${FREE_DECISIONS} free decisions, which is the safe direction \u2014 no money moves because a trial expired. Your agent is frozen, not exposed. Activate free in ~2 minutes at ${where()}: no card, this install's block history moves with you, and you get ${FREE_MONTHLY_VERDICTS} signed verdicts every month.`;
1094
+ case "unproven":
1095
+ return `\u26A0 ${BRAND}: your agent is still being blocked locally, but its blocks are no longer PROVABLE \u2014 the monthly signed-verdict allowance is spent, so nothing new can be shown to an auditor, insurer or counterparty. Restore proof at https://app.fidacy.com/billing (pay-as-you-go, no plan, from $0.04 per verdict).`;
1096
+ case "protected_keyed":
1097
+ return null;
1098
+ }
1099
+ }
1100
+ function protectionBootLine(decisionsUsed2) {
1101
+ const state2 = protectionState(decisionsUsed2);
1102
+ switch (state2) {
1103
+ case "halted":
1104
+ return ` \u26D4 NOT ACTIVATED \u2014 your agent cannot pay until you activate at ${where()} (free, no card).`;
1105
+ case "protected_low":
1106
+ return ` \u26A0 ${Math.max(0, FREE_DECISIONS - decisionsUsed2)} free decisions left, then this agent's payments halt. Free key: ${where()}`;
1107
+ case "unproven":
1108
+ return ` \u26A0 Blocking locally, but blocks are no longer provable \u2014 allowance spent. Restore at https://app.fidacy.com/billing`;
1109
+ case "protected_trial":
1110
+ return ` ${BRAND} active \u2014 ${decisionsUsed2}/${FREE_DECISIONS} free decisions used.`;
1111
+ case "protected_keyed":
1112
+ return ` ${BRAND} active and signing \u2014 blocks are provable to third parties.`;
1113
+ }
1114
+ }
1115
+
1047
1116
  // src/reporting.ts
1048
1117
  function withinDays(records, days, now = Date.now()) {
1049
1118
  if (!Number.isFinite(days) || days <= 0) return [...records];
@@ -1312,7 +1381,7 @@ function renderAlertLine(alerts) {
1312
1381
  var state = ensureState();
1313
1382
  var core = makeCore();
1314
1383
  var subject = process.env.FIDACY_SUBJECT ?? "agent:demo";
1315
- var SERVER_VERSION = true ? "0.3.3" : "dev";
1384
+ var SERVER_VERSION = true ? "0.4.0" : "dev";
1316
1385
  var bootClaim = claimUrl();
1317
1386
  var server = new McpServer(
1318
1387
  // The commercial identity. `name` is the stable slug hosts key on; `title` is
@@ -1354,7 +1423,8 @@ server.registerTool(
1354
1423
  const gate = activationGate();
1355
1424
  if (gate) {
1356
1425
  const out2 = { status: "DENY", decisionId: "activation_required", violatedRule: "activation_required" };
1357
- return { content: [{ type: "text", text: gate.message }], structuredContent: out2 };
1426
+ const halted = protectionWarning(decisionsUsed()) ?? gate.message;
1427
+ return { content: [{ type: "text", text: halted }], structuredContent: out2 };
1358
1428
  }
1359
1429
  const req = args;
1360
1430
  let sentinelAlerts = [];
@@ -1368,9 +1438,11 @@ server.registerTool(
1368
1438
  const sentinelLine = d.status === "ALLOW" ? renderAlertLine(sentinelAlerts) : "";
1369
1439
  const base = d.status === "ALLOW" ? `ALLOW (decision ${d.decisionId})${req.invoiceRef ? ` for invoice ${req.invoiceRef}` : ""}.${sentinelLine} To settle, call execute_payment with the SAME payee, amount, currency, and idempotencyKey, and set "grant" to EXACTLY this signed value:
1370
1440
  ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}. No grant issued, this payment cannot proceed. The denial itself is recorded in the tamper-evident, hash-chained audit: call get_audit_proof with decisionId ${d.decisionId} for the proof of what was blocked.${d.violatedRule?.startsWith("payee_not_in_allowlist") ? ` If the user trusts this payee, add "${req.payee}" to mandate.payees in ~/.fidacy/config.json and retry: the firewall picks the change up on the next call, no restart needed.` : d.violatedRule?.startsWith("category_not_allowed") ? ` If the user considers "${req.category}" a legitimate purpose for this agent, add it to mandate.categories in ~/.fidacy/config.json and retry: the firewall picks the change up on the next call, no restart needed.` : d.violatedRule?.startsWith("currency_not_allowed") ? ` The mandate is single-currency and set to another one (mandate.currency in ~/.fidacy/config.json, default USD). If this agent legitimately pays in ${req.currency}, update it there and retry: hot-reloaded, no restart.` : ""}`;
1441
+ const badge = d.status === "DENY" ? protectionBadge() : "";
1371
1442
  const nudge = decisionNudge(d.status, d.violatedRule, "upgrade");
1372
1443
  const countdown = trialCountdownLine();
1373
- const human = [base, nudge, countdown].filter(Boolean).join(" ");
1444
+ const lapse = isHostedLapsed() ? protectionWarning(decisionsUsed()) : "";
1445
+ const human = [badge, base, nudge, countdown, lapse].filter(Boolean).join(" ");
1374
1446
  return { content: [{ type: "text", text: human }], structuredContent: out };
1375
1447
  }
1376
1448
  );
@@ -1537,6 +1609,7 @@ server.registerTool(
1537
1609
  { kind, mandate, mandateType, idempotencyKey, spendingMandate, a2a },
1538
1610
  { engineUrl, apiKey }
1539
1611
  );
1612
+ clearHostedLapsed();
1540
1613
  return {
1541
1614
  content: [{ type: "text", text: `${r.decision} (score ${r.score}) signed by ${r.signingKeyId}` }],
1542
1615
  structuredContent: r
@@ -1544,6 +1617,7 @@ server.registerTool(
1544
1617
  } catch (e) {
1545
1618
  if (e instanceof AssessError) {
1546
1619
  if (e.status === 402) {
1620
+ markHostedLapsed();
1547
1621
  return { content: [{ type: "text", text: hostedWallCta("verdict", e.billing) }], isError: true };
1548
1622
  }
1549
1623
  const reasons = e.rejection_reasons?.length ? " (" + e.rejection_reasons.map((x) => x.key).join(",") + ")" : "";
@@ -1609,6 +1683,7 @@ server.registerTool(
1609
1683
  } catch (e) {
1610
1684
  if (e instanceof AssessError) {
1611
1685
  if (e.status === 402) {
1686
+ markHostedLapsed();
1612
1687
  return { content: [{ type: "text", text: hostedWallCta("anchor", e.billing) }], isError: true };
1613
1688
  }
1614
1689
  return { content: [{ type: "text", text: `ANCHOR ${e.status}: ${e.type}` }], isError: true };
@@ -1704,7 +1779,11 @@ async function main() {
1704
1779
  tier: state.config.tier,
1705
1780
  firstRun: state.firstRun,
1706
1781
  claim: claimUrl(),
1707
- weekNudge: weekOneBootNudge("upgrade") ?? ""
1782
+ // The protection state leads the banner nudge slot: an operator who reads
1783
+ // one line of our output should learn whether this agent is covered, about
1784
+ // to halt, or blocking without being able to prove it. The week-one note
1785
+ // only shows when there is no protection gap worth naming.
1786
+ weekNudge: protectionBootLine(decisionsUsed()) ?? weekOneBootNudge("upgrade") ?? ""
1708
1787
  })
1709
1788
  );
1710
1789
  }
package/dist/lib.js CHANGED
@@ -588,7 +588,8 @@ function readConfig() {
588
588
  // reset every once-per-install nudge into an every-time nag.
589
589
  created_at: typeof raw.created_at === "string" ? raw.created_at : void 0,
590
590
  nudges: raw.nudges && typeof raw.nudges === "object" ? raw.nudges : void 0,
591
- decisions_count: typeof raw.decisions_count === "number" ? raw.decisions_count : void 0
591
+ decisions_count: typeof raw.decisions_count === "number" ? raw.decisions_count : void 0,
592
+ hosted_lapsed_at: typeof raw.hosted_lapsed_at === "string" ? raw.hosted_lapsed_at : void 0
592
593
  };
593
594
  } catch {
594
595
  return null;
@@ -640,7 +641,7 @@ function resolveMandateRules(cfg) {
640
641
  }
641
642
 
642
643
  // src/telemetry.ts
643
- var CLIENT_VERSION = true ? "0.3.3" : "dev";
644
+ var CLIENT_VERSION = true ? "0.4.0" : "dev";
644
645
  function bandOf(amount) {
645
646
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
646
647
  if (amount < 10) return "lt10";
@@ -828,7 +829,7 @@ function requestUpgrade() {
828
829
  }
829
830
 
830
831
  // src/provision.ts
831
- var CLIENT_VERSION2 = true ? "0.3.3" : "dev";
832
+ var CLIENT_VERSION2 = true ? "0.4.0" : "dev";
832
833
  function provisionEnabled() {
833
834
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
834
835
  return !(v === "1" || v === "true" || v === "yes");
@@ -0,0 +1,64 @@
1
+ /**
2
+ * PROTECTION STATE — the one place that decides what the operator is told about
3
+ * the state of their agent's defence, and the only place allowed to name the
4
+ * product to them.
5
+ *
6
+ * Doctrine (the commercial engine, 2026-07-24): every free thing is a lure to
7
+ * the paid thing, so every save must be ATTRIBUTED and every gap must be FELT.
8
+ * A block the operator never hears about buys us nothing; a lapse they never
9
+ * notice converts nobody.
10
+ *
11
+ * The one line we will not cross: this product never silently stops protecting.
12
+ * It either blocks the payment or it halts the agent. So we never claim "your
13
+ * agent is unprotected" while the local firewall is still denying — that would
14
+ * contradict the boot banner two lines above it, and a security vendor caught
15
+ * inflating its own alarm loses the only thing it sells. The states below are
16
+ * written to be maximally urgent AND literally true, which is also the version
17
+ * that survives a customer reading the source (ours is public).
18
+ *
19
+ * protected_trial local firewall on, anonymous, trial decisions remain
20
+ * protected_low same, but 5 or fewer left — the wall is announced
21
+ * halted anonymous trial spent: the firewall FAILS CLOSED, so the
22
+ * agent cannot pay at all until it is activated
23
+ * protected_keyed activated: local blocking + the monthly signed-verdict
24
+ * allowance
25
+ * unproven activated but the hosted allowance is gone: the agent is
26
+ * STILL blocked locally, and its blocks stopped being
27
+ * provable to anyone outside this machine
28
+ */
29
+ /** The commercial name. Coined on every surface an operator can read. */
30
+ export declare const BRAND = "Fidacy AI Agent Firewall";
31
+ /** Free signed verdicts included every month on a (free) account. */
32
+ export declare const FREE_MONTHLY_VERDICTS = 300;
33
+ export type ProtectionState = "protected_trial" | "protected_low" | "halted" | "protected_keyed" | "unproven";
34
+ /**
35
+ * Remember that the hosted allowance ran out, so the warning survives restarts.
36
+ * "Stopped paying but still installed" is precisely the state that must keep
37
+ * nagging — it is the highest-intent audience there is: they already wired us in.
38
+ */
39
+ export declare function markHostedLapsed(): void;
40
+ /** A hosted call succeeded again — the gap closed, so stop warning about it. */
41
+ export declare function clearHostedLapsed(): void;
42
+ export declare function isHostedLapsed(): boolean;
43
+ export declare function protectionState(decisionsUsed: number, keyOverride?: string): ProtectionState;
44
+ /**
45
+ * The attribution line that rides EVERY block. This is the product's single most
46
+ * valuable sentence: the moment it just earned its keep, said in the operator's
47
+ * language, with the name we want them to repeat. It is never a nag — a save is
48
+ * not the moment to sell, it is the moment to be credited.
49
+ */
50
+ export declare function protectionBadge(): string;
51
+ /**
52
+ * The gap warning, escalating by state. Null when there is nothing true to warn
53
+ * about (an activated install inside its allowance is simply working).
54
+ *
55
+ * Every branch names the brand, states the CONSEQUENCE in the operator's terms,
56
+ * and gives exactly one route forward — free first, paid second, in that order,
57
+ * because the free rung is what makes the paid rung legible.
58
+ */
59
+ export declare function protectionWarning(decisionsUsed: number, keyOverride?: string): string | null;
60
+ /**
61
+ * One line for the boot banner: what state this install is in, before anything
62
+ * happens. An operator who reads nothing else reads this.
63
+ */
64
+ export declare function protectionBootLine(decisionsUsed: number): string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fidacy/mcp",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
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)",
@@ -64,7 +64,7 @@
64
64
  "vitest": "^3.2.4",
65
65
  "@fidacy/firewall": "0.1.1"
66
66
  },
67
- "mcpName": "com.fidacy/mcp",
67
+ "mcpName": "com.fidacy/ai-agent-firewall",
68
68
  "repository": {
69
69
  "type": "git",
70
70
  "url": "git+https://github.com/lucaslubi/fidacy-mcp.git"