@fidacy/openclaw-plugin 0.2.4 → 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 +307 -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);
|
|
@@ -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
|
}
|
|
@@ -4566,13 +4574,16 @@ var FileAuditStore = class {
|
|
|
4566
4574
|
append(decision) {
|
|
4567
4575
|
const prevHash = this.head();
|
|
4568
4576
|
const seq = this.chain.length;
|
|
4569
|
-
const
|
|
4577
|
+
const ts2 = decision.ts;
|
|
4570
4578
|
const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
|
|
4571
|
-
const hash = sha256(`${prevHash}|${digest}|${seq}|${
|
|
4572
|
-
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 };
|
|
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.6" : "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.6" : "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,202 @@ 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
|
+
|
|
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
|
+
|
|
4999
5215
|
// ../mcp/src/assess.ts
|
|
5000
5216
|
var AssessError = class extends Error {
|
|
5001
5217
|
type;
|
|
@@ -5250,8 +5466,16 @@ var index_default = defineToolPlugin({
|
|
|
5250
5466
|
if (gate) {
|
|
5251
5467
|
return { status: "DENY", decisionId: "activation_required", violatedRule: "activation_required", message: gate.message };
|
|
5252
5468
|
}
|
|
5253
|
-
const
|
|
5254
|
-
|
|
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:
|
|
5255
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.` : ""}`;
|
|
5256
5480
|
const nudge = decisionNudge(d.status, d.violatedRule, "fidacy_upgrade");
|
|
5257
5481
|
const countdown = trialCountdownLine(config.engineApiKey);
|
|
@@ -5296,6 +5520,77 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
|
|
|
5296
5520
|
return proof;
|
|
5297
5521
|
}
|
|
5298
5522
|
}),
|
|
5523
|
+
// OPERATOR REPORTING. Same three surfaces as @fidacy/mcp, same pure functions,
|
|
5524
|
+
// so an operator gets the identical answers whichever shell their agent runs.
|
|
5525
|
+
// Never gated: reading what your own agents did moves no money.
|
|
5526
|
+
tool({
|
|
5527
|
+
name: "spend_summary",
|
|
5528
|
+
label: "Spend Summary (what my agents spent and what was blocked)",
|
|
5529
|
+
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.",
|
|
5530
|
+
parameters: typebox_exports.Object({
|
|
5531
|
+
days: typebox_exports.Optional(typebox_exports.Number({ description: "Window in days, counting back from now. Default 7." }))
|
|
5532
|
+
}),
|
|
5533
|
+
async execute(params, config) {
|
|
5534
|
+
const records = await boot().history(5e3);
|
|
5535
|
+
const summary = summarize(records, params.days ?? 7);
|
|
5536
|
+
const hasKey = (config.engineApiKey ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim().length > 0;
|
|
5537
|
+
const claim = hasKey ? null : claimUrl();
|
|
5538
|
+
return {
|
|
5539
|
+
summary,
|
|
5540
|
+
report: renderSummary(summary),
|
|
5541
|
+
...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}` } : {}
|
|
5542
|
+
};
|
|
5543
|
+
}
|
|
5544
|
+
}),
|
|
5545
|
+
tool({
|
|
5546
|
+
name: "list_decisions",
|
|
5547
|
+
label: "List Recent Payment Decisions",
|
|
5548
|
+
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.",
|
|
5549
|
+
parameters: typebox_exports.Object({
|
|
5550
|
+
limit: typebox_exports.Optional(typebox_exports.Number({ description: "How many decisions to return. Default 20." })),
|
|
5551
|
+
status: typebox_exports.Optional(typebox_exports.String({ description: "Filter by outcome: ALLOW, DENY or all. Default all." }))
|
|
5552
|
+
}),
|
|
5553
|
+
async execute(params) {
|
|
5554
|
+
const records = await boot().history(5e3);
|
|
5555
|
+
const filter = params.status === "ALLOW" || params.status === "DENY" ? params.status : "all";
|
|
5556
|
+
const rows = toRows(records, filter, params.limit ?? 20);
|
|
5557
|
+
return { count: rows.length, decisions: rows, report: renderRows(rows) };
|
|
5558
|
+
}
|
|
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
|
+
}),
|
|
5574
|
+
tool({
|
|
5575
|
+
name: "explain_decision",
|
|
5576
|
+
label: "Explain a Decision (why was this allowed or blocked)",
|
|
5577
|
+
description: "Explain one decision in plain language: what was requested, what the mandate did about it, why, and whether the audit chain is intact.",
|
|
5578
|
+
parameters: typebox_exports.Object({
|
|
5579
|
+
decisionId: typebox_exports.String({ description: "Decision id from request_payment, list_decisions or spend_summary" })
|
|
5580
|
+
}),
|
|
5581
|
+
async execute(params) {
|
|
5582
|
+
const c = boot();
|
|
5583
|
+
const records = await c.history(5e3);
|
|
5584
|
+
const record2 = records.find((r) => r.decisionId === params.decisionId);
|
|
5585
|
+
if (!record2) throw new Error(`No decision ${params.decisionId} on this chain. Run list_decisions to see the ids that exist here.`);
|
|
5586
|
+
const proof = await c.getProof(params.decisionId);
|
|
5587
|
+
return {
|
|
5588
|
+
record: record2,
|
|
5589
|
+
explanation: explainRule(record2.violatedRule, record2.payee),
|
|
5590
|
+
chainIntact: proof?.chainIntact ?? null
|
|
5591
|
+
};
|
|
5592
|
+
}
|
|
5593
|
+
}),
|
|
5299
5594
|
// VERDICT LAYER (advisory). Calls the LIVE Fidacy engine POST /v1/assess and
|
|
5300
5595
|
// returns the SIGNED trust verdict (riskPayloadJws + signingKeyId), verifiable
|
|
5301
5596
|
// 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.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",
|