@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 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);
@@ -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 ts = decision.ts;
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}|${ts}`);
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.5" : "dev";
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.5" : "dev";
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 d = await boot().decide(params, subjectOf(config));
5387
- 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:
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)",
@@ -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.5",
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.5",
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",