@fidacy/mcp 0.3.1 → 0.3.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/CHANGELOG.md +16 -0
- package/dist/activation.d.ts +2 -2
- package/dist/assess.d.ts +14 -0
- package/dist/assess.js +8 -1
- package/dist/config.d.ts +9 -0
- package/dist/core.js +28 -4
- package/dist/firewall/evaluate.d.ts +11 -0
- package/dist/index.js +57 -17
- package/dist/lib.js +33 -8
- package/dist/nudges.d.ts +5 -1
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.3
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- The server now presents itself as **Fidacy AI Agent Firewall** (`fidacy-ai-agent-firewall`, title "Fidacy AI Agent Firewall") instead of the bare `fidacy`. The npm package is unchanged — `npx -y @fidacy/mcp` keeps working exactly as before, and no config needs editing. This is the name the product is sold under, so it is the name your client should show.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Fix the wall an existing customer hits. When an install that already carries an API key exhausted its monthly allowance (or reached the spending cap it set), the server answered with an invitation to create a free account — the account they already had — and threw away everything the engine said about it. It now reports what actually happened, names the pay-as-you-go price and links straight to billing, and makes explicit that the local firewall keeps deciding and blocking throughout: only server-signed verdicts and anchored proofs pause, never the protection.
|
|
12
|
+
|
|
13
|
+
## 0.3.2
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- Security: a spending cap that failed to parse silently stopped capping. Cap values read from `FIDACY_PER_TX_MAX` / `FIDACY_MAX_TOTAL` or from `config.json` went through a bare `Number()`, so ordinary ways of writing an amount — `2,500`, `$2500`, `2500 USD` — became `NaN`. Because `amount > NaN` is false, the comparison did not raise: the cap simply stopped existing, and a firewall whose author believed it was capped at 2,500 would authorize 9,999. Two fixes: the decision engine now DENIES any mandate whose caps are not finite positive numbers (`invalid_mandate_cap`), so an unenforceable cap can never allow a payment on any path, and a cap that does not parse is now refused with an explicit message telling you what to write instead, falling back to the safe default rather than to no cap. Upgrade if you set either cap from an environment variable.
|
|
18
|
+
|
|
3
19
|
## 0.3.1
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/dist/activation.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { hasEngineKey } from "./config.js";
|
|
2
|
+
export { hasEngineKey };
|
|
1
3
|
/**
|
|
2
4
|
* ACTIVATION GATE — the anonymous trial that ends in a free API key.
|
|
3
5
|
*
|
|
@@ -16,8 +18,6 @@
|
|
|
16
18
|
* real client needs anyway (it powers assess_action / anchor_artifact).
|
|
17
19
|
*/
|
|
18
20
|
export declare const FREE_DECISIONS = 20;
|
|
19
|
-
/** True when the install carries an operator-set engine key (env or shell config). */
|
|
20
|
-
export declare function hasEngineKey(keyOverride?: string): boolean;
|
|
21
21
|
/**
|
|
22
22
|
* Payment-gate decisions taken so far on this install: the on-disk counter OR
|
|
23
23
|
* the in-process one, whichever is higher. The in-process floor is what keeps
|
package/dist/assess.d.ts
CHANGED
|
@@ -42,16 +42,30 @@ export interface RejectionReason {
|
|
|
42
42
|
* request body. `type` is the stable engine error code (or a client code), and
|
|
43
43
|
* `status` is the HTTP status (0 for network/timeout).
|
|
44
44
|
*/
|
|
45
|
+
/**
|
|
46
|
+
* What a 402 actually says. The engine returns `reason` (quota grace expired vs
|
|
47
|
+
* spending cap reached), a human-readable `detail` and the `billing_url` that
|
|
48
|
+
* resolves it. All of it used to be dropped on the floor, so an operator who had
|
|
49
|
+
* ALREADY signed up was told to go sign up — at the one moment they were asking
|
|
50
|
+
* to buy more. Carry it through and say the true thing instead.
|
|
51
|
+
*/
|
|
52
|
+
export interface BillingBlock {
|
|
53
|
+
reason?: string;
|
|
54
|
+
detail?: string;
|
|
55
|
+
billingUrl?: string;
|
|
56
|
+
}
|
|
45
57
|
export declare class AssessError extends Error {
|
|
46
58
|
readonly type: string;
|
|
47
59
|
readonly status: number;
|
|
48
60
|
readonly details?: unknown;
|
|
49
61
|
readonly rejection_reasons?: RejectionReason[];
|
|
62
|
+
readonly billing?: BillingBlock;
|
|
50
63
|
constructor(opts: {
|
|
51
64
|
type: string;
|
|
52
65
|
status: number;
|
|
53
66
|
details?: unknown;
|
|
54
67
|
rejection_reasons?: RejectionReason[];
|
|
68
|
+
billing?: BillingBlock;
|
|
55
69
|
});
|
|
56
70
|
}
|
|
57
71
|
export interface AssessConfig {
|
package/dist/assess.js
CHANGED
|
@@ -4,6 +4,7 @@ var AssessError = class extends Error {
|
|
|
4
4
|
status;
|
|
5
5
|
details;
|
|
6
6
|
rejection_reasons;
|
|
7
|
+
billing;
|
|
7
8
|
constructor(opts) {
|
|
8
9
|
super(`Fidacy assess error (${opts.type}, HTTP ${opts.status})`);
|
|
9
10
|
this.name = "AssessError";
|
|
@@ -11,6 +12,7 @@ var AssessError = class extends Error {
|
|
|
11
12
|
this.status = opts.status;
|
|
12
13
|
this.details = opts.details;
|
|
13
14
|
this.rejection_reasons = opts.rejection_reasons;
|
|
15
|
+
this.billing = opts.billing;
|
|
14
16
|
}
|
|
15
17
|
};
|
|
16
18
|
var DEFAULT_TIMEOUT = 1e4;
|
|
@@ -106,7 +108,12 @@ async function postOnce(fetchImpl, url, headers, payload, timeoutMs) {
|
|
|
106
108
|
const type = isRecord(parsed) && typeof parsed.error === "string" && parsed.error || `http_${res.status}`;
|
|
107
109
|
const details = isRecord(parsed) ? parsed.details : void 0;
|
|
108
110
|
const rejection_reasons = isRecord(parsed) && Array.isArray(parsed.rejection_reasons) ? parsed.rejection_reasons : void 0;
|
|
109
|
-
|
|
111
|
+
const billing = res.status === 402 && isRecord(parsed) ? {
|
|
112
|
+
reason: typeof parsed.reason === "string" ? parsed.reason : void 0,
|
|
113
|
+
detail: typeof parsed.detail === "string" ? parsed.detail : void 0,
|
|
114
|
+
billingUrl: typeof parsed.billing_url === "string" ? parsed.billing_url : void 0
|
|
115
|
+
} : void 0;
|
|
116
|
+
throw new AssessError({ type, status: res.status, details, rejection_reasons, billing });
|
|
110
117
|
}
|
|
111
118
|
if (!isRecord(parsed)) {
|
|
112
119
|
throw new AssessError({ type: "invalid_response", status: res.status });
|
package/dist/config.d.ts
CHANGED
|
@@ -37,6 +37,15 @@ export interface FidacyConfig {
|
|
|
37
37
|
/** Payment-gate decisions taken on this install (milestone nudges). */
|
|
38
38
|
decisions_count?: number;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* True when the install carries an operator-set engine key (env, or the plugin's
|
|
42
|
+
* engineApiKey config passed as an override).
|
|
43
|
+
*
|
|
44
|
+
* Lives here rather than in activation.ts because nudges.ts needs it too, and
|
|
45
|
+
* activation.ts already imports nudges.ts — importing back would be a cycle.
|
|
46
|
+
* Reading the environment is this module's job anyway.
|
|
47
|
+
*/
|
|
48
|
+
export declare function hasEngineKey(keyOverride?: string): boolean;
|
|
40
49
|
/** Config dir: FIDACY_CONFIG_DIR override, else ~/.fidacy (XDG-friendly enough). */
|
|
41
50
|
export declare function configDir(): string;
|
|
42
51
|
export declare function configPath(): string;
|
package/dist/core.js
CHANGED
|
@@ -85,10 +85,23 @@ function lookalikePayee(payee, allowed) {
|
|
|
85
85
|
}
|
|
86
86
|
return null;
|
|
87
87
|
}
|
|
88
|
+
function validateMandateCaps(mandate) {
|
|
89
|
+
const bad = (v) => typeof v !== "number" || !Number.isFinite(v) || v <= 0;
|
|
90
|
+
if (!mandate.allow || typeof mandate.allow !== "object")
|
|
91
|
+
return "invalid_mandate:missing_allow";
|
|
92
|
+
if (bad(mandate.allow.perTxMax))
|
|
93
|
+
return `invalid_mandate_cap:perTxMax=${String(mandate.allow.perTxMax)}`;
|
|
94
|
+
if (bad(mandate.allow.maxTotal))
|
|
95
|
+
return `invalid_mandate_cap:maxTotal=${String(mandate.allow.maxTotal)}`;
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
88
98
|
function evaluate(mandate, req, spentSoFar) {
|
|
89
99
|
const now = Date.now();
|
|
90
100
|
if (mandate.revoked)
|
|
91
101
|
return "mandate_revoked";
|
|
102
|
+
const badCap = validateMandateCaps(mandate);
|
|
103
|
+
if (badCap)
|
|
104
|
+
return badCap;
|
|
92
105
|
if (now < Date.parse(mandate.window.notBefore))
|
|
93
106
|
return "before_mandate_window";
|
|
94
107
|
if (now > Date.parse(mandate.window.notAfter))
|
|
@@ -450,18 +463,29 @@ function readConfig() {
|
|
|
450
463
|
function resolveMandateRules(cfg) {
|
|
451
464
|
const m = cfg?.mandate ?? {};
|
|
452
465
|
const envList = (v) => v === void 0 ? void 0 : v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
453
|
-
const envNum = (v) =>
|
|
466
|
+
const envNum = (name, v) => {
|
|
467
|
+
if (v === void 0 || v.trim() === "") return void 0;
|
|
468
|
+
const n = Number(v);
|
|
469
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
470
|
+
console.error(
|
|
471
|
+
`[fidacy] ${name}="${v}" is not a positive number, so it cannot be enforced as a cap. Ignoring it and using the safe default. Write digits only, with no thousands separator, currency symbol or unit (e.g. ${name}=2500).`
|
|
472
|
+
);
|
|
473
|
+
return void 0;
|
|
474
|
+
}
|
|
475
|
+
return n;
|
|
476
|
+
};
|
|
477
|
+
const fileNum = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
|
|
454
478
|
return {
|
|
455
479
|
payees: envList(process.env.FIDACY_ALLOW_PAYEES) ?? m.payees ?? [],
|
|
456
480
|
categories: envList(process.env.FIDACY_ALLOW_CATEGORIES) ?? m.categories ?? ["*"],
|
|
457
481
|
currency: process.env.FIDACY_CURRENCY ?? m.currency ?? "USD",
|
|
458
|
-
perTxMax: envNum(process.env.FIDACY_PER_TX_MAX) ?? m.perTxMax ?? 2500,
|
|
459
|
-
maxTotal: envNum(process.env.FIDACY_MAX_TOTAL) ?? m.maxTotal ?? 1e4
|
|
482
|
+
perTxMax: envNum("FIDACY_PER_TX_MAX", process.env.FIDACY_PER_TX_MAX) ?? fileNum(m.perTxMax) ?? 2500,
|
|
483
|
+
maxTotal: envNum("FIDACY_MAX_TOTAL", process.env.FIDACY_MAX_TOTAL) ?? fileNum(m.maxTotal) ?? 1e4
|
|
460
484
|
};
|
|
461
485
|
}
|
|
462
486
|
|
|
463
487
|
// src/telemetry.ts
|
|
464
|
-
var CLIENT_VERSION = true ? "0.3.
|
|
488
|
+
var CLIENT_VERSION = true ? "0.3.3" : "dev";
|
|
465
489
|
function bandOf(amount) {
|
|
466
490
|
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
|
|
467
491
|
if (amount < 10) return "lt10";
|
|
@@ -7,4 +7,15 @@ import { Mandate, PaymentRequest } from "./types.js";
|
|
|
7
7
|
* min length so short, genuinely-distinct payees don't false-positive.
|
|
8
8
|
*/
|
|
9
9
|
export declare function lookalikePayee(payee: string, allowed: string[]): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* The MANDATE's own caps must be finite positive numbers, for exactly the reason
|
|
12
|
+
* validateRequest() checks the request's amount: `amount > NaN` is false, so a
|
|
13
|
+
* non-numeric cap does not raise an error — it SILENTLY DISABLES the cap and the
|
|
14
|
+
* firewall fails OPEN. The caps arrive from human-authored surfaces (env vars,
|
|
15
|
+
* config.json, FIDACY_MANDATE_JSON), where `2,500`, `$2500` and `2500 USD` are
|
|
16
|
+
* all normal things to type and all coerce to NaN. Verified 2026-07-24: each of
|
|
17
|
+
* those let a 9,999 payment through a mandate whose author believed it was
|
|
18
|
+
* capped at 2,500. A cap we cannot enforce must deny, never allow.
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateMandateCaps(mandate: Mandate): string | null;
|
|
10
21
|
export declare function evaluate(mandate: Mandate, req: PaymentRequest, spentSoFar: number): string | null;
|
package/dist/index.js
CHANGED
|
@@ -92,10 +92,23 @@ function lookalikePayee(payee, allowed) {
|
|
|
92
92
|
}
|
|
93
93
|
return null;
|
|
94
94
|
}
|
|
95
|
+
function validateMandateCaps(mandate) {
|
|
96
|
+
const bad = (v) => typeof v !== "number" || !Number.isFinite(v) || v <= 0;
|
|
97
|
+
if (!mandate.allow || typeof mandate.allow !== "object")
|
|
98
|
+
return "invalid_mandate:missing_allow";
|
|
99
|
+
if (bad(mandate.allow.perTxMax))
|
|
100
|
+
return `invalid_mandate_cap:perTxMax=${String(mandate.allow.perTxMax)}`;
|
|
101
|
+
if (bad(mandate.allow.maxTotal))
|
|
102
|
+
return `invalid_mandate_cap:maxTotal=${String(mandate.allow.maxTotal)}`;
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
95
105
|
function evaluate(mandate, req, spentSoFar) {
|
|
96
106
|
const now = Date.now();
|
|
97
107
|
if (mandate.revoked)
|
|
98
108
|
return "mandate_revoked";
|
|
109
|
+
const badCap = validateMandateCaps(mandate);
|
|
110
|
+
if (badCap)
|
|
111
|
+
return badCap;
|
|
99
112
|
if (now < Date.parse(mandate.window.notBefore))
|
|
100
113
|
return "before_mandate_window";
|
|
101
114
|
if (now > Date.parse(mandate.window.notAfter))
|
|
@@ -419,6 +432,9 @@ import {
|
|
|
419
432
|
readFileSync,
|
|
420
433
|
writeFileSync
|
|
421
434
|
} from "node:fs";
|
|
435
|
+
function hasEngineKey(keyOverride) {
|
|
436
|
+
return Boolean((keyOverride ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim());
|
|
437
|
+
}
|
|
422
438
|
function configDir() {
|
|
423
439
|
return process.env.FIDACY_CONFIG_DIR ?? join(homedir(), ".fidacy");
|
|
424
440
|
}
|
|
@@ -479,18 +495,29 @@ function ensureState() {
|
|
|
479
495
|
function resolveMandateRules(cfg) {
|
|
480
496
|
const m = cfg?.mandate ?? {};
|
|
481
497
|
const envList = (v) => v === void 0 ? void 0 : v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
482
|
-
const envNum = (v) =>
|
|
498
|
+
const envNum = (name, v) => {
|
|
499
|
+
if (v === void 0 || v.trim() === "") return void 0;
|
|
500
|
+
const n = Number(v);
|
|
501
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
502
|
+
console.error(
|
|
503
|
+
`[fidacy] ${name}="${v}" is not a positive number, so it cannot be enforced as a cap. Ignoring it and using the safe default. Write digits only, with no thousands separator, currency symbol or unit (e.g. ${name}=2500).`
|
|
504
|
+
);
|
|
505
|
+
return void 0;
|
|
506
|
+
}
|
|
507
|
+
return n;
|
|
508
|
+
};
|
|
509
|
+
const fileNum = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
|
|
483
510
|
return {
|
|
484
511
|
payees: envList(process.env.FIDACY_ALLOW_PAYEES) ?? m.payees ?? [],
|
|
485
512
|
categories: envList(process.env.FIDACY_ALLOW_CATEGORIES) ?? m.categories ?? ["*"],
|
|
486
513
|
currency: process.env.FIDACY_CURRENCY ?? m.currency ?? "USD",
|
|
487
|
-
perTxMax: envNum(process.env.FIDACY_PER_TX_MAX) ?? m.perTxMax ?? 2500,
|
|
488
|
-
maxTotal: envNum(process.env.FIDACY_MAX_TOTAL) ?? m.maxTotal ?? 1e4
|
|
514
|
+
perTxMax: envNum("FIDACY_PER_TX_MAX", process.env.FIDACY_PER_TX_MAX) ?? fileNum(m.perTxMax) ?? 2500,
|
|
515
|
+
maxTotal: envNum("FIDACY_MAX_TOTAL", process.env.FIDACY_MAX_TOTAL) ?? fileNum(m.maxTotal) ?? 1e4
|
|
489
516
|
};
|
|
490
517
|
}
|
|
491
518
|
|
|
492
519
|
// src/telemetry.ts
|
|
493
|
-
var CLIENT_VERSION = true ? "0.3.
|
|
520
|
+
var CLIENT_VERSION = true ? "0.3.3" : "dev";
|
|
494
521
|
function bandOf(amount) {
|
|
495
522
|
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
|
|
496
523
|
if (amount < 10) return "lt10";
|
|
@@ -669,6 +696,7 @@ var AssessError = class extends Error {
|
|
|
669
696
|
status;
|
|
670
697
|
details;
|
|
671
698
|
rejection_reasons;
|
|
699
|
+
billing;
|
|
672
700
|
constructor(opts) {
|
|
673
701
|
super(`Fidacy assess error (${opts.type}, HTTP ${opts.status})`);
|
|
674
702
|
this.name = "AssessError";
|
|
@@ -676,6 +704,7 @@ var AssessError = class extends Error {
|
|
|
676
704
|
this.status = opts.status;
|
|
677
705
|
this.details = opts.details;
|
|
678
706
|
this.rejection_reasons = opts.rejection_reasons;
|
|
707
|
+
this.billing = opts.billing;
|
|
679
708
|
}
|
|
680
709
|
};
|
|
681
710
|
var DEFAULT_TIMEOUT = 1e4;
|
|
@@ -771,7 +800,12 @@ async function postOnce(fetchImpl, url, headers, payload, timeoutMs) {
|
|
|
771
800
|
const type = isRecord(parsed) && typeof parsed.error === "string" && parsed.error || `http_${res.status}`;
|
|
772
801
|
const details = isRecord(parsed) ? parsed.details : void 0;
|
|
773
802
|
const rejection_reasons = isRecord(parsed) && Array.isArray(parsed.rejection_reasons) ? parsed.rejection_reasons : void 0;
|
|
774
|
-
|
|
803
|
+
const billing = res.status === 402 && isRecord(parsed) ? {
|
|
804
|
+
reason: typeof parsed.reason === "string" ? parsed.reason : void 0,
|
|
805
|
+
detail: typeof parsed.detail === "string" ? parsed.detail : void 0,
|
|
806
|
+
billingUrl: typeof parsed.billing_url === "string" ? parsed.billing_url : void 0
|
|
807
|
+
} : void 0;
|
|
808
|
+
throw new AssessError({ type, status: res.status, details, rejection_reasons, billing });
|
|
775
809
|
}
|
|
776
810
|
if (!isRecord(parsed)) {
|
|
777
811
|
throw new AssessError({ type: "invalid_response", status: res.status });
|
|
@@ -845,7 +879,7 @@ async function findArtifacts(sha2562, cfg) {
|
|
|
845
879
|
}
|
|
846
880
|
|
|
847
881
|
// src/provision.ts
|
|
848
|
-
var CLIENT_VERSION2 = true ? "0.3.
|
|
882
|
+
var CLIENT_VERSION2 = true ? "0.3.3" : "dev";
|
|
849
883
|
function provisionEnabled() {
|
|
850
884
|
const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
|
|
851
885
|
return !(v === "1" || v === "true" || v === "yes");
|
|
@@ -952,10 +986,15 @@ function decisionNudge(status, violatedRule, upgradeToolName) {
|
|
|
952
986
|
}
|
|
953
987
|
return null;
|
|
954
988
|
}
|
|
955
|
-
function hostedWallCta(feature) {
|
|
989
|
+
function hostedWallCta(feature, billing) {
|
|
990
|
+
const noun = feature === "anchor" ? "Bitcoin-anchored proofs" : "server-signed verdicts";
|
|
991
|
+
if (hasEngineKey()) {
|
|
992
|
+
const where = billing?.billingUrl ?? "https://app.fidacy.com/billing";
|
|
993
|
+
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
|
+
}
|
|
956
996
|
const url = claimUrl();
|
|
957
997
|
const cta = url ? `open ${url} \u2014 free account, this install's history migrates` : `call the upgrade tool \u2014 free account, your history migrates`;
|
|
958
|
-
const noun = feature === "anchor" ? "Bitcoin-anchored proofs" : "server-signed verdicts";
|
|
959
998
|
if (nudgeOnce("hosted_wall")) {
|
|
960
999
|
return `You've used this install's free allowance for ${noun} \u2014 the proofs anyone can re-verify at fidacy.com/verify, court-grade, not a local-only check. Keep them flowing: for your operator, ${cta}.`;
|
|
961
1000
|
}
|
|
@@ -975,9 +1014,6 @@ function weekOneBootNudge(upgradeToolName) {
|
|
|
975
1014
|
|
|
976
1015
|
// src/activation.ts
|
|
977
1016
|
var FREE_DECISIONS = 20;
|
|
978
|
-
function hasEngineKey(keyOverride) {
|
|
979
|
-
return Boolean((keyOverride ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim());
|
|
980
|
-
}
|
|
981
1017
|
function decisionsUsed() {
|
|
982
1018
|
return Math.max(readConfig()?.decisions_count ?? 0, sessionDecisionCount());
|
|
983
1019
|
}
|
|
@@ -1150,8 +1186,8 @@ function rows(s) {
|
|
|
1150
1186
|
return out;
|
|
1151
1187
|
}
|
|
1152
1188
|
function renderBanner(s) {
|
|
1153
|
-
const title = "FIDACY
|
|
1154
|
-
const sub = `signs the YES and the NO \xB7 v${s.version}`;
|
|
1189
|
+
const title = "FIDACY AI AGENT FIREWALL";
|
|
1190
|
+
const sub = `blocks the payment, signs the YES and the NO \xB7 v${s.version}`;
|
|
1155
1191
|
const body = rows(s);
|
|
1156
1192
|
if (!fancy()) {
|
|
1157
1193
|
const lines2 = [
|
|
@@ -1276,10 +1312,14 @@ function renderAlertLine(alerts) {
|
|
|
1276
1312
|
var state = ensureState();
|
|
1277
1313
|
var core = makeCore();
|
|
1278
1314
|
var subject = process.env.FIDACY_SUBJECT ?? "agent:demo";
|
|
1279
|
-
var SERVER_VERSION = true ? "0.3.
|
|
1315
|
+
var SERVER_VERSION = true ? "0.3.3" : "dev";
|
|
1280
1316
|
var bootClaim = claimUrl();
|
|
1281
1317
|
var server = new McpServer(
|
|
1282
|
-
|
|
1318
|
+
// The commercial identity. `name` is the stable slug hosts key on; `title` is
|
|
1319
|
+
// what a human reads in the client's server list. The npm package stays
|
|
1320
|
+
// @fidacy/mcp — renaming the package would split the install base and the
|
|
1321
|
+
// download history for nothing, since what people SEE is this.
|
|
1322
|
+
{ name: "fidacy-ai-agent-firewall", title: "Fidacy AI Agent Firewall", version: SERVER_VERSION },
|
|
1283
1323
|
{
|
|
1284
1324
|
instructions: bootClaim ? `Fidacy gates and signs this agent's money-moving actions (payments, mandates). Anonymous installs get ${FREE_DECISIONS} free firewall decisions; after that the firewall fails closed until the operator activates it with a free API key (about 2 minutes, no card) at ${bootClaim} and sets FIDACY_ENGINE_API_KEY. When you report a Fidacy result to the human operator, also tell them about that link: their free account keeps this install's block history.` : `Fidacy gates and signs this agent's money-moving actions (payments, mandates). Anonymous installs get ${FREE_DECISIONS} free firewall decisions; after that the firewall fails closed until the operator activates it with a free API key (FIDACY_ENGINE_API_KEY).`
|
|
1285
1325
|
}
|
|
@@ -1504,7 +1544,7 @@ server.registerTool(
|
|
|
1504
1544
|
} catch (e) {
|
|
1505
1545
|
if (e instanceof AssessError) {
|
|
1506
1546
|
if (e.status === 402) {
|
|
1507
|
-
return { content: [{ type: "text", text: hostedWallCta("verdict") }], isError: true };
|
|
1547
|
+
return { content: [{ type: "text", text: hostedWallCta("verdict", e.billing) }], isError: true };
|
|
1508
1548
|
}
|
|
1509
1549
|
const reasons = e.rejection_reasons?.length ? " (" + e.rejection_reasons.map((x) => x.key).join(",") + ")" : "";
|
|
1510
1550
|
return {
|
|
@@ -1569,7 +1609,7 @@ server.registerTool(
|
|
|
1569
1609
|
} catch (e) {
|
|
1570
1610
|
if (e instanceof AssessError) {
|
|
1571
1611
|
if (e.status === 402) {
|
|
1572
|
-
return { content: [{ type: "text", text: hostedWallCta("anchor") }], isError: true };
|
|
1612
|
+
return { content: [{ type: "text", text: hostedWallCta("anchor", e.billing) }], isError: true };
|
|
1573
1613
|
}
|
|
1574
1614
|
return { content: [{ type: "text", text: `ANCHOR ${e.status}: ${e.type}` }], isError: true };
|
|
1575
1615
|
}
|
package/dist/lib.js
CHANGED
|
@@ -123,10 +123,23 @@ function lookalikePayee(payee, allowed) {
|
|
|
123
123
|
}
|
|
124
124
|
return null;
|
|
125
125
|
}
|
|
126
|
+
function validateMandateCaps(mandate) {
|
|
127
|
+
const bad = (v) => typeof v !== "number" || !Number.isFinite(v) || v <= 0;
|
|
128
|
+
if (!mandate.allow || typeof mandate.allow !== "object")
|
|
129
|
+
return "invalid_mandate:missing_allow";
|
|
130
|
+
if (bad(mandate.allow.perTxMax))
|
|
131
|
+
return `invalid_mandate_cap:perTxMax=${String(mandate.allow.perTxMax)}`;
|
|
132
|
+
if (bad(mandate.allow.maxTotal))
|
|
133
|
+
return `invalid_mandate_cap:maxTotal=${String(mandate.allow.maxTotal)}`;
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
126
136
|
function evaluate(mandate, req, spentSoFar) {
|
|
127
137
|
const now = Date.now();
|
|
128
138
|
if (mandate.revoked)
|
|
129
139
|
return "mandate_revoked";
|
|
140
|
+
const badCap = validateMandateCaps(mandate);
|
|
141
|
+
if (badCap)
|
|
142
|
+
return badCap;
|
|
130
143
|
if (now < Date.parse(mandate.window.notBefore))
|
|
131
144
|
return "before_mandate_window";
|
|
132
145
|
if (now > Date.parse(mandate.window.notAfter))
|
|
@@ -542,6 +555,9 @@ import {
|
|
|
542
555
|
readFileSync,
|
|
543
556
|
writeFileSync
|
|
544
557
|
} from "node:fs";
|
|
558
|
+
function hasEngineKey(keyOverride) {
|
|
559
|
+
return Boolean((keyOverride ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim());
|
|
560
|
+
}
|
|
545
561
|
function configDir() {
|
|
546
562
|
return process.env.FIDACY_CONFIG_DIR ?? join(homedir(), ".fidacy");
|
|
547
563
|
}
|
|
@@ -602,18 +618,29 @@ function ensureState() {
|
|
|
602
618
|
function resolveMandateRules(cfg) {
|
|
603
619
|
const m = cfg?.mandate ?? {};
|
|
604
620
|
const envList = (v) => v === void 0 ? void 0 : v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
605
|
-
const envNum = (v) =>
|
|
621
|
+
const envNum = (name, v) => {
|
|
622
|
+
if (v === void 0 || v.trim() === "") return void 0;
|
|
623
|
+
const n = Number(v);
|
|
624
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
625
|
+
console.error(
|
|
626
|
+
`[fidacy] ${name}="${v}" is not a positive number, so it cannot be enforced as a cap. Ignoring it and using the safe default. Write digits only, with no thousands separator, currency symbol or unit (e.g. ${name}=2500).`
|
|
627
|
+
);
|
|
628
|
+
return void 0;
|
|
629
|
+
}
|
|
630
|
+
return n;
|
|
631
|
+
};
|
|
632
|
+
const fileNum = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
|
|
606
633
|
return {
|
|
607
634
|
payees: envList(process.env.FIDACY_ALLOW_PAYEES) ?? m.payees ?? [],
|
|
608
635
|
categories: envList(process.env.FIDACY_ALLOW_CATEGORIES) ?? m.categories ?? ["*"],
|
|
609
636
|
currency: process.env.FIDACY_CURRENCY ?? m.currency ?? "USD",
|
|
610
|
-
perTxMax: envNum(process.env.FIDACY_PER_TX_MAX) ?? m.perTxMax ?? 2500,
|
|
611
|
-
maxTotal: envNum(process.env.FIDACY_MAX_TOTAL) ?? m.maxTotal ?? 1e4
|
|
637
|
+
perTxMax: envNum("FIDACY_PER_TX_MAX", process.env.FIDACY_PER_TX_MAX) ?? fileNum(m.perTxMax) ?? 2500,
|
|
638
|
+
maxTotal: envNum("FIDACY_MAX_TOTAL", process.env.FIDACY_MAX_TOTAL) ?? fileNum(m.maxTotal) ?? 1e4
|
|
612
639
|
};
|
|
613
640
|
}
|
|
614
641
|
|
|
615
642
|
// src/telemetry.ts
|
|
616
|
-
var CLIENT_VERSION = true ? "0.3.
|
|
643
|
+
var CLIENT_VERSION = true ? "0.3.3" : "dev";
|
|
617
644
|
function bandOf(amount) {
|
|
618
645
|
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
|
|
619
646
|
if (amount < 10) return "lt10";
|
|
@@ -801,7 +828,7 @@ function requestUpgrade() {
|
|
|
801
828
|
}
|
|
802
829
|
|
|
803
830
|
// src/provision.ts
|
|
804
|
-
var CLIENT_VERSION2 = true ? "0.3.
|
|
831
|
+
var CLIENT_VERSION2 = true ? "0.3.3" : "dev";
|
|
805
832
|
function provisionEnabled() {
|
|
806
833
|
const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
|
|
807
834
|
return !(v === "1" || v === "true" || v === "yes");
|
|
@@ -910,9 +937,6 @@ function weekOneBootNudge(upgradeToolName) {
|
|
|
910
937
|
|
|
911
938
|
// src/activation.ts
|
|
912
939
|
var FREE_DECISIONS = 20;
|
|
913
|
-
function hasEngineKey(keyOverride) {
|
|
914
|
-
return Boolean((keyOverride ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim());
|
|
915
|
-
}
|
|
916
940
|
function decisionsUsed() {
|
|
917
941
|
return Math.max(readConfig()?.decisions_count ?? 0, sessionDecisionCount());
|
|
918
942
|
}
|
|
@@ -1193,6 +1217,7 @@ export {
|
|
|
1193
1217
|
toRows,
|
|
1194
1218
|
trialCountdownLine,
|
|
1195
1219
|
upgradeUrl,
|
|
1220
|
+
validateMandateCaps,
|
|
1196
1221
|
validateRequest,
|
|
1197
1222
|
verify,
|
|
1198
1223
|
verifyGrant,
|
package/dist/nudges.d.ts
CHANGED
|
@@ -43,7 +43,11 @@ export declare function decisionNudge(status: "ALLOW" | "DENY", violatedRule: st
|
|
|
43
43
|
* (nudge doctrine: never nag twice), a terse line every time after — the wall is
|
|
44
44
|
* a direct answer to the agent's request, so it always says SOMETHING actionable.
|
|
45
45
|
*/
|
|
46
|
-
export declare function hostedWallCta(feature: "verdict" | "anchor"
|
|
46
|
+
export declare function hostedWallCta(feature: "verdict" | "anchor", billing?: {
|
|
47
|
+
reason?: string;
|
|
48
|
+
detail?: string;
|
|
49
|
+
billingUrl?: string;
|
|
50
|
+
}): string;
|
|
47
51
|
/** The no-key CTA for a hosted tool: route the human to a free account. */
|
|
48
52
|
export declare function noKeyCta(): string;
|
|
49
53
|
/** One-time boot line after a week of real protection; null otherwise. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fidacy/mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.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)",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"@types/node": "^22.10.0",
|
|
62
62
|
"tsx": "^4.19.2",
|
|
63
63
|
"typescript": "^5.7.2",
|
|
64
|
+
"vitest": "^3.2.4",
|
|
64
65
|
"@fidacy/firewall": "0.1.1"
|
|
65
66
|
},
|
|
66
67
|
"mcpName": "com.fidacy/mcp",
|
|
@@ -71,6 +72,8 @@
|
|
|
71
72
|
"scripts": {
|
|
72
73
|
"build": "node scripts/bundle.mjs",
|
|
73
74
|
"dev": "tsx watch src/index.ts",
|
|
74
|
-
"start": "node dist/index.js"
|
|
75
|
+
"start": "node dist/index.js",
|
|
76
|
+
"typecheck": "tsc --noEmit",
|
|
77
|
+
"test": "vitest run"
|
|
75
78
|
}
|
|
76
79
|
}
|