@fidacy/openclaw-plugin 0.2.10 → 0.3.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/dist/index.js +126 -13
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4308,10 +4308,23 @@ function lookalikePayee(payee, allowed) {
|
|
|
4308
4308
|
}
|
|
4309
4309
|
return null;
|
|
4310
4310
|
}
|
|
4311
|
+
function validateMandateCaps(mandate) {
|
|
4312
|
+
const bad = (v) => typeof v !== "number" || !Number.isFinite(v) || v <= 0;
|
|
4313
|
+
if (!mandate.allow || typeof mandate.allow !== "object")
|
|
4314
|
+
return "invalid_mandate:missing_allow";
|
|
4315
|
+
if (bad(mandate.allow.perTxMax))
|
|
4316
|
+
return `invalid_mandate_cap:perTxMax=${String(mandate.allow.perTxMax)}`;
|
|
4317
|
+
if (bad(mandate.allow.maxTotal))
|
|
4318
|
+
return `invalid_mandate_cap:maxTotal=${String(mandate.allow.maxTotal)}`;
|
|
4319
|
+
return null;
|
|
4320
|
+
}
|
|
4311
4321
|
function evaluate(mandate, req, spentSoFar) {
|
|
4312
4322
|
const now = Date.now();
|
|
4313
4323
|
if (mandate.revoked)
|
|
4314
4324
|
return "mandate_revoked";
|
|
4325
|
+
const badCap = validateMandateCaps(mandate);
|
|
4326
|
+
if (badCap)
|
|
4327
|
+
return badCap;
|
|
4315
4328
|
if (now < Date.parse(mandate.window.notBefore))
|
|
4316
4329
|
return "before_mandate_window";
|
|
4317
4330
|
if (now > Date.parse(mandate.window.notAfter))
|
|
@@ -4638,6 +4651,9 @@ import {
|
|
|
4638
4651
|
readFileSync,
|
|
4639
4652
|
writeFileSync
|
|
4640
4653
|
} from "node:fs";
|
|
4654
|
+
function hasEngineKey(keyOverride) {
|
|
4655
|
+
return Boolean((keyOverride ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim());
|
|
4656
|
+
}
|
|
4641
4657
|
function configDir() {
|
|
4642
4658
|
return process.env.FIDACY_CONFIG_DIR ?? join(homedir(), ".fidacy");
|
|
4643
4659
|
}
|
|
@@ -4668,7 +4684,8 @@ function readConfig() {
|
|
|
4668
4684
|
// reset every once-per-install nudge into an every-time nag.
|
|
4669
4685
|
created_at: typeof raw.created_at === "string" ? raw.created_at : void 0,
|
|
4670
4686
|
nudges: raw.nudges && typeof raw.nudges === "object" ? raw.nudges : void 0,
|
|
4671
|
-
decisions_count: typeof raw.decisions_count === "number" ? raw.decisions_count : void 0
|
|
4687
|
+
decisions_count: typeof raw.decisions_count === "number" ? raw.decisions_count : void 0,
|
|
4688
|
+
hosted_lapsed_at: typeof raw.hosted_lapsed_at === "string" ? raw.hosted_lapsed_at : void 0
|
|
4672
4689
|
};
|
|
4673
4690
|
} catch {
|
|
4674
4691
|
return null;
|
|
@@ -4698,18 +4715,29 @@ function ensureState() {
|
|
|
4698
4715
|
function resolveMandateRules(cfg) {
|
|
4699
4716
|
const m = cfg?.mandate ?? {};
|
|
4700
4717
|
const envList = (v) => v === void 0 ? void 0 : v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
4701
|
-
const envNum = (v) =>
|
|
4718
|
+
const envNum = (name, v) => {
|
|
4719
|
+
if (v === void 0 || v.trim() === "") return void 0;
|
|
4720
|
+
const n = Number(v);
|
|
4721
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
4722
|
+
console.error(
|
|
4723
|
+
`[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).`
|
|
4724
|
+
);
|
|
4725
|
+
return void 0;
|
|
4726
|
+
}
|
|
4727
|
+
return n;
|
|
4728
|
+
};
|
|
4729
|
+
const fileNum = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
|
|
4702
4730
|
return {
|
|
4703
4731
|
payees: envList(process.env.FIDACY_ALLOW_PAYEES) ?? m.payees ?? [],
|
|
4704
4732
|
categories: envList(process.env.FIDACY_ALLOW_CATEGORIES) ?? m.categories ?? ["*"],
|
|
4705
4733
|
currency: process.env.FIDACY_CURRENCY ?? m.currency ?? "USD",
|
|
4706
|
-
perTxMax: envNum(process.env.FIDACY_PER_TX_MAX) ?? m.perTxMax ?? 2500,
|
|
4707
|
-
maxTotal: envNum(process.env.FIDACY_MAX_TOTAL) ?? m.maxTotal ?? 1e4
|
|
4734
|
+
perTxMax: envNum("FIDACY_PER_TX_MAX", process.env.FIDACY_PER_TX_MAX) ?? fileNum(m.perTxMax) ?? 2500,
|
|
4735
|
+
maxTotal: envNum("FIDACY_MAX_TOTAL", process.env.FIDACY_MAX_TOTAL) ?? fileNum(m.maxTotal) ?? 1e4
|
|
4708
4736
|
};
|
|
4709
4737
|
}
|
|
4710
4738
|
|
|
4711
4739
|
// ../mcp/src/telemetry.ts
|
|
4712
|
-
var CLIENT_VERSION = true ? "0.
|
|
4740
|
+
var CLIENT_VERSION = true ? "0.3.0" : "dev";
|
|
4713
4741
|
function bandOf(amount) {
|
|
4714
4742
|
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
|
|
4715
4743
|
if (amount < 10) return "lt10";
|
|
@@ -4897,7 +4925,7 @@ function requestUpgrade() {
|
|
|
4897
4925
|
}
|
|
4898
4926
|
|
|
4899
4927
|
// ../mcp/src/provision.ts
|
|
4900
|
-
var CLIENT_VERSION2 = true ? "0.
|
|
4928
|
+
var CLIENT_VERSION2 = true ? "0.3.0" : "dev";
|
|
4901
4929
|
function provisionEnabled() {
|
|
4902
4930
|
const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
|
|
4903
4931
|
return !(v === "1" || v === "true" || v === "yes");
|
|
@@ -4987,6 +5015,20 @@ function decisionNudge(status, violatedRule, upgradeToolName) {
|
|
|
4987
5015
|
}
|
|
4988
5016
|
return null;
|
|
4989
5017
|
}
|
|
5018
|
+
function hostedWallCta(feature, billing) {
|
|
5019
|
+
const noun = feature === "anchor" ? "Bitcoin-anchored proofs" : "server-signed verdicts";
|
|
5020
|
+
if (hasEngineKey()) {
|
|
5021
|
+
const where2 = billing?.billingUrl ?? "https://app.fidacy.com/billing";
|
|
5022
|
+
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.`;
|
|
5023
|
+
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.`;
|
|
5024
|
+
}
|
|
5025
|
+
const url = claimUrl();
|
|
5026
|
+
const cta = url ? `open ${url} \u2014 free account, this install's history migrates` : `call the upgrade tool \u2014 free account, your history migrates`;
|
|
5027
|
+
if (nudgeOnce("hosted_wall")) {
|
|
5028
|
+
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}.`;
|
|
5029
|
+
}
|
|
5030
|
+
return `Free allowance for ${noun} reached. For your operator: ${cta}.`;
|
|
5031
|
+
}
|
|
4990
5032
|
function noKeyCta() {
|
|
4991
5033
|
const url = claimUrl();
|
|
4992
5034
|
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`;
|
|
@@ -4994,9 +5036,6 @@ function noKeyCta() {
|
|
|
4994
5036
|
|
|
4995
5037
|
// ../mcp/src/activation.ts
|
|
4996
5038
|
var FREE_DECISIONS = 20;
|
|
4997
|
-
function hasEngineKey(keyOverride) {
|
|
4998
|
-
return Boolean((keyOverride ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim());
|
|
4999
|
-
}
|
|
5000
5039
|
function decisionsUsed() {
|
|
5001
5040
|
return Math.max(readConfig()?.decisions_count ?? 0, sessionDecisionCount());
|
|
5002
5041
|
}
|
|
@@ -5027,6 +5066,59 @@ function trialCountdownLine(keyOverride) {
|
|
|
5027
5066
|
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.`;
|
|
5028
5067
|
}
|
|
5029
5068
|
|
|
5069
|
+
// ../mcp/src/protection.ts
|
|
5070
|
+
var BRAND = "Fidacy AI Agent Firewall";
|
|
5071
|
+
var FREE_MONTHLY_VERDICTS = 300;
|
|
5072
|
+
function markHostedLapsed() {
|
|
5073
|
+
const cfg = readConfig();
|
|
5074
|
+
if (!cfg || cfg.hosted_lapsed_at) return;
|
|
5075
|
+
cfg.hosted_lapsed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
5076
|
+
try {
|
|
5077
|
+
writeConfig(cfg);
|
|
5078
|
+
} catch {
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
function clearHostedLapsed() {
|
|
5082
|
+
const cfg = readConfig();
|
|
5083
|
+
if (!cfg?.hosted_lapsed_at) return;
|
|
5084
|
+
delete cfg.hosted_lapsed_at;
|
|
5085
|
+
try {
|
|
5086
|
+
writeConfig(cfg);
|
|
5087
|
+
} catch {
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
function isHostedLapsed() {
|
|
5091
|
+
return Boolean(readConfig()?.hosted_lapsed_at);
|
|
5092
|
+
}
|
|
5093
|
+
function protectionState(decisionsUsed2, keyOverride) {
|
|
5094
|
+
if (hasEngineKey(keyOverride)) return isHostedLapsed() ? "unproven" : "protected_keyed";
|
|
5095
|
+
if (decisionsUsed2 >= FREE_DECISIONS) return "halted";
|
|
5096
|
+
return FREE_DECISIONS - decisionsUsed2 <= 5 ? "protected_low" : "protected_trial";
|
|
5097
|
+
}
|
|
5098
|
+
function where() {
|
|
5099
|
+
return claimUrl() ?? "https://fidacy.com/claim";
|
|
5100
|
+
}
|
|
5101
|
+
function protectionBadge() {
|
|
5102
|
+
return `\u{1F6E1} ${BRAND} blocked this payment. Your agent was protected.`;
|
|
5103
|
+
}
|
|
5104
|
+
function protectionWarning(decisionsUsed2, keyOverride) {
|
|
5105
|
+
const state = protectionState(decisionsUsed2, keyOverride);
|
|
5106
|
+
const left = Math.max(0, FREE_DECISIONS - decisionsUsed2);
|
|
5107
|
+
switch (state) {
|
|
5108
|
+
case "protected_trial":
|
|
5109
|
+
return null;
|
|
5110
|
+
// working and nothing is expiring soon: silence is the respect.
|
|
5111
|
+
case "protected_low":
|
|
5112
|
+
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.`;
|
|
5113
|
+
case "halted":
|
|
5114
|
+
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.`;
|
|
5115
|
+
case "unproven":
|
|
5116
|
+
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.`;
|
|
5117
|
+
case "protected_keyed":
|
|
5118
|
+
return null;
|
|
5119
|
+
}
|
|
5120
|
+
}
|
|
5121
|
+
|
|
5030
5122
|
// ../mcp/src/reporting.ts
|
|
5031
5123
|
function withinDays(records, days, now = Date.now()) {
|
|
5032
5124
|
if (!Number.isFinite(days) || days <= 0) return [...records];
|
|
@@ -5229,6 +5321,7 @@ var AssessError = class extends Error {
|
|
|
5229
5321
|
status;
|
|
5230
5322
|
details;
|
|
5231
5323
|
rejection_reasons;
|
|
5324
|
+
billing;
|
|
5232
5325
|
constructor(opts) {
|
|
5233
5326
|
super(`Fidacy assess error (${opts.type}, HTTP ${opts.status})`);
|
|
5234
5327
|
this.name = "AssessError";
|
|
@@ -5236,6 +5329,7 @@ var AssessError = class extends Error {
|
|
|
5236
5329
|
this.status = opts.status;
|
|
5237
5330
|
this.details = opts.details;
|
|
5238
5331
|
this.rejection_reasons = opts.rejection_reasons;
|
|
5332
|
+
this.billing = opts.billing;
|
|
5239
5333
|
}
|
|
5240
5334
|
};
|
|
5241
5335
|
var DEFAULT_TIMEOUT = 1e4;
|
|
@@ -5331,7 +5425,12 @@ async function postOnce(fetchImpl, url, headers, payload, timeoutMs) {
|
|
|
5331
5425
|
const type = isRecord(parsed) && typeof parsed.error === "string" && parsed.error || `http_${res.status}`;
|
|
5332
5426
|
const details = isRecord(parsed) ? parsed.details : void 0;
|
|
5333
5427
|
const rejection_reasons = isRecord(parsed) && Array.isArray(parsed.rejection_reasons) ? parsed.rejection_reasons : void 0;
|
|
5334
|
-
|
|
5428
|
+
const billing = res.status === 402 && isRecord(parsed) ? {
|
|
5429
|
+
reason: typeof parsed.reason === "string" ? parsed.reason : void 0,
|
|
5430
|
+
detail: typeof parsed.detail === "string" ? parsed.detail : void 0,
|
|
5431
|
+
billingUrl: typeof parsed.billing_url === "string" ? parsed.billing_url : void 0
|
|
5432
|
+
} : void 0;
|
|
5433
|
+
throw new AssessError({ type, status: res.status, details, rejection_reasons, billing });
|
|
5335
5434
|
}
|
|
5336
5435
|
if (!isRecord(parsed)) {
|
|
5337
5436
|
throw new AssessError({ type: "invalid_response", status: res.status });
|
|
@@ -5476,7 +5575,8 @@ var index_default = defineToolPlugin({
|
|
|
5476
5575
|
async execute(params, config) {
|
|
5477
5576
|
const gate = activationGate(config.engineApiKey);
|
|
5478
5577
|
if (gate) {
|
|
5479
|
-
|
|
5578
|
+
const halted = protectionWarning(decisionsUsed(), config.engineApiKey) ?? gate.message;
|
|
5579
|
+
return { status: "DENY", decisionId: "activation_required", violatedRule: "activation_required", message: halted };
|
|
5480
5580
|
}
|
|
5481
5581
|
const c = boot();
|
|
5482
5582
|
let sentinelAlerts = [];
|
|
@@ -5489,9 +5589,11 @@ var index_default = defineToolPlugin({
|
|
|
5489
5589
|
const sentinelLine = d.status === "ALLOW" ? renderAlertLine(sentinelAlerts) : "";
|
|
5490
5590
|
const base = d.status === "ALLOW" ? `ALLOW (decision ${d.decisionId})${params.invoiceRef ? ` for invoice ${params.invoiceRef}` : ""}.${sentinelLine} To settle, call the executor with the SAME payee, amount, currency, and idempotencyKey, and set "grant" to EXACTLY this signed value:
|
|
5491
5591
|
${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 "${params.payee}" to mandate.payees in ~/.fidacy/config.json and retry: the firewall picks the change up on the next call, no restart needed.` : ""}`;
|
|
5592
|
+
const badge = d.status === "DENY" ? protectionBadge() : "";
|
|
5492
5593
|
const nudge = decisionNudge(d.status, d.violatedRule, "fidacy_upgrade");
|
|
5493
5594
|
const countdown = trialCountdownLine(config.engineApiKey);
|
|
5494
|
-
const
|
|
5595
|
+
const lapse = isHostedLapsed() ? protectionWarning(decisionsUsed(), config.engineApiKey) : "";
|
|
5596
|
+
const message = [badge, base, nudge, countdown, lapse].filter(Boolean).join(" ");
|
|
5495
5597
|
return { status: d.status, decisionId: d.decisionId, grant: d.grant, violatedRule: d.violatedRule, message };
|
|
5496
5598
|
}
|
|
5497
5599
|
}),
|
|
@@ -5635,9 +5737,14 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
|
|
|
5635
5737
|
{ kind: params.kind, mandate: params.mandate, mandateType: params.mandateType, idempotencyKey: params.idempotencyKey, spendingMandate: params.spendingMandate, a2a: params.a2a },
|
|
5636
5738
|
{ engineUrl, apiKey }
|
|
5637
5739
|
);
|
|
5740
|
+
clearHostedLapsed();
|
|
5638
5741
|
return { summary: `${r.decision} (score ${r.score}) signed by ${r.signingKeyId}`, ...r };
|
|
5639
5742
|
} catch (e) {
|
|
5640
5743
|
if (e instanceof AssessError) {
|
|
5744
|
+
if (e.status === 402) {
|
|
5745
|
+
markHostedLapsed();
|
|
5746
|
+
throw new Error(hostedWallCta("verdict", e.billing));
|
|
5747
|
+
}
|
|
5641
5748
|
const reasons = e.rejection_reasons?.length ? " (" + e.rejection_reasons.map((x) => x.key).join(",") + ")" : "";
|
|
5642
5749
|
throw new Error(`ASSESS ${e.status}: ${e.type}${reasons}`);
|
|
5643
5750
|
}
|
|
@@ -5689,7 +5796,13 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
|
|
|
5689
5796
|
...r
|
|
5690
5797
|
};
|
|
5691
5798
|
} catch (e) {
|
|
5692
|
-
if (e instanceof AssessError)
|
|
5799
|
+
if (e instanceof AssessError) {
|
|
5800
|
+
if (e.status === 402) {
|
|
5801
|
+
markHostedLapsed();
|
|
5802
|
+
throw new Error(hostedWallCta("anchor", e.billing));
|
|
5803
|
+
}
|
|
5804
|
+
throw new Error(`ANCHOR ${e.status}: ${e.type}`);
|
|
5805
|
+
}
|
|
5693
5806
|
throw new Error("ANCHOR failed: unexpected error");
|
|
5694
5807
|
}
|
|
5695
5808
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "fidacy",
|
|
3
|
-
"name": "Fidacy
|
|
3
|
+
"name": "Fidacy AI Agent Firewall",
|
|
4
4
|
"description": "A signed, independently-verifiable verdict on every money-moving agent action. Blocks wrong/lookalike payee, over-cap, and duplicate-invoice fraud before money moves. Non-custodial, local-first, deny-by-default.",
|
|
5
5
|
"icon": "https://fidacy.com/logo.png",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.3.0",
|
|
7
7
|
"contracts": {
|
|
8
8
|
"tools": [
|
|
9
9
|
"request_payment",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fidacy/openclaw-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Fidacy payment firewall as a native OpenClaw plugin: signed, verifiable verdicts on every money-moving agent action, in-process (no MCP subprocess).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Fidacy (ZeepCode Group Technology LLC) <hello@fidacy.com> (https://fidacy.com)",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"typebox": "1.1.39",
|
|
58
58
|
"typescript": "^5.6.3",
|
|
59
59
|
"@fidacy/firewall": "0.1.1",
|
|
60
|
-
"@fidacy/mcp": "0.
|
|
60
|
+
"@fidacy/mcp": "0.4.2"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build": "node scripts/bundle.mjs",
|