@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 CHANGED
@@ -4433,17 +4433,17 @@ var DevFidacyCore = class {
4433
4433
  }
4434
4434
  async decide(req, subject) {
4435
4435
  const decisionId = randomUUID();
4436
- const ts = (/* @__PURE__ */ new Date()).toISOString();
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 ts = decision.ts;
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}|${ts}`);
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.4" : "dev";
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.4" : "dev";
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 d = await boot().decide(params, subjectOf(config));
5254
- 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:
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.
@@ -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.4",
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.4",
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",