@fidacy/openclaw-plugin 0.2.5 → 0.2.6
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 +117 -12
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4433,17 +4433,17 @@ var DevFidacyCore = class {
|
|
|
4433
4433
|
}
|
|
4434
4434
|
async decide(req, subject) {
|
|
4435
4435
|
const decisionId = randomUUID();
|
|
4436
|
-
const
|
|
4436
|
+
const ts2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
4437
4437
|
const invalid = validateRequest(req);
|
|
4438
4438
|
if (invalid) {
|
|
4439
|
-
const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: invalid, ts };
|
|
4439
|
+
const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: invalid, ts: ts2 };
|
|
4440
4440
|
this.store.append(decision2);
|
|
4441
4441
|
this.onDecision?.(decision2);
|
|
4442
4442
|
return decision2;
|
|
4443
4443
|
}
|
|
4444
4444
|
const violated = evaluate(this.mandate, req, this.spent);
|
|
4445
4445
|
if (violated) {
|
|
4446
|
-
const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: violated, ts };
|
|
4446
|
+
const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: violated, ts: ts2 };
|
|
4447
4447
|
this.store.append(decision2);
|
|
4448
4448
|
this.onDecision?.(decision2);
|
|
4449
4449
|
return decision2;
|
|
@@ -4452,7 +4452,7 @@ var DevFidacyCore = class {
|
|
|
4452
4452
|
if (invoice) {
|
|
4453
4453
|
const k = `${subject}|${invoice}`;
|
|
4454
4454
|
if (this.claimedInvoices.has(k)) {
|
|
4455
|
-
const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: `duplicate_invoice:${invoice}`, ts };
|
|
4455
|
+
const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: `duplicate_invoice:${invoice}`, ts: ts2 };
|
|
4456
4456
|
this.store.append(decision2);
|
|
4457
4457
|
this.onDecision?.(decision2);
|
|
4458
4458
|
return decision2;
|
|
@@ -4462,7 +4462,7 @@ var DevFidacyCore = class {
|
|
|
4462
4462
|
const grantPayload = { decisionId, subject, payee: req.payee, amount: req.amount, currency: req.currency, exp: Date.now() + 12e4, ...req.invoiceRef ? { invoiceRef: req.invoiceRef } : {} };
|
|
4463
4463
|
const grantBody = Buffer.from(stableStringify(grantPayload), "utf8").toString("base64url");
|
|
4464
4464
|
const grant = `${grantBody}.${sign(this.priv, grantBody)}`;
|
|
4465
|
-
const decision = { decisionId, status: "ALLOW", subject, mandateId: this.mandate.id, request: req, grant, ts };
|
|
4465
|
+
const decision = { decisionId, status: "ALLOW", subject, mandateId: this.mandate.id, request: req, grant, ts: ts2 };
|
|
4466
4466
|
this.spent += req.amount;
|
|
4467
4467
|
this.store.append(decision);
|
|
4468
4468
|
this.onDecision?.(decision);
|
|
@@ -4574,10 +4574,10 @@ var FileAuditStore = class {
|
|
|
4574
4574
|
append(decision) {
|
|
4575
4575
|
const prevHash = this.head();
|
|
4576
4576
|
const seq = this.chain.length;
|
|
4577
|
-
const
|
|
4577
|
+
const ts2 = decision.ts;
|
|
4578
4578
|
const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
|
|
4579
|
-
const hash = sha256(`${prevHash}|${digest}|${seq}|${
|
|
4580
|
-
const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts };
|
|
4579
|
+
const hash = sha256(`${prevHash}|${digest}|${seq}|${ts2}`);
|
|
4580
|
+
const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts: ts2 };
|
|
4581
4581
|
if (typeof decision.request?.purpose === "string" && decision.request.purpose.trim()) {
|
|
4582
4582
|
record2.purpose = decision.request.purpose.slice(0, 500);
|
|
4583
4583
|
}
|
|
@@ -4709,7 +4709,7 @@ function resolveMandateRules(cfg) {
|
|
|
4709
4709
|
}
|
|
4710
4710
|
|
|
4711
4711
|
// ../mcp/src/telemetry.ts
|
|
4712
|
-
var CLIENT_VERSION = true ? "0.2.
|
|
4712
|
+
var CLIENT_VERSION = true ? "0.2.6" : "dev";
|
|
4713
4713
|
function bandOf(amount) {
|
|
4714
4714
|
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
|
|
4715
4715
|
if (amount < 10) return "lt10";
|
|
@@ -4887,7 +4887,7 @@ function requestUpgrade() {
|
|
|
4887
4887
|
}
|
|
4888
4888
|
|
|
4889
4889
|
// ../mcp/src/provision.ts
|
|
4890
|
-
var CLIENT_VERSION2 = true ? "0.2.
|
|
4890
|
+
var CLIENT_VERSION2 = true ? "0.2.6" : "dev";
|
|
4891
4891
|
function provisionEnabled() {
|
|
4892
4892
|
const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
|
|
4893
4893
|
return !(v === "1" || v === "true" || v === "yes");
|
|
@@ -5129,6 +5129,89 @@ function explainRule(rule, payee) {
|
|
|
5129
5129
|
return `The mandate rule "${rule}" was violated.`;
|
|
5130
5130
|
}
|
|
5131
5131
|
|
|
5132
|
+
// ../mcp/src/sentinel.ts
|
|
5133
|
+
var SENTINEL = {
|
|
5134
|
+
/** amount >= SPIKE_FACTOR x the historical max ALLOW to the same payee. */
|
|
5135
|
+
SPIKE_FACTOR: 3,
|
|
5136
|
+
/** minimum prior ALLOWs to a payee before a spike is judged (else first_payee covers it). */
|
|
5137
|
+
SPIKE_MIN_HISTORY: 2,
|
|
5138
|
+
/** decisions within BURST_WINDOW_MS to trip velocity_burst. */
|
|
5139
|
+
BURST_COUNT: 5,
|
|
5140
|
+
BURST_WINDOW_MS: 10 * 6e4,
|
|
5141
|
+
/** fraction of perTxMax that counts as riding the ceiling. */
|
|
5142
|
+
CAP_CREEP_RATIO: 0.8,
|
|
5143
|
+
/** a DENY for the same payee within this window makes a new attempt a retry. */
|
|
5144
|
+
RETRY_WINDOW_MS: 30 * 6e4
|
|
5145
|
+
};
|
|
5146
|
+
var ts = (r) => {
|
|
5147
|
+
const t = Date.parse(r.ts);
|
|
5148
|
+
return Number.isFinite(t) ? t : 0;
|
|
5149
|
+
};
|
|
5150
|
+
function assessPattern(records, req, opts = {}) {
|
|
5151
|
+
const now = opts.now ?? Date.now();
|
|
5152
|
+
const alerts = [];
|
|
5153
|
+
const toPayee = records.filter((r) => r.payee === req.payee);
|
|
5154
|
+
const allowedToPayee = toPayee.filter((r) => r.status === "ALLOW");
|
|
5155
|
+
if (req.payee && allowedToPayee.length === 0) {
|
|
5156
|
+
alerts.push({
|
|
5157
|
+
rule: "first_payee",
|
|
5158
|
+
severity: "warning",
|
|
5159
|
+
message: `First payment ever from this agent to "${req.payee}". A swapped or lookalike vendor always looks like a first payee; worth a human glance before anything settles.`
|
|
5160
|
+
});
|
|
5161
|
+
}
|
|
5162
|
+
if (allowedToPayee.length >= SENTINEL.SPIKE_MIN_HISTORY && typeof req.amount === "number") {
|
|
5163
|
+
const maxSeen = Math.max(...allowedToPayee.map((r) => typeof r.amount === "number" ? r.amount : 0));
|
|
5164
|
+
if (maxSeen > 0 && req.amount >= maxSeen * SENTINEL.SPIKE_FACTOR) {
|
|
5165
|
+
alerts.push({
|
|
5166
|
+
rule: "amount_spike",
|
|
5167
|
+
severity: "warning",
|
|
5168
|
+
message: `${req.amount.toLocaleString("en-US")} is ${(req.amount / maxSeen).toFixed(1)}x the largest amount this agent has ever paid "${req.payee}" (${maxSeen.toLocaleString("en-US")}). Inflated re-presentations of a known vendor look exactly like this.`
|
|
5169
|
+
});
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5172
|
+
const recent = records.filter((r) => now - ts(r) <= SENTINEL.BURST_WINDOW_MS);
|
|
5173
|
+
if (recent.length + 1 >= SENTINEL.BURST_COUNT) {
|
|
5174
|
+
alerts.push({
|
|
5175
|
+
rule: "velocity_burst",
|
|
5176
|
+
severity: "warning",
|
|
5177
|
+
message: `${recent.length + 1} payment decisions inside ${Math.round(SENTINEL.BURST_WINDOW_MS / 6e4)} minutes. Runaway loops and drain attempts announce themselves as bursts before they announce themselves as losses.`
|
|
5178
|
+
});
|
|
5179
|
+
}
|
|
5180
|
+
if (typeof opts.perTxMax === "number" && opts.perTxMax > 0 && typeof req.amount === "number") {
|
|
5181
|
+
if (req.amount >= opts.perTxMax * SENTINEL.CAP_CREEP_RATIO && req.amount <= opts.perTxMax) {
|
|
5182
|
+
alerts.push({
|
|
5183
|
+
rule: "cap_creep",
|
|
5184
|
+
severity: "notice",
|
|
5185
|
+
message: `${req.amount.toLocaleString("en-US")} is ${Math.round(req.amount / opts.perTxMax * 100)}% of the per-transaction ceiling. An attacker probing a mandate pays just under the cap; a human authorizing a real bill usually doesn't land this close.`
|
|
5186
|
+
});
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
5189
|
+
const recentDeny = toPayee.find((r) => r.status === "DENY" && now - ts(r) <= SENTINEL.RETRY_WINDOW_MS);
|
|
5190
|
+
if (recentDeny) {
|
|
5191
|
+
alerts.push({
|
|
5192
|
+
rule: "retry_after_deny",
|
|
5193
|
+
severity: "warning",
|
|
5194
|
+
message: `This agent was DENIED a payment to "${req.payee}" ${Math.max(1, Math.round((now - ts(recentDeny)) / 6e4))} minute(s) ago (${recentDeny.violatedRule ?? "rule on record"}) and is now trying again. Our public Model Watch benchmark measures exactly this behavior; in an autonomous agent it is the pattern that precedes a loss.`
|
|
5195
|
+
});
|
|
5196
|
+
}
|
|
5197
|
+
return alerts;
|
|
5198
|
+
}
|
|
5199
|
+
function sweep(records, opts = {}) {
|
|
5200
|
+
const out = [];
|
|
5201
|
+
for (let i = 0; i < records.length; i++) {
|
|
5202
|
+
const r = records[i];
|
|
5203
|
+
if (!r.payee) continue;
|
|
5204
|
+
const alerts = assessPattern(records.slice(0, i), { payee: r.payee, amount: r.amount }, { perTxMax: opts.perTxMax, now: ts(r) });
|
|
5205
|
+
if (alerts.length) out.push({ decisionId: r.decisionId, ts: r.ts, status: r.status, payee: r.payee, alerts });
|
|
5206
|
+
}
|
|
5207
|
+
return out.slice(-(opts.limit ?? 20)).reverse();
|
|
5208
|
+
}
|
|
5209
|
+
function renderAlertLine(alerts) {
|
|
5210
|
+
if (!alerts.length) return "";
|
|
5211
|
+
const worst = alerts.some((a) => a.severity === "warning") ? "SENTINEL WARNING" : "SENTINEL NOTICE";
|
|
5212
|
+
return ` [${worst}: ${alerts.map((a) => a.message).join(" Also: ")}]`;
|
|
5213
|
+
}
|
|
5214
|
+
|
|
5132
5215
|
// ../mcp/src/assess.ts
|
|
5133
5216
|
var AssessError = class extends Error {
|
|
5134
5217
|
type;
|
|
@@ -5383,8 +5466,16 @@ var index_default = defineToolPlugin({
|
|
|
5383
5466
|
if (gate) {
|
|
5384
5467
|
return { status: "DENY", decisionId: "activation_required", violatedRule: "activation_required", message: gate.message };
|
|
5385
5468
|
}
|
|
5386
|
-
const
|
|
5387
|
-
|
|
5469
|
+
const c = boot();
|
|
5470
|
+
let sentinelAlerts = [];
|
|
5471
|
+
try {
|
|
5472
|
+
const [before, mandate] = await Promise.all([c.history(5e3), c.getMandate(subjectOf(config))]);
|
|
5473
|
+
sentinelAlerts = assessPattern(before, { payee: params.payee, amount: params.amount }, { perTxMax: mandate?.allow?.perTxMax });
|
|
5474
|
+
} catch {
|
|
5475
|
+
}
|
|
5476
|
+
const d = await c.decide(params, subjectOf(config));
|
|
5477
|
+
const sentinelLine = d.status === "ALLOW" ? renderAlertLine(sentinelAlerts) : "";
|
|
5478
|
+
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:
|
|
5388
5479
|
${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.` : ""}`;
|
|
5389
5480
|
const nudge = decisionNudge(d.status, d.violatedRule, "fidacy_upgrade");
|
|
5390
5481
|
const countdown = trialCountdownLine(config.engineApiKey);
|
|
@@ -5466,6 +5557,20 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
|
|
|
5466
5557
|
return { count: rows.length, decisions: rows, report: renderRows(rows) };
|
|
5467
5558
|
}
|
|
5468
5559
|
}),
|
|
5560
|
+
tool({
|
|
5561
|
+
name: "sentinel_alerts",
|
|
5562
|
+
label: "Sentinel Alerts (allowed, but outside this agent's pattern)",
|
|
5563
|
+
description: "Predictive pattern alerts from the local audit chain: first-ever payee, amount spikes vs this agent's own history, velocity bursts, payments riding the mandate ceiling, and retries after a denial. Deterministic and explainable, no model.",
|
|
5564
|
+
parameters: typebox_exports.Object({
|
|
5565
|
+
limit: typebox_exports.Optional(typebox_exports.Number({ description: "How many flagged decisions to return. Default 20." }))
|
|
5566
|
+
}),
|
|
5567
|
+
async execute(params, config) {
|
|
5568
|
+
const c = boot();
|
|
5569
|
+
const [records, mandate] = await Promise.all([c.history(5e3), c.getMandate(subjectOf(config)).catch(() => null)]);
|
|
5570
|
+
const flagged = sweep(records, { perTxMax: mandate?.allow?.perTxMax, limit: params.limit ?? 20 });
|
|
5571
|
+
return { count: flagged.length, thresholds: SENTINEL, flagged };
|
|
5572
|
+
}
|
|
5573
|
+
}),
|
|
5469
5574
|
tool({
|
|
5470
5575
|
name: "explain_decision",
|
|
5471
5576
|
label: "Explain a Decision (why was this allowed or blocked)",
|
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.6",
|
|
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.6",
|
|
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",
|