@fidacy/openclaw-plugin 0.2.4 → 0.2.5
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 +192 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4474,6 +4474,10 @@ var DevFidacyCore = class {
|
|
|
4474
4474
|
return null;
|
|
4475
4475
|
return { record: record2, chainIntact: this.store.intact(), verifiedAgainstPublicKey: this.pubPem };
|
|
4476
4476
|
}
|
|
4477
|
+
async history(limit = 50) {
|
|
4478
|
+
const all = this.store.records();
|
|
4479
|
+
return limit > 0 && all.length > limit ? all.slice(-limit) : all;
|
|
4480
|
+
}
|
|
4477
4481
|
publicKey() {
|
|
4478
4482
|
return this.pubPem;
|
|
4479
4483
|
}
|
|
@@ -4508,6 +4512,10 @@ var HttpFidacyCore = class {
|
|
|
4508
4512
|
async getProof(decisionId) {
|
|
4509
4513
|
return this.call("/v1/audit/proof", { decisionId });
|
|
4510
4514
|
}
|
|
4515
|
+
async history(limit = 50) {
|
|
4516
|
+
const res = await this.call("/v1/audit/list", { limit });
|
|
4517
|
+
return res.records ?? [];
|
|
4518
|
+
}
|
|
4511
4519
|
publicKey() {
|
|
4512
4520
|
return this.subjectPub;
|
|
4513
4521
|
}
|
|
@@ -4573,6 +4581,9 @@ var FileAuditStore = class {
|
|
|
4573
4581
|
if (typeof decision.request?.purpose === "string" && decision.request.purpose.trim()) {
|
|
4574
4582
|
record2.purpose = decision.request.purpose.slice(0, 500);
|
|
4575
4583
|
}
|
|
4584
|
+
if (typeof decision.request?.payee === "string" && decision.request.payee.trim()) {
|
|
4585
|
+
record2.payee = decision.request.payee.slice(0, 200);
|
|
4586
|
+
}
|
|
4576
4587
|
if (decision.status === "ALLOW") {
|
|
4577
4588
|
const req = decision.request;
|
|
4578
4589
|
if (typeof req?.amount === "number" && Number.isFinite(req.amount))
|
|
@@ -4582,6 +4593,14 @@ var FileAuditStore = class {
|
|
|
4582
4593
|
const invoice = canonInvoice(req?.invoiceRef);
|
|
4583
4594
|
if (invoice)
|
|
4584
4595
|
record2.invoiceRef = invoice;
|
|
4596
|
+
} else {
|
|
4597
|
+
if (decision.violatedRule)
|
|
4598
|
+
record2.violatedRule = decision.violatedRule;
|
|
4599
|
+
const req = decision.request;
|
|
4600
|
+
if (typeof req?.amount === "number" && Number.isFinite(req.amount))
|
|
4601
|
+
record2.amount = req.amount;
|
|
4602
|
+
if (typeof req?.currency === "string")
|
|
4603
|
+
record2.currency = req.currency;
|
|
4585
4604
|
}
|
|
4586
4605
|
fs.appendFileSync(this.path, JSON.stringify(record2) + "\n");
|
|
4587
4606
|
this.chain.push(record2);
|
|
@@ -4690,7 +4709,7 @@ function resolveMandateRules(cfg) {
|
|
|
4690
4709
|
}
|
|
4691
4710
|
|
|
4692
4711
|
// ../mcp/src/telemetry.ts
|
|
4693
|
-
var CLIENT_VERSION = true ? "0.2.
|
|
4712
|
+
var CLIENT_VERSION = true ? "0.2.5" : "dev";
|
|
4694
4713
|
function bandOf(amount) {
|
|
4695
4714
|
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
|
|
4696
4715
|
if (amount < 10) return "lt10";
|
|
@@ -4850,6 +4869,7 @@ function makeCore() {
|
|
|
4850
4869
|
return dev.decide(req, subject);
|
|
4851
4870
|
},
|
|
4852
4871
|
getProof: (decisionId) => dev.getProof(decisionId),
|
|
4872
|
+
history: (limit) => dev.history(limit),
|
|
4853
4873
|
publicKey: () => dev.publicKey()
|
|
4854
4874
|
};
|
|
4855
4875
|
}
|
|
@@ -4867,7 +4887,7 @@ function requestUpgrade() {
|
|
|
4867
4887
|
}
|
|
4868
4888
|
|
|
4869
4889
|
// ../mcp/src/provision.ts
|
|
4870
|
-
var CLIENT_VERSION2 = true ? "0.2.
|
|
4890
|
+
var CLIENT_VERSION2 = true ? "0.2.5" : "dev";
|
|
4871
4891
|
function provisionEnabled() {
|
|
4872
4892
|
const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
|
|
4873
4893
|
return !(v === "1" || v === "true" || v === "yes");
|
|
@@ -4996,6 +5016,119 @@ function trialCountdownLine(keyOverride) {
|
|
|
4996
5016
|
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.`;
|
|
4997
5017
|
}
|
|
4998
5018
|
|
|
5019
|
+
// ../mcp/src/reporting.ts
|
|
5020
|
+
function withinDays(records, days, now = Date.now()) {
|
|
5021
|
+
if (!Number.isFinite(days) || days <= 0) return [...records];
|
|
5022
|
+
const cutoff = now - days * 864e5;
|
|
5023
|
+
return records.filter((r) => {
|
|
5024
|
+
const t = Date.parse(r.ts);
|
|
5025
|
+
return Number.isFinite(t) && t >= cutoff;
|
|
5026
|
+
});
|
|
5027
|
+
}
|
|
5028
|
+
var addTo = (bucket, key, value) => {
|
|
5029
|
+
bucket[key] = (bucket[key] ?? 0) + value;
|
|
5030
|
+
};
|
|
5031
|
+
function summarize(records, days, now = Date.now()) {
|
|
5032
|
+
const rows = withinDays(records, days, now);
|
|
5033
|
+
const out = {
|
|
5034
|
+
window_days: days,
|
|
5035
|
+
decisions: rows.length,
|
|
5036
|
+
allowed: 0,
|
|
5037
|
+
denied: 0,
|
|
5038
|
+
allowed_totals: {},
|
|
5039
|
+
blocked_totals: {},
|
|
5040
|
+
denied_by_rule: {},
|
|
5041
|
+
payees_paid: [],
|
|
5042
|
+
amount_unknown: 0,
|
|
5043
|
+
first_ts: rows.length ? rows[0].ts : null,
|
|
5044
|
+
last_ts: rows.length ? rows[rows.length - 1].ts : null
|
|
5045
|
+
};
|
|
5046
|
+
const paid = /* @__PURE__ */ new Set();
|
|
5047
|
+
for (const r of rows) {
|
|
5048
|
+
const known = typeof r.amount === "number" && Number.isFinite(r.amount);
|
|
5049
|
+
if (!known) out.amount_unknown += 1;
|
|
5050
|
+
const ccy = r.currency ?? "unknown";
|
|
5051
|
+
if (r.status === "ALLOW") {
|
|
5052
|
+
out.allowed += 1;
|
|
5053
|
+
if (known) addTo(out.allowed_totals, ccy, r.amount);
|
|
5054
|
+
if (r.payee) paid.add(r.payee);
|
|
5055
|
+
} else {
|
|
5056
|
+
out.denied += 1;
|
|
5057
|
+
if (known) addTo(out.blocked_totals, ccy, r.amount);
|
|
5058
|
+
addTo(out.denied_by_rule, r.violatedRule ?? "not_recorded", 1);
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
out.payees_paid = [...paid].sort();
|
|
5062
|
+
return out;
|
|
5063
|
+
}
|
|
5064
|
+
var money = (totals) => {
|
|
5065
|
+
const parts = Object.entries(totals).map(([ccy, v]) => `${v.toLocaleString("en-US")} ${ccy}`);
|
|
5066
|
+
return parts.length ? parts.join(", ") : "0";
|
|
5067
|
+
};
|
|
5068
|
+
function renderSummary(s) {
|
|
5069
|
+
if (s.decisions === 0) {
|
|
5070
|
+
return `No agent payment decisions in the last ${s.window_days} day(s). The firewall is active and has nothing to report, which is the expected state until an agent attempts a payment.`;
|
|
5071
|
+
}
|
|
5072
|
+
const lines = [
|
|
5073
|
+
`Last ${s.window_days} day(s): ${s.decisions} payment decision(s), ${s.allowed} allowed and ${s.denied} blocked before execution.`,
|
|
5074
|
+
`Paid: ${money(s.allowed_totals)}${s.payees_paid.length ? ` across ${s.payees_paid.length} payee(s): ${s.payees_paid.slice(0, 8).join(", ")}${s.payees_paid.length > 8 ? ", \u2026" : ""}` : ""}.`
|
|
5075
|
+
];
|
|
5076
|
+
if (s.denied > 0) {
|
|
5077
|
+
const ranked = Object.entries(s.denied_by_rule).sort((a, b) => b[1] - a[1]);
|
|
5078
|
+
lines.push(`Blocked: ${money(s.blocked_totals)} in attempted value. By rule: ${ranked.map(([rule, n]) => `${rule} (${n})`).join(", ")}.`);
|
|
5079
|
+
}
|
|
5080
|
+
if (s.amount_unknown > 0) {
|
|
5081
|
+
lines.push(`${s.amount_unknown} record(s) in this window predate amount logging, so the totals above are a floor, not the exact figure.`);
|
|
5082
|
+
}
|
|
5083
|
+
return lines.join("\n");
|
|
5084
|
+
}
|
|
5085
|
+
function toRows(records, status, limit) {
|
|
5086
|
+
const filtered = status === "all" ? [...records] : records.filter((r) => r.status === status);
|
|
5087
|
+
return filtered.slice(-Math.max(1, limit)).reverse().map((r) => ({
|
|
5088
|
+
seq: r.seq,
|
|
5089
|
+
decisionId: r.decisionId,
|
|
5090
|
+
status: r.status,
|
|
5091
|
+
ts: r.ts,
|
|
5092
|
+
payee: r.payee,
|
|
5093
|
+
amount: r.amount,
|
|
5094
|
+
currency: r.currency,
|
|
5095
|
+
purpose: r.purpose,
|
|
5096
|
+
violatedRule: r.violatedRule,
|
|
5097
|
+
invoiceRef: r.invoiceRef
|
|
5098
|
+
}));
|
|
5099
|
+
}
|
|
5100
|
+
function renderRows(rows) {
|
|
5101
|
+
if (!rows.length) return "No decisions match that filter yet.";
|
|
5102
|
+
return rows.map((r) => {
|
|
5103
|
+
const amount = typeof r.amount === "number" ? `${r.amount.toLocaleString("en-US")} ${r.currency ?? ""}`.trim() : "amount not recorded";
|
|
5104
|
+
const who = r.payee ?? "payee not recorded";
|
|
5105
|
+
const why = r.status === "DENY" ? ` blocked by ${r.violatedRule ?? "a rule not recorded on this older entry"}.` : "";
|
|
5106
|
+
const said = r.purpose ? ` Agent's stated reason: "${r.purpose}".` : "";
|
|
5107
|
+
return `#${r.seq} ${r.ts} ${r.status} ${amount} to ${who}.${why}${said} (decision ${r.decisionId})`;
|
|
5108
|
+
}).join("\n");
|
|
5109
|
+
}
|
|
5110
|
+
function explainRule(rule, payee) {
|
|
5111
|
+
if (!rule) return "This entry predates readable rule logging, so the specific rule is not on the record. The decision payload is still covered by the chain digest.";
|
|
5112
|
+
if (rule.startsWith("payee_not_in_allowlist"))
|
|
5113
|
+
return `The payee${payee ? ` "${payee}"` : ""} is not on the mandate's allowlist. This is the deny-by-default rule doing its job: an agent can only pay counterparties a human put on the list, which is what stops a swapped or lookalike vendor.`;
|
|
5114
|
+
if (rule.startsWith("category_not_allowed"))
|
|
5115
|
+
return "The stated purpose category is not one the mandate permits for this agent.";
|
|
5116
|
+
if (rule.startsWith("currency_not_allowed"))
|
|
5117
|
+
return "The payment currency is not the mandate's currency.";
|
|
5118
|
+
if (rule.startsWith("per_tx") || rule.includes("perTxMax"))
|
|
5119
|
+
return "The amount exceeds the per-transaction ceiling in the mandate.";
|
|
5120
|
+
if (rule.includes("maxTotal") || rule.startsWith("budget"))
|
|
5121
|
+
return "The payment would push cumulative spend past the mandate's total budget for the window.";
|
|
5122
|
+
if (rule.startsWith("invoice") || rule.includes("duplicate"))
|
|
5123
|
+
return "This invoice was already paid once. One payment per invoice is enforced regardless of amount, which is the control that stops a re-presented invoice at a higher figure.";
|
|
5124
|
+
if (rule.startsWith("window") || rule.includes("expired"))
|
|
5125
|
+
return "The mandate was outside its validity window at the moment of the request.";
|
|
5126
|
+
if (rule.startsWith("revoked")) return "The mandate had been revoked.";
|
|
5127
|
+
if (rule.startsWith("activation_required"))
|
|
5128
|
+
return "The anonymous trial had been used up, so the firewall failed closed. No money can move until the install is activated with a free API key.";
|
|
5129
|
+
return `The mandate rule "${rule}" was violated.`;
|
|
5130
|
+
}
|
|
5131
|
+
|
|
4999
5132
|
// ../mcp/src/assess.ts
|
|
5000
5133
|
var AssessError = class extends Error {
|
|
5001
5134
|
type;
|
|
@@ -5296,6 +5429,63 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
|
|
|
5296
5429
|
return proof;
|
|
5297
5430
|
}
|
|
5298
5431
|
}),
|
|
5432
|
+
// OPERATOR REPORTING. Same three surfaces as @fidacy/mcp, same pure functions,
|
|
5433
|
+
// so an operator gets the identical answers whichever shell their agent runs.
|
|
5434
|
+
// Never gated: reading what your own agents did moves no money.
|
|
5435
|
+
tool({
|
|
5436
|
+
name: "spend_summary",
|
|
5437
|
+
label: "Spend Summary (what my agents spent and what was blocked)",
|
|
5438
|
+
description: "Operator report over a time window: how many payment decisions were made, how much was paid and to whom, how much was blocked before executing, and which mandate rules did the blocking. Read-only, works offline against the local audit chain.",
|
|
5439
|
+
parameters: typebox_exports.Object({
|
|
5440
|
+
days: typebox_exports.Optional(typebox_exports.Number({ description: "Window in days, counting back from now. Default 7." }))
|
|
5441
|
+
}),
|
|
5442
|
+
async execute(params, config) {
|
|
5443
|
+
const records = await boot().history(5e3);
|
|
5444
|
+
const summary = summarize(records, params.days ?? 7);
|
|
5445
|
+
const hasKey = (config.engineApiKey ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim().length > 0;
|
|
5446
|
+
const claim = hasKey ? null : claimUrl();
|
|
5447
|
+
return {
|
|
5448
|
+
summary,
|
|
5449
|
+
report: renderSummary(summary),
|
|
5450
|
+
...claim ? { message: `For your operator: this report is built from the local chain on this machine. A free account keeps it across machines and anchors it to Bitcoin, and this install's history migrates: ${claim}` } : {}
|
|
5451
|
+
};
|
|
5452
|
+
}
|
|
5453
|
+
}),
|
|
5454
|
+
tool({
|
|
5455
|
+
name: "list_decisions",
|
|
5456
|
+
label: "List Recent Payment Decisions",
|
|
5457
|
+
description: "List recent agent payment decisions, newest first, with payee, amount, the agent's stated reason, and for blocked ones the rule that stopped it. Read-only, works offline.",
|
|
5458
|
+
parameters: typebox_exports.Object({
|
|
5459
|
+
limit: typebox_exports.Optional(typebox_exports.Number({ description: "How many decisions to return. Default 20." })),
|
|
5460
|
+
status: typebox_exports.Optional(typebox_exports.String({ description: "Filter by outcome: ALLOW, DENY or all. Default all." }))
|
|
5461
|
+
}),
|
|
5462
|
+
async execute(params) {
|
|
5463
|
+
const records = await boot().history(5e3);
|
|
5464
|
+
const filter = params.status === "ALLOW" || params.status === "DENY" ? params.status : "all";
|
|
5465
|
+
const rows = toRows(records, filter, params.limit ?? 20);
|
|
5466
|
+
return { count: rows.length, decisions: rows, report: renderRows(rows) };
|
|
5467
|
+
}
|
|
5468
|
+
}),
|
|
5469
|
+
tool({
|
|
5470
|
+
name: "explain_decision",
|
|
5471
|
+
label: "Explain a Decision (why was this allowed or blocked)",
|
|
5472
|
+
description: "Explain one decision in plain language: what was requested, what the mandate did about it, why, and whether the audit chain is intact.",
|
|
5473
|
+
parameters: typebox_exports.Object({
|
|
5474
|
+
decisionId: typebox_exports.String({ description: "Decision id from request_payment, list_decisions or spend_summary" })
|
|
5475
|
+
}),
|
|
5476
|
+
async execute(params) {
|
|
5477
|
+
const c = boot();
|
|
5478
|
+
const records = await c.history(5e3);
|
|
5479
|
+
const record2 = records.find((r) => r.decisionId === params.decisionId);
|
|
5480
|
+
if (!record2) throw new Error(`No decision ${params.decisionId} on this chain. Run list_decisions to see the ids that exist here.`);
|
|
5481
|
+
const proof = await c.getProof(params.decisionId);
|
|
5482
|
+
return {
|
|
5483
|
+
record: record2,
|
|
5484
|
+
explanation: explainRule(record2.violatedRule, record2.payee),
|
|
5485
|
+
chainIntact: proof?.chainIntact ?? null
|
|
5486
|
+
};
|
|
5487
|
+
}
|
|
5488
|
+
}),
|
|
5299
5489
|
// VERDICT LAYER (advisory). Calls the LIVE Fidacy engine POST /v1/assess and
|
|
5300
5490
|
// returns the SIGNED trust verdict (riskPayloadJws + signingKeyId), verifiable
|
|
5301
5491
|
// by anyone via @fidacy/verify against the engine JWKS. Moves no money.
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Fidacy — Payment 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.2.
|
|
6
|
+
"version": "0.2.5",
|
|
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.2.
|
|
3
|
+
"version": "0.2.5",
|
|
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
|
"homepage": "https://fidacy.com",
|