@fidacy/openclaw-plugin 0.1.2 → 0.1.4
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 +13 -0
- package/dist/index.js +174 -67
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -25,3 +25,16 @@ First release. Fidacy payment firewall as a native OpenClaw plugin:
|
|
|
25
25
|
ClawHub-native adoption is attributable (was indistinguishable from MCP).
|
|
26
26
|
- DENY responses now point to the `fidacy_upgrade` tool.
|
|
27
27
|
- Inherits the self-documenting first-run config + friendlier boot line.
|
|
28
|
+
|
|
29
|
+
## 0.1.3 — 2026-07-02
|
|
30
|
+
|
|
31
|
+
- Same upgrade micro-trigger system as @fidacy/mcp 0.1.11 (once-per-install,
|
|
32
|
+
value-proven moments only), routed through the fidacy_upgrade tool.
|
|
33
|
+
|
|
34
|
+
## 0.1.4 — 2026-07-02
|
|
35
|
+
|
|
36
|
+
- Inherits @fidacy/mcp 0.1.12's restart-durable firewall state: the
|
|
37
|
+
duplicate-invoice (BEC) guard and the spend cap now rehydrate from the local
|
|
38
|
+
audit log at boot, so restarting the agent no longer re-opens a paid invoice
|
|
39
|
+
or resets the running total. Torn-tail audit logs (crash mid-append) are
|
|
40
|
+
salvaged instead of quarantined.
|
package/dist/index.js
CHANGED
|
@@ -4259,66 +4259,9 @@ function sha256(input) {
|
|
|
4259
4259
|
|
|
4260
4260
|
// ../firewall/dist/audit-store.js
|
|
4261
4261
|
import fs from "node:fs";
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
constructor(path) {
|
|
4266
|
-
this.path = path;
|
|
4267
|
-
this.load();
|
|
4268
|
-
}
|
|
4269
|
-
load() {
|
|
4270
|
-
if (!fs.existsSync(this.path))
|
|
4271
|
-
return;
|
|
4272
|
-
try {
|
|
4273
|
-
const raw = fs.readFileSync(this.path, "utf8");
|
|
4274
|
-
const parsed = [];
|
|
4275
|
-
for (const line of raw.split("\n")) {
|
|
4276
|
-
if (!line.trim())
|
|
4277
|
-
continue;
|
|
4278
|
-
parsed.push(JSON.parse(line));
|
|
4279
|
-
}
|
|
4280
|
-
this.chain = parsed;
|
|
4281
|
-
if (!this.intact())
|
|
4282
|
-
throw new Error("audit chain integrity broken");
|
|
4283
|
-
} catch (err) {
|
|
4284
|
-
this.chain = [];
|
|
4285
|
-
try {
|
|
4286
|
-
const quarantine = `${this.path}.corrupt-${Date.now()}`;
|
|
4287
|
-
fs.renameSync(this.path, quarantine);
|
|
4288
|
-
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered (${err.message}); quarantined to ${quarantine}, starting a fresh chain.`);
|
|
4289
|
-
} catch {
|
|
4290
|
-
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered and could not be quarantined; starting a fresh in-memory chain.`);
|
|
4291
|
-
}
|
|
4292
|
-
}
|
|
4293
|
-
}
|
|
4294
|
-
head() {
|
|
4295
|
-
return this.chain.length ? this.chain[this.chain.length - 1].hash : "GENESIS";
|
|
4296
|
-
}
|
|
4297
|
-
append(decision) {
|
|
4298
|
-
const prevHash = this.head();
|
|
4299
|
-
const seq = this.chain.length;
|
|
4300
|
-
const ts = decision.ts;
|
|
4301
|
-
const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
|
|
4302
|
-
const hash = sha256(`${prevHash}|${digest}|${seq}|${ts}`);
|
|
4303
|
-
const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts };
|
|
4304
|
-
fs.appendFileSync(this.path, JSON.stringify(record2) + "\n");
|
|
4305
|
-
this.chain.push(record2);
|
|
4306
|
-
return record2;
|
|
4307
|
-
}
|
|
4308
|
-
find(decisionId) {
|
|
4309
|
-
return this.chain.find((r) => r.decisionId === decisionId);
|
|
4310
|
-
}
|
|
4311
|
-
intact() {
|
|
4312
|
-
let prev = "GENESIS";
|
|
4313
|
-
for (const r of this.chain) {
|
|
4314
|
-
const expected = sha256(`${prev}|${r.digest}|${r.seq}|${r.ts}`);
|
|
4315
|
-
if (expected !== r.hash || r.prevHash !== prev)
|
|
4316
|
-
return false;
|
|
4317
|
-
prev = r.hash;
|
|
4318
|
-
}
|
|
4319
|
-
return true;
|
|
4320
|
-
}
|
|
4321
|
-
};
|
|
4262
|
+
|
|
4263
|
+
// ../firewall/dist/core.js
|
|
4264
|
+
import { randomUUID } from "node:crypto";
|
|
4322
4265
|
|
|
4323
4266
|
// ../firewall/dist/evaluate.js
|
|
4324
4267
|
function fold(s) {
|
|
@@ -4395,7 +4338,6 @@ function evaluate(mandate, req, spentSoFar) {
|
|
|
4395
4338
|
}
|
|
4396
4339
|
|
|
4397
4340
|
// ../firewall/dist/core.js
|
|
4398
|
-
import { randomUUID } from "node:crypto";
|
|
4399
4341
|
function canonInvoice(ref) {
|
|
4400
4342
|
if (typeof ref !== "string")
|
|
4401
4343
|
return "";
|
|
@@ -4443,6 +4385,35 @@ var DevFidacyCore = class {
|
|
|
4443
4385
|
this.mandate = opts.mandate;
|
|
4444
4386
|
this.store = new FileAuditStore(opts.auditLogPath ?? "./fidacy-audit.log");
|
|
4445
4387
|
this.onDecision = opts.onDecision;
|
|
4388
|
+
this.rehydrate();
|
|
4389
|
+
}
|
|
4390
|
+
/**
|
|
4391
|
+
* Rebuild the in-memory decision state from the persisted audit log, so a process
|
|
4392
|
+
* restart cannot re-open a claimed invoice (BEC dedup) or reset the maxTotal
|
|
4393
|
+
* counter. One O(records) pass at boot over the already-loaded chain:
|
|
4394
|
+
* - claimedInvoices: every ALLOW that carried an invoiceRef, regardless of age —
|
|
4395
|
+
* a paid invoice stays paid.
|
|
4396
|
+
* - spent: ALLOW amounts whose timestamp falls inside the CURRENT mandate window
|
|
4397
|
+
* and whose currency matches the mandate (the cap is defined per window/currency).
|
|
4398
|
+
* Records from older versions lack the rehydration fields and are skipped.
|
|
4399
|
+
*/
|
|
4400
|
+
rehydrate() {
|
|
4401
|
+
const notBefore = Date.parse(this.mandate.window.notBefore);
|
|
4402
|
+
const notAfter = Date.parse(this.mandate.window.notAfter);
|
|
4403
|
+
for (const r of this.store.records()) {
|
|
4404
|
+
if (r.status !== "ALLOW")
|
|
4405
|
+
continue;
|
|
4406
|
+
if (r.invoiceRef)
|
|
4407
|
+
this.claimedInvoices.add(`${r.subject}|${r.invoiceRef}`);
|
|
4408
|
+
if (typeof r.amount !== "number" || !Number.isFinite(r.amount))
|
|
4409
|
+
continue;
|
|
4410
|
+
if (r.currency !== this.mandate.allow.currency)
|
|
4411
|
+
continue;
|
|
4412
|
+
const t = Date.parse(r.ts);
|
|
4413
|
+
if (!Number.isFinite(t) || t < notBefore || t > notAfter)
|
|
4414
|
+
continue;
|
|
4415
|
+
this.spent += r.amount;
|
|
4416
|
+
}
|
|
4446
4417
|
}
|
|
4447
4418
|
async getMandate() {
|
|
4448
4419
|
return this.mandate;
|
|
@@ -4529,6 +4500,96 @@ var HttpFidacyCore = class {
|
|
|
4529
4500
|
}
|
|
4530
4501
|
};
|
|
4531
4502
|
|
|
4503
|
+
// ../firewall/dist/audit-store.js
|
|
4504
|
+
var FileAuditStore = class {
|
|
4505
|
+
path;
|
|
4506
|
+
chain = [];
|
|
4507
|
+
constructor(path) {
|
|
4508
|
+
this.path = path;
|
|
4509
|
+
this.load();
|
|
4510
|
+
}
|
|
4511
|
+
load() {
|
|
4512
|
+
if (!fs.existsSync(this.path))
|
|
4513
|
+
return;
|
|
4514
|
+
try {
|
|
4515
|
+
const raw = fs.readFileSync(this.path, "utf8");
|
|
4516
|
+
const lines = raw.split("\n");
|
|
4517
|
+
const parsed = [];
|
|
4518
|
+
let torn = false;
|
|
4519
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4520
|
+
if (!lines[i].trim())
|
|
4521
|
+
continue;
|
|
4522
|
+
try {
|
|
4523
|
+
parsed.push(JSON.parse(lines[i]));
|
|
4524
|
+
} catch {
|
|
4525
|
+
if (lines.slice(i + 1).some((l) => l.trim()))
|
|
4526
|
+
throw new Error(`unparseable record at line ${i + 1}`);
|
|
4527
|
+
torn = true;
|
|
4528
|
+
break;
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4531
|
+
this.chain = parsed;
|
|
4532
|
+
if (!this.intact())
|
|
4533
|
+
throw new Error("audit chain integrity broken");
|
|
4534
|
+
if (torn) {
|
|
4535
|
+
const clean = parsed.map((r) => JSON.stringify(r)).join("\n") + (parsed.length ? "\n" : "");
|
|
4536
|
+
fs.writeFileSync(this.path, clean);
|
|
4537
|
+
console.error(`[fidacy] audit log at ${this.path} had a torn trailing line (crash during append?); salvaged ${parsed.length} intact records and truncated the tail.`);
|
|
4538
|
+
}
|
|
4539
|
+
} catch (err) {
|
|
4540
|
+
this.chain = [];
|
|
4541
|
+
try {
|
|
4542
|
+
const quarantine = `${this.path}.corrupt-${Date.now()}`;
|
|
4543
|
+
fs.renameSync(this.path, quarantine);
|
|
4544
|
+
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered (${err.message}); quarantined to ${quarantine}, starting a fresh chain.`);
|
|
4545
|
+
} catch {
|
|
4546
|
+
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered and could not be quarantined; starting a fresh in-memory chain.`);
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
head() {
|
|
4551
|
+
return this.chain.length ? this.chain[this.chain.length - 1].hash : "GENESIS";
|
|
4552
|
+
}
|
|
4553
|
+
append(decision) {
|
|
4554
|
+
const prevHash = this.head();
|
|
4555
|
+
const seq = this.chain.length;
|
|
4556
|
+
const ts = decision.ts;
|
|
4557
|
+
const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
|
|
4558
|
+
const hash = sha256(`${prevHash}|${digest}|${seq}|${ts}`);
|
|
4559
|
+
const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts };
|
|
4560
|
+
if (decision.status === "ALLOW") {
|
|
4561
|
+
const req = decision.request;
|
|
4562
|
+
if (typeof req?.amount === "number" && Number.isFinite(req.amount))
|
|
4563
|
+
record2.amount = req.amount;
|
|
4564
|
+
if (typeof req?.currency === "string")
|
|
4565
|
+
record2.currency = req.currency;
|
|
4566
|
+
const invoice = canonInvoice(req?.invoiceRef);
|
|
4567
|
+
if (invoice)
|
|
4568
|
+
record2.invoiceRef = invoice;
|
|
4569
|
+
}
|
|
4570
|
+
fs.appendFileSync(this.path, JSON.stringify(record2) + "\n");
|
|
4571
|
+
this.chain.push(record2);
|
|
4572
|
+
return record2;
|
|
4573
|
+
}
|
|
4574
|
+
find(decisionId) {
|
|
4575
|
+
return this.chain.find((r) => r.decisionId === decisionId);
|
|
4576
|
+
}
|
|
4577
|
+
/** The full loaded chain, oldest first. Read-only; used to rehydrate core state at boot. */
|
|
4578
|
+
records() {
|
|
4579
|
+
return this.chain;
|
|
4580
|
+
}
|
|
4581
|
+
intact() {
|
|
4582
|
+
let prev = "GENESIS";
|
|
4583
|
+
for (const r of this.chain) {
|
|
4584
|
+
const expected = sha256(`${prev}|${r.digest}|${r.seq}|${r.ts}`);
|
|
4585
|
+
if (expected !== r.hash || r.prevHash !== prev)
|
|
4586
|
+
return false;
|
|
4587
|
+
prev = r.hash;
|
|
4588
|
+
}
|
|
4589
|
+
return true;
|
|
4590
|
+
}
|
|
4591
|
+
};
|
|
4592
|
+
|
|
4532
4593
|
// ../mcp/src/config.ts
|
|
4533
4594
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4534
4595
|
import { homedir } from "node:os";
|
|
@@ -4564,7 +4625,12 @@ function readConfig() {
|
|
|
4564
4625
|
anon_id: raw.anon_id,
|
|
4565
4626
|
tier: raw.tier === "paid" ? "paid" : "free",
|
|
4566
4627
|
api_key: typeof raw.api_key === "string" ? raw.api_key : null,
|
|
4567
|
-
mandate: raw.mandate
|
|
4628
|
+
mandate: raw.mandate,
|
|
4629
|
+
// Install-state passthrough: dropping these on a read→write cycle would
|
|
4630
|
+
// reset every once-per-install nudge into an every-time nag.
|
|
4631
|
+
created_at: typeof raw.created_at === "string" ? raw.created_at : void 0,
|
|
4632
|
+
nudges: raw.nudges && typeof raw.nudges === "object" ? raw.nudges : void 0,
|
|
4633
|
+
decisions_count: typeof raw.decisions_count === "number" ? raw.decisions_count : void 0
|
|
4568
4634
|
};
|
|
4569
4635
|
} catch {
|
|
4570
4636
|
return null;
|
|
@@ -4582,7 +4648,8 @@ function ensureState() {
|
|
|
4582
4648
|
anon_id: randomUUID2(),
|
|
4583
4649
|
tier: "free",
|
|
4584
4650
|
api_key: null,
|
|
4585
|
-
mandate: { payees: [], categories: ["*"], currency: "USD", perTxMax: 2500, maxTotal: 1e4 }
|
|
4651
|
+
mandate: { payees: [], categories: ["*"], currency: "USD", perTxMax: 2500, maxTotal: 1e4 },
|
|
4652
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4586
4653
|
};
|
|
4587
4654
|
try {
|
|
4588
4655
|
writeConfig(config);
|
|
@@ -4604,7 +4671,7 @@ function resolveMandateRules(cfg) {
|
|
|
4604
4671
|
}
|
|
4605
4672
|
|
|
4606
4673
|
// ../mcp/src/telemetry.ts
|
|
4607
|
-
var CLIENT_VERSION = true ? "0.1.
|
|
4674
|
+
var CLIENT_VERSION = true ? "0.1.4" : "dev";
|
|
4608
4675
|
var currentShell = "mcp";
|
|
4609
4676
|
function setTelemetryShell(shell) {
|
|
4610
4677
|
currentShell = shell;
|
|
@@ -4727,7 +4794,7 @@ function requestUpgrade() {
|
|
|
4727
4794
|
}
|
|
4728
4795
|
|
|
4729
4796
|
// ../mcp/src/provision.ts
|
|
4730
|
-
var CLIENT_VERSION2 = true ? "0.1.
|
|
4797
|
+
var CLIENT_VERSION2 = true ? "0.1.4" : "dev";
|
|
4731
4798
|
function provisionEnabled() {
|
|
4732
4799
|
const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
|
|
4733
4800
|
return !(v === "1" || v === "true" || v === "yes");
|
|
@@ -4762,6 +4829,44 @@ async function autoProvision() {
|
|
|
4762
4829
|
}
|
|
4763
4830
|
}
|
|
4764
4831
|
|
|
4832
|
+
// ../mcp/src/nudges.ts
|
|
4833
|
+
function nudgeOnce(key) {
|
|
4834
|
+
const cfg = readConfig();
|
|
4835
|
+
if (!cfg) return false;
|
|
4836
|
+
if (cfg.nudges?.[key]) return false;
|
|
4837
|
+
cfg.nudges = { ...cfg.nudges ?? {}, [key]: true };
|
|
4838
|
+
try {
|
|
4839
|
+
writeConfig(cfg);
|
|
4840
|
+
} catch {
|
|
4841
|
+
return false;
|
|
4842
|
+
}
|
|
4843
|
+
return true;
|
|
4844
|
+
}
|
|
4845
|
+
function bumpDecisionCount() {
|
|
4846
|
+
const cfg = readConfig();
|
|
4847
|
+
if (!cfg) return 0;
|
|
4848
|
+
const n = (cfg.decisions_count ?? 0) + 1;
|
|
4849
|
+
cfg.decisions_count = n;
|
|
4850
|
+
try {
|
|
4851
|
+
writeConfig(cfg);
|
|
4852
|
+
} catch {
|
|
4853
|
+
}
|
|
4854
|
+
return n;
|
|
4855
|
+
}
|
|
4856
|
+
function decisionNudge(status, violatedRule, upgradeToolName) {
|
|
4857
|
+
const count = bumpDecisionCount();
|
|
4858
|
+
if (status === "DENY" && violatedRule?.startsWith("duplicate_invoice") && nudgeOnce("bec_catch")) {
|
|
4859
|
+
return `That was a duplicate-invoice (BEC) catch \u2014 the #1 payment fraud. On a free account every block like this is server-signed and anchored to Bitcoin: court-grade evidence. Call the ${upgradeToolName} tool to start.`;
|
|
4860
|
+
}
|
|
4861
|
+
if (status === "DENY" && nudgeOnce("first_deny")) {
|
|
4862
|
+
return `Fidacy just blocked this for you \u2014 for server-signed, anchored proof of every block (free account, local history migrates), call the ${upgradeToolName} tool.`;
|
|
4863
|
+
}
|
|
4864
|
+
if (count === 10 && nudgeOnce("milestone_10")) {
|
|
4865
|
+
return `That's 10 actions gated on this install. For a signed, Bitcoin-anchored history of all of them (free account), call the ${upgradeToolName} tool.`;
|
|
4866
|
+
}
|
|
4867
|
+
return null;
|
|
4868
|
+
}
|
|
4869
|
+
|
|
4765
4870
|
// ../mcp/src/assess.ts
|
|
4766
4871
|
var AssessError = class extends Error {
|
|
4767
4872
|
type;
|
|
@@ -4942,8 +5047,10 @@ var index_default = defineToolPlugin({
|
|
|
4942
5047
|
}),
|
|
4943
5048
|
async execute(params, config) {
|
|
4944
5049
|
const d = await boot().decide(params, subjectOf(config));
|
|
4945
|
-
const
|
|
4946
|
-
${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
|
|
5050
|
+
const base = d.status === "ALLOW" ? `ALLOW (decision ${d.decisionId})${params.invoiceRef ? ` for invoice ${params.invoiceRef}` : ""}. To settle, call the executor with the SAME payee, amount, currency, and idempotencyKey, and set "grant" to EXACTLY this signed value:
|
|
5051
|
+
${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.`;
|
|
5052
|
+
const nudge = decisionNudge(d.status, d.violatedRule, "fidacy_upgrade");
|
|
5053
|
+
const message = nudge ? `${base} ${nudge}` : base;
|
|
4947
5054
|
return { status: d.status, decisionId: d.decisionId, grant: d.grant, violatedRule: d.violatedRule, message };
|
|
4948
5055
|
}
|
|
4949
5056
|
}),
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "fidacy",
|
|
3
3
|
"name": "Fidacy \u2014 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
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.3",
|
|
6
6
|
"contracts": {
|
|
7
7
|
"tools": [
|
|
8
8
|
"request_payment",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fidacy/openclaw-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"typebox": "1.1.39",
|
|
57
57
|
"typescript": "^5.6.3",
|
|
58
58
|
"@fidacy/firewall": "0.1.0",
|
|
59
|
-
"@fidacy/mcp": "0.1.
|
|
59
|
+
"@fidacy/mcp": "0.1.12"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "node scripts/bundle.mjs",
|