@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 +35 -0
- package/dist/index.js +132 -30
- package/openclaw.plugin.json +10 -1
- package/package.json +10 -9
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.
|
|
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.
|
|
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
|
|
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" ? `
|
|
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")
|
|
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
|
|
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
|
|
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
|
|
5730
|
-
if (!
|
|
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
|
-
|
|
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
|
|
5773
|
-
if (!
|
|
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(
|
|
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
|
|
5823
|
-
if (!
|
|
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(
|
|
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
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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
|
+
"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
|
+
}
|