@fidacy/mcp 0.1.16 → 0.1.18

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/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to `@fidacy/mcp` are documented here. This project follows
4
4
  semantic versioning.
5
5
 
6
+ ## 0.1.17 — 2026-07-03
7
+
8
+ - Config HOT-RELOAD: editing `~/.fidacy/config.json` (payees, caps) takes effect
9
+ on the next call, no host restart. Spend and invoice-dedup state is rebuilt
10
+ from the audit log on reload, so a reload never weakens the firewall.
11
+ - The `payee_not_in_allowlist` DENY now tells the agent exactly how to allow a
12
+ trusted payee (friction fix from a clean-room install test).
13
+
6
14
  ## 0.1.16 — 2026-07-03
7
15
 
8
16
  - `anchor_artifact`/`check_artifact`: new kind `conversation` — anchor chatbot
package/README.md CHANGED
@@ -73,6 +73,9 @@ Mental model: `assess_action` -> **engine** (signed verdict);
73
73
  | `request_payment` | core | Authorize a payment action. ALLOW + grant, or DENY + rule. |
74
74
  | `verify_mandate` | core | Read the mandate envelope + Fidacy public key. |
75
75
  | `get_audit_proof` | core | Hash-chained proof for a decision id. |
76
+ | `anchor_artifact` | engine | Bitcoin-anchored integrity proof for any file (contract, invoice, prescription, claim, image, audio, video, conversation). Hashed locally; only the SHA-256 leaves. Returns a signed receipt. |
77
+ | `check_artifact` | engine | Check whether a file (or hash) was anchored, and its Bitcoin checkpoint state. A mismatch is the tampering signal. |
78
+ | `upgrade` | — | Start upgrading this local install to a real Fidacy account; preserves and migrates anonymous usage. |
76
79
 
77
80
  ### `assess_action`
78
81
 
@@ -145,6 +148,21 @@ repository stays private. Set `FIDACY_MODE=http` and implement three endpoints:
145
148
 
146
149
  No change to the MCP layer is needed.
147
150
 
151
+ ## Telemetry
152
+
153
+ The install emits **anonymous, opt-out** usage telemetry so we can measure
154
+ traction (installs, active agents, decision counts, deny-rate). It is
155
+ best-effort and never on the decision critical path, so it can never block or
156
+ slow a verdict.
157
+
158
+ - It carries an anonymous install id and counters only. It **never** includes
159
+ payment content — no payee, amount, invoice, or mandate ever leaves your
160
+ machine. Payload-carrying types are rejected by a strict schema.
161
+ - Turn it off completely with `FIDACY_DISABLE_TELEMETRY=1` (or `true`).
162
+
163
+ Consistent with the product: you don't have to trust us, you can verify. The
164
+ firewall runs fully with telemetry disabled.
165
+
148
166
  ## Dev
149
167
 
150
168
  ```bash
package/dist/assess.js CHANGED
@@ -88,7 +88,7 @@ async function postOnce(fetchImpl, url, headers, payload, timeoutMs) {
88
88
  body: payload,
89
89
  signal: controller.signal
90
90
  });
91
- } catch (err) {
91
+ } catch {
92
92
  const aborted = controller.signal.aborted;
93
93
  throw new AssessError({ type: aborted ? "timeout" : "network_error", status: 0 });
94
94
  } finally {
package/dist/core.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/core.ts
2
+ import { statSync } from "node:fs";
3
+
1
4
  // ../firewall/dist/util.js
2
5
  function stableStringify(obj) {
3
6
  if (obj === null || typeof obj !== "object")
@@ -189,6 +192,19 @@ var DevFidacyCore = class {
189
192
  this.spent += r.amount;
190
193
  }
191
194
  }
195
+ /**
196
+ * Swap the active mandate (shell config hot-reload: edit config.json, the next
197
+ * call picks it up, no host restart). Decision state is REBUILT from the audit
198
+ * log under the new window/currency, so a reload can never reset the BEC
199
+ * invoice dedup or undercount spend: a paid invoice stays paid, spent is
200
+ * recomputed.
201
+ */
202
+ setMandate(m) {
203
+ this.mandate = m;
204
+ this.spent = 0;
205
+ this.claimedInvoices.clear();
206
+ this.rehydrate();
207
+ }
192
208
  async getMandate() {
193
209
  return this.mandate;
194
210
  }
@@ -331,6 +347,9 @@ var FileAuditStore = class {
331
347
  const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
332
348
  const hash = sha256(`${prevHash}|${digest}|${seq}|${ts}`);
333
349
  const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts };
350
+ if (typeof decision.request?.purpose === "string" && decision.request.purpose.trim()) {
351
+ record2.purpose = decision.request.purpose.slice(0, 500);
352
+ }
334
353
  if (decision.status === "ALLOW") {
335
354
  const req = decision.request;
336
355
  if (typeof req?.amount === "number" && Number.isFinite(req.amount))
@@ -423,7 +442,7 @@ function resolveMandateRules(cfg) {
423
442
  }
424
443
 
425
444
  // src/telemetry.ts
426
- var CLIENT_VERSION = true ? "0.1.16" : "dev";
445
+ var CLIENT_VERSION = true ? "0.1.18" : "dev";
427
446
  var currentShell = "mcp";
428
447
  function telemetryEnabled() {
429
448
  const v = (process.env.FIDACY_DISABLE_TELEMETRY ?? "").trim().toLowerCase();
@@ -525,11 +544,32 @@ function makeCore() {
525
544
  if (!url || !key) throw new Error("FIDACY_MODE=http requires FIDACY_API_URL and FIDACY_API_KEY");
526
545
  return new HttpFidacyCore(url, key, process.env.FIDACY_PUBLIC_KEY_PEM ?? "");
527
546
  }
528
- return new DevFidacyCore({
547
+ const dev = new DevFidacyCore({
529
548
  mandate: buildMandate(),
530
549
  auditLogPath: auditLogPath(),
531
550
  onDecision: (d) => recordDecision(d.status, d.violatedRule)
532
551
  });
552
+ const mtimeOf = () => statSync(configPath(), { throwIfNoEntry: false })?.mtimeMs ?? 0;
553
+ let seenMtime = mtimeOf();
554
+ const maybeReload = () => {
555
+ const m = mtimeOf();
556
+ if (m !== seenMtime) {
557
+ seenMtime = m;
558
+ dev.setMandate(buildMandate());
559
+ }
560
+ };
561
+ return {
562
+ getMandate: () => {
563
+ maybeReload();
564
+ return dev.getMandate();
565
+ },
566
+ decide: (req, subject) => {
567
+ maybeReload();
568
+ return dev.decide(req, subject);
569
+ },
570
+ getProof: (decisionId) => dev.getProof(decisionId),
571
+ publicKey: () => dev.publicKey()
572
+ };
533
573
  }
534
574
  export {
535
575
  makeCore
package/dist/index.js CHANGED
@@ -5,6 +5,9 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { z } from "zod";
7
7
 
8
+ // src/core.ts
9
+ import { statSync } from "node:fs";
10
+
8
11
  // ../firewall/dist/util.js
9
12
  function stableStringify(obj) {
10
13
  if (obj === null || typeof obj !== "object")
@@ -196,6 +199,19 @@ var DevFidacyCore = class {
196
199
  this.spent += r.amount;
197
200
  }
198
201
  }
202
+ /**
203
+ * Swap the active mandate (shell config hot-reload: edit config.json, the next
204
+ * call picks it up, no host restart). Decision state is REBUILT from the audit
205
+ * log under the new window/currency, so a reload can never reset the BEC
206
+ * invoice dedup or undercount spend: a paid invoice stays paid, spent is
207
+ * recomputed.
208
+ */
209
+ setMandate(m) {
210
+ this.mandate = m;
211
+ this.spent = 0;
212
+ this.claimedInvoices.clear();
213
+ this.rehydrate();
214
+ }
199
215
  async getMandate() {
200
216
  return this.mandate;
201
217
  }
@@ -338,6 +354,9 @@ var FileAuditStore = class {
338
354
  const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
339
355
  const hash = sha256(`${prevHash}|${digest}|${seq}|${ts}`);
340
356
  const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts };
357
+ if (typeof decision.request?.purpose === "string" && decision.request.purpose.trim()) {
358
+ record2.purpose = decision.request.purpose.slice(0, 500);
359
+ }
341
360
  if (decision.status === "ALLOW") {
342
361
  const req = decision.request;
343
362
  if (typeof req?.amount === "number" && Number.isFinite(req.amount))
@@ -452,7 +471,7 @@ function resolveMandateRules(cfg) {
452
471
  }
453
472
 
454
473
  // src/telemetry.ts
455
- var CLIENT_VERSION = true ? "0.1.16" : "dev";
474
+ var CLIENT_VERSION = true ? "0.1.18" : "dev";
456
475
  var currentShell = "mcp";
457
476
  function telemetryEnabled() {
458
477
  const v = (process.env.FIDACY_DISABLE_TELEMETRY ?? "").trim().toLowerCase();
@@ -557,11 +576,32 @@ function makeCore() {
557
576
  if (!url || !key) throw new Error("FIDACY_MODE=http requires FIDACY_API_URL and FIDACY_API_KEY");
558
577
  return new HttpFidacyCore(url, key, process.env.FIDACY_PUBLIC_KEY_PEM ?? "");
559
578
  }
560
- return new DevFidacyCore({
579
+ const dev = new DevFidacyCore({
561
580
  mandate: buildMandate(),
562
581
  auditLogPath: auditLogPath(),
563
582
  onDecision: (d) => recordDecision(d.status, d.violatedRule)
564
583
  });
584
+ const mtimeOf = () => statSync(configPath(), { throwIfNoEntry: false })?.mtimeMs ?? 0;
585
+ let seenMtime = mtimeOf();
586
+ const maybeReload = () => {
587
+ const m = mtimeOf();
588
+ if (m !== seenMtime) {
589
+ seenMtime = m;
590
+ dev.setMandate(buildMandate());
591
+ }
592
+ };
593
+ return {
594
+ getMandate: () => {
595
+ maybeReload();
596
+ return dev.getMandate();
597
+ },
598
+ decide: (req, subject2) => {
599
+ maybeReload();
600
+ return dev.decide(req, subject2);
601
+ },
602
+ getProof: (decisionId) => dev.getProof(decisionId),
603
+ publicKey: () => dev.publicKey()
604
+ };
565
605
  }
566
606
 
567
607
  // src/assess.ts
@@ -654,7 +694,7 @@ async function postOnce(fetchImpl, url, headers, payload, timeoutMs) {
654
694
  body: payload,
655
695
  signal: controller.signal
656
696
  });
657
- } catch (err) {
697
+ } catch {
658
698
  const aborted = controller.signal.aborted;
659
699
  throw new AssessError({ type: aborted ? "timeout" : "network_error", status: 0 });
660
700
  } finally {
@@ -746,7 +786,7 @@ async function findArtifacts(sha2562, cfg) {
746
786
  }
747
787
 
748
788
  // src/provision.ts
749
- var CLIENT_VERSION2 = true ? "0.1.16" : "dev";
789
+ var CLIENT_VERSION2 = true ? "0.1.18" : "dev";
750
790
  function provisionEnabled() {
751
791
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
752
792
  return !(v === "1" || v === "true" || v === "yes");
@@ -846,7 +886,7 @@ function weekOneBootNudge(upgradeToolName) {
846
886
  var state = ensureState();
847
887
  var core = makeCore();
848
888
  var subject = process.env.FIDACY_SUBJECT ?? "agent:demo";
849
- var SERVER_VERSION = true ? "0.1.16" : "dev";
889
+ var SERVER_VERSION = true ? "0.1.18" : "dev";
850
890
  var server = new McpServer({ name: "fidacy", version: SERVER_VERSION });
851
891
  server.registerTool(
852
892
  "request_payment",
@@ -857,7 +897,9 @@ server.registerTool(
857
897
  payee: z.string().describe("Payee identifier"),
858
898
  amount: z.number().positive().describe("Amount in the mandate currency"),
859
899
  currency: z.string().length(3).describe("ISO 4217 currency code"),
860
- purpose: z.string().describe("Human-readable purpose"),
900
+ purpose: z.string().describe(
901
+ "Your stated reason for this payment, in your own words. It is persisted readably on the tamper-evident audit record (ALLOW and DENY alike) and becomes part of the after-the-fact proof, so write the real reason."
902
+ ),
861
903
  category: z.string().describe("Purpose category (must be allowed by the mandate)"),
862
904
  idempotencyKey: z.string().describe("Caller-supplied idempotency key"),
863
905
  invoiceRef: z.string().describe(
@@ -877,7 +919,7 @@ server.registerTool(
877
919
  const d = await core.decide(req, subject);
878
920
  const out = { status: d.status, decisionId: d.decisionId, grant: d.grant, violatedRule: d.violatedRule };
879
921
  const base = d.status === "ALLOW" ? `ALLOW (decision ${d.decisionId})${req.invoiceRef ? ` for invoice ${req.invoiceRef}` : ""}. To settle, call execute_payment with the SAME payee, amount, currency, and idempotencyKey, and set "grant" to EXACTLY this signed value:
880
- ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}. No grant issued, this payment cannot proceed. The denial itself is recorded in the tamper-evident, hash-chained audit: call get_audit_proof with decisionId ${d.decisionId} for the proof of what was blocked.`;
922
+ ${d.grant}` : `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}. No grant issued, this payment cannot proceed. The denial itself is recorded in the tamper-evident, hash-chained audit: call get_audit_proof with decisionId ${d.decisionId} for the proof of what was blocked.${d.violatedRule?.startsWith("payee_not_in_allowlist") ? ` If the user trusts this payee, add "${req.payee}" to mandate.payees in ~/.fidacy/config.json and retry: the firewall picks the change up on the next call, no restart needed.` : ""}`;
881
923
  const nudge = decisionNudge(d.status, d.violatedRule, "upgrade");
882
924
  const human = nudge ? `${base} ${nudge}` : base;
883
925
  return { content: [{ type: "text", text: human }], structuredContent: out };
package/dist/lib.js CHANGED
@@ -230,6 +230,19 @@ var DevFidacyCore = class {
230
230
  this.spent += r.amount;
231
231
  }
232
232
  }
233
+ /**
234
+ * Swap the active mandate (shell config hot-reload: edit config.json, the next
235
+ * call picks it up, no host restart). Decision state is REBUILT from the audit
236
+ * log under the new window/currency, so a reload can never reset the BEC
237
+ * invoice dedup or undercount spend: a paid invoice stays paid, spent is
238
+ * recomputed.
239
+ */
240
+ setMandate(m) {
241
+ this.mandate = m;
242
+ this.spent = 0;
243
+ this.claimedInvoices.clear();
244
+ this.rehydrate();
245
+ }
233
246
  async getMandate() {
234
247
  return this.mandate;
235
248
  }
@@ -372,6 +385,9 @@ var FileAuditStore = class {
372
385
  const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
373
386
  const hash = sha256(`${prevHash}|${digest}|${seq}|${ts}`);
374
387
  const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts };
388
+ if (typeof decision.request?.purpose === "string" && decision.request.purpose.trim()) {
389
+ record2.purpose = decision.request.purpose.slice(0, 500);
390
+ }
375
391
  if (decision.status === "ALLOW") {
376
392
  const req = decision.request;
377
393
  if (typeof req?.amount === "number" && Number.isFinite(req.amount))
@@ -444,6 +460,10 @@ var GrantEnforcingExecutor = class {
444
460
  this.rail = rail;
445
461
  this.pub = createPublicKey(publicKeyPem2);
446
462
  this.redeem = opts?.redeem;
463
+ const isProd = typeof process !== "undefined" && process.env?.NODE_ENV === "production";
464
+ if (isProd && !this.redeem && !opts?.allowUnsafeInMemorySingleUse) {
465
+ throw new Error("GrantEnforcingExecutor in production requires a durable `redeem` (single-use across instances). Wire httpRedeem(coreUrl, apiKey), or pass allowUnsafeInMemorySingleUse:true only for a true single-process deployment.");
466
+ }
447
467
  }
448
468
  async execute(req, grant) {
449
469
  if (!grant)
@@ -490,6 +510,9 @@ var GrantEnforcingExecutor = class {
490
510
  }
491
511
  };
492
512
 
513
+ // src/core.ts
514
+ import { statSync } from "node:fs";
515
+
493
516
  // src/config.ts
494
517
  import { randomUUID as randomUUID3 } from "node:crypto";
495
518
  import { homedir } from "node:os";
@@ -571,7 +594,7 @@ function resolveMandateRules(cfg) {
571
594
  }
572
595
 
573
596
  // src/telemetry.ts
574
- var CLIENT_VERSION = true ? "0.1.16" : "dev";
597
+ var CLIENT_VERSION = true ? "0.1.18" : "dev";
575
598
  var currentShell = "mcp";
576
599
  function setTelemetryShell(shell) {
577
600
  currentShell = shell;
@@ -679,11 +702,32 @@ function makeCore() {
679
702
  if (!url || !key) throw new Error("FIDACY_MODE=http requires FIDACY_API_URL and FIDACY_API_KEY");
680
703
  return new HttpFidacyCore(url, key, process.env.FIDACY_PUBLIC_KEY_PEM ?? "");
681
704
  }
682
- return new DevFidacyCore({
705
+ const dev = new DevFidacyCore({
683
706
  mandate: buildMandate(),
684
707
  auditLogPath: auditLogPath(),
685
708
  onDecision: (d) => recordDecision(d.status, d.violatedRule)
686
709
  });
710
+ const mtimeOf = () => statSync(configPath(), { throwIfNoEntry: false })?.mtimeMs ?? 0;
711
+ let seenMtime = mtimeOf();
712
+ const maybeReload = () => {
713
+ const m = mtimeOf();
714
+ if (m !== seenMtime) {
715
+ seenMtime = m;
716
+ dev.setMandate(buildMandate());
717
+ }
718
+ };
719
+ return {
720
+ getMandate: () => {
721
+ maybeReload();
722
+ return dev.getMandate();
723
+ },
724
+ decide: (req, subject) => {
725
+ maybeReload();
726
+ return dev.decide(req, subject);
727
+ },
728
+ getProof: (decisionId) => dev.getProof(decisionId),
729
+ publicKey: () => dev.publicKey()
730
+ };
687
731
  }
688
732
 
689
733
  // src/upgrade.ts
@@ -699,7 +743,7 @@ function requestUpgrade() {
699
743
  }
700
744
 
701
745
  // src/provision.ts
702
- var CLIENT_VERSION2 = true ? "0.1.16" : "dev";
746
+ var CLIENT_VERSION2 = true ? "0.1.18" : "dev";
703
747
  function provisionEnabled() {
704
748
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
705
749
  return !(v === "1" || v === "true" || v === "yes");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fidacy/mcp",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Fidacy action firewall for AI agents. Mandate-gated payment authorization as an MCP server.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://fidacy.com",