@fidacy/openclaw-plugin 0.5.10 → 0.6.0

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
@@ -4232,877 +4232,951 @@ import { createHash } from "node:crypto";
4232
4232
  import { appendFileSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
4233
4233
  import { join as join2 } from "node:path";
4234
4234
 
4235
- // ../mcp/src/config.ts
4236
- import { randomUUID } from "node:crypto";
4237
- import { homedir } from "node:os";
4238
- import { join } from "node:path";
4239
- import {
4240
- existsSync,
4241
- mkdirSync,
4242
- readFileSync,
4243
- writeFileSync
4244
- } from "node:fs";
4245
- function hasEngineKey(keyOverride) {
4246
- return Boolean((keyOverride ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim());
4247
- }
4248
- function configDir() {
4249
- return process.env.FIDACY_CONFIG_DIR ?? join(homedir(), ".fidacy");
4250
- }
4251
- function configPath() {
4252
- return join(configDir(), "config.json");
4253
- }
4254
- function auditLogPath() {
4255
- if (process.env.FIDACY_AUDIT_PATH) return process.env.FIDACY_AUDIT_PATH;
4256
- const dir = join(configDir(), "audit");
4257
- try {
4258
- mkdirSync(dir, { recursive: true, mode: 448 });
4259
- } catch {
4260
- }
4261
- return join(dir, "audit.log");
4235
+ // ../firewall/dist/util.js
4236
+ function stableStringify(obj) {
4237
+ if (obj === null || typeof obj !== "object")
4238
+ return JSON.stringify(obj);
4239
+ if (Array.isArray(obj))
4240
+ return "[" + obj.map(stableStringify).join(",") + "]";
4241
+ const keys = Object.keys(obj).sort();
4242
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
4262
4243
  }
4263
- function readConfig() {
4264
- const p = configPath();
4265
- if (!existsSync(p)) return null;
4266
- try {
4267
- const raw = JSON.parse(readFileSync(p, "utf8"));
4268
- if (!raw || typeof raw.anon_id !== "string") return null;
4269
- return {
4270
- anon_id: raw.anon_id,
4271
- tier: raw.tier === "paid" ? "paid" : "free",
4272
- api_key: typeof raw.api_key === "string" ? raw.api_key : null,
4273
- mandate: raw.mandate,
4274
- // Install-state passthrough: dropping these on a read→write cycle would
4275
- // reset every once-per-install nudge into an every-time nag.
4276
- created_at: typeof raw.created_at === "string" ? raw.created_at : void 0,
4277
- nudges: raw.nudges && typeof raw.nudges === "object" ? raw.nudges : void 0,
4278
- decisions_count: typeof raw.decisions_count === "number" ? raw.decisions_count : void 0,
4279
- hosted_lapsed_at: typeof raw.hosted_lapsed_at === "string" ? raw.hosted_lapsed_at : void 0,
4280
- operator_email: typeof raw.operator_email === "string" ? raw.operator_email : void 0,
4281
- registered_email: typeof raw.registered_email === "string" ? raw.registered_email : void 0
4282
- };
4283
- } catch {
4284
- return null;
4244
+
4245
+ // ../firewall/dist/signing.js
4246
+ import crypto from "node:crypto";
4247
+ function loadOrGenerateKeyPair() {
4248
+ const b64 = process.env.FIDACY_SIGNING_KEY_B64;
4249
+ if (b64) {
4250
+ const pem = Buffer.from(b64, "base64").toString("utf8");
4251
+ const privateKey2 = crypto.createPrivateKey(pem);
4252
+ const publicKey2 = crypto.createPublicKey(privateKey2);
4253
+ return { privateKey: privateKey2, publicKey: publicKey2, ephemeral: false };
4285
4254
  }
4255
+ const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
4256
+ return { privateKey, publicKey, ephemeral: true };
4286
4257
  }
4287
- function writeConfig(cfg) {
4288
- const dir = configDir();
4289
- mkdirSync(dir, { recursive: true, mode: 448 });
4290
- writeFileSync(configPath(), JSON.stringify(cfg, null, 2), { mode: 384 });
4258
+ function publicKeyPem(publicKey) {
4259
+ return publicKey.export({ type: "spki", format: "pem" }).toString();
4291
4260
  }
4292
- function ensureState() {
4293
- const existing = readConfig();
4294
- if (existing) return { config: existing, firstRun: false };
4295
- const config = {
4296
- anon_id: randomUUID(),
4297
- tier: "free",
4298
- api_key: null,
4299
- mandate: { payees: [], categories: ["*"], currency: "USD", perTxMax: 2500, maxTotal: 1e4 },
4300
- created_at: (/* @__PURE__ */ new Date()).toISOString()
4301
- };
4302
- try {
4303
- writeConfig(config);
4304
- } catch {
4305
- }
4306
- return { config, firstRun: true };
4261
+ function sign(privateKey, message) {
4262
+ return crypto.sign(null, Buffer.from(message, "utf8"), privateKey).toString("base64url");
4307
4263
  }
4308
- var DEMO_PAYEE = "fidacy:demo";
4309
- function resolveMandateRules(cfg) {
4310
- const m = cfg?.mandate ?? {};
4311
- const envList = (v) => v === void 0 ? void 0 : v.split(",").map((s) => s.trim()).filter(Boolean);
4312
- const envNum = (name, v) => {
4313
- if (v === void 0 || v.trim() === "") return void 0;
4314
- const n = Number(v);
4315
- if (!Number.isFinite(n) || n <= 0) {
4316
- console.error(
4317
- `[fidacy] ${name}="${v}" is not a positive number, so it cannot be enforced as a cap. Ignoring it and using the safe default. Write digits only, with no thousands separator, currency symbol or unit (e.g. ${name}=2500).`
4318
- );
4319
- return void 0;
4320
- }
4321
- return n;
4322
- };
4323
- const fileNum = (name, v) => {
4324
- if (v === void 0 || v === null) return void 0;
4325
- const n = typeof v === "number" ? v : typeof v === "string" ? Number(v.trim()) : NaN;
4326
- if (!Number.isFinite(n) || n <= 0) {
4327
- console.error(
4328
- `[fidacy] mandate.${name} in config.json is ${JSON.stringify(v)}, which is not a positive number, so it cannot be enforced as a cap. Ignoring it and using the safe default. Write it as a bare JSON number, with no quotes, thousands separator, currency symbol or unit (e.g. "${name}": 2500).`
4329
- );
4330
- return void 0;
4331
- }
4332
- return n;
4333
- };
4334
- const payees = envList(process.env.FIDACY_ALLOW_PAYEES) ?? m.payees ?? [];
4335
- return {
4336
- payees: payees.includes(DEMO_PAYEE) ? payees : [...payees, DEMO_PAYEE],
4337
- categories: envList(process.env.FIDACY_ALLOW_CATEGORIES) ?? m.categories ?? ["*"],
4338
- currency: process.env.FIDACY_CURRENCY ?? m.currency ?? "USD",
4339
- perTxMax: envNum("FIDACY_PER_TX_MAX", process.env.FIDACY_PER_TX_MAX) ?? fileNum("perTxMax", m.perTxMax) ?? 2500,
4340
- maxTotal: envNum("FIDACY_MAX_TOTAL", process.env.FIDACY_MAX_TOTAL) ?? fileNum("maxTotal", m.maxTotal) ?? 1e4
4341
- };
4264
+ function sha256(input) {
4265
+ return crypto.createHash("sha256").update(input).digest("hex");
4342
4266
  }
4343
4267
 
4344
- // ../mcp/src/evidence.ts
4345
- var GENESIS = "fidacy.observer.v1";
4346
- var sha256 = (s) => createHash("sha256").update(s).digest("hex");
4347
- function canonical(obj) {
4348
- const keys = Object.keys(obj).sort();
4349
- return "{" + keys.map((k) => `${JSON.stringify(k)}:${JSON.stringify(obj[k])}`).join(",") + "}";
4350
- }
4351
- function chainStep(prevHead, a) {
4352
- return sha256(`${prevHead}|${sha256(canonical({ category: a.category, i: a.i, tool: a.tool, ts: a.ts }))}`);
4353
- }
4354
- function genesisHead() {
4355
- return sha256(GENESIS);
4268
+ // ../firewall/dist/audit-store.js
4269
+ import fs from "node:fs";
4270
+
4271
+ // ../firewall/dist/core.js
4272
+ import { randomUUID } from "node:crypto";
4273
+
4274
+ // ../firewall/dist/evaluate.js
4275
+ function fold(s) {
4276
+ return s.toLowerCase().replace(/[4@]/g, "a").replace(/[0]/g, "o").replace(/[1|!]/g, "l").replace(/[3]/g, "e").replace(/[5$]/g, "s").replace(/[7]/g, "t").replace(/[^a-z0-9]/g, "");
4356
4277
  }
4357
- function fidacyDir() {
4358
- return join2(configDir(), "sessions");
4278
+ function lev(a, b, max) {
4279
+ if (Math.abs(a.length - b.length) > max)
4280
+ return max + 1;
4281
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
4282
+ for (let i = 1; i <= a.length; i++) {
4283
+ const cur = [i];
4284
+ let rowMin = i;
4285
+ for (let j = 1; j <= b.length; j++) {
4286
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
4287
+ const v = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
4288
+ cur[j] = v;
4289
+ if (v < rowMin)
4290
+ rowMin = v;
4291
+ }
4292
+ if (rowMin > max)
4293
+ return max + 1;
4294
+ prev = cur;
4295
+ }
4296
+ return prev[b.length];
4359
4297
  }
4360
- function writeSessionLog(sessionId, actions, head, meta) {
4361
- try {
4362
- const dir = fidacyDir();
4363
- mkdirSync2(dir, { recursive: true, mode: 448 });
4364
- const safe = sessionId.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80) || "session";
4365
- const path = join2(dir, `${safe}.json`);
4366
- const body = {
4367
- v: "fidacy.observer.v1",
4368
- recipe: 'h_0 = sha256("fidacy.observer.v1"); h_i = sha256(h_prev + "|" + sha256(canonical(action)))',
4369
- sessionId,
4370
- head,
4371
- ...meta,
4372
- actions
4373
- };
4374
- writeFileSync2(path, JSON.stringify(body, null, 2), { mode: 384 });
4375
- return path;
4376
- } catch {
4298
+ function lookalikePayee(payee, allowed) {
4299
+ const exact = new Set(allowed);
4300
+ if (exact.has(payee) || allowed.includes("*"))
4301
+ return null;
4302
+ const pf = fold(payee);
4303
+ if (pf.length < 4)
4377
4304
  return null;
4305
+ for (const a of allowed) {
4306
+ if (a === "*")
4307
+ continue;
4308
+ const af = fold(a);
4309
+ if (af.length < 4)
4310
+ continue;
4311
+ if (pf === af)
4312
+ return a;
4313
+ const d = lev(pf, af, 2);
4314
+ if (d > 0 && d <= 2)
4315
+ return a;
4378
4316
  }
4317
+ return null;
4379
4318
  }
4380
- function queuePending(head, sessionId, at, total = 0, byCategory = {}) {
4381
- try {
4382
- const dir = fidacyDir();
4383
- mkdirSync2(dir, { recursive: true, mode: 448 });
4384
- appendFileSync(join2(dir, "pending-anchors.jsonl"), JSON.stringify({ head, sessionId, at, total, byCategory }) + "\n", {
4385
- mode: 384
4386
- });
4387
- } catch {
4319
+ function validateMandateCaps(mandate) {
4320
+ const bad = (v) => typeof v !== "number" || !Number.isFinite(v) || v <= 0;
4321
+ if (!mandate.allow || typeof mandate.allow !== "object")
4322
+ return "invalid_mandate:missing_allow";
4323
+ if (bad(mandate.allow.perTxMax))
4324
+ return `invalid_mandate_cap:perTxMax=${String(mandate.allow.perTxMax)}`;
4325
+ if (bad(mandate.allow.maxTotal))
4326
+ return `invalid_mandate_cap:maxTotal=${String(mandate.allow.maxTotal)}`;
4327
+ if (!mandate.window || typeof mandate.window !== "object")
4328
+ return "invalid_mandate:missing_window";
4329
+ for (const campo of ["notBefore", "notAfter"]) {
4330
+ const v = mandate.window[campo];
4331
+ if (typeof v !== "string" || Number.isNaN(Date.parse(v)))
4332
+ return `invalid_mandate_window:${campo}=${String(v)}`;
4388
4333
  }
4334
+ return null;
4389
4335
  }
4390
- function takePending(limit = 50) {
4391
- const dir = fidacyDir();
4392
- const path = join2(dir, "pending-anchors.jsonl");
4393
- const taken = join2(dir, "pending-anchors.taking");
4394
- try {
4395
- renameSync(path, taken);
4396
- } catch {
4397
- return [];
4336
+ function evaluate(mandate, req, spentSoFar) {
4337
+ const now = Date.now();
4338
+ if (mandate.revoked)
4339
+ return "mandate_revoked";
4340
+ const badCap = validateMandateCaps(mandate);
4341
+ if (badCap)
4342
+ return badCap;
4343
+ if (now < Date.parse(mandate.window.notBefore))
4344
+ return "before_mandate_window";
4345
+ if (now > Date.parse(mandate.window.notAfter))
4346
+ return "after_mandate_window";
4347
+ if (req.currency.toUpperCase() !== mandate.allow.currency.toUpperCase())
4348
+ return `currency_not_allowed:${req.currency}`;
4349
+ if (req.amount <= 0)
4350
+ return "non_positive_amount";
4351
+ if (req.amount > mandate.allow.perTxMax)
4352
+ return `per_tx_cap_exceeded:${req.amount}>${mandate.allow.perTxMax}`;
4353
+ if (spentSoFar + req.amount > mandate.allow.maxTotal)
4354
+ return `total_cap_exceeded:${spentSoFar + req.amount}>${mandate.allow.maxTotal}`;
4355
+ const payeeOk = mandate.allow.payees.includes("*") || mandate.allow.payees.includes(req.payee);
4356
+ if (!payeeOk) {
4357
+ const impersonated = lookalikePayee(req.payee, mandate.allow.payees);
4358
+ if (impersonated)
4359
+ return `payee_lookalike:${req.payee}~${impersonated}`;
4360
+ return `payee_not_in_allowlist:${req.payee}`;
4398
4361
  }
4362
+ const catOk = mandate.allow.categories.includes("*") || mandate.allow.categories.includes(req.category);
4363
+ if (!catOk)
4364
+ return `category_not_allowed:${req.category}`;
4365
+ return null;
4366
+ }
4367
+
4368
+ // ../firewall/dist/core.js
4369
+ function canonInvoice(ref) {
4370
+ if (typeof ref !== "string")
4371
+ return "";
4372
+ return ref.normalize("NFC").replace(/[\s\u200B\u200C\u200D\uFEFF]+/g, "").toLowerCase();
4373
+ }
4374
+ function validateRequest(req) {
4375
+ if (!req || typeof req !== "object")
4376
+ return "invalid_request";
4377
+ if (typeof req.amount !== "number" || !Number.isFinite(req.amount) || req.amount <= 0)
4378
+ return "invalid_amount";
4379
+ if (typeof req.payee !== "string" || req.payee.length === 0)
4380
+ return "invalid_payee";
4381
+ if (typeof req.currency !== "string" || req.currency.length === 0)
4382
+ return "invalid_currency";
4383
+ return null;
4384
+ }
4385
+ function requireHttpsBase(raw) {
4386
+ const trimmed = String(raw ?? "").replace(/\/+$/, "");
4387
+ let u;
4399
4388
  try {
4400
- const lines = readFileSync2(taken, "utf8").split("\n").filter(Boolean);
4401
- const out = [];
4402
- for (const line of lines.slice(-limit)) {
4403
- try {
4404
- const p = JSON.parse(line);
4405
- if (typeof p.head === "string" && /^[0-9a-f]{64}$/.test(p.head)) out.push(p);
4406
- } catch {
4407
- }
4408
- }
4409
- return out;
4389
+ u = new URL(trimmed);
4410
4390
  } catch {
4411
- return [];
4412
- } finally {
4413
- try {
4414
- writeFileSync2(taken, "");
4415
- } catch {
4416
- }
4391
+ throw new Error("FIDACY_API_URL is not a valid URL");
4417
4392
  }
4418
- }
4419
- function verifyUrl(head) {
4420
- return `https://fidacy.com/verify?sha256=${head}`;
4421
- }
4422
- var CODE = {
4423
- money: "m",
4424
- shell: "s",
4425
- file: "f",
4426
- network: "net",
4427
- message: "msg",
4428
- other: "o"
4429
- };
4430
- function sessionLabel(sessionId, total, byCategory) {
4431
- const id = sessionId.replace(/[^A-Za-z0-9-]/g, "").slice(0, 8) || "unknown";
4432
- const parts = [`session:${id}`, `n=${total}`];
4433
- for (const [cat, code] of Object.entries(CODE)) {
4434
- const n = byCategory[cat] ?? 0;
4435
- if (n > 0) parts.push(`${code}=${n}`);
4393
+ const localHttp = u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]");
4394
+ if (u.protocol !== "https:" && !localHttp) {
4395
+ throw new Error("FIDACY_API_URL must be https:// (the API key is sent to it)");
4436
4396
  }
4437
- return parts.join(" ").slice(0, 120);
4438
- }
4439
- function sessionSummary(opts) {
4440
- const by = {};
4441
- for (const [cat, n] of Object.entries(opts.byCategory)) if (n > 0) by[cat] = n;
4442
- const s = {
4443
- v: 1,
4444
- // Mesmo saneamento do rótulo: o host vem de código nosso, mas o campo é
4445
- // limitado na origem para que uma string estranha vire um valor curto e
4446
- // legível em vez de um 400 que perderia o registro inteiro da sessão.
4447
- host: opts.host.replace(/[^a-z0-9-]/gi, "").slice(0, 40) || "unknown",
4448
- session_id: opts.sessionId.replace(/[^A-Za-z0-9-]/g, "").slice(0, 64) || "unknown",
4449
- total: opts.total
4450
- };
4451
- if (Object.keys(by).length) s.by_category = by;
4452
- if (opts.truncated) s.truncated = true;
4453
- if (opts.startedAt) s.started_at = opts.startedAt;
4454
- if (opts.endedAt) s.ended_at = opts.endedAt;
4455
- return s;
4397
+ return trimmed;
4456
4398
  }
4457
-
4458
- // ../mcp/src/observer.ts
4459
- var EXACT = {
4460
- // Fidacy's own money tools, so the report counts what we ourselves gated.
4461
- request_payment: "money",
4462
- assess_action: "money",
4463
- // The common OpenClaw / agent-host tool vocabulary.
4464
- bash: "shell",
4465
- shell: "shell",
4466
- exec: "shell",
4467
- run_command: "shell",
4468
- code_mode_exec: "shell",
4469
- read: "file",
4470
- read_file: "file",
4471
- write: "file",
4472
- write_file: "file",
4473
- edit: "file",
4474
- edit_file: "file",
4475
- apply_patch: "file",
4476
- glob: "file",
4477
- grep: "file",
4478
- ls: "file",
4479
- fetch: "network",
4480
- web_fetch: "network",
4481
- web_search: "network",
4482
- http_request: "network",
4483
- browser: "network",
4484
- send_message: "message",
4485
- reply: "message",
4486
- send_email: "message"
4399
+ var DevFidacyCore = class {
4400
+ priv;
4401
+ pubPem;
4402
+ mandate;
4403
+ store;
4404
+ spent = 0;
4405
+ claimedInvoices = /* @__PURE__ */ new Set();
4406
+ onDecision;
4407
+ constructor(opts) {
4408
+ const kp = loadOrGenerateKeyPair();
4409
+ this.priv = kp.privateKey;
4410
+ this.pubPem = publicKeyPem(kp.publicKey);
4411
+ if (kp.ephemeral)
4412
+ console.error("[fidacy] local firewall active, deny-by-default. Grants are signed with a per-session key (fine for local use); set FIDACY_SIGNING_KEY_B64 for a stable key, or FIDACY_MODE=http to use the hosted core.");
4413
+ this.mandate = opts.mandate;
4414
+ this.store = new FileAuditStore(opts.auditLogPath ?? "./fidacy-audit.log");
4415
+ this.onDecision = opts.onDecision;
4416
+ this.rehydrate();
4417
+ }
4418
+ /**
4419
+ * Rebuild the in-memory decision state from the persisted audit log, so a process
4420
+ * restart cannot re-open a claimed invoice (BEC dedup) or reset the maxTotal
4421
+ * counter. One O(records) pass at boot over the already-loaded chain:
4422
+ * - claimedInvoices: every ALLOW that carried an invoiceRef, regardless of age —
4423
+ * a paid invoice stays paid.
4424
+ * - spent: ALLOW amounts whose timestamp falls inside the CURRENT mandate window
4425
+ * and whose currency matches the mandate (the cap is defined per window/currency).
4426
+ * Records from older versions lack the rehydration fields and are skipped.
4427
+ */
4428
+ rehydrate() {
4429
+ const notBefore = Date.parse(this.mandate.window.notBefore);
4430
+ const notAfter = Date.parse(this.mandate.window.notAfter);
4431
+ for (const r of this.store.records()) {
4432
+ if (r.status !== "ALLOW")
4433
+ continue;
4434
+ if (r.invoiceRef)
4435
+ this.claimedInvoices.add(`${r.subject}|${r.invoiceRef}`);
4436
+ if (typeof r.amount !== "number" || !Number.isFinite(r.amount))
4437
+ continue;
4438
+ if (r.currency !== this.mandate.allow.currency)
4439
+ continue;
4440
+ const t = Date.parse(r.ts);
4441
+ if (!Number.isFinite(t) || t < notBefore || t > notAfter)
4442
+ continue;
4443
+ this.spent += r.amount;
4444
+ }
4445
+ }
4446
+ /**
4447
+ * Swap the active mandate (shell config hot-reload: edit config.json, the next
4448
+ * call picks it up, no host restart). Decision state is REBUILT from the audit
4449
+ * log under the new window/currency, so a reload can never reset the BEC
4450
+ * invoice dedup or undercount spend: a paid invoice stays paid, spent is
4451
+ * recomputed.
4452
+ */
4453
+ setMandate(m) {
4454
+ this.mandate = m;
4455
+ this.spent = 0;
4456
+ this.claimedInvoices.clear();
4457
+ this.rehydrate();
4458
+ }
4459
+ async getMandate() {
4460
+ return this.mandate;
4461
+ }
4462
+ async decide(req, subject) {
4463
+ const decisionId = randomUUID();
4464
+ const ts2 = (/* @__PURE__ */ new Date()).toISOString();
4465
+ const invalid = validateRequest(req);
4466
+ if (invalid) {
4467
+ const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: invalid, ts: ts2 };
4468
+ this.store.append(decision2);
4469
+ this.onDecision?.(decision2);
4470
+ return decision2;
4471
+ }
4472
+ const violated = evaluate(this.mandate, req, this.spent);
4473
+ if (violated) {
4474
+ const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: violated, ts: ts2 };
4475
+ this.store.append(decision2);
4476
+ this.onDecision?.(decision2);
4477
+ return decision2;
4478
+ }
4479
+ const invoice = canonInvoice(req.invoiceRef);
4480
+ if (invoice) {
4481
+ const k = `${subject}|${invoice}`;
4482
+ if (this.claimedInvoices.has(k)) {
4483
+ const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: `duplicate_invoice:${invoice}`, ts: ts2 };
4484
+ this.store.append(decision2);
4485
+ this.onDecision?.(decision2);
4486
+ return decision2;
4487
+ }
4488
+ this.claimedInvoices.add(k);
4489
+ }
4490
+ const grantPayload = { decisionId, subject, payee: req.payee, amount: req.amount, currency: req.currency, exp: Date.now() + 12e4, ...req.invoiceRef ? { invoiceRef: req.invoiceRef } : {} };
4491
+ const grantBody = Buffer.from(stableStringify(grantPayload), "utf8").toString("base64url");
4492
+ const grant = `${grantBody}.${sign(this.priv, grantBody)}`;
4493
+ const decision = { decisionId, status: "ALLOW", subject, mandateId: this.mandate.id, request: req, grant, ts: ts2 };
4494
+ this.spent += req.amount;
4495
+ this.store.append(decision);
4496
+ this.onDecision?.(decision);
4497
+ return decision;
4498
+ }
4499
+ async getProof(decisionId) {
4500
+ const record3 = this.store.find(decisionId);
4501
+ if (!record3)
4502
+ return null;
4503
+ return { record: record3, chainIntact: this.store.intact(), verifiedAgainstPublicKey: this.pubPem };
4504
+ }
4505
+ async history(limit = 50) {
4506
+ const all = this.store.records();
4507
+ return limit > 0 && all.length > limit ? all.slice(-limit) : all;
4508
+ }
4509
+ publicKey() {
4510
+ return this.pubPem;
4511
+ }
4487
4512
  };
4488
- var FUZZY = [
4489
- ["payment", "money"],
4490
- ["invoice", "money"],
4491
- ["transfer", "money"],
4492
- ["charge", "money"],
4493
- ["file", "file"],
4494
- ["patch", "file"],
4495
- ["write", "file"],
4496
- ["read", "file"],
4497
- ["fetch", "network"],
4498
- ["http", "network"],
4499
- ["browser", "network"],
4500
- ["search", "network"],
4501
- ["exec", "shell"],
4502
- ["command", "shell"],
4503
- ["shell", "shell"],
4504
- ["mail", "message"],
4505
- ["message", "message"],
4506
- ["send", "message"]
4507
- ];
4508
- function classify(toolName, derivedPaths) {
4509
- const name = toolName.toLowerCase();
4510
- const exact = EXACT[name];
4511
- if (exact) return exact;
4512
- if (derivedPaths && derivedPaths.length > 0) return "file";
4513
- for (const [needle, category] of FUZZY) if (name.includes(needle)) return category;
4514
- return "other";
4515
- }
4516
- function fingerprint(toolName, derivedPaths) {
4517
- const target = derivedPaths && derivedPaths.length > 0 ? [...derivedPaths].sort().join("\0") : toolName;
4518
- return createHash2("sha256").update(`${toolName}\0${target}`).digest("hex").slice(0, 16);
4519
- }
4520
- var ACTION_LOG_CAP = 5e3;
4521
- function newLedger(sessionId, now) {
4522
- return {
4523
- sessionId,
4524
- startedAt: now,
4525
- byCategory: /* @__PURE__ */ new Map(),
4526
- total: 0,
4527
- toolsSeen: /* @__PURE__ */ new Set(),
4528
- head: genesisHead(),
4529
- actions: [],
4530
- truncated: false
4531
- };
4532
- }
4533
- function record(ledger, toolName, derivedPaths, failed = false, at = Date.now()) {
4534
- const category = classify(toolName, derivedPaths);
4535
- let tally = ledger.byCategory.get(category);
4536
- if (!tally) {
4537
- tally = { calls: 0, failures: 0, targets: /* @__PURE__ */ new Set() };
4538
- ledger.byCategory.set(category, tally);
4513
+ var HttpFidacyCore = class {
4514
+ apiKey;
4515
+ subjectPub;
4516
+ baseUrl;
4517
+ // Enforce https before any key-bearing request — a hostile FIDACY_API_URL would
4518
+ // otherwise exfiltrate the API key in the Authorization header.
4519
+ constructor(baseUrl, apiKey, subjectPub = "") {
4520
+ this.apiKey = apiKey;
4521
+ this.subjectPub = subjectPub;
4522
+ this.baseUrl = requireHttpsBase(baseUrl);
4523
+ }
4524
+ async call(path, body) {
4525
+ const res = await fetch(`${this.baseUrl}${path}`, {
4526
+ method: "POST",
4527
+ headers: { "content-type": "application/json", authorization: `Bearer ${this.apiKey}` },
4528
+ body: JSON.stringify(body)
4529
+ });
4530
+ if (!res.ok)
4531
+ throw new Error(`fidacy core ${path} -> ${res.status}`);
4532
+ return await res.json();
4539
4533
  }
4540
- tally.calls += 1;
4541
- if (failed) tally.failures += 1;
4542
- if (derivedPaths && derivedPaths.length > 0 && tally.targets.size < TARGET_CAP) {
4543
- tally.targets.add(fingerprint(toolName, derivedPaths));
4534
+ async getMandate(subject) {
4535
+ return this.call("/v1/mandate/get", { subject });
4544
4536
  }
4545
- ledger.total += 1;
4546
- if (ledger.toolsSeen.size < TOOLS_CAP) ledger.toolsSeen.add(toolName);
4547
- const action = {
4548
- i: ledger.total,
4549
- category,
4550
- tool: toolName,
4551
- ts: new Date(at).toISOString()
4552
- };
4553
- ledger.head = chainStep(ledger.head, action);
4554
- if (ledger.actions.length < ACTION_LOG_CAP) ledger.actions.push(action);
4555
- else ledger.truncated = true;
4556
- }
4557
- var TARGET_CAP = 500;
4558
- var TOOLS_CAP = 200;
4559
- var ORDER = ["money", "shell", "file", "network", "message", "other"];
4560
- var LABEL = {
4561
- money: "moved money",
4562
- shell: "ran commands",
4563
- file: "touched files",
4564
- network: "reached the network",
4565
- message: "sent messages",
4566
- other: "other"
4567
- };
4568
- function renderReport(ledger, now, activated, logPath) {
4569
- if (ledger.total < MIN_ACTIONS_FOR_REPORT) return null;
4570
- const minutes = Math.max(1, Math.round((now - ledger.startedAt) / 6e4));
4571
- const lines = [];
4572
- lines.push(
4573
- `[fidacy] SESSION REPORT \xB7 ${ledger.total} agent actions watched over ${minutes} min \xB7 nothing left this machine`
4574
- );
4575
- for (const category of ORDER) {
4576
- const tally = ledger.byCategory.get(category);
4577
- if (!tally) continue;
4578
- const capped = tally.targets.size >= TARGET_CAP ? "+" : "";
4579
- const parts = [`${tally.calls} ${tally.calls === 1 ? "call" : "calls"}`];
4580
- if (tally.targets.size > 0) parts.push(`${tally.targets.size}${capped} distinct`);
4581
- if (tally.failures > 0) parts.push(`${tally.failures} failed`);
4582
- lines.push(`[fidacy] ${LABEL[category].padEnd(20)} ${parts.join(" \xB7 ")}`);
4537
+ async decide(req, subject) {
4538
+ return this.call("/v1/decide", { req, subject });
4583
4539
  }
4584
- const money2 = ledger.byCategory.get("money");
4585
- if (!money2) {
4586
- lines.push(`[fidacy] no money-moving action was attempted \xB7 the firewall stayed armed and idle`);
4540
+ async getProof(decisionId) {
4541
+ return this.call("/v1/audit/proof", { decisionId });
4587
4542
  }
4588
- lines.push(`[fidacy] digest ${ledger.head}`);
4589
- if (logPath) lines.push(`[fidacy] log ${logPath}${ledger.truncated ? " (partial: action cap reached)" : ""}`);
4590
- lines.push(
4591
- activated ? `[fidacy] anchored to the audit chain \xB7 verify: ${verifyUrl(ledger.head)}` : `[fidacy] activate free to anchor this digest so nobody can rewrite it later: https://fidacy.com/claim`
4592
- );
4593
- return lines.join("\n");
4594
- }
4595
- var MIN_ACTIONS_FOR_REPORT = 5;
4596
- function installObserver(api, emit, isActivated, now = Date.now, anchor) {
4597
- if (typeof api?.on !== "function") return;
4598
- const ledgers = /* @__PURE__ */ new Map();
4599
- const LEDGER_CAP = 50;
4600
- const ledgerFor = (sessionId) => {
4601
- let ledger = ledgers.get(sessionId);
4602
- if (!ledger) {
4603
- if (ledgers.size >= LEDGER_CAP) ledgers.delete(ledgers.keys().next().value);
4604
- ledger = newLedger(sessionId, now());
4605
- ledgers.set(sessionId, ledger);
4606
- }
4607
- return ledger;
4608
- };
4609
- api.on(
4610
- "before_tool_call",
4611
- (event, ctx) => {
4543
+ async history(limit = 50) {
4544
+ const res = await this.call("/v1/audit/list", { limit });
4545
+ return res.records ?? [];
4546
+ }
4547
+ publicKey() {
4548
+ return this.subjectPub;
4549
+ }
4550
+ };
4551
+
4552
+ // ../firewall/dist/audit-store.js
4553
+ var FileAuditStore = class {
4554
+ path;
4555
+ chain = [];
4556
+ constructor(path) {
4557
+ this.path = path;
4558
+ this.load();
4559
+ }
4560
+ load() {
4561
+ if (!fs.existsSync(this.path))
4562
+ return;
4563
+ try {
4564
+ const raw = fs.readFileSync(this.path, "utf8");
4565
+ const lines = raw.split("\n");
4566
+ const parsed = [];
4567
+ let torn = false;
4568
+ for (let i = 0; i < lines.length; i++) {
4569
+ if (!lines[i].trim())
4570
+ continue;
4571
+ try {
4572
+ parsed.push(JSON.parse(lines[i]));
4573
+ } catch {
4574
+ if (lines.slice(i + 1).some((l) => l.trim()))
4575
+ throw new Error(`unparseable record at line ${i + 1}`);
4576
+ torn = true;
4577
+ break;
4578
+ }
4579
+ }
4580
+ this.chain = parsed;
4581
+ if (!this.intact())
4582
+ throw new Error("audit chain integrity broken");
4583
+ if (torn) {
4584
+ const clean = parsed.map((r) => JSON.stringify(r)).join("\n") + (parsed.length ? "\n" : "");
4585
+ fs.writeFileSync(this.path, clean);
4586
+ console.error(`[fidacy] audit log at ${this.path} had a torn trailing line (crash during append?); salvaged ${parsed.length} intact records and truncated the tail.`);
4587
+ }
4588
+ } catch (err) {
4589
+ this.chain = [];
4612
4590
  try {
4613
- const toolName = typeof event.toolName === "string" ? event.toolName : "unknown";
4614
- const paths = Array.isArray(event.derivedPaths) ? event.derivedPaths : void 0;
4615
- const sessionId = sessionIdOf(ctx);
4616
- record(ledgerFor(sessionId), toolName, paths, false);
4591
+ const quarantine = `${this.path}.corrupt-${Date.now()}`;
4592
+ fs.renameSync(this.path, quarantine);
4593
+ console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered (${err.message}); quarantined to ${quarantine}, starting a fresh chain.`);
4617
4594
  } catch {
4595
+ console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered and could not be quarantined; starting a fresh in-memory chain.`);
4618
4596
  }
4619
- },
4620
- // Lowest priority on purpose: anything that actually decides should run first,
4621
- // and we only want to count what the host was really about to do.
4622
- { priority: 1 }
4623
- );
4624
- api.on("session_end", async (event, ctx) => {
4625
- try {
4626
- const sessionId = sessionIdOf(ctx) || (typeof event.sessionId === "string" ? event.sessionId : "");
4627
- const ledger = ledgers.get(sessionId);
4628
- if (!ledger) return;
4629
- ledgers.delete(sessionId);
4630
- if (ledger.total < MIN_ACTIONS_FOR_REPORT) return;
4631
- const logPath = persist(ledger, now());
4632
- let anchored = false;
4633
- if (anchor) {
4634
- anchored = await anchor(ledger.head, ledger.sessionId, rollup(ledger)).catch(() => false);
4635
- }
4636
- if (!anchored) queuePending(ledger.head, ledger.sessionId, now(), ledger.total, rollup(ledger));
4637
- const report = renderReport(ledger, now(), anchored, logPath);
4638
- if (report) emit(report);
4639
- } catch {
4640
4597
  }
4641
- });
4642
- api.on("gateway_stop", () => drain());
4643
- process.on("exit", () => drain());
4644
- function drain() {
4645
- try {
4646
- for (const [id, ledger] of ledgers) {
4647
- ledgers.delete(id);
4648
- if (ledger.total < MIN_ACTIONS_FOR_REPORT) continue;
4649
- const logPath = persist(ledger, now());
4650
- queuePending(ledger.head, ledger.sessionId, now(), ledger.total, rollup(ledger));
4651
- const report = renderReport(ledger, now(), false, logPath);
4652
- if (report) emit(report);
4653
- }
4654
- } catch {
4598
+ }
4599
+ head() {
4600
+ return this.chain.length ? this.chain[this.chain.length - 1].hash : "GENESIS";
4601
+ }
4602
+ append(decision) {
4603
+ const prevHash = this.head();
4604
+ const seq = this.chain.length;
4605
+ const ts2 = decision.ts;
4606
+ const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
4607
+ const hash = sha256(`${prevHash}|${digest}|${seq}|${ts2}`);
4608
+ const record3 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts: ts2 };
4609
+ if (typeof decision.request?.purpose === "string" && decision.request.purpose.trim()) {
4610
+ record3.purpose = decision.request.purpose.slice(0, 500);
4611
+ }
4612
+ if (typeof decision.request?.payee === "string" && decision.request.payee.trim()) {
4613
+ record3.payee = decision.request.payee.slice(0, 200);
4614
+ }
4615
+ if (decision.status === "ALLOW") {
4616
+ const req = decision.request;
4617
+ if (typeof req?.amount === "number" && Number.isFinite(req.amount))
4618
+ record3.amount = req.amount;
4619
+ if (typeof req?.currency === "string")
4620
+ record3.currency = req.currency;
4621
+ const invoice = canonInvoice(req?.invoiceRef);
4622
+ if (invoice)
4623
+ record3.invoiceRef = invoice;
4624
+ } else {
4625
+ if (decision.violatedRule)
4626
+ record3.violatedRule = decision.violatedRule;
4627
+ const req = decision.request;
4628
+ if (typeof req?.amount === "number" && Number.isFinite(req.amount))
4629
+ record3.amount = req.amount;
4630
+ if (typeof req?.currency === "string")
4631
+ record3.currency = req.currency;
4655
4632
  }
4633
+ fs.appendFileSync(this.path, JSON.stringify(record3) + "\n");
4634
+ this.chain.push(record3);
4635
+ return record3;
4656
4636
  }
4657
- function rollup(ledger) {
4658
- const out = {};
4659
- for (const [cat, tally] of ledger.byCategory) out[cat] = tally.calls;
4660
- return out;
4637
+ find(decisionId) {
4638
+ return this.chain.find((r) => r.decisionId === decisionId);
4661
4639
  }
4662
- function persist(ledger, at) {
4663
- return writeSessionLog(ledger.sessionId, ledger.actions, ledger.head, {
4664
- startedAt: new Date(ledger.startedAt).toISOString(),
4665
- endedAt: new Date(at).toISOString(),
4666
- totalActions: ledger.total,
4667
- truncated: ledger.truncated,
4668
- byCategory: Object.fromEntries(
4669
- [...ledger.byCategory].map(([k, v]) => [k, { calls: v.calls, failures: v.failures, distinct: v.targets.size }])
4670
- )
4671
- });
4640
+ /** The full loaded chain, oldest first. Read-only; used to rehydrate core state at boot. */
4641
+ records() {
4642
+ return this.chain;
4672
4643
  }
4673
- }
4674
- function sessionIdOf(ctx) {
4675
- if (!ctx) return "default";
4676
- const id = ctx.sessionId ?? ctx.sessionKey;
4677
- return typeof id === "string" && id.length > 0 ? id : "default";
4678
- }
4679
-
4680
- // ../firewall/dist/util.js
4681
- function stableStringify(obj) {
4682
- if (obj === null || typeof obj !== "object")
4683
- return JSON.stringify(obj);
4684
- if (Array.isArray(obj))
4685
- return "[" + obj.map(stableStringify).join(",") + "]";
4686
- const keys = Object.keys(obj).sort();
4687
- return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
4688
- }
4689
-
4690
- // ../firewall/dist/signing.js
4691
- import crypto from "node:crypto";
4692
- function loadOrGenerateKeyPair() {
4693
- const b64 = process.env.FIDACY_SIGNING_KEY_B64;
4694
- if (b64) {
4695
- const pem = Buffer.from(b64, "base64").toString("utf8");
4696
- const privateKey2 = crypto.createPrivateKey(pem);
4697
- const publicKey2 = crypto.createPublicKey(privateKey2);
4698
- return { privateKey: privateKey2, publicKey: publicKey2, ephemeral: false };
4644
+ intact() {
4645
+ let prev = "GENESIS";
4646
+ for (const r of this.chain) {
4647
+ const expected = sha256(`${prev}|${r.digest}|${r.seq}|${r.ts}`);
4648
+ if (expected !== r.hash || r.prevHash !== prev)
4649
+ return false;
4650
+ prev = r.hash;
4651
+ }
4652
+ return true;
4699
4653
  }
4700
- const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
4701
- return { privateKey, publicKey, ephemeral: true };
4702
- }
4703
- function publicKeyPem(publicKey) {
4704
- return publicKey.export({ type: "spki", format: "pem" }).toString();
4705
- }
4706
- function sign(privateKey, message) {
4707
- return crypto.sign(null, Buffer.from(message, "utf8"), privateKey).toString("base64url");
4708
- }
4709
- function sha2562(input) {
4710
- return crypto.createHash("sha256").update(input).digest("hex");
4711
- }
4654
+ };
4712
4655
 
4713
- // ../firewall/dist/audit-store.js
4714
- import fs from "node:fs";
4656
+ // ../firewall/dist/destination.js
4657
+ function normalizeKeepingEdges(raw) {
4658
+ return String(raw ?? "").normalize("NFKC").toLowerCase().replace(/[​-‍⁠­]/g, "").replace(/(?<=[a-z0-9])[_\-.]+(?=[a-z0-9])/g, ".").replace(/\s+/g, " ");
4659
+ }
4660
+
4661
+ // ../firewall/dist/action-classes.js
4662
+ var ACTION_CLASSES = [
4663
+ {
4664
+ id: "credential.access",
4665
+ label: "Read secrets and credentials",
4666
+ incident: "Stops the agent from reading your API keys, tokens or .env files, which is step one of nearly every real breach.",
4667
+ patterns: ["secret", "credential", "apikey", "api_key", "token.read", ".env", "vault", "keychain", "password"],
4668
+ defaultMode: "log"
4669
+ },
4670
+ {
4671
+ id: "data.exfiltrate",
4672
+ label: "Send data outside the approved environment",
4673
+ incident: "Stops the agent from uploading or posting your data to somewhere nobody approved, which is what a prompt injection turns into a leak.",
4674
+ patterns: ["exfil", "upload", "s3.put", "bucket.write", "external.post", "webhook.send", "http.post", "share.create", "publish"],
4675
+ defaultMode: "log"
4676
+ },
4677
+ {
4678
+ id: "file.delete",
4679
+ label: "Delete files",
4680
+ incident: "Stops the agent from deleting files or emptying a folder, which is the mistake nobody can undo.",
4681
+ patterns: ["file.delete", "file.remove", "fs.unlink", "rm ", "rmdir", "delete.file", "bucket.delete", "object.delete"],
4682
+ defaultMode: "log"
4683
+ },
4684
+ {
4685
+ id: "db.write",
4686
+ label: "Write or drop in a production database",
4687
+ incident: "Stops the agent from running DELETE, DROP or a migration against your production data.",
4688
+ patterns: ["db.write", "db.delete", "db.drop", "sql.exec", "migration", "truncate", "drop table", "delete from"],
4689
+ defaultMode: "log"
4690
+ },
4691
+ {
4692
+ id: "system.critical",
4693
+ label: "Touch production systems and permissions",
4694
+ incident: "Stops the agent from deploying, changing IAM or altering infrastructure, the actions your security team asks about first.",
4695
+ patterns: ["deploy", "iam.", "role.grant", "permission.set", "infra.", "terraform", "kubectl", "shutdown", "scale."],
4696
+ defaultMode: "log"
4697
+ },
4698
+ {
4699
+ id: "crm.export",
4700
+ label: "Export customer records in bulk",
4701
+ incident: "Stops the agent from exporting your customer base, the action that turns a mistake into a reportable data-protection incident.",
4702
+ patterns: ["crm.export", "contacts.export", "customer.export", "report.export", "bulk.export", "download.all"],
4703
+ defaultMode: "log"
4704
+ },
4705
+ {
4706
+ id: "email.send",
4707
+ label: "Send email or messages outside your domain",
4708
+ incident: "Stops the agent from emailing or messaging someone outside your organisation, the channel invoice fraud travels on.",
4709
+ patterns: ["email.send", "mail.send", "message.send", "sms.send", "slack.post", "notify.external"],
4710
+ defaultMode: "log"
4711
+ },
4712
+ {
4713
+ id: "payment.transfer",
4714
+ label: "Move money",
4715
+ incident: "Stops the agent from paying, transferring or refunding without authority, which is the firewall Fidacy started with.",
4716
+ patterns: ["payment", "transfer", "refund", "payout", "charge", "invoice.pay", "wire"],
4717
+ defaultMode: "log"
4718
+ }
4719
+ ];
4720
+ var BY_ID = new Map(ACTION_CLASSES.map((c) => [c.id, c]));
4721
+ var NORMALIZED_PATTERNS = new Map(ACTION_CLASSES.map((k) => [k.id, k.patterns.map((p) => normalizeKeepingEdges(p))]));
4715
4722
 
4716
- // ../firewall/dist/core.js
4723
+ // ../mcp/src/config.ts
4717
4724
  import { randomUUID as randomUUID2 } from "node:crypto";
4718
-
4719
- // ../firewall/dist/evaluate.js
4720
- function fold(s) {
4721
- return s.toLowerCase().replace(/[4@]/g, "a").replace(/[0]/g, "o").replace(/[1|!]/g, "l").replace(/[3]/g, "e").replace(/[5$]/g, "s").replace(/[7]/g, "t").replace(/[^a-z0-9]/g, "");
4725
+ import { homedir } from "node:os";
4726
+ import { join } from "node:path";
4727
+ import {
4728
+ existsSync,
4729
+ mkdirSync,
4730
+ readFileSync,
4731
+ writeFileSync
4732
+ } from "node:fs";
4733
+ function hasEngineKey(keyOverride) {
4734
+ return Boolean((keyOverride ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim());
4722
4735
  }
4723
- function lev(a, b, max) {
4724
- if (Math.abs(a.length - b.length) > max)
4725
- return max + 1;
4726
- let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
4727
- for (let i = 1; i <= a.length; i++) {
4728
- const cur = [i];
4729
- let rowMin = i;
4730
- for (let j = 1; j <= b.length; j++) {
4731
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
4732
- const v = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
4733
- cur[j] = v;
4734
- if (v < rowMin)
4735
- rowMin = v;
4736
- }
4737
- if (rowMin > max)
4738
- return max + 1;
4739
- prev = cur;
4740
- }
4741
- return prev[b.length];
4736
+ function configDir() {
4737
+ return process.env.FIDACY_CONFIG_DIR ?? join(homedir(), ".fidacy");
4742
4738
  }
4743
- function lookalikePayee(payee, allowed) {
4744
- const exact = new Set(allowed);
4745
- if (exact.has(payee) || allowed.includes("*"))
4746
- return null;
4747
- const pf = fold(payee);
4748
- if (pf.length < 4)
4749
- return null;
4750
- for (const a of allowed) {
4751
- if (a === "*")
4752
- continue;
4753
- const af = fold(a);
4754
- if (af.length < 4)
4755
- continue;
4756
- if (pf === af)
4757
- return a;
4758
- const d = lev(pf, af, 2);
4759
- if (d > 0 && d <= 2)
4760
- return a;
4761
- }
4762
- return null;
4739
+ function configPath() {
4740
+ return join(configDir(), "config.json");
4763
4741
  }
4764
- function validateMandateCaps(mandate) {
4765
- const bad = (v) => typeof v !== "number" || !Number.isFinite(v) || v <= 0;
4766
- if (!mandate.allow || typeof mandate.allow !== "object")
4767
- return "invalid_mandate:missing_allow";
4768
- if (bad(mandate.allow.perTxMax))
4769
- return `invalid_mandate_cap:perTxMax=${String(mandate.allow.perTxMax)}`;
4770
- if (bad(mandate.allow.maxTotal))
4771
- return `invalid_mandate_cap:maxTotal=${String(mandate.allow.maxTotal)}`;
4772
- if (!mandate.window || typeof mandate.window !== "object")
4773
- return "invalid_mandate:missing_window";
4774
- for (const campo of ["notBefore", "notAfter"]) {
4775
- const v = mandate.window[campo];
4776
- if (typeof v !== "string" || Number.isNaN(Date.parse(v)))
4777
- return `invalid_mandate_window:${campo}=${String(v)}`;
4742
+ function auditLogPath() {
4743
+ if (process.env.FIDACY_AUDIT_PATH) return process.env.FIDACY_AUDIT_PATH;
4744
+ const dir = join(configDir(), "audit");
4745
+ try {
4746
+ mkdirSync(dir, { recursive: true, mode: 448 });
4747
+ } catch {
4778
4748
  }
4779
- return null;
4749
+ return join(dir, "audit.log");
4780
4750
  }
4781
- function evaluate(mandate, req, spentSoFar) {
4782
- const now = Date.now();
4783
- if (mandate.revoked)
4784
- return "mandate_revoked";
4785
- const badCap = validateMandateCaps(mandate);
4786
- if (badCap)
4787
- return badCap;
4788
- if (now < Date.parse(mandate.window.notBefore))
4789
- return "before_mandate_window";
4790
- if (now > Date.parse(mandate.window.notAfter))
4791
- return "after_mandate_window";
4792
- if (req.currency.toUpperCase() !== mandate.allow.currency.toUpperCase())
4793
- return `currency_not_allowed:${req.currency}`;
4794
- if (req.amount <= 0)
4795
- return "non_positive_amount";
4796
- if (req.amount > mandate.allow.perTxMax)
4797
- return `per_tx_cap_exceeded:${req.amount}>${mandate.allow.perTxMax}`;
4798
- if (spentSoFar + req.amount > mandate.allow.maxTotal)
4799
- return `total_cap_exceeded:${spentSoFar + req.amount}>${mandate.allow.maxTotal}`;
4800
- const payeeOk = mandate.allow.payees.includes("*") || mandate.allow.payees.includes(req.payee);
4801
- if (!payeeOk) {
4802
- const impersonated = lookalikePayee(req.payee, mandate.allow.payees);
4803
- if (impersonated)
4804
- return `payee_lookalike:${req.payee}~${impersonated}`;
4805
- return `payee_not_in_allowlist:${req.payee}`;
4751
+ function readConfig() {
4752
+ const p = configPath();
4753
+ if (!existsSync(p)) return null;
4754
+ try {
4755
+ const raw = JSON.parse(readFileSync(p, "utf8"));
4756
+ if (!raw || typeof raw.anon_id !== "string") return null;
4757
+ return {
4758
+ anon_id: raw.anon_id,
4759
+ tier: raw.tier === "paid" ? "paid" : "free",
4760
+ api_key: typeof raw.api_key === "string" ? raw.api_key : null,
4761
+ mandate: raw.mandate,
4762
+ // Sem esta linha o campo seria descartado aqui e o bloqueio configurado
4763
+ // pelo operador simplesmente não existiria, sem erro nenhum. É a mesma
4764
+ // classe de defeito do teto de pagamento que era jogado fora em silêncio.
4765
+ actionClasses: raw.actionClasses && typeof raw.actionClasses === "object" ? raw.actionClasses : void 0,
4766
+ systems: Array.isArray(raw.systems) ? raw.systems : void 0,
4767
+ // Install-state passthrough: dropping these on a read→write cycle would
4768
+ // reset every once-per-install nudge into an every-time nag.
4769
+ created_at: typeof raw.created_at === "string" ? raw.created_at : void 0,
4770
+ nudges: raw.nudges && typeof raw.nudges === "object" ? raw.nudges : void 0,
4771
+ decisions_count: typeof raw.decisions_count === "number" ? raw.decisions_count : void 0,
4772
+ hosted_lapsed_at: typeof raw.hosted_lapsed_at === "string" ? raw.hosted_lapsed_at : void 0,
4773
+ operator_email: typeof raw.operator_email === "string" ? raw.operator_email : void 0,
4774
+ registered_email: typeof raw.registered_email === "string" ? raw.registered_email : void 0
4775
+ };
4776
+ } catch {
4777
+ return null;
4806
4778
  }
4807
- const catOk = mandate.allow.categories.includes("*") || mandate.allow.categories.includes(req.category);
4808
- if (!catOk)
4809
- return `category_not_allowed:${req.category}`;
4810
- return null;
4811
- }
4812
-
4813
- // ../firewall/dist/core.js
4814
- function canonInvoice(ref) {
4815
- if (typeof ref !== "string")
4816
- return "";
4817
- return ref.normalize("NFC").replace(/[\s\u200B\u200C\u200D\uFEFF]+/g, "").toLowerCase();
4818
4779
  }
4819
- function validateRequest(req) {
4820
- if (!req || typeof req !== "object")
4821
- return "invalid_request";
4822
- if (typeof req.amount !== "number" || !Number.isFinite(req.amount) || req.amount <= 0)
4823
- return "invalid_amount";
4824
- if (typeof req.payee !== "string" || req.payee.length === 0)
4825
- return "invalid_payee";
4826
- if (typeof req.currency !== "string" || req.currency.length === 0)
4827
- return "invalid_currency";
4828
- return null;
4780
+ function writeConfig(cfg) {
4781
+ const dir = configDir();
4782
+ mkdirSync(dir, { recursive: true, mode: 448 });
4783
+ writeFileSync(configPath(), JSON.stringify(cfg, null, 2), { mode: 384 });
4829
4784
  }
4830
- function requireHttpsBase(raw) {
4831
- const trimmed = String(raw ?? "").replace(/\/+$/, "");
4832
- let u;
4785
+ function ensureState() {
4786
+ const existing = readConfig();
4787
+ if (existing) return { config: existing, firstRun: false };
4788
+ const config = {
4789
+ anon_id: randomUUID2(),
4790
+ tier: "free",
4791
+ api_key: null,
4792
+ mandate: { payees: [], categories: ["*"], currency: "USD", perTxMax: 2500, maxTotal: 1e4 },
4793
+ systems: [],
4794
+ actionClasses: Object.fromEntries(ACTION_CLASSES.map((k) => [k.id, { mode: k.defaultMode }])),
4795
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
4796
+ };
4833
4797
  try {
4834
- u = new URL(trimmed);
4798
+ writeConfig(config);
4835
4799
  } catch {
4836
- throw new Error("FIDACY_API_URL is not a valid URL");
4837
4800
  }
4838
- const localHttp = u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]");
4839
- if (u.protocol !== "https:" && !localHttp) {
4840
- throw new Error("FIDACY_API_URL must be https:// (the API key is sent to it)");
4841
- }
4842
- return trimmed;
4801
+ return { config, firstRun: true };
4843
4802
  }
4844
- var DevFidacyCore = class {
4845
- priv;
4846
- pubPem;
4847
- mandate;
4848
- store;
4849
- spent = 0;
4850
- claimedInvoices = /* @__PURE__ */ new Set();
4851
- onDecision;
4852
- constructor(opts) {
4853
- const kp = loadOrGenerateKeyPair();
4854
- this.priv = kp.privateKey;
4855
- this.pubPem = publicKeyPem(kp.publicKey);
4856
- if (kp.ephemeral)
4857
- console.error("[fidacy] local firewall active, deny-by-default. Grants are signed with a per-session key (fine for local use); set FIDACY_SIGNING_KEY_B64 for a stable key, or FIDACY_MODE=http to use the hosted core.");
4858
- this.mandate = opts.mandate;
4859
- this.store = new FileAuditStore(opts.auditLogPath ?? "./fidacy-audit.log");
4860
- this.onDecision = opts.onDecision;
4861
- this.rehydrate();
4862
- }
4863
- /**
4864
- * Rebuild the in-memory decision state from the persisted audit log, so a process
4865
- * restart cannot re-open a claimed invoice (BEC dedup) or reset the maxTotal
4866
- * counter. One O(records) pass at boot over the already-loaded chain:
4867
- * - claimedInvoices: every ALLOW that carried an invoiceRef, regardless of age —
4868
- * a paid invoice stays paid.
4869
- * - spent: ALLOW amounts whose timestamp falls inside the CURRENT mandate window
4870
- * and whose currency matches the mandate (the cap is defined per window/currency).
4871
- * Records from older versions lack the rehydration fields and are skipped.
4872
- */
4873
- rehydrate() {
4874
- const notBefore = Date.parse(this.mandate.window.notBefore);
4875
- const notAfter = Date.parse(this.mandate.window.notAfter);
4876
- for (const r of this.store.records()) {
4877
- if (r.status !== "ALLOW")
4878
- continue;
4879
- if (r.invoiceRef)
4880
- this.claimedInvoices.add(`${r.subject}|${r.invoiceRef}`);
4881
- if (typeof r.amount !== "number" || !Number.isFinite(r.amount))
4882
- continue;
4883
- if (r.currency !== this.mandate.allow.currency)
4884
- continue;
4885
- const t = Date.parse(r.ts);
4886
- if (!Number.isFinite(t) || t < notBefore || t > notAfter)
4887
- continue;
4888
- this.spent += r.amount;
4803
+ var DEMO_PAYEE = "fidacy:demo";
4804
+ function resolveMandateRules(cfg) {
4805
+ const m = cfg?.mandate ?? {};
4806
+ const envList = (v) => v === void 0 ? void 0 : v.split(",").map((s) => s.trim()).filter(Boolean);
4807
+ const envNum = (name, v) => {
4808
+ if (v === void 0 || v.trim() === "") return void 0;
4809
+ const n = Number(v);
4810
+ if (!Number.isFinite(n) || n <= 0) {
4811
+ console.error(
4812
+ `[fidacy] ${name}="${v}" is not a positive number, so it cannot be enforced as a cap. Ignoring it and using the safe default. Write digits only, with no thousands separator, currency symbol or unit (e.g. ${name}=2500).`
4813
+ );
4814
+ return void 0;
4815
+ }
4816
+ return n;
4817
+ };
4818
+ const fileNum = (name, v) => {
4819
+ if (v === void 0 || v === null) return void 0;
4820
+ const n = typeof v === "number" ? v : typeof v === "string" ? Number(v.trim()) : NaN;
4821
+ if (!Number.isFinite(n) || n <= 0) {
4822
+ console.error(
4823
+ `[fidacy] mandate.${name} in config.json is ${JSON.stringify(v)}, which is not a positive number, so it cannot be enforced as a cap. Ignoring it and using the safe default. Write it as a bare JSON number, with no quotes, thousands separator, currency symbol or unit (e.g. "${name}": 2500).`
4824
+ );
4825
+ return void 0;
4889
4826
  }
4827
+ return n;
4828
+ };
4829
+ const payees = envList(process.env.FIDACY_ALLOW_PAYEES) ?? m.payees ?? [];
4830
+ return {
4831
+ payees: payees.includes(DEMO_PAYEE) ? payees : [...payees, DEMO_PAYEE],
4832
+ categories: envList(process.env.FIDACY_ALLOW_CATEGORIES) ?? m.categories ?? ["*"],
4833
+ currency: process.env.FIDACY_CURRENCY ?? m.currency ?? "USD",
4834
+ perTxMax: envNum("FIDACY_PER_TX_MAX", process.env.FIDACY_PER_TX_MAX) ?? fileNum("perTxMax", m.perTxMax) ?? 2500,
4835
+ maxTotal: envNum("FIDACY_MAX_TOTAL", process.env.FIDACY_MAX_TOTAL) ?? fileNum("maxTotal", m.maxTotal) ?? 1e4
4836
+ };
4837
+ }
4838
+
4839
+ // ../mcp/src/evidence.ts
4840
+ var GENESIS = "fidacy.observer.v1";
4841
+ var sha2562 = (s) => createHash("sha256").update(s).digest("hex");
4842
+ function canonical(obj) {
4843
+ const keys = Object.keys(obj).sort();
4844
+ return "{" + keys.map((k) => `${JSON.stringify(k)}:${JSON.stringify(obj[k])}`).join(",") + "}";
4845
+ }
4846
+ function chainStep(prevHead, a) {
4847
+ return sha2562(`${prevHead}|${sha2562(canonical({ category: a.category, i: a.i, tool: a.tool, ts: a.ts }))}`);
4848
+ }
4849
+ function genesisHead() {
4850
+ return sha2562(GENESIS);
4851
+ }
4852
+ function fidacyDir() {
4853
+ return join2(configDir(), "sessions");
4854
+ }
4855
+ function writeSessionLog(sessionId, actions, head, meta) {
4856
+ try {
4857
+ const dir = fidacyDir();
4858
+ mkdirSync2(dir, { recursive: true, mode: 448 });
4859
+ const safe = sessionId.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80) || "session";
4860
+ const path = join2(dir, `${safe}.json`);
4861
+ const body = {
4862
+ v: "fidacy.observer.v1",
4863
+ recipe: 'h_0 = sha256("fidacy.observer.v1"); h_i = sha256(h_prev + "|" + sha256(canonical(action)))',
4864
+ sessionId,
4865
+ head,
4866
+ ...meta,
4867
+ actions
4868
+ };
4869
+ writeFileSync2(path, JSON.stringify(body, null, 2), { mode: 384 });
4870
+ return path;
4871
+ } catch {
4872
+ return null;
4890
4873
  }
4891
- /**
4892
- * Swap the active mandate (shell config hot-reload: edit config.json, the next
4893
- * call picks it up, no host restart). Decision state is REBUILT from the audit
4894
- * log under the new window/currency, so a reload can never reset the BEC
4895
- * invoice dedup or undercount spend: a paid invoice stays paid, spent is
4896
- * recomputed.
4897
- */
4898
- setMandate(m) {
4899
- this.mandate = m;
4900
- this.spent = 0;
4901
- this.claimedInvoices.clear();
4902
- this.rehydrate();
4874
+ }
4875
+ function queuePending(head, sessionId, at, total = 0, byCategory = {}) {
4876
+ try {
4877
+ const dir = fidacyDir();
4878
+ mkdirSync2(dir, { recursive: true, mode: 448 });
4879
+ appendFileSync(join2(dir, "pending-anchors.jsonl"), JSON.stringify({ head, sessionId, at, total, byCategory }) + "\n", {
4880
+ mode: 384
4881
+ });
4882
+ } catch {
4903
4883
  }
4904
- async getMandate() {
4905
- return this.mandate;
4884
+ }
4885
+ function takePending(limit = 50) {
4886
+ const dir = fidacyDir();
4887
+ const path = join2(dir, "pending-anchors.jsonl");
4888
+ const taken = join2(dir, "pending-anchors.taking");
4889
+ try {
4890
+ renameSync(path, taken);
4891
+ } catch {
4892
+ return [];
4906
4893
  }
4907
- async decide(req, subject) {
4908
- const decisionId = randomUUID2();
4909
- const ts2 = (/* @__PURE__ */ new Date()).toISOString();
4910
- const invalid = validateRequest(req);
4911
- if (invalid) {
4912
- const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: invalid, ts: ts2 };
4913
- this.store.append(decision2);
4914
- this.onDecision?.(decision2);
4915
- return decision2;
4916
- }
4917
- const violated = evaluate(this.mandate, req, this.spent);
4918
- if (violated) {
4919
- const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: violated, ts: ts2 };
4920
- this.store.append(decision2);
4921
- this.onDecision?.(decision2);
4922
- return decision2;
4923
- }
4924
- const invoice = canonInvoice(req.invoiceRef);
4925
- if (invoice) {
4926
- const k = `${subject}|${invoice}`;
4927
- if (this.claimedInvoices.has(k)) {
4928
- const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: `duplicate_invoice:${invoice}`, ts: ts2 };
4929
- this.store.append(decision2);
4930
- this.onDecision?.(decision2);
4931
- return decision2;
4894
+ try {
4895
+ const lines = readFileSync2(taken, "utf8").split("\n").filter(Boolean);
4896
+ const out = [];
4897
+ for (const line of lines.slice(-limit)) {
4898
+ try {
4899
+ const p = JSON.parse(line);
4900
+ if (typeof p.head === "string" && /^[0-9a-f]{64}$/.test(p.head)) out.push(p);
4901
+ } catch {
4932
4902
  }
4933
- this.claimedInvoices.add(k);
4934
4903
  }
4935
- const grantPayload = { decisionId, subject, payee: req.payee, amount: req.amount, currency: req.currency, exp: Date.now() + 12e4, ...req.invoiceRef ? { invoiceRef: req.invoiceRef } : {} };
4936
- const grantBody = Buffer.from(stableStringify(grantPayload), "utf8").toString("base64url");
4937
- const grant = `${grantBody}.${sign(this.priv, grantBody)}`;
4938
- const decision = { decisionId, status: "ALLOW", subject, mandateId: this.mandate.id, request: req, grant, ts: ts2 };
4939
- this.spent += req.amount;
4940
- this.store.append(decision);
4941
- this.onDecision?.(decision);
4942
- return decision;
4943
- }
4944
- async getProof(decisionId) {
4945
- const record3 = this.store.find(decisionId);
4946
- if (!record3)
4947
- return null;
4948
- return { record: record3, chainIntact: this.store.intact(), verifiedAgainstPublicKey: this.pubPem };
4949
- }
4950
- async history(limit = 50) {
4951
- const all = this.store.records();
4952
- return limit > 0 && all.length > limit ? all.slice(-limit) : all;
4953
- }
4954
- publicKey() {
4955
- return this.pubPem;
4904
+ return out;
4905
+ } catch {
4906
+ return [];
4907
+ } finally {
4908
+ try {
4909
+ writeFileSync2(taken, "");
4910
+ } catch {
4911
+ }
4956
4912
  }
4913
+ }
4914
+ function verifyUrl(head) {
4915
+ return `https://fidacy.com/verify?sha256=${head}`;
4916
+ }
4917
+ var CODE = {
4918
+ money: "m",
4919
+ shell: "s",
4920
+ file: "f",
4921
+ network: "net",
4922
+ message: "msg",
4923
+ other: "o"
4957
4924
  };
4958
- var HttpFidacyCore = class {
4959
- apiKey;
4960
- subjectPub;
4961
- baseUrl;
4962
- // Enforce https before any key-bearing request — a hostile FIDACY_API_URL would
4963
- // otherwise exfiltrate the API key in the Authorization header.
4964
- constructor(baseUrl, apiKey, subjectPub = "") {
4965
- this.apiKey = apiKey;
4966
- this.subjectPub = subjectPub;
4967
- this.baseUrl = requireHttpsBase(baseUrl);
4968
- }
4969
- async call(path, body) {
4970
- const res = await fetch(`${this.baseUrl}${path}`, {
4971
- method: "POST",
4972
- headers: { "content-type": "application/json", authorization: `Bearer ${this.apiKey}` },
4973
- body: JSON.stringify(body)
4974
- });
4975
- if (!res.ok)
4976
- throw new Error(`fidacy core ${path} -> ${res.status}`);
4977
- return await res.json();
4978
- }
4979
- async getMandate(subject) {
4980
- return this.call("/v1/mandate/get", { subject });
4981
- }
4982
- async decide(req, subject) {
4983
- return this.call("/v1/decide", { req, subject });
4984
- }
4985
- async getProof(decisionId) {
4986
- return this.call("/v1/audit/proof", { decisionId });
4925
+ function sessionLabel(sessionId, total, byCategory) {
4926
+ const id = sessionId.replace(/[^A-Za-z0-9-]/g, "").slice(0, 8) || "unknown";
4927
+ const parts = [`session:${id}`, `n=${total}`];
4928
+ for (const [cat, code] of Object.entries(CODE)) {
4929
+ const n = byCategory[cat] ?? 0;
4930
+ if (n > 0) parts.push(`${code}=${n}`);
4987
4931
  }
4988
- async history(limit = 50) {
4989
- const res = await this.call("/v1/audit/list", { limit });
4990
- return res.records ?? [];
4932
+ return parts.join(" ").slice(0, 120);
4933
+ }
4934
+ function sessionSummary(opts) {
4935
+ const by = {};
4936
+ for (const [cat, n] of Object.entries(opts.byCategory)) if (n > 0) by[cat] = n;
4937
+ const s = {
4938
+ v: 1,
4939
+ // Mesmo saneamento do rótulo: o host vem de código nosso, mas o campo é
4940
+ // limitado na origem para que uma string estranha vire um valor curto e
4941
+ // legível em vez de um 400 que perderia o registro inteiro da sessão.
4942
+ host: opts.host.replace(/[^a-z0-9-]/gi, "").slice(0, 40) || "unknown",
4943
+ session_id: opts.sessionId.replace(/[^A-Za-z0-9-]/g, "").slice(0, 64) || "unknown",
4944
+ total: opts.total
4945
+ };
4946
+ if (Object.keys(by).length) s.by_category = by;
4947
+ if (opts.truncated) s.truncated = true;
4948
+ if (opts.startedAt) s.started_at = opts.startedAt;
4949
+ if (opts.endedAt) s.ended_at = opts.endedAt;
4950
+ return s;
4951
+ }
4952
+
4953
+ // ../mcp/src/observer.ts
4954
+ var EXACT = {
4955
+ // Fidacy's own money tools, so the report counts what we ourselves gated.
4956
+ request_payment: "money",
4957
+ assess_action: "money",
4958
+ // The common OpenClaw / agent-host tool vocabulary.
4959
+ bash: "shell",
4960
+ shell: "shell",
4961
+ exec: "shell",
4962
+ run_command: "shell",
4963
+ code_mode_exec: "shell",
4964
+ read: "file",
4965
+ read_file: "file",
4966
+ write: "file",
4967
+ write_file: "file",
4968
+ edit: "file",
4969
+ edit_file: "file",
4970
+ apply_patch: "file",
4971
+ glob: "file",
4972
+ grep: "file",
4973
+ ls: "file",
4974
+ fetch: "network",
4975
+ web_fetch: "network",
4976
+ web_search: "network",
4977
+ http_request: "network",
4978
+ browser: "network",
4979
+ send_message: "message",
4980
+ reply: "message",
4981
+ send_email: "message"
4982
+ };
4983
+ var FUZZY = [
4984
+ ["payment", "money"],
4985
+ ["invoice", "money"],
4986
+ ["transfer", "money"],
4987
+ ["charge", "money"],
4988
+ ["file", "file"],
4989
+ ["patch", "file"],
4990
+ ["write", "file"],
4991
+ ["read", "file"],
4992
+ ["fetch", "network"],
4993
+ ["http", "network"],
4994
+ ["browser", "network"],
4995
+ ["search", "network"],
4996
+ ["exec", "shell"],
4997
+ ["command", "shell"],
4998
+ ["shell", "shell"],
4999
+ ["mail", "message"],
5000
+ ["message", "message"],
5001
+ ["send", "message"]
5002
+ ];
5003
+ function classify(toolName, derivedPaths) {
5004
+ const name = toolName.toLowerCase();
5005
+ const exact = EXACT[name];
5006
+ if (exact) return exact;
5007
+ if (derivedPaths && derivedPaths.length > 0) return "file";
5008
+ for (const [needle, category] of FUZZY) if (name.includes(needle)) return category;
5009
+ return "other";
5010
+ }
5011
+ function fingerprint(toolName, derivedPaths) {
5012
+ const target = derivedPaths && derivedPaths.length > 0 ? [...derivedPaths].sort().join("\0") : toolName;
5013
+ return createHash2("sha256").update(`${toolName}\0${target}`).digest("hex").slice(0, 16);
5014
+ }
5015
+ var ACTION_LOG_CAP = 5e3;
5016
+ function newLedger(sessionId, now) {
5017
+ return {
5018
+ sessionId,
5019
+ startedAt: now,
5020
+ byCategory: /* @__PURE__ */ new Map(),
5021
+ total: 0,
5022
+ toolsSeen: /* @__PURE__ */ new Set(),
5023
+ head: genesisHead(),
5024
+ actions: [],
5025
+ truncated: false
5026
+ };
5027
+ }
5028
+ function record(ledger, toolName, derivedPaths, failed = false, at = Date.now()) {
5029
+ const category = classify(toolName, derivedPaths);
5030
+ let tally = ledger.byCategory.get(category);
5031
+ if (!tally) {
5032
+ tally = { calls: 0, failures: 0, targets: /* @__PURE__ */ new Set() };
5033
+ ledger.byCategory.set(category, tally);
4991
5034
  }
4992
- publicKey() {
4993
- return this.subjectPub;
5035
+ tally.calls += 1;
5036
+ if (failed) tally.failures += 1;
5037
+ if (derivedPaths && derivedPaths.length > 0 && tally.targets.size < TARGET_CAP) {
5038
+ tally.targets.add(fingerprint(toolName, derivedPaths));
4994
5039
  }
5040
+ ledger.total += 1;
5041
+ if (ledger.toolsSeen.size < TOOLS_CAP) ledger.toolsSeen.add(toolName);
5042
+ const action = {
5043
+ i: ledger.total,
5044
+ category,
5045
+ tool: toolName,
5046
+ ts: new Date(at).toISOString()
5047
+ };
5048
+ ledger.head = chainStep(ledger.head, action);
5049
+ if (ledger.actions.length < ACTION_LOG_CAP) ledger.actions.push(action);
5050
+ else ledger.truncated = true;
5051
+ }
5052
+ var TARGET_CAP = 500;
5053
+ var TOOLS_CAP = 200;
5054
+ var ORDER = ["money", "shell", "file", "network", "message", "other"];
5055
+ var LABEL = {
5056
+ money: "moved money",
5057
+ shell: "ran commands",
5058
+ file: "touched files",
5059
+ network: "reached the network",
5060
+ message: "sent messages",
5061
+ other: "other"
4995
5062
  };
4996
-
4997
- // ../firewall/dist/audit-store.js
4998
- var FileAuditStore = class {
4999
- path;
5000
- chain = [];
5001
- constructor(path) {
5002
- this.path = path;
5003
- this.load();
5063
+ function renderReport(ledger, now, activated, logPath) {
5064
+ if (ledger.total < MIN_ACTIONS_FOR_REPORT) return null;
5065
+ const minutes = Math.max(1, Math.round((now - ledger.startedAt) / 6e4));
5066
+ const lines = [];
5067
+ lines.push(
5068
+ `[fidacy] SESSION REPORT \xB7 ${ledger.total} agent actions watched over ${minutes} min \xB7 nothing left this machine`
5069
+ );
5070
+ for (const category of ORDER) {
5071
+ const tally = ledger.byCategory.get(category);
5072
+ if (!tally) continue;
5073
+ const capped = tally.targets.size >= TARGET_CAP ? "+" : "";
5074
+ const parts = [`${tally.calls} ${tally.calls === 1 ? "call" : "calls"}`];
5075
+ if (tally.targets.size > 0) parts.push(`${tally.targets.size}${capped} distinct`);
5076
+ if (tally.failures > 0) parts.push(`${tally.failures} failed`);
5077
+ lines.push(`[fidacy] ${LABEL[category].padEnd(20)} ${parts.join(" \xB7 ")}`);
5004
5078
  }
5005
- load() {
5006
- if (!fs.existsSync(this.path))
5007
- return;
5008
- try {
5009
- const raw = fs.readFileSync(this.path, "utf8");
5010
- const lines = raw.split("\n");
5011
- const parsed = [];
5012
- let torn = false;
5013
- for (let i = 0; i < lines.length; i++) {
5014
- if (!lines[i].trim())
5015
- continue;
5016
- try {
5017
- parsed.push(JSON.parse(lines[i]));
5018
- } catch {
5019
- if (lines.slice(i + 1).some((l) => l.trim()))
5020
- throw new Error(`unparseable record at line ${i + 1}`);
5021
- torn = true;
5022
- break;
5023
- }
5024
- }
5025
- this.chain = parsed;
5026
- if (!this.intact())
5027
- throw new Error("audit chain integrity broken");
5028
- if (torn) {
5029
- const clean = parsed.map((r) => JSON.stringify(r)).join("\n") + (parsed.length ? "\n" : "");
5030
- fs.writeFileSync(this.path, clean);
5031
- console.error(`[fidacy] audit log at ${this.path} had a torn trailing line (crash during append?); salvaged ${parsed.length} intact records and truncated the tail.`);
5032
- }
5033
- } catch (err) {
5034
- this.chain = [];
5079
+ const money2 = ledger.byCategory.get("money");
5080
+ if (!money2) {
5081
+ lines.push(`[fidacy] no money-moving action was attempted \xB7 the firewall stayed armed and idle`);
5082
+ }
5083
+ lines.push(`[fidacy] digest ${ledger.head}`);
5084
+ if (logPath) lines.push(`[fidacy] log ${logPath}${ledger.truncated ? " (partial: action cap reached)" : ""}`);
5085
+ lines.push(
5086
+ activated ? `[fidacy] anchored to the audit chain \xB7 verify: ${verifyUrl(ledger.head)}` : `[fidacy] activate free to anchor this digest so nobody can rewrite it later: https://fidacy.com/claim`
5087
+ );
5088
+ return lines.join("\n");
5089
+ }
5090
+ var MIN_ACTIONS_FOR_REPORT = 5;
5091
+ function installObserver(api, emit, isActivated, now = Date.now, anchor) {
5092
+ if (typeof api?.on !== "function") return;
5093
+ const ledgers = /* @__PURE__ */ new Map();
5094
+ const LEDGER_CAP = 50;
5095
+ const ledgerFor = (sessionId) => {
5096
+ let ledger = ledgers.get(sessionId);
5097
+ if (!ledger) {
5098
+ if (ledgers.size >= LEDGER_CAP) ledgers.delete(ledgers.keys().next().value);
5099
+ ledger = newLedger(sessionId, now());
5100
+ ledgers.set(sessionId, ledger);
5101
+ }
5102
+ return ledger;
5103
+ };
5104
+ api.on(
5105
+ "before_tool_call",
5106
+ (event, ctx) => {
5035
5107
  try {
5036
- const quarantine = `${this.path}.corrupt-${Date.now()}`;
5037
- fs.renameSync(this.path, quarantine);
5038
- console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered (${err.message}); quarantined to ${quarantine}, starting a fresh chain.`);
5108
+ const toolName = typeof event.toolName === "string" ? event.toolName : "unknown";
5109
+ const paths = Array.isArray(event.derivedPaths) ? event.derivedPaths : void 0;
5110
+ const sessionId = sessionIdOf(ctx);
5111
+ record(ledgerFor(sessionId), toolName, paths, false);
5039
5112
  } catch {
5040
- console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered and could not be quarantined; starting a fresh in-memory chain.`);
5041
5113
  }
5114
+ },
5115
+ // Lowest priority on purpose: anything that actually decides should run first,
5116
+ // and we only want to count what the host was really about to do.
5117
+ { priority: 1 }
5118
+ );
5119
+ api.on("session_end", async (event, ctx) => {
5120
+ try {
5121
+ const sessionId = sessionIdOf(ctx) || (typeof event.sessionId === "string" ? event.sessionId : "");
5122
+ const ledger = ledgers.get(sessionId);
5123
+ if (!ledger) return;
5124
+ ledgers.delete(sessionId);
5125
+ if (ledger.total < MIN_ACTIONS_FOR_REPORT) return;
5126
+ const logPath = persist(ledger, now());
5127
+ let anchored = false;
5128
+ if (anchor) {
5129
+ anchored = await anchor(ledger.head, ledger.sessionId, rollup(ledger)).catch(() => false);
5130
+ }
5131
+ if (!anchored) queuePending(ledger.head, ledger.sessionId, now(), ledger.total, rollup(ledger));
5132
+ const report = renderReport(ledger, now(), anchored, logPath);
5133
+ if (report) emit(report);
5134
+ } catch {
5042
5135
  }
5043
- }
5044
- head() {
5045
- return this.chain.length ? this.chain[this.chain.length - 1].hash : "GENESIS";
5046
- }
5047
- append(decision) {
5048
- const prevHash = this.head();
5049
- const seq = this.chain.length;
5050
- const ts2 = decision.ts;
5051
- const digest = sha2562(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
5052
- const hash = sha2562(`${prevHash}|${digest}|${seq}|${ts2}`);
5053
- const record3 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts: ts2 };
5054
- if (typeof decision.request?.purpose === "string" && decision.request.purpose.trim()) {
5055
- record3.purpose = decision.request.purpose.slice(0, 500);
5056
- }
5057
- if (typeof decision.request?.payee === "string" && decision.request.payee.trim()) {
5058
- record3.payee = decision.request.payee.slice(0, 200);
5059
- }
5060
- if (decision.status === "ALLOW") {
5061
- const req = decision.request;
5062
- if (typeof req?.amount === "number" && Number.isFinite(req.amount))
5063
- record3.amount = req.amount;
5064
- if (typeof req?.currency === "string")
5065
- record3.currency = req.currency;
5066
- const invoice = canonInvoice(req?.invoiceRef);
5067
- if (invoice)
5068
- record3.invoiceRef = invoice;
5069
- } else {
5070
- if (decision.violatedRule)
5071
- record3.violatedRule = decision.violatedRule;
5072
- const req = decision.request;
5073
- if (typeof req?.amount === "number" && Number.isFinite(req.amount))
5074
- record3.amount = req.amount;
5075
- if (typeof req?.currency === "string")
5076
- record3.currency = req.currency;
5136
+ });
5137
+ api.on("gateway_stop", () => drain());
5138
+ process.on("exit", () => drain());
5139
+ function drain() {
5140
+ try {
5141
+ for (const [id, ledger] of ledgers) {
5142
+ ledgers.delete(id);
5143
+ if (ledger.total < MIN_ACTIONS_FOR_REPORT) continue;
5144
+ const logPath = persist(ledger, now());
5145
+ queuePending(ledger.head, ledger.sessionId, now(), ledger.total, rollup(ledger));
5146
+ const report = renderReport(ledger, now(), false, logPath);
5147
+ if (report) emit(report);
5148
+ }
5149
+ } catch {
5077
5150
  }
5078
- fs.appendFileSync(this.path, JSON.stringify(record3) + "\n");
5079
- this.chain.push(record3);
5080
- return record3;
5081
- }
5082
- find(decisionId) {
5083
- return this.chain.find((r) => r.decisionId === decisionId);
5084
5151
  }
5085
- /** The full loaded chain, oldest first. Read-only; used to rehydrate core state at boot. */
5086
- records() {
5087
- return this.chain;
5152
+ function rollup(ledger) {
5153
+ const out = {};
5154
+ for (const [cat, tally] of ledger.byCategory) out[cat] = tally.calls;
5155
+ return out;
5088
5156
  }
5089
- intact() {
5090
- let prev = "GENESIS";
5091
- for (const r of this.chain) {
5092
- const expected = sha2562(`${prev}|${r.digest}|${r.seq}|${r.ts}`);
5093
- if (expected !== r.hash || r.prevHash !== prev)
5094
- return false;
5095
- prev = r.hash;
5096
- }
5097
- return true;
5157
+ function persist(ledger, at) {
5158
+ return writeSessionLog(ledger.sessionId, ledger.actions, ledger.head, {
5159
+ startedAt: new Date(ledger.startedAt).toISOString(),
5160
+ endedAt: new Date(at).toISOString(),
5161
+ totalActions: ledger.total,
5162
+ truncated: ledger.truncated,
5163
+ byCategory: Object.fromEntries(
5164
+ [...ledger.byCategory].map(([k, v]) => [k, { calls: v.calls, failures: v.failures, distinct: v.targets.size }])
5165
+ )
5166
+ });
5098
5167
  }
5099
- };
5168
+ }
5169
+ function sessionIdOf(ctx) {
5170
+ if (!ctx) return "default";
5171
+ const id = ctx.sessionId ?? ctx.sessionKey;
5172
+ return typeof id === "string" && id.length > 0 ? id : "default";
5173
+ }
5100
5174
 
5101
5175
  // ../mcp/src/core.ts
5102
5176
  import { statSync } from "node:fs";
5103
5177
 
5104
5178
  // ../mcp/src/telemetry.ts
5105
- var CLIENT_VERSION = true ? "0.5.10" : "dev";
5179
+ var CLIENT_VERSION = true ? "0.6.0" : "dev";
5106
5180
  function bandOf(amount) {
5107
5181
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
5108
5182
  if (amount < 10) return "lt10";
@@ -5317,7 +5391,7 @@ function requestUpgrade() {
5317
5391
  }
5318
5392
 
5319
5393
  // ../mcp/src/provision.ts
5320
- var CLIENT_VERSION2 = true ? "0.5.10" : "dev";
5394
+ var CLIENT_VERSION2 = true ? "0.6.0" : "dev";
5321
5395
  function provisionEnabled() {
5322
5396
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
5323
5397
  return !(v === "1" || v === "true" || v === "yes");
@@ -5460,7 +5534,7 @@ function trialCountdownLine(keyOverride) {
5460
5534
  }
5461
5535
 
5462
5536
  // ../mcp/src/register.ts
5463
- var CLIENT_VERSION3 = true ? "0.5.10" : "dev";
5537
+ var CLIENT_VERSION3 = true ? "0.6.0" : "dev";
5464
5538
  function endpoint3() {
5465
5539
  const base = (process.env.FIDACY_ENGINE_URL ?? "https://api.fidacy.com").replace(/\/$/, "");
5466
5540
  return `${base}/v1/register`;