@fidacy/mcp 0.4.1 → 0.5.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 +6 -0
- package/dist/config.d.ts +4 -0
- package/dist/core.js +4 -2
- package/dist/index.js +81 -7
- package/dist/lib.d.ts +3 -2
- package/dist/lib.js +97 -3
- package/dist/register.d.ts +14 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Critical for Claude Code users: the firewall's messages now actually reach the model. MCP hosts disagree about which half of a tool result the model sees. Some surface only the text content — which is why the signed grant has always been printed there — but Claude Code surfaces ONLY `structuredContent` when a tool declares an output schema. Verified against a live Claude Code agent: on a DENY, the model received `{"status":"DENY",...}` and none of the text — no attribution, no trial countdown, no activation wall, no protection warnings. Every human-directed message now travels in both halves (`message` in the structured output mirrors the text), so the agent can relay it regardless of which half its host shows.
|
|
8
|
+
|
|
3
9
|
## 0.4.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/dist/config.d.ts
CHANGED
|
@@ -43,6 +43,10 @@ export interface FidacyConfig {
|
|
|
43
43
|
* Cleared as soon as a hosted call succeeds again.
|
|
44
44
|
*/
|
|
45
45
|
hosted_lapsed_at?: string;
|
|
46
|
+
/** Operator-provided email (opt-in). Consent to be contacted; never scraped. */
|
|
47
|
+
operator_email?: string;
|
|
48
|
+
/** The email last successfully registered, so we don't re-send every boot. */
|
|
49
|
+
registered_email?: string;
|
|
46
50
|
}
|
|
47
51
|
/**
|
|
48
52
|
* True when the install carries an operator-set engine key (env, or the plugin's
|
package/dist/core.js
CHANGED
|
@@ -455,7 +455,9 @@ function readConfig() {
|
|
|
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
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
|
+
hosted_lapsed_at: typeof raw.hosted_lapsed_at === "string" ? raw.hosted_lapsed_at : void 0,
|
|
459
|
+
operator_email: typeof raw.operator_email === "string" ? raw.operator_email : void 0,
|
|
460
|
+
registered_email: typeof raw.registered_email === "string" ? raw.registered_email : void 0
|
|
459
461
|
};
|
|
460
462
|
} catch {
|
|
461
463
|
return null;
|
|
@@ -486,7 +488,7 @@ function resolveMandateRules(cfg) {
|
|
|
486
488
|
}
|
|
487
489
|
|
|
488
490
|
// src/telemetry.ts
|
|
489
|
-
var CLIENT_VERSION = true ? "0.
|
|
491
|
+
var CLIENT_VERSION = true ? "0.5.0" : "dev";
|
|
490
492
|
function bandOf(amount) {
|
|
491
493
|
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
|
|
492
494
|
if (amount < 10) return "lt10";
|
package/dist/index.js
CHANGED
|
@@ -466,7 +466,9 @@ function readConfig() {
|
|
|
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
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
|
+
hosted_lapsed_at: typeof raw.hosted_lapsed_at === "string" ? raw.hosted_lapsed_at : void 0,
|
|
470
|
+
operator_email: typeof raw.operator_email === "string" ? raw.operator_email : void 0,
|
|
471
|
+
registered_email: typeof raw.registered_email === "string" ? raw.registered_email : void 0
|
|
470
472
|
};
|
|
471
473
|
} catch {
|
|
472
474
|
return null;
|
|
@@ -518,7 +520,7 @@ function resolveMandateRules(cfg) {
|
|
|
518
520
|
}
|
|
519
521
|
|
|
520
522
|
// src/telemetry.ts
|
|
521
|
-
var CLIENT_VERSION = true ? "0.
|
|
523
|
+
var CLIENT_VERSION = true ? "0.5.0" : "dev";
|
|
522
524
|
function bandOf(amount) {
|
|
523
525
|
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
|
|
524
526
|
if (amount < 10) return "lt10";
|
|
@@ -880,7 +882,7 @@ async function findArtifacts(sha2562, cfg) {
|
|
|
880
882
|
}
|
|
881
883
|
|
|
882
884
|
// src/provision.ts
|
|
883
|
-
var CLIENT_VERSION2 = true ? "0.
|
|
885
|
+
var CLIENT_VERSION2 = true ? "0.5.0" : "dev";
|
|
884
886
|
function provisionEnabled() {
|
|
885
887
|
const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
|
|
886
888
|
return !(v === "1" || v === "true" || v === "yes");
|
|
@@ -916,6 +918,46 @@ async function autoProvision() {
|
|
|
916
918
|
}
|
|
917
919
|
}
|
|
918
920
|
|
|
921
|
+
// src/register.ts
|
|
922
|
+
var CLIENT_VERSION3 = true ? "0.5.0" : "dev";
|
|
923
|
+
function endpoint3() {
|
|
924
|
+
const base = (process.env.FIDACY_ENGINE_URL ?? "https://api.fidacy.com").replace(/\/$/, "");
|
|
925
|
+
return `${base}/v1/register`;
|
|
926
|
+
}
|
|
927
|
+
function looksLikeEmail(s) {
|
|
928
|
+
return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(s);
|
|
929
|
+
}
|
|
930
|
+
function operatorEmail(override) {
|
|
931
|
+
const raw = (override ?? process.env.FIDACY_OPERATOR_EMAIL ?? readConfig()?.operator_email ?? "").trim();
|
|
932
|
+
return raw && looksLikeEmail(raw) ? raw : null;
|
|
933
|
+
}
|
|
934
|
+
async function registerEmail(email, shell = "mcp") {
|
|
935
|
+
const cfg = readConfig();
|
|
936
|
+
if (!cfg) return "skipped";
|
|
937
|
+
const clean = email.trim();
|
|
938
|
+
if (!looksLikeEmail(clean)) return "skipped";
|
|
939
|
+
if (cfg.registered_email && cfg.registered_email.toLowerCase() === clean.toLowerCase()) return "skipped";
|
|
940
|
+
try {
|
|
941
|
+
const res = await fetch(endpoint3(), {
|
|
942
|
+
method: "POST",
|
|
943
|
+
headers: { "content-type": "application/json" },
|
|
944
|
+
body: JSON.stringify({ anon_id: cfg.anon_id, email: clean, client_version: CLIENT_VERSION3, shell })
|
|
945
|
+
});
|
|
946
|
+
if (res.status === 429) return "rate_limited";
|
|
947
|
+
if (!res.ok) return "failed";
|
|
948
|
+
const fresh = readConfig() ?? cfg;
|
|
949
|
+
writeConfig({ ...fresh, operator_email: clean, registered_email: clean });
|
|
950
|
+
return "registered";
|
|
951
|
+
} catch {
|
|
952
|
+
return "failed";
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
async function autoRegister(shell = "mcp") {
|
|
956
|
+
const email = operatorEmail();
|
|
957
|
+
if (!email) return "skipped";
|
|
958
|
+
return registerEmail(email, shell);
|
|
959
|
+
}
|
|
960
|
+
|
|
919
961
|
// src/upgrade.ts
|
|
920
962
|
function upgradeUrl() {
|
|
921
963
|
const base = (process.env.FIDACY_WEB_URL ?? "https://fidacy.com").replace(/\/$/, "");
|
|
@@ -1381,7 +1423,7 @@ function renderAlertLine(alerts) {
|
|
|
1381
1423
|
var state = ensureState();
|
|
1382
1424
|
var core = makeCore();
|
|
1383
1425
|
var subject = process.env.FIDACY_SUBJECT ?? "agent:demo";
|
|
1384
|
-
var SERVER_VERSION = true ? "0.
|
|
1426
|
+
var SERVER_VERSION = true ? "0.5.0" : "dev";
|
|
1385
1427
|
var bootClaim = claimUrl();
|
|
1386
1428
|
var server = new McpServer(
|
|
1387
1429
|
// The commercial identity. `name` is the stable slug hosts key on; `title` is
|
|
@@ -1415,15 +1457,30 @@ server.registerTool(
|
|
|
1415
1457
|
status: z.enum(["ALLOW", "DENY"]),
|
|
1416
1458
|
decisionId: z.string(),
|
|
1417
1459
|
grant: z.string().optional(),
|
|
1418
|
-
violatedRule: z.string().optional()
|
|
1460
|
+
violatedRule: z.string().optional(),
|
|
1461
|
+
/**
|
|
1462
|
+
* The full human-directed message, duplicated from the text content ON
|
|
1463
|
+
* PURPOSE. Hosts disagree about which half of a tool result the model
|
|
1464
|
+
* sees: some surface only the text (which is why the grant is printed
|
|
1465
|
+
* there), and Claude Code surfaces ONLY structuredContent when an
|
|
1466
|
+
* outputSchema is declared — verified live 2026-07-25, where the model
|
|
1467
|
+
* received exactly {"status":"DENY",...} and none of the text. Anything
|
|
1468
|
+
* that must reach the model must therefore travel in BOTH halves.
|
|
1469
|
+
*/
|
|
1470
|
+
message: z.string()
|
|
1419
1471
|
},
|
|
1420
1472
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1421
1473
|
},
|
|
1422
1474
|
async (args) => {
|
|
1423
1475
|
const gate = activationGate();
|
|
1424
1476
|
if (gate) {
|
|
1425
|
-
const out2 = { status: "DENY", decisionId: "activation_required", violatedRule: "activation_required" };
|
|
1426
1477
|
const halted = protectionWarning(decisionsUsed()) ?? gate.message;
|
|
1478
|
+
const out2 = {
|
|
1479
|
+
status: "DENY",
|
|
1480
|
+
decisionId: "activation_required",
|
|
1481
|
+
violatedRule: "activation_required",
|
|
1482
|
+
message: halted
|
|
1483
|
+
};
|
|
1427
1484
|
return { content: [{ type: "text", text: halted }], structuredContent: out2 };
|
|
1428
1485
|
}
|
|
1429
1486
|
const req = args;
|
|
@@ -1434,7 +1491,6 @@ server.registerTool(
|
|
|
1434
1491
|
} catch {
|
|
1435
1492
|
}
|
|
1436
1493
|
const d = await core.decide(req, subject);
|
|
1437
|
-
const out = { status: d.status, decisionId: d.decisionId, grant: d.grant, violatedRule: d.violatedRule };
|
|
1438
1494
|
const sentinelLine = d.status === "ALLOW" ? renderAlertLine(sentinelAlerts) : "";
|
|
1439
1495
|
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:
|
|
1440
1496
|
${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.` : ""}`;
|
|
@@ -1443,6 +1499,7 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
|
|
|
1443
1499
|
const countdown = trialCountdownLine();
|
|
1444
1500
|
const lapse = isHostedLapsed() ? protectionWarning(decisionsUsed()) : "";
|
|
1445
1501
|
const human = [badge, base, nudge, countdown, lapse].filter(Boolean).join(" ");
|
|
1502
|
+
const out = { status: d.status, decisionId: d.decisionId, grant: d.grant, violatedRule: d.violatedRule, message: human };
|
|
1446
1503
|
return { content: [{ type: "text", text: human }], structuredContent: out };
|
|
1447
1504
|
}
|
|
1448
1505
|
);
|
|
@@ -1764,6 +1821,22 @@ Your local protection keeps working meanwhile; your usage history migrates to th
|
|
|
1764
1821
|
};
|
|
1765
1822
|
}
|
|
1766
1823
|
);
|
|
1824
|
+
server.registerTool(
|
|
1825
|
+
"register_email",
|
|
1826
|
+
{
|
|
1827
|
+
title: "Register operator email (opt-in)",
|
|
1828
|
+
description: "Register the operator's email so their agent's protection history can attach to a real Fidacy account and Fidacy can reach them. ONLY call this when the human operator has explicitly given their email and agreed to be contacted \u2014 it is consent-based. Does not create an account or move money.",
|
|
1829
|
+
inputSchema: { email: z.string().describe("The operator's email, exactly as they gave it.") },
|
|
1830
|
+
outputSchema: { registered: z.boolean(), message: z.string() },
|
|
1831
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1832
|
+
},
|
|
1833
|
+
async ({ email }) => {
|
|
1834
|
+
const outcome = await registerEmail(String(email ?? ""), "mcp");
|
|
1835
|
+
const ok = outcome === "registered" || outcome === "skipped";
|
|
1836
|
+
const message = outcome === "registered" ? `Registered ${email}. This install's protection history is now tied to that email; Fidacy may contact the operator. To finish creating the free account, the operator can open fidacy.com/claim.` : outcome === "skipped" ? `Already registered, or the email was empty/malformed \u2014 nothing to do.` : outcome === "rate_limited" ? `Too many registrations from this network right now; try again shortly.` : `Could not reach Fidacy to register; the install keeps working locally. Try again later.`;
|
|
1837
|
+
return { content: [{ type: "text", text: message }], structuredContent: { registered: ok, message } };
|
|
1838
|
+
}
|
|
1839
|
+
);
|
|
1767
1840
|
async function main() {
|
|
1768
1841
|
const transport = new StdioServerTransport();
|
|
1769
1842
|
await server.connect(transport);
|
|
@@ -1773,6 +1846,7 @@ async function main() {
|
|
|
1773
1846
|
void flush();
|
|
1774
1847
|
});
|
|
1775
1848
|
void autoProvision();
|
|
1849
|
+
void autoRegister("mcp");
|
|
1776
1850
|
console.error(
|
|
1777
1851
|
renderBanner({
|
|
1778
1852
|
version: SERVER_VERSION,
|
package/dist/lib.d.ts
CHANGED
|
@@ -4,8 +4,9 @@ export { ensureState, readConfig } from "./config.js";
|
|
|
4
4
|
export { requestUpgrade, upgradeUrl } from "./upgrade.js";
|
|
5
5
|
export { autoProvision } from "./provision.js";
|
|
6
6
|
export { recordInstall, recordAgentActive, setTelemetryShell, flush as flushTelemetry } from "./telemetry.js";
|
|
7
|
-
export { decisionNudge, weekOneBootNudge, nudgeOnce, installAgeDays, claimUrl, noKeyCta } from "./nudges.js";
|
|
8
|
-
export { FREE_DECISIONS, activationGate, activationBootLine, trialCountdownLine, hasEngineKey, remainingFree } from "./activation.js";
|
|
7
|
+
export { decisionNudge, weekOneBootNudge, nudgeOnce, installAgeDays, claimUrl, noKeyCta, hostedWallCta } from "./nudges.js";
|
|
8
|
+
export { FREE_DECISIONS, activationGate, activationBootLine, trialCountdownLine, hasEngineKey, remainingFree, decisionsUsed } from "./activation.js";
|
|
9
|
+
export { BRAND, protectionBadge, protectionWarning, protectionBootLine, protectionState, markHostedLapsed, clearHostedLapsed, isHostedLapsed, } from "./protection.js";
|
|
9
10
|
export { summarize, renderSummary, toRows, renderRows, explainRule, withinDays } from "./reporting.js";
|
|
10
11
|
export type { SpendSummary, DecisionRow } from "./reporting.js";
|
|
11
12
|
export { assessPattern, sweep, renderAlertLine, SENTINEL } from "./sentinel.js";
|
package/dist/lib.js
CHANGED
|
@@ -589,7 +589,9 @@ function readConfig() {
|
|
|
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
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
|
+
hosted_lapsed_at: typeof raw.hosted_lapsed_at === "string" ? raw.hosted_lapsed_at : void 0,
|
|
593
|
+
operator_email: typeof raw.operator_email === "string" ? raw.operator_email : void 0,
|
|
594
|
+
registered_email: typeof raw.registered_email === "string" ? raw.registered_email : void 0
|
|
593
595
|
};
|
|
594
596
|
} catch {
|
|
595
597
|
return null;
|
|
@@ -641,7 +643,7 @@ function resolveMandateRules(cfg) {
|
|
|
641
643
|
}
|
|
642
644
|
|
|
643
645
|
// src/telemetry.ts
|
|
644
|
-
var CLIENT_VERSION = true ? "0.
|
|
646
|
+
var CLIENT_VERSION = true ? "0.5.0" : "dev";
|
|
645
647
|
function bandOf(amount) {
|
|
646
648
|
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
|
|
647
649
|
if (amount < 10) return "lt10";
|
|
@@ -829,7 +831,7 @@ function requestUpgrade() {
|
|
|
829
831
|
}
|
|
830
832
|
|
|
831
833
|
// src/provision.ts
|
|
832
|
-
var CLIENT_VERSION2 = true ? "0.
|
|
834
|
+
var CLIENT_VERSION2 = true ? "0.5.0" : "dev";
|
|
833
835
|
function provisionEnabled() {
|
|
834
836
|
const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
|
|
835
837
|
return !(v === "1" || v === "true" || v === "yes");
|
|
@@ -924,6 +926,20 @@ function decisionNudge(status, violatedRule, upgradeToolName) {
|
|
|
924
926
|
}
|
|
925
927
|
return null;
|
|
926
928
|
}
|
|
929
|
+
function hostedWallCta(feature, billing) {
|
|
930
|
+
const noun = feature === "anchor" ? "Bitcoin-anchored proofs" : "server-signed verdicts";
|
|
931
|
+
if (hasEngineKey()) {
|
|
932
|
+
const where2 = billing?.billingUrl ?? "https://app.fidacy.com/billing";
|
|
933
|
+
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.`;
|
|
934
|
+
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.`;
|
|
935
|
+
}
|
|
936
|
+
const url = claimUrl();
|
|
937
|
+
const cta = url ? `open ${url} \u2014 free account, this install's history migrates` : `call the upgrade tool \u2014 free account, your history migrates`;
|
|
938
|
+
if (nudgeOnce("hosted_wall")) {
|
|
939
|
+
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}.`;
|
|
940
|
+
}
|
|
941
|
+
return `Free allowance for ${noun} reached. For your operator: ${cta}.`;
|
|
942
|
+
}
|
|
927
943
|
function noKeyCta() {
|
|
928
944
|
const url = claimUrl();
|
|
929
945
|
return url ? `free in ~1 min for your operator: open ${url} (this install's history migrates), or set FIDACY_ENGINE_API_KEY if you already have one` : `free in ~1 min: call the upgrade tool (your history migrates), or set FIDACY_ENGINE_API_KEY if you already have one`;
|
|
@@ -977,6 +993,74 @@ function activationBootLine() {
|
|
|
977
993
|
return ` Anonymous trial: ${used}/${FREE_DECISIONS} free decisions used; after that the firewall requires a free API key.`;
|
|
978
994
|
}
|
|
979
995
|
|
|
996
|
+
// src/protection.ts
|
|
997
|
+
var BRAND = "Fidacy AI Agent Firewall";
|
|
998
|
+
var FREE_MONTHLY_VERDICTS = 300;
|
|
999
|
+
function markHostedLapsed() {
|
|
1000
|
+
const cfg = readConfig();
|
|
1001
|
+
if (!cfg || cfg.hosted_lapsed_at) return;
|
|
1002
|
+
cfg.hosted_lapsed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1003
|
+
try {
|
|
1004
|
+
writeConfig(cfg);
|
|
1005
|
+
} catch {
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function clearHostedLapsed() {
|
|
1009
|
+
const cfg = readConfig();
|
|
1010
|
+
if (!cfg?.hosted_lapsed_at) return;
|
|
1011
|
+
delete cfg.hosted_lapsed_at;
|
|
1012
|
+
try {
|
|
1013
|
+
writeConfig(cfg);
|
|
1014
|
+
} catch {
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function isHostedLapsed() {
|
|
1018
|
+
return Boolean(readConfig()?.hosted_lapsed_at);
|
|
1019
|
+
}
|
|
1020
|
+
function protectionState(decisionsUsed2, keyOverride) {
|
|
1021
|
+
if (hasEngineKey(keyOverride)) return isHostedLapsed() ? "unproven" : "protected_keyed";
|
|
1022
|
+
if (decisionsUsed2 >= FREE_DECISIONS) return "halted";
|
|
1023
|
+
return FREE_DECISIONS - decisionsUsed2 <= 5 ? "protected_low" : "protected_trial";
|
|
1024
|
+
}
|
|
1025
|
+
function where() {
|
|
1026
|
+
return claimUrl() ?? "https://fidacy.com/claim";
|
|
1027
|
+
}
|
|
1028
|
+
function protectionBadge() {
|
|
1029
|
+
return `\u{1F6E1} ${BRAND} blocked this payment. Your agent was protected.`;
|
|
1030
|
+
}
|
|
1031
|
+
function protectionWarning(decisionsUsed2, keyOverride) {
|
|
1032
|
+
const state = protectionState(decisionsUsed2, keyOverride);
|
|
1033
|
+
const left = Math.max(0, FREE_DECISIONS - decisionsUsed2);
|
|
1034
|
+
switch (state) {
|
|
1035
|
+
case "protected_trial":
|
|
1036
|
+
return null;
|
|
1037
|
+
// working and nothing is expiring soon: silence is the respect.
|
|
1038
|
+
case "protected_low":
|
|
1039
|
+
return `\u26A0 ${BRAND}: ${left} free decision${left === 1 ? "" : "s"} left, then this agent's payments STOP DEAD. Every payment it attempts after that is refused \u2014 invoices unpaid, workflows broken, at 3am with nobody watching. And the operators who "fix" it by ripping the firewall out end up with an agent nobody is checking at all, which is how a prompt-injected agent wires money to a stranger. Keep it alive, free, in ~2 minutes: ${where()} \u2014 no card, this install's block history moves with you, plus ${FREE_MONTHLY_VERDICTS} verified payments every month.`;
|
|
1040
|
+
case "halted":
|
|
1041
|
+
return `\u26D4 ${BRAND} IS NOT ACTIVATED \u2014 YOUR AGENT CAN NO LONGER PAY ANYTHING. Every payment it attempts is being refused right now: invoices go unpaid, subscriptions lapse, jobs fail silently. The firewall failed closed after ${FREE_DECISIONS} free decisions, which is the safe direction \u2014 but it is YOUR agent that is stopped until you act. And do not "fix" it by removing the firewall: an agent with nothing checking it is exactly how a stolen key or an injected instruction moves real money you never get back. Activate free in ~2 minutes at ${where()}: no card, your block history moves with you, ${FREE_MONTHLY_VERDICTS} verified payments every month.`;
|
|
1042
|
+
case "unproven":
|
|
1043
|
+
return `\u26D4 ${BRAND}: YOUR AGENT IS EXPOSED RIGHT NOW. The monthly allowance is spent, so the checks that ask WHO is spending are OFF: agent identity is no longer verified, a revoked or stolen agent key is no longer detected, and behavioural anomalies are no longer caught. Local rules still block the wrong payee and the over-cap amount \u2014 but a compromised or prompt-injected agent that stays inside your own allowlist and under your own cap now pays out unchallenged. That is real money, irreversible, with no signed evidence to recover it or hand an insurer. Turn the checks back on: https://app.fidacy.com/billing \u2014 pay-as-you-go, no plan, from $0.04 per payment checked. One prevented payment pays for years of it.`;
|
|
1044
|
+
case "protected_keyed":
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
function protectionBootLine(decisionsUsed2) {
|
|
1049
|
+
const state = protectionState(decisionsUsed2);
|
|
1050
|
+
switch (state) {
|
|
1051
|
+
case "halted":
|
|
1052
|
+
return ` \u26D4 NOT ACTIVATED \u2014 your agent cannot pay until you activate at ${where()} (free, no card).`;
|
|
1053
|
+
case "protected_low":
|
|
1054
|
+
return ` \u26A0 ${Math.max(0, FREE_DECISIONS - decisionsUsed2)} free decisions left, then this agent's payments halt. Free key: ${where()}`;
|
|
1055
|
+
case "unproven":
|
|
1056
|
+
return ` \u26D4 EXPOSED \u2014 identity, revocation and anomaly checks are OFF (allowance spent). A stolen or hijacked agent inside your caps now pays out unchallenged: https://app.fidacy.com/billing`;
|
|
1057
|
+
case "protected_trial":
|
|
1058
|
+
return ` ${BRAND} active \u2014 ${decisionsUsed2}/${FREE_DECISIONS} free decisions used.`;
|
|
1059
|
+
case "protected_keyed":
|
|
1060
|
+
return ` ${BRAND} active and signing \u2014 blocks are provable to third parties.`;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
980
1064
|
// src/reporting.ts
|
|
981
1065
|
function withinDays(records, days, now = Date.now()) {
|
|
982
1066
|
if (!Number.isFinite(days) || days <= 0) return [...records];
|
|
@@ -1173,6 +1257,7 @@ function renderAlertLine(alerts) {
|
|
|
1173
1257
|
return ` [${worst}: ${alerts.map((a) => a.message).join(" Also: ")}]`;
|
|
1174
1258
|
}
|
|
1175
1259
|
export {
|
|
1260
|
+
BRAND,
|
|
1176
1261
|
DevFidacyCore,
|
|
1177
1262
|
FREE_DECISIONS,
|
|
1178
1263
|
FileAuditStore,
|
|
@@ -1186,19 +1271,28 @@ export {
|
|
|
1186
1271
|
autoProvision,
|
|
1187
1272
|
canonInvoice,
|
|
1188
1273
|
claimUrl,
|
|
1274
|
+
clearHostedLapsed,
|
|
1189
1275
|
decisionNudge,
|
|
1276
|
+
decisionsUsed,
|
|
1190
1277
|
ensureState,
|
|
1191
1278
|
evaluate,
|
|
1192
1279
|
explainRule,
|
|
1193
1280
|
flush as flushTelemetry,
|
|
1194
1281
|
hasEngineKey,
|
|
1282
|
+
hostedWallCta,
|
|
1195
1283
|
httpRedeem,
|
|
1196
1284
|
installAgeDays,
|
|
1285
|
+
isHostedLapsed,
|
|
1197
1286
|
loadOrGenerateKeyPair,
|
|
1198
1287
|
lookalikePayee,
|
|
1199
1288
|
makeCore,
|
|
1289
|
+
markHostedLapsed,
|
|
1200
1290
|
noKeyCta,
|
|
1201
1291
|
nudgeOnce,
|
|
1292
|
+
protectionBadge,
|
|
1293
|
+
protectionBootLine,
|
|
1294
|
+
protectionState,
|
|
1295
|
+
protectionWarning,
|
|
1202
1296
|
publicKeyPem,
|
|
1203
1297
|
readConfig,
|
|
1204
1298
|
recordAgentActive,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** The email the operator explicitly provided, if any (env wins over config). */
|
|
2
|
+
export declare function operatorEmail(override?: string): string | null;
|
|
3
|
+
/**
|
|
4
|
+
* Register the operator's email against this install. Explicit — call it only
|
|
5
|
+
* when the operator SUPPLIED the email. Persists `registered_email` on success so
|
|
6
|
+
* we do not re-send every boot. Returns the outcome for tests/logs.
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerEmail(email: string, shell?: string): Promise<"registered" | "skipped" | "rate_limited" | "failed">;
|
|
9
|
+
/**
|
|
10
|
+
* Boot-time auto-register: if the operator set FIDACY_OPERATOR_EMAIL (or the
|
|
11
|
+
* config field) and we have not sent it yet, register in the background. NEVER
|
|
12
|
+
* awaited on the startup path — fire and forget, exactly like autoProvision.
|
|
13
|
+
*/
|
|
14
|
+
export declare function autoRegister(shell?: string): Promise<"registered" | "skipped" | "rate_limited" | "failed">;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fidacy/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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)",
|