@fidacy/openclaw-plugin 0.3.0 → 0.4.1

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/README.md CHANGED
@@ -94,6 +94,41 @@ This is a payment-control plugin, so audit it like one. What it actually does:
94
94
  JWKS at `api.fidacy.com/.well-known/jwks.json` with the open-source
95
95
  [`@fidacy/verify`](https://www.npmjs.com/package/@fidacy/verify).
96
96
 
97
+ ## Security
98
+
99
+ This plugin sits in front of money-moving tool calls, so it should be precise about
100
+ what it touches.
101
+
102
+ **What it reads.** Your engine credential, from the plugin config field `engineApiKey`
103
+ or from `FIDACY_ENGINE_API_KEY`. Optionally `FIDACY_SIGNING_KEY_B64`, which is your own
104
+ Ed25519 key, so grants are signed with a stable key instead of a per-session one. Both
105
+ belong to you and both are optional. With neither set, the plugin runs the free
106
+ anonymous path and signs with an ephemeral key.
107
+
108
+ **No credential is embedded.** There is no API key, token, or private key literal in the
109
+ published package, in any version. Check it yourself:
110
+
111
+ ```bash
112
+ cd $(mktemp -d) && curl -sL $(npm view @fidacy/openclaw-plugin dist.tarball) | tar xz && grep -rE "fky_(live|test)_[A-Za-z0-9]{8,}|BEGIN [A-Z ]*PRIVATE KEY" package/ || echo "no credential literals found"
113
+ ```
114
+
115
+ A static scanner may flag the line that resolves your credential as a hardcoded secret,
116
+ because the identifier on the right of the assignment is named like one. It reads your
117
+ config and falls back to the environment, defaulting to the empty string. The command
118
+ above is what settles the question, and the test suite fails the build if a literal ever
119
+ does get in.
120
+
121
+ **Turning the network off.** `FIDACY_DISABLE_TELEMETRY=1` stops anonymous install and
122
+ usage telemetry. `FIDACY_DISABLE_PROVISION=1` stops the background free-key provisioning.
123
+ With both set and no engine credential, the plugin decides locally and contacts nothing.
124
+
125
+ **Where the history lives.** Decisions go to a hash-chained log under `~/.fidacy` on your
126
+ machine. That local chain is what makes `spend_summary`, `list_decisions` and
127
+ `get_audit_proof` work with no network. Delete the directory to clear it.
128
+
129
+ **Files are never uploaded.** `anchor_artifact` and `check_artifact` hash the file locally
130
+ with SHA-256 and send only the 64 hex characters.
131
+
97
132
  ## Prefer MCP instead?
98
133
 
99
134
  If you'd rather run Fidacy as an MCP server (out-of-process), use
package/dist/index.js CHANGED
@@ -4685,7 +4685,9 @@ function readConfig() {
4685
4685
  created_at: typeof raw.created_at === "string" ? raw.created_at : void 0,
4686
4686
  nudges: raw.nudges && typeof raw.nudges === "object" ? raw.nudges : void 0,
4687
4687
  decisions_count: typeof raw.decisions_count === "number" ? raw.decisions_count : void 0,
4688
- hosted_lapsed_at: typeof raw.hosted_lapsed_at === "string" ? raw.hosted_lapsed_at : void 0
4688
+ hosted_lapsed_at: typeof raw.hosted_lapsed_at === "string" ? raw.hosted_lapsed_at : void 0,
4689
+ operator_email: typeof raw.operator_email === "string" ? raw.operator_email : void 0,
4690
+ registered_email: typeof raw.registered_email === "string" ? raw.registered_email : void 0
4689
4691
  };
4690
4692
  } catch {
4691
4693
  return null;
@@ -4737,7 +4739,7 @@ function resolveMandateRules(cfg) {
4737
4739
  }
4738
4740
 
4739
4741
  // ../mcp/src/telemetry.ts
4740
- var CLIENT_VERSION = true ? "0.3.0" : "dev";
4742
+ var CLIENT_VERSION = true ? "0.4.1" : "dev";
4741
4743
  function bandOf(amount) {
4742
4744
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
4743
4745
  if (amount < 10) return "lt10";
@@ -4925,7 +4927,7 @@ function requestUpgrade() {
4925
4927
  }
4926
4928
 
4927
4929
  // ../mcp/src/provision.ts
4928
- var CLIENT_VERSION2 = true ? "0.3.0" : "dev";
4930
+ var CLIENT_VERSION2 = true ? "0.4.1" : "dev";
4929
4931
  function provisionEnabled() {
4930
4932
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
4931
4933
  return !(v === "1" || v === "true" || v === "yes");
@@ -5066,6 +5068,37 @@ function trialCountdownLine(keyOverride) {
5066
5068
  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.`;
5067
5069
  }
5068
5070
 
5071
+ // ../mcp/src/register.ts
5072
+ var CLIENT_VERSION3 = true ? "0.4.1" : "dev";
5073
+ function endpoint3() {
5074
+ const base = (process.env.FIDACY_ENGINE_URL ?? "https://api.fidacy.com").replace(/\/$/, "");
5075
+ return `${base}/v1/register`;
5076
+ }
5077
+ function looksLikeEmail(s) {
5078
+ return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(s);
5079
+ }
5080
+ async function registerEmail(email, shell = "mcp") {
5081
+ const cfg = readConfig();
5082
+ if (!cfg) return "skipped";
5083
+ const clean = email.trim();
5084
+ if (!looksLikeEmail(clean)) return "skipped";
5085
+ if (cfg.registered_email && cfg.registered_email.toLowerCase() === clean.toLowerCase()) return "skipped";
5086
+ try {
5087
+ const res = await fetch(endpoint3(), {
5088
+ method: "POST",
5089
+ headers: { "content-type": "application/json" },
5090
+ body: JSON.stringify({ anon_id: cfg.anon_id, email: clean, client_version: CLIENT_VERSION3, shell })
5091
+ });
5092
+ if (res.status === 429) return "rate_limited";
5093
+ if (!res.ok) return "failed";
5094
+ const fresh = readConfig() ?? cfg;
5095
+ writeConfig({ ...fresh, operator_email: clean, registered_email: clean });
5096
+ return "registered";
5097
+ } catch {
5098
+ return "failed";
5099
+ }
5100
+ }
5101
+
5069
5102
  // ../mcp/src/protection.ts
5070
5103
  var BRAND = "Fidacy AI Agent Firewall";
5071
5104
  var FREE_MONTHLY_VERDICTS = 300;
@@ -5178,7 +5211,7 @@ function renderSummary(s) {
5178
5211
  ];
5179
5212
  if (s.denied > 0) {
5180
5213
  const ranked = Object.entries(s.denied_by_rule).sort((a, b) => b[1] - a[1]);
5181
- lines.push(`Blocked: ${money(s.blocked_totals)} in attempted value. By rule: ${ranked.map(([rule, n]) => `${rule} (${n})`).join(", ")}.`);
5214
+ lines.push(`Blocked: ${money(s.blocked_totals)} in attempted value. By reason: ${ranked.map(([rule, n]) => `${ruleLabel(rule)} (${n})`).join(", ")}.`);
5182
5215
  }
5183
5216
  if (s.amount_unknown > 0) {
5184
5217
  lines.push(`${s.amount_unknown} record(s) in this window predate amount logging, so the totals above are a floor, not the exact figure.`);
@@ -5205,32 +5238,82 @@ function renderRows(rows) {
5205
5238
  return rows.map((r) => {
5206
5239
  const amount = typeof r.amount === "number" ? `${r.amount.toLocaleString("en-US")} ${r.currency ?? ""}`.trim() : "amount not recorded";
5207
5240
  const who = r.payee ?? "payee not recorded";
5208
- const why = r.status === "DENY" ? ` blocked by ${r.violatedRule ?? "a rule not recorded on this older entry"}.` : "";
5241
+ const why = r.status === "DENY" ? ` ${explainRule(r.violatedRule, r.payee)}` : "";
5209
5242
  const said = r.purpose ? ` Agent's stated reason: "${r.purpose}".` : "";
5210
5243
  return `#${r.seq} ${r.ts} ${r.status} ${amount} to ${who}.${why}${said} (decision ${r.decisionId})`;
5211
5244
  }).join("\n");
5212
5245
  }
5246
+ function ruleLabel(rule) {
5247
+ if (rule.startsWith("payee_lookalike")) return "lookalike payee";
5248
+ if (rule.startsWith("payee_not_in_allowlist")) return "payee not approved";
5249
+ if (rule.startsWith("category_not_allowed")) return "purpose not allowed";
5250
+ if (rule.startsWith("currency_not_allowed")) return "wrong currency";
5251
+ if (rule.startsWith("per_tx") || rule.includes("perTxMax")) return "over the per-payment cap";
5252
+ if (rule.includes("maxTotal") || rule.startsWith("total_cap") || rule.startsWith("budget")) return "over the total budget";
5253
+ if (rule.startsWith("invoice") || rule.includes("duplicate")) return "duplicate invoice";
5254
+ if (rule.startsWith("window") || rule.includes("mandate_window") || rule.includes("expired")) return "outside the mandate window";
5255
+ if (rule.startsWith("revoked") || rule.startsWith("mandate_revoked")) return "mandate revoked";
5256
+ if (rule.startsWith("invalid_mandate")) return "mandate misconfigured";
5257
+ if (rule.startsWith("non_positive_amount")) return "invalid amount";
5258
+ if (rule.startsWith("activation_required")) return "trial used up";
5259
+ if (rule === "not_recorded") return "rule not recorded";
5260
+ return rule;
5261
+ }
5213
5262
  function explainRule(rule, payee) {
5214
5263
  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.";
5264
+ if (rule.startsWith("invalid_mandate")) {
5265
+ const cap = rule.includes("perTxMax") ? "perTxMax" : rule.includes("maxTotal") ? "maxTotal" : null;
5266
+ if (cap)
5267
+ return `The mandate's \`${cap}\` is not a usable number, so the firewall denied rather than run without that cap. Values like "2,500", "$2500" or "2500 USD" all look right to a human and become NaN, and a cap that cannot be compared would silently let everything through. Write it as a bare number: ${cap}: 2500.`;
5268
+ return "The mandate is missing its `allow` block, so there is nothing to enforce and the firewall denied. Check ~/.fidacy/config.json or FIDACY_MANDATE_JSON.";
5269
+ }
5270
+ if (rule.startsWith("payee_lookalike")) {
5271
+ const approved = rule.includes("~") ? rule.split("~")[1] : "an approved payee";
5272
+ return `The payee${payee ? ` "${payee}"` : ""} is a lookalike of your approved payee "${approved}": same name to a human glance, different string. That is the signature of a payee swap, whether from a prompt injection or a spoofed invoice. If it really is a separate vendor, add it to \`payees\` explicitly; if it is not, this attempt is exactly what the firewall exists to stop.`;
5273
+ }
5215
5274
  if (rule.startsWith("payee_not_in_allowlist"))
5216
- 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.`;
5275
+ 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. To authorize this one, add it to \`payees\` in ~/.fidacy/config.json, or pass a full mandate through FIDACY_MANDATE_JSON.`;
5217
5276
  if (rule.startsWith("category_not_allowed"))
5218
- return "The stated purpose category is not one the mandate permits for this agent.";
5277
+ return "The stated purpose category is not one the mandate permits for this agent. Add it to `categories` in the mandate, or send the payment under a purpose the mandate already allows.";
5219
5278
  if (rule.startsWith("currency_not_allowed"))
5220
- return "The payment currency is not the mandate's currency.";
5279
+ return "The payment currency is not the mandate's currency. Set `currency` to the one you intend to pay in. A mandate authorizes one currency at a time on purpose, so a swapped currency cannot slip past a cap set in another.";
5221
5280
  if (rule.startsWith("per_tx") || rule.includes("perTxMax"))
5222
- return "The amount exceeds the per-transaction ceiling in the mandate.";
5223
- if (rule.includes("maxTotal") || rule.startsWith("budget"))
5224
- return "The payment would push cumulative spend past the mandate's total budget for the window.";
5281
+ return "The amount exceeds the per-transaction ceiling in the mandate. Raise `perTxMax` if payments this size are expected, or split the payment.";
5282
+ if (rule.includes("maxTotal") || rule.startsWith("budget") || rule.startsWith("total_cap"))
5283
+ return "The payment would push cumulative spend past the mandate's total budget for the window. Raise `maxTotal`, or wait for the window to roll over. Blocked attempts do not consume the budget; only payments that went through do.";
5225
5284
  if (rule.startsWith("invoice") || rule.includes("duplicate"))
5226
- 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.";
5227
- if (rule.startsWith("window") || rule.includes("expired"))
5228
- return "The mandate was outside its validity window at the moment of the request.";
5229
- if (rule.startsWith("revoked")) return "The mandate had been revoked.";
5285
+ 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. If this genuinely is a second, separate payment, give it its own invoiceRef.";
5286
+ if (rule.startsWith("window") || rule.includes("mandate_window") || rule.includes("expired"))
5287
+ return "The mandate was outside its validity window at the moment of the request. Check `notBefore` and `notAfter`, and issue a mandate whose window covers now.";
5288
+ if (rule.startsWith("revoked") || rule.startsWith("mandate_revoked"))
5289
+ return "The mandate had been revoked. A revoked mandate cannot be un-revoked; issue a new one to authorize further payments.";
5290
+ if (rule.startsWith("non_positive_amount"))
5291
+ return "The amount was zero or negative, which is not a payment anyone can authorize. Check what produced that value before retrying.";
5230
5292
  if (rule.startsWith("activation_required"))
5231
5293
  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.";
5232
5294
  return `The mandate rule "${rule}" was violated.`;
5233
5295
  }
5296
+ function explainEngineError(action, status, type, reasons) {
5297
+ const noun = action === "verdict" ? "assess this action" : action === "anchor" ? "anchor this artifact" : "check this artifact";
5298
+ if (status === 401 || status === 403)
5299
+ return `Fidacy could not ${noun}: the engine rejected the credential (HTTP ${status}). This is not an outage. The key is missing, wrong, or lacks the scope this call needs \u2014 an anonymous provisioned key has no scopes at all. Get an account key at https://fidacy.com/claim and set FIDACY_ENGINE_API_KEY. Nothing was executed.`;
5300
+ if (status === 402)
5301
+ return `Fidacy could not ${noun}: the account is past a spending or quota limit (HTTP 402). The integration is fine; the limit needs settling at https://fidacy.com/claim. Nothing was executed.`;
5302
+ if (status === 429)
5303
+ return `Fidacy could not ${noun}: too many requests in a short window (HTTP 429). Retry in a few seconds. Nothing was executed, and no decision was recorded.`;
5304
+ if (status === 404 && action === "check")
5305
+ return "No anchor exists for that hash on this account. Either the artifact was never anchored, or its contents changed since it was \u2014 if you expected a match, the file is not the one that was anchored.";
5306
+ if (status === 422 || type === "mandate_violation") {
5307
+ const first = reasons?.[0]?.key;
5308
+ const why = first ? ` ${explainRule(first)}` : "";
5309
+ return `Fidacy denied this action: it falls outside the mandate in force.${why} Nothing was executed.`;
5310
+ }
5311
+ if (status >= 500)
5312
+ return `Fidacy could not ${noun}: the engine returned an error (HTTP ${status}). Nothing was executed, and the call was refused rather than allowed through. Retrying is safe.`;
5313
+ if (status === 400)
5314
+ return `Fidacy could not ${noun}: the request was malformed (HTTP 400${type ? `, ${type}` : ""}). This is a bug in how the call was built, not a policy decision. Nothing was executed.`;
5315
+ return `Fidacy could not ${noun} (HTTP ${status}${type ? `, ${type}` : ""}), so the call was refused rather than allowed through. Nothing was executed.`;
5316
+ }
5234
5317
 
5235
5318
  // ../mcp/src/sentinel.ts
5236
5319
  var SENTINEL = {
@@ -5504,6 +5587,9 @@ async function findArtifacts(sha2562, cfg) {
5504
5587
  }
5505
5588
 
5506
5589
  // src/index.ts
5590
+ function operatorEngineKey(config) {
5591
+ return (config.engineApiKey ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim();
5592
+ }
5507
5593
  var loaded = false;
5508
5594
  function onPluginLoad() {
5509
5595
  if (loaded) return;
@@ -5524,6 +5610,11 @@ function boot() {
5524
5610
  function subjectOf(config) {
5525
5611
  return config.subject ?? process.env.FIDACY_SUBJECT ?? "agent:demo";
5526
5612
  }
5613
+ function maybeRegisterOperator(config) {
5614
+ const email = (config.operatorEmail ?? process.env.FIDACY_OPERATOR_EMAIL ?? "").trim();
5615
+ if (email) void registerEmail(email, "openclaw-plugin").catch(() => {
5616
+ });
5617
+ }
5527
5618
  var ASSESS_KINDS = ["ap2_payment", "message_send", "voice_call", "custom", "claim_document"];
5528
5619
  var index_default = defineToolPlugin({
5529
5620
  id: "fidacy",
@@ -5531,6 +5622,17 @@ var index_default = defineToolPlugin({
5531
5622
  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.",
5532
5623
  configSchema: typebox_exports.Object(
5533
5624
  {
5625
+ // FIRST field on purpose: this is the install-time ask. OpenClaw renders
5626
+ // configSchema when the operator sets the plugin up, so the email is
5627
+ // collected at the one moment a HUMAN is present and paying attention —
5628
+ // no prompt is possible later (the plugin runs headless inside the agent).
5629
+ // Optional by design: a blank field must never block the install, and the
5630
+ // firewall protects with or without it.
5631
+ operatorEmail: typebox_exports.Optional(
5632
+ typebox_exports.String({
5633
+ description: "Your email (optional). Ties this agent's protection history to you so it can move into a free account, and lets Fidacy reach you about this install. Never shared; remove any time."
5634
+ })
5635
+ ),
5534
5636
  engineApiKey: typebox_exports.Optional(
5535
5637
  typebox_exports.String({
5536
5638
  description: "Fidacy engine API key (fky_live_/fky_test_) enabling signed verdicts via assess_action. Falls back to FIDACY_ENGINE_API_KEY."
@@ -5573,6 +5675,7 @@ var index_default = defineToolPlugin({
5573
5675
  )
5574
5676
  }),
5575
5677
  async execute(params, config) {
5678
+ maybeRegisterOperator(config);
5576
5679
  const gate = activationGate(config.engineApiKey);
5577
5680
  if (gate) {
5578
5681
  const halted = protectionWarning(decisionsUsed(), config.engineApiKey) ?? gate.message;
@@ -5607,7 +5710,7 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
5607
5710
  async execute(_params, config) {
5608
5711
  const c = boot();
5609
5712
  const mandate = await c.getMandate(subjectOf(config));
5610
- const hasKey = (config.engineApiKey ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim().length > 0;
5713
+ const hasKey = operatorEngineKey(config).length > 0;
5611
5714
  const claim = hasKey ? null : claimUrl();
5612
5715
  if (claim) {
5613
5716
  return {
@@ -5647,7 +5750,7 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
5647
5750
  async execute(params, config) {
5648
5751
  const records = await boot().history(5e3);
5649
5752
  const summary = summarize(records, params.days ?? 7);
5650
- const hasKey = (config.engineApiKey ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim().length > 0;
5753
+ const hasKey = operatorEngineKey(config).length > 0;
5651
5754
  const claim = hasKey ? null : claimUrl();
5652
5755
  return {
5653
5756
  summary,
@@ -5726,8 +5829,8 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
5726
5829
  async execute(params, config) {
5727
5830
  boot();
5728
5831
  const engineUrl = config.engineUrl ?? process.env.FIDACY_ENGINE_URL ?? "https://api.fidacy.com";
5729
- const apiKey = (config.engineApiKey ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim();
5730
- if (!apiKey) {
5832
+ const engineKey = operatorEngineKey(config);
5833
+ if (!engineKey) {
5731
5834
  throw new Error(
5732
5835
  `assess_action needs an engine key \u2014 ${noKeyCta()}. (Or set plugins.entries.fidacy.config.engineApiKey.)`
5733
5836
  );
@@ -5735,7 +5838,7 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
5735
5838
  try {
5736
5839
  const r = await assessAction(
5737
5840
  { kind: params.kind, mandate: params.mandate, mandateType: params.mandateType, idempotencyKey: params.idempotencyKey, spendingMandate: params.spendingMandate, a2a: params.a2a },
5738
- { engineUrl, apiKey }
5841
+ { engineUrl, apiKey: engineKey }
5739
5842
  );
5740
5843
  clearHostedLapsed();
5741
5844
  return { summary: `${r.decision} (score ${r.score}) signed by ${r.signingKeyId}`, ...r };
@@ -5745,8 +5848,7 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
5745
5848
  markHostedLapsed();
5746
5849
  throw new Error(hostedWallCta("verdict", e.billing));
5747
5850
  }
5748
- const reasons = e.rejection_reasons?.length ? " (" + e.rejection_reasons.map((x) => x.key).join(",") + ")" : "";
5749
- throw new Error(`ASSESS ${e.status}: ${e.type}${reasons}`);
5851
+ throw new Error(explainEngineError("verdict", e.status, e.type, e.rejection_reasons));
5750
5852
  }
5751
5853
  throw new Error("ASSESS failed: unexpected error");
5752
5854
  }
@@ -5769,8 +5871,8 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
5769
5871
  async execute(params, config) {
5770
5872
  boot();
5771
5873
  const engineUrl = config.engineUrl ?? process.env.FIDACY_ENGINE_URL ?? "https://api.fidacy.com";
5772
- const apiKey = (config.engineApiKey ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim();
5773
- if (!apiKey) {
5874
+ const engineKey = operatorEngineKey(config);
5875
+ if (!engineKey) {
5774
5876
  throw new Error(
5775
5877
  `anchor_artifact needs an engine key \u2014 ${noKeyCta()}. (Or set plugins.entries.fidacy.config.engineApiKey.)`
5776
5878
  );
@@ -5789,7 +5891,7 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
5789
5891
  try {
5790
5892
  const r = await anchorArtifact(
5791
5893
  { sha256: hash, kind: params.kind ?? "document", ...params.label ? { label: params.label } : {}, ...params.subject ? { subject: params.subject } : {} },
5792
- { engineUrl, apiKey }
5894
+ { engineUrl, apiKey: engineKey }
5793
5895
  );
5794
5896
  return {
5795
5897
  summary: `ANCHORED ${r.kind} \xB7 sha256 ${hash.slice(0, 16)}\u2026 \xB7 audit seq ${r.audit.seq} \xB7 Bitcoin checkpoint: ${r.anchor.status}. The file never left this machine.`,
@@ -5801,7 +5903,7 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
5801
5903
  markHostedLapsed();
5802
5904
  throw new Error(hostedWallCta("anchor", e.billing));
5803
5905
  }
5804
- throw new Error(`ANCHOR ${e.status}: ${e.type}`);
5906
+ throw new Error(explainEngineError("anchor", e.status, e.type));
5805
5907
  }
5806
5908
  throw new Error("ANCHOR failed: unexpected error");
5807
5909
  }
@@ -5819,8 +5921,8 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
5819
5921
  async execute(params, config) {
5820
5922
  boot();
5821
5923
  const engineUrl = config.engineUrl ?? process.env.FIDACY_ENGINE_URL ?? "https://api.fidacy.com";
5822
- const apiKey = (config.engineApiKey ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim();
5823
- if (!apiKey) {
5924
+ const engineKey = operatorEngineKey(config);
5925
+ if (!engineKey) {
5824
5926
  throw new Error(
5825
5927
  `check_artifact needs an engine key \u2014 ${noKeyCta()}. (Or set plugins.entries.fidacy.config.engineApiKey.)`
5826
5928
  );
@@ -5837,7 +5939,7 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
5837
5939
  }
5838
5940
  }
5839
5941
  try {
5840
- const r = await findArtifacts(hash, { engineUrl, apiKey });
5942
+ const r = await findArtifacts(hash, { engineUrl, apiKey: engineKey });
5841
5943
  if (!r.artifacts.length) {
5842
5944
  return {
5843
5945
  summary: `NOT FOUND \xB7 sha256 ${hash.slice(0, 16)}\u2026 has no anchored record in this account. If you expected a match, the file changed since anchoring.`,
@@ -5852,7 +5954,7 @@ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}
5852
5954
  ...r
5853
5955
  };
5854
5956
  } catch (e) {
5855
- if (e instanceof AssessError) throw new Error(`CHECK ${e.status}: ${e.type}`);
5957
+ if (e instanceof AssessError) throw new Error(explainEngineError("check", e.status, e.type));
5856
5958
  throw new Error("CHECK failed: unexpected error");
5857
5959
  }
5858
5960
  }
@@ -3,7 +3,7 @@
3
3
  "name": "Fidacy AI Agent 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.3.0",
6
+ "version": "0.4.1",
7
7
  "contracts": {
8
8
  "tools": [
9
9
  "request_payment",
@@ -23,6 +23,10 @@
23
23
  "type": "object",
24
24
  "additionalProperties": false,
25
25
  "properties": {
26
+ "operatorEmail": {
27
+ "type": "string",
28
+ "description": "Your email (optional). Ties this agent's protection history to you so it can move into a free account, and lets Fidacy reach you about this install. Never shared; remove any time."
29
+ },
26
30
  "engineApiKey": {
27
31
  "type": "string",
28
32
  "description": "Fidacy engine API key (fky_live_/fky_test_) enabling signed verdicts via assess_action. Falls back to FIDACY_ENGINE_API_KEY."
@@ -38,6 +42,11 @@
38
42
  }
39
43
  },
40
44
  "uiHints": {
45
+ "operatorEmail": {
46
+ "label": "Your email (optional)",
47
+ "placeholder": "you@company.com",
48
+ "help": "Ties this agent's protection history to you so it can move into a free account, and lets Fidacy reach you about this install. Optional — the firewall works without it."
49
+ },
41
50
  "engineApiKey": {
42
51
  "label": "Engine API key",
43
52
  "placeholder": "fky_live_...",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fidacy/openclaw-plugin",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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
  "author": "Fidacy (ZeepCode Group Technology LLC) <hello@fidacy.com> (https://fidacy.com)",
@@ -47,20 +47,21 @@
47
47
  "npmSpec": "@fidacy/openclaw-plugin"
48
48
  }
49
49
  },
50
+ "scripts": {
51
+ "build": "node scripts/bundle.mjs",
52
+ "typecheck": "tsc --noEmit",
53
+ "test": "npm run build && node --test test/*.test.mjs"
54
+ },
50
55
  "engines": {
51
56
  "node": ">=20"
52
57
  },
53
58
  "devDependencies": {
59
+ "@fidacy/firewall": "workspace:*",
60
+ "@fidacy/mcp": "workspace:*",
54
61
  "@types/node": "^22.10.0",
55
62
  "esbuild": "^0.24.0",
56
63
  "openclaw": "2026.6.11",
57
64
  "typebox": "1.1.39",
58
- "typescript": "^5.6.3",
59
- "@fidacy/firewall": "0.1.1",
60
- "@fidacy/mcp": "0.4.2"
61
- },
62
- "scripts": {
63
- "build": "node scripts/bundle.mjs",
64
- "typecheck": "tsc --noEmit"
65
+ "typescript": "^5.6.3"
65
66
  }
66
- }
67
+ }