@fidacy/mcp 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assess.js +110 -129
- package/dist/core.js +406 -147
- package/dist/index.js +719 -103
- package/dist/lib.js +527 -5
- package/package.json +19 -11
- package/dist/assess.d.ts +0 -78
- package/dist/assess.js.map +0 -1
- package/dist/audit-store.d.ts +0 -11
- package/dist/audit-store.js +0 -51
- package/dist/audit-store.js.map +0 -1
- package/dist/core.d.ts +0 -34
- package/dist/core.js.map +0 -1
- package/dist/executor.d.ts +0 -30
- package/dist/executor.js +0 -64
- package/dist/executor.js.map +0 -1
- package/dist/grant.d.ts +0 -20
- package/dist/grant.js +0 -41
- package/dist/grant.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js.map +0 -1
- package/dist/lib.d.ts +0 -4
- package/dist/lib.js.map +0 -1
- package/dist/signing.d.ts +0 -10
- package/dist/signing.js +0 -32
- package/dist/signing.js.map +0 -1
- package/dist/types.d.ts +0 -52
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/util.d.ts +0 -1
- package/dist/util.js +0 -9
- package/dist/util.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,143 +1,759 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
2
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
6
|
import { z } from "zod";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
|
|
8
|
+
// ../firewall/dist/util.js
|
|
9
|
+
function stableStringify(obj) {
|
|
10
|
+
if (obj === null || typeof obj !== "object")
|
|
11
|
+
return JSON.stringify(obj);
|
|
12
|
+
if (Array.isArray(obj))
|
|
13
|
+
return "[" + obj.map(stableStringify).join(",") + "]";
|
|
14
|
+
const keys = Object.keys(obj).sort();
|
|
15
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ../firewall/dist/signing.js
|
|
19
|
+
import crypto from "node:crypto";
|
|
20
|
+
function loadOrGenerateKeyPair() {
|
|
21
|
+
const b64 = process.env.FIDACY_SIGNING_KEY_B64;
|
|
22
|
+
if (b64) {
|
|
23
|
+
const pem = Buffer.from(b64, "base64").toString("utf8");
|
|
24
|
+
const privateKey2 = crypto.createPrivateKey(pem);
|
|
25
|
+
const publicKey2 = crypto.createPublicKey(privateKey2);
|
|
26
|
+
return { privateKey: privateKey2, publicKey: publicKey2, ephemeral: false };
|
|
27
|
+
}
|
|
28
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
|
|
29
|
+
return { privateKey, publicKey, ephemeral: true };
|
|
30
|
+
}
|
|
31
|
+
function publicKeyPem(publicKey) {
|
|
32
|
+
return publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
33
|
+
}
|
|
34
|
+
function sign(privateKey, message) {
|
|
35
|
+
return crypto.sign(null, Buffer.from(message, "utf8"), privateKey).toString("base64url");
|
|
36
|
+
}
|
|
37
|
+
function sha256(input) {
|
|
38
|
+
return crypto.createHash("sha256").update(input).digest("hex");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ../firewall/dist/audit-store.js
|
|
42
|
+
import fs from "node:fs";
|
|
43
|
+
var FileAuditStore = class {
|
|
44
|
+
path;
|
|
45
|
+
chain = [];
|
|
46
|
+
constructor(path) {
|
|
47
|
+
this.path = path;
|
|
48
|
+
this.load();
|
|
49
|
+
}
|
|
50
|
+
load() {
|
|
51
|
+
if (!fs.existsSync(this.path))
|
|
52
|
+
return;
|
|
53
|
+
try {
|
|
54
|
+
const raw = fs.readFileSync(this.path, "utf8");
|
|
55
|
+
const parsed = [];
|
|
56
|
+
for (const line of raw.split("\n")) {
|
|
57
|
+
if (!line.trim())
|
|
58
|
+
continue;
|
|
59
|
+
parsed.push(JSON.parse(line));
|
|
60
|
+
}
|
|
61
|
+
this.chain = parsed;
|
|
62
|
+
if (!this.intact())
|
|
63
|
+
throw new Error("audit chain integrity broken");
|
|
64
|
+
} catch (err) {
|
|
65
|
+
this.chain = [];
|
|
66
|
+
try {
|
|
67
|
+
const quarantine = `${this.path}.corrupt-${Date.now()}`;
|
|
68
|
+
fs.renameSync(this.path, quarantine);
|
|
69
|
+
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered (${err.message}); quarantined to ${quarantine}, starting a fresh chain.`);
|
|
70
|
+
} catch {
|
|
71
|
+
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered and could not be quarantined; starting a fresh in-memory chain.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
head() {
|
|
76
|
+
return this.chain.length ? this.chain[this.chain.length - 1].hash : "GENESIS";
|
|
77
|
+
}
|
|
78
|
+
append(decision) {
|
|
79
|
+
const prevHash = this.head();
|
|
80
|
+
const seq = this.chain.length;
|
|
81
|
+
const ts = decision.ts;
|
|
82
|
+
const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
|
|
83
|
+
const hash = sha256(`${prevHash}|${digest}|${seq}|${ts}`);
|
|
84
|
+
const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts };
|
|
85
|
+
fs.appendFileSync(this.path, JSON.stringify(record2) + "\n");
|
|
86
|
+
this.chain.push(record2);
|
|
87
|
+
return record2;
|
|
88
|
+
}
|
|
89
|
+
find(decisionId) {
|
|
90
|
+
return this.chain.find((r) => r.decisionId === decisionId);
|
|
91
|
+
}
|
|
92
|
+
intact() {
|
|
93
|
+
let prev = "GENESIS";
|
|
94
|
+
for (const r of this.chain) {
|
|
95
|
+
const expected = sha256(`${prev}|${r.digest}|${r.seq}|${r.ts}`);
|
|
96
|
+
if (expected !== r.hash || r.prevHash !== prev)
|
|
97
|
+
return false;
|
|
98
|
+
prev = r.hash;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// ../firewall/dist/evaluate.js
|
|
105
|
+
function evaluate(mandate, req, spentSoFar) {
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
if (mandate.revoked)
|
|
108
|
+
return "mandate_revoked";
|
|
109
|
+
if (now < Date.parse(mandate.window.notBefore))
|
|
110
|
+
return "before_mandate_window";
|
|
111
|
+
if (now > Date.parse(mandate.window.notAfter))
|
|
112
|
+
return "after_mandate_window";
|
|
113
|
+
if (req.currency !== mandate.allow.currency)
|
|
114
|
+
return `currency_not_allowed:${req.currency}`;
|
|
115
|
+
if (req.amount <= 0)
|
|
116
|
+
return "non_positive_amount";
|
|
117
|
+
if (req.amount > mandate.allow.perTxMax)
|
|
118
|
+
return `per_tx_cap_exceeded:${req.amount}>${mandate.allow.perTxMax}`;
|
|
119
|
+
if (spentSoFar + req.amount > mandate.allow.maxTotal)
|
|
120
|
+
return `total_cap_exceeded:${spentSoFar + req.amount}>${mandate.allow.maxTotal}`;
|
|
121
|
+
const payeeOk = mandate.allow.payees.includes("*") || mandate.allow.payees.includes(req.payee);
|
|
122
|
+
if (!payeeOk)
|
|
123
|
+
return `payee_not_in_allowlist:${req.payee}`;
|
|
124
|
+
const catOk = mandate.allow.categories.includes("*") || mandate.allow.categories.includes(req.category);
|
|
125
|
+
if (!catOk)
|
|
126
|
+
return `category_not_allowed:${req.category}`;
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ../firewall/dist/core.js
|
|
131
|
+
import { randomUUID } from "node:crypto";
|
|
132
|
+
function canonInvoice(ref) {
|
|
133
|
+
if (typeof ref !== "string")
|
|
134
|
+
return "";
|
|
135
|
+
return ref.normalize("NFC").replace(/[\s\u200B\u200C\u200D\uFEFF]+/g, "").toLowerCase();
|
|
136
|
+
}
|
|
137
|
+
function validateRequest(req) {
|
|
138
|
+
if (!req || typeof req !== "object")
|
|
139
|
+
return "invalid_request";
|
|
140
|
+
if (typeof req.amount !== "number" || !Number.isFinite(req.amount) || req.amount <= 0)
|
|
141
|
+
return "invalid_amount";
|
|
142
|
+
if (typeof req.payee !== "string" || req.payee.length === 0)
|
|
143
|
+
return "invalid_payee";
|
|
144
|
+
if (typeof req.currency !== "string" || req.currency.length === 0)
|
|
145
|
+
return "invalid_currency";
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
function requireHttpsBase(raw) {
|
|
149
|
+
const trimmed = String(raw ?? "").replace(/\/+$/, "");
|
|
150
|
+
let u;
|
|
151
|
+
try {
|
|
152
|
+
u = new URL(trimmed);
|
|
153
|
+
} catch {
|
|
154
|
+
throw new Error("FIDACY_API_URL is not a valid URL");
|
|
155
|
+
}
|
|
156
|
+
const localHttp = u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]");
|
|
157
|
+
if (u.protocol !== "https:" && !localHttp) {
|
|
158
|
+
throw new Error("FIDACY_API_URL must be https:// (the API key is sent to it)");
|
|
159
|
+
}
|
|
160
|
+
return trimmed;
|
|
161
|
+
}
|
|
162
|
+
var DevFidacyCore = class {
|
|
163
|
+
priv;
|
|
164
|
+
pubPem;
|
|
165
|
+
mandate;
|
|
166
|
+
store;
|
|
167
|
+
spent = 0;
|
|
168
|
+
claimedInvoices = /* @__PURE__ */ new Set();
|
|
169
|
+
onDecision;
|
|
170
|
+
constructor(opts) {
|
|
171
|
+
const kp = loadOrGenerateKeyPair();
|
|
172
|
+
this.priv = kp.privateKey;
|
|
173
|
+
this.pubPem = publicKeyPem(kp.publicKey);
|
|
174
|
+
if (kp.ephemeral)
|
|
175
|
+
console.error("[fidacy] dev mode: ephemeral signing key. Production: set FIDACY_SIGNING_KEY_B64, or FIDACY_MODE=http + FIDACY_API_URL + FIDACY_API_KEY to use the live core.");
|
|
176
|
+
this.mandate = opts.mandate;
|
|
177
|
+
this.store = new FileAuditStore(opts.auditLogPath ?? "./fidacy-audit.log");
|
|
178
|
+
this.onDecision = opts.onDecision;
|
|
179
|
+
}
|
|
180
|
+
async getMandate() {
|
|
181
|
+
return this.mandate;
|
|
182
|
+
}
|
|
183
|
+
async decide(req, subject2) {
|
|
184
|
+
const decisionId = randomUUID();
|
|
185
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
186
|
+
const invalid = validateRequest(req);
|
|
187
|
+
if (invalid) {
|
|
188
|
+
const decision2 = { decisionId, status: "DENY", subject: subject2, mandateId: this.mandate.id, request: req, violatedRule: invalid, ts };
|
|
189
|
+
this.store.append(decision2);
|
|
190
|
+
this.onDecision?.(decision2);
|
|
191
|
+
return decision2;
|
|
192
|
+
}
|
|
193
|
+
const violated = evaluate(this.mandate, req, this.spent);
|
|
194
|
+
if (violated) {
|
|
195
|
+
const decision2 = { decisionId, status: "DENY", subject: subject2, mandateId: this.mandate.id, request: req, violatedRule: violated, ts };
|
|
196
|
+
this.store.append(decision2);
|
|
197
|
+
this.onDecision?.(decision2);
|
|
198
|
+
return decision2;
|
|
199
|
+
}
|
|
200
|
+
const invoice = canonInvoice(req.invoiceRef);
|
|
201
|
+
if (invoice) {
|
|
202
|
+
const k = `${subject2}|${invoice}`;
|
|
203
|
+
if (this.claimedInvoices.has(k)) {
|
|
204
|
+
const decision2 = { decisionId, status: "DENY", subject: subject2, mandateId: this.mandate.id, request: req, violatedRule: `duplicate_invoice:${invoice}`, ts };
|
|
205
|
+
this.store.append(decision2);
|
|
206
|
+
this.onDecision?.(decision2);
|
|
207
|
+
return decision2;
|
|
208
|
+
}
|
|
209
|
+
this.claimedInvoices.add(k);
|
|
210
|
+
}
|
|
211
|
+
const grantPayload = { decisionId, subject: subject2, payee: req.payee, amount: req.amount, currency: req.currency, exp: Date.now() + 12e4, ...req.invoiceRef ? { invoiceRef: req.invoiceRef } : {} };
|
|
212
|
+
const grantBody = Buffer.from(stableStringify(grantPayload), "utf8").toString("base64url");
|
|
213
|
+
const grant = `${grantBody}.${sign(this.priv, grantBody)}`;
|
|
214
|
+
const decision = { decisionId, status: "ALLOW", subject: subject2, mandateId: this.mandate.id, request: req, grant, ts };
|
|
215
|
+
this.spent += req.amount;
|
|
216
|
+
this.store.append(decision);
|
|
217
|
+
this.onDecision?.(decision);
|
|
218
|
+
return decision;
|
|
219
|
+
}
|
|
220
|
+
async getProof(decisionId) {
|
|
221
|
+
const record2 = this.store.find(decisionId);
|
|
222
|
+
if (!record2)
|
|
223
|
+
return null;
|
|
224
|
+
return { record: record2, chainIntact: this.store.intact(), verifiedAgainstPublicKey: this.pubPem };
|
|
225
|
+
}
|
|
226
|
+
publicKey() {
|
|
227
|
+
return this.pubPem;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
var HttpFidacyCore = class {
|
|
231
|
+
apiKey;
|
|
232
|
+
subjectPub;
|
|
233
|
+
baseUrl;
|
|
234
|
+
// Enforce https before any key-bearing request — a hostile FIDACY_API_URL would
|
|
235
|
+
// otherwise exfiltrate the API key in the Authorization header.
|
|
236
|
+
constructor(baseUrl, apiKey, subjectPub = "") {
|
|
237
|
+
this.apiKey = apiKey;
|
|
238
|
+
this.subjectPub = subjectPub;
|
|
239
|
+
this.baseUrl = requireHttpsBase(baseUrl);
|
|
240
|
+
}
|
|
241
|
+
async call(path, body) {
|
|
242
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
243
|
+
method: "POST",
|
|
244
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${this.apiKey}` },
|
|
245
|
+
body: JSON.stringify(body)
|
|
246
|
+
});
|
|
247
|
+
if (!res.ok)
|
|
248
|
+
throw new Error(`fidacy core ${path} -> ${res.status}`);
|
|
249
|
+
return await res.json();
|
|
250
|
+
}
|
|
251
|
+
async getMandate(subject2) {
|
|
252
|
+
return this.call("/v1/mandate/get", { subject: subject2 });
|
|
253
|
+
}
|
|
254
|
+
async decide(req, subject2) {
|
|
255
|
+
return this.call("/v1/decide", { req, subject: subject2 });
|
|
256
|
+
}
|
|
257
|
+
async getProof(decisionId) {
|
|
258
|
+
return this.call("/v1/audit/proof", { decisionId });
|
|
259
|
+
}
|
|
260
|
+
publicKey() {
|
|
261
|
+
return this.subjectPub;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// src/config.ts
|
|
266
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
267
|
+
import { homedir } from "node:os";
|
|
268
|
+
import { join } from "node:path";
|
|
269
|
+
import {
|
|
270
|
+
existsSync,
|
|
271
|
+
mkdirSync,
|
|
272
|
+
readFileSync,
|
|
273
|
+
writeFileSync
|
|
274
|
+
} from "node:fs";
|
|
275
|
+
function configDir() {
|
|
276
|
+
return process.env.FIDACY_CONFIG_DIR ?? join(homedir(), ".fidacy");
|
|
277
|
+
}
|
|
278
|
+
function configPath() {
|
|
279
|
+
return join(configDir(), "config.json");
|
|
280
|
+
}
|
|
281
|
+
function auditLogPath() {
|
|
282
|
+
if (process.env.FIDACY_AUDIT_PATH) return process.env.FIDACY_AUDIT_PATH;
|
|
283
|
+
const dir = join(configDir(), "audit");
|
|
284
|
+
try {
|
|
285
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
return join(dir, "audit.log");
|
|
289
|
+
}
|
|
290
|
+
function readConfig() {
|
|
291
|
+
const p = configPath();
|
|
292
|
+
if (!existsSync(p)) return null;
|
|
293
|
+
try {
|
|
294
|
+
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
295
|
+
if (!raw || typeof raw.anon_id !== "string") return null;
|
|
296
|
+
return {
|
|
297
|
+
anon_id: raw.anon_id,
|
|
298
|
+
tier: raw.tier === "paid" ? "paid" : "free",
|
|
299
|
+
api_key: typeof raw.api_key === "string" ? raw.api_key : null,
|
|
300
|
+
mandate: raw.mandate
|
|
301
|
+
};
|
|
302
|
+
} catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function writeConfig(cfg) {
|
|
307
|
+
const dir = configDir();
|
|
308
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
309
|
+
writeFileSync(configPath(), JSON.stringify(cfg, null, 2), { mode: 384 });
|
|
310
|
+
}
|
|
311
|
+
function ensureState() {
|
|
312
|
+
const existing = readConfig();
|
|
313
|
+
if (existing) return { config: existing, firstRun: false };
|
|
314
|
+
const config = { anon_id: randomUUID2(), tier: "free", api_key: null };
|
|
315
|
+
try {
|
|
316
|
+
writeConfig(config);
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
return { config, firstRun: true };
|
|
320
|
+
}
|
|
321
|
+
function resolveMandateRules(cfg) {
|
|
322
|
+
const m = cfg?.mandate ?? {};
|
|
323
|
+
const envList = (v) => v === void 0 ? void 0 : v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
324
|
+
const envNum = (v) => v === void 0 || v === "" ? void 0 : Number(v);
|
|
325
|
+
return {
|
|
326
|
+
payees: envList(process.env.FIDACY_ALLOW_PAYEES) ?? m.payees ?? [],
|
|
327
|
+
categories: envList(process.env.FIDACY_ALLOW_CATEGORIES) ?? m.categories ?? ["*"],
|
|
328
|
+
currency: process.env.FIDACY_CURRENCY ?? m.currency ?? "USD",
|
|
329
|
+
perTxMax: envNum(process.env.FIDACY_PER_TX_MAX) ?? m.perTxMax ?? 2500,
|
|
330
|
+
maxTotal: envNum(process.env.FIDACY_MAX_TOTAL) ?? m.maxTotal ?? 1e4
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/telemetry.ts
|
|
335
|
+
var CLIENT_VERSION = "0.1.5";
|
|
336
|
+
function telemetryEnabled() {
|
|
337
|
+
const v = (process.env.FIDACY_DISABLE_TELEMETRY ?? "").trim().toLowerCase();
|
|
338
|
+
return !(v === "1" || v === "true" || v === "yes");
|
|
339
|
+
}
|
|
340
|
+
function endpoint() {
|
|
341
|
+
const base = (process.env.FIDACY_ENGINE_URL ?? "https://api.fidacy.com").replace(/\/$/, "");
|
|
342
|
+
return `${base}/v1/telemetry`;
|
|
343
|
+
}
|
|
344
|
+
var anonIdCache = null;
|
|
345
|
+
function anonId() {
|
|
346
|
+
if (anonIdCache) return anonIdCache;
|
|
347
|
+
anonIdCache = readConfig()?.anon_id ?? null;
|
|
348
|
+
return anonIdCache;
|
|
349
|
+
}
|
|
350
|
+
var buffer = [];
|
|
351
|
+
var flushTimer = null;
|
|
352
|
+
function resultOf(status, violatedRule) {
|
|
353
|
+
if (status === "ALLOW") return "allow";
|
|
354
|
+
const r = violatedRule ?? "";
|
|
355
|
+
if (r.startsWith("per_tx_cap") || r.startsWith("total_cap")) return "deny_cap";
|
|
356
|
+
if (r.startsWith("payee_not_in_allowlist")) return "deny_payee";
|
|
357
|
+
return "deny_scope";
|
|
358
|
+
}
|
|
359
|
+
function record(type, result) {
|
|
360
|
+
if (!telemetryEnabled()) return;
|
|
361
|
+
buffer.push({ type, ts: (/* @__PURE__ */ new Date()).toISOString(), client_version: CLIENT_VERSION, ...result ? { result } : {} });
|
|
362
|
+
if (!flushTimer) {
|
|
363
|
+
flushTimer = setTimeout(() => {
|
|
364
|
+
void flush();
|
|
365
|
+
}, 2e3);
|
|
366
|
+
if (typeof flushTimer.unref === "function") flushTimer.unref();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
var recordInstall = () => record("install");
|
|
370
|
+
var recordAgentActive = () => record("agent_active");
|
|
371
|
+
var recordDecision = (status, violatedRule) => record("decision", resultOf(status, violatedRule));
|
|
372
|
+
var recordUpgradeIntent = () => record("upgrade_intent");
|
|
373
|
+
async function flush() {
|
|
374
|
+
if (flushTimer) {
|
|
375
|
+
clearTimeout(flushTimer);
|
|
376
|
+
flushTimer = null;
|
|
377
|
+
}
|
|
378
|
+
if (!telemetryEnabled() || buffer.length === 0) return;
|
|
379
|
+
const id = anonId();
|
|
380
|
+
if (!id) {
|
|
381
|
+
buffer = [];
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const events = buffer;
|
|
385
|
+
buffer = [];
|
|
386
|
+
try {
|
|
387
|
+
await fetch(endpoint(), {
|
|
388
|
+
method: "POST",
|
|
389
|
+
headers: { "content-type": "application/json" },
|
|
390
|
+
body: JSON.stringify({ anon_id: id, events })
|
|
391
|
+
});
|
|
392
|
+
} catch {
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/core.ts
|
|
397
|
+
function buildMandate() {
|
|
398
|
+
if (process.env.FIDACY_MANDATE_JSON) {
|
|
399
|
+
try {
|
|
400
|
+
const parsed = JSON.parse(process.env.FIDACY_MANDATE_JSON);
|
|
401
|
+
if (parsed && typeof parsed === "object" && parsed.allow && parsed.window) return parsed;
|
|
402
|
+
console.error("[fidacy] FIDACY_MANDATE_JSON is missing allow/window; ignoring it and using the safe default mandate.");
|
|
403
|
+
} catch (err) {
|
|
404
|
+
console.error(`[fidacy] FIDACY_MANDATE_JSON is not valid JSON (${err.message}); ignoring it and using the safe default mandate.`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const subject2 = process.env.FIDACY_SUBJECT ?? "agent:demo";
|
|
408
|
+
const rules = resolveMandateRules(readConfig());
|
|
409
|
+
return {
|
|
410
|
+
id: "mandate:local",
|
|
411
|
+
subject: subject2,
|
|
412
|
+
version: "ap2.v0.2.0",
|
|
413
|
+
allow: {
|
|
414
|
+
payees: rules.payees,
|
|
415
|
+
categories: rules.categories,
|
|
416
|
+
currency: rules.currency,
|
|
417
|
+
maxTotal: rules.maxTotal,
|
|
418
|
+
perTxMax: rules.perTxMax
|
|
419
|
+
},
|
|
420
|
+
window: {
|
|
421
|
+
notBefore: process.env.FIDACY_NOT_BEFORE ?? new Date(Date.now() - 36e5).toISOString(),
|
|
422
|
+
notAfter: process.env.FIDACY_NOT_AFTER ?? new Date(Date.now() + 30 * 864e5).toISOString()
|
|
423
|
+
},
|
|
424
|
+
revoked: false
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function makeCore() {
|
|
428
|
+
if ((process.env.FIDACY_MODE ?? "dev") === "http") {
|
|
429
|
+
const url = process.env.FIDACY_API_URL;
|
|
430
|
+
const key = process.env.FIDACY_API_KEY;
|
|
431
|
+
if (!url || !key) throw new Error("FIDACY_MODE=http requires FIDACY_API_URL and FIDACY_API_KEY");
|
|
432
|
+
return new HttpFidacyCore(url, key, process.env.FIDACY_PUBLIC_KEY_PEM ?? "");
|
|
433
|
+
}
|
|
434
|
+
return new DevFidacyCore({
|
|
435
|
+
mandate: buildMandate(),
|
|
436
|
+
auditLogPath: auditLogPath(),
|
|
437
|
+
onDecision: (d) => recordDecision(d.status, d.violatedRule)
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/assess.ts
|
|
442
|
+
var AssessError = class extends Error {
|
|
443
|
+
type;
|
|
444
|
+
status;
|
|
445
|
+
details;
|
|
446
|
+
rejection_reasons;
|
|
447
|
+
constructor(opts) {
|
|
448
|
+
super(`Fidacy assess error (${opts.type}, HTTP ${opts.status})`);
|
|
449
|
+
this.name = "AssessError";
|
|
450
|
+
this.type = opts.type;
|
|
451
|
+
this.status = opts.status;
|
|
452
|
+
this.details = opts.details;
|
|
453
|
+
this.rejection_reasons = opts.rejection_reasons;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
457
|
+
var DEFAULT_RETRIES = 2;
|
|
458
|
+
var defaultBackoff = (n) => Math.min(200 * 2 ** n, 2e3);
|
|
459
|
+
function isRecord(v) {
|
|
460
|
+
return typeof v === "object" && v !== null;
|
|
461
|
+
}
|
|
462
|
+
function sleep(ms) {
|
|
463
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
464
|
+
}
|
|
465
|
+
function requireSafeBaseUrl(raw) {
|
|
466
|
+
const trimmed = String(raw ?? "").replace(/\/+$/, "");
|
|
467
|
+
let u;
|
|
468
|
+
try {
|
|
469
|
+
u = new URL(trimmed);
|
|
470
|
+
} catch {
|
|
471
|
+
throw new AssessError({ type: "config_error", status: 0 });
|
|
472
|
+
}
|
|
473
|
+
const isLocalHttp = u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]");
|
|
474
|
+
if (u.protocol !== "https:" && !isLocalHttp) {
|
|
475
|
+
throw new AssessError({ type: "config_error", status: 0 });
|
|
476
|
+
}
|
|
477
|
+
return trimmed;
|
|
478
|
+
}
|
|
479
|
+
async function assessAction(params, cfg) {
|
|
480
|
+
const fetchImpl = cfg.fetchImpl ?? globalThis.fetch;
|
|
481
|
+
if (typeof fetchImpl !== "function") {
|
|
482
|
+
throw new AssessError({ type: "config_error", status: 0 });
|
|
483
|
+
}
|
|
484
|
+
const baseUrl = requireSafeBaseUrl(cfg.engineUrl);
|
|
485
|
+
const url = `${baseUrl}/v1/assess`;
|
|
486
|
+
const timeoutMs = cfg.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
487
|
+
const maxRetries = cfg.maxRetries ?? DEFAULT_RETRIES;
|
|
488
|
+
const backoffMs = cfg.backoffMs ?? defaultBackoff;
|
|
489
|
+
const body = {
|
|
490
|
+
kind: params.kind ?? "ap2_payment",
|
|
491
|
+
mandate: params.mandate
|
|
492
|
+
};
|
|
493
|
+
if (params.mandateType !== void 0) body.mandateType = params.mandateType;
|
|
494
|
+
if (params.a2a !== void 0) body.a2a = params.a2a;
|
|
495
|
+
if (params.spendingMandate !== void 0) body.spending_mandate = params.spendingMandate;
|
|
496
|
+
if (params.idempotencyKey !== void 0) body.idempotency_key = params.idempotencyKey;
|
|
497
|
+
const payload = JSON.stringify(body);
|
|
498
|
+
if (payload.length > 256 * 1024) {
|
|
499
|
+
throw new AssessError({ type: "config_error", status: 0 });
|
|
500
|
+
}
|
|
501
|
+
const headers = {
|
|
502
|
+
"content-type": "application/json",
|
|
503
|
+
authorization: `Bearer ${cfg.apiKey}`
|
|
504
|
+
};
|
|
505
|
+
if (params.a2a !== void 0) headers["A2A-Version"] = "0.1.0";
|
|
506
|
+
const canRetryTransient = params.idempotencyKey !== void 0;
|
|
507
|
+
let attempt = 0;
|
|
508
|
+
for (; ; ) {
|
|
509
|
+
try {
|
|
510
|
+
return await postOnce(fetchImpl, url, headers, payload, timeoutMs);
|
|
511
|
+
} catch (err) {
|
|
512
|
+
const isTransient = err instanceof AssessError && (err.status === 0 || err.status === 429 || err.status >= 500);
|
|
513
|
+
const canRetry = isTransient && canRetryTransient && attempt < maxRetries;
|
|
514
|
+
if (!canRetry) throw err;
|
|
515
|
+
await sleep(backoffMs(attempt));
|
|
516
|
+
attempt += 1;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async function postOnce(fetchImpl, url, headers, payload, timeoutMs) {
|
|
521
|
+
const controller = new AbortController();
|
|
522
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
523
|
+
let res;
|
|
524
|
+
try {
|
|
525
|
+
res = await fetchImpl(url, {
|
|
526
|
+
method: "POST",
|
|
527
|
+
headers,
|
|
528
|
+
body: payload,
|
|
529
|
+
signal: controller.signal
|
|
530
|
+
});
|
|
531
|
+
} catch (err) {
|
|
532
|
+
const aborted = controller.signal.aborted;
|
|
533
|
+
throw new AssessError({ type: aborted ? "timeout" : "network_error", status: 0 });
|
|
534
|
+
} finally {
|
|
535
|
+
clearTimeout(timer);
|
|
536
|
+
}
|
|
537
|
+
const text = await res.text();
|
|
538
|
+
let parsed = void 0;
|
|
539
|
+
if (text) {
|
|
540
|
+
try {
|
|
541
|
+
parsed = JSON.parse(text);
|
|
542
|
+
} catch {
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (!res.ok) {
|
|
546
|
+
const type = isRecord(parsed) && typeof parsed.error === "string" && parsed.error || `http_${res.status}`;
|
|
547
|
+
const details = isRecord(parsed) ? parsed.details : void 0;
|
|
548
|
+
const rejection_reasons = isRecord(parsed) && Array.isArray(parsed.rejection_reasons) ? parsed.rejection_reasons : void 0;
|
|
549
|
+
throw new AssessError({ type, status: res.status, details, rejection_reasons });
|
|
550
|
+
}
|
|
551
|
+
if (!isRecord(parsed)) {
|
|
552
|
+
throw new AssessError({ type: "invalid_response", status: res.status });
|
|
553
|
+
}
|
|
554
|
+
return parsed;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/provision.ts
|
|
558
|
+
var CLIENT_VERSION2 = "0.1.5";
|
|
559
|
+
function provisionEnabled() {
|
|
560
|
+
const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
|
|
561
|
+
return !(v === "1" || v === "true" || v === "yes");
|
|
562
|
+
}
|
|
563
|
+
function endpoint2() {
|
|
564
|
+
const base = (process.env.FIDACY_ENGINE_URL ?? "https://api.fidacy.com").replace(/\/$/, "");
|
|
565
|
+
return `${base}/v1/provision`;
|
|
566
|
+
}
|
|
567
|
+
async function autoProvision() {
|
|
568
|
+
if (!provisionEnabled()) return "skipped";
|
|
569
|
+
const cfg = readConfig();
|
|
570
|
+
if (!cfg) return "skipped";
|
|
571
|
+
if (cfg.api_key) return "skipped";
|
|
572
|
+
try {
|
|
573
|
+
const res = await fetch(endpoint2(), {
|
|
574
|
+
method: "POST",
|
|
575
|
+
headers: { "content-type": "application/json" },
|
|
576
|
+
body: JSON.stringify({ anon_id: cfg.anon_id, client_version: CLIENT_VERSION2 })
|
|
577
|
+
});
|
|
578
|
+
if (res.status === 429) return "rate_limited";
|
|
579
|
+
if (!res.ok) return "failed";
|
|
580
|
+
const data = await res.json();
|
|
581
|
+
if (typeof data.api_key !== "string" || !data.api_key) return "failed";
|
|
582
|
+
writeConfig({
|
|
583
|
+
...cfg,
|
|
584
|
+
api_key: data.api_key,
|
|
585
|
+
tier: data.tier === "paid" ? "paid" : "free"
|
|
586
|
+
});
|
|
587
|
+
return "provisioned";
|
|
588
|
+
} catch {
|
|
589
|
+
return "failed";
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/upgrade.ts
|
|
594
|
+
function upgradeUrl() {
|
|
595
|
+
const base = (process.env.FIDACY_APP_URL ?? "https://app.fidacy.com").replace(/\/$/, "");
|
|
596
|
+
const anon = readConfig()?.anon_id ?? "";
|
|
597
|
+
return `${base}/upgrade?anon=${encodeURIComponent(anon)}`;
|
|
598
|
+
}
|
|
599
|
+
function requestUpgrade() {
|
|
600
|
+
recordUpgradeIntent();
|
|
601
|
+
const anonId2 = readConfig()?.anon_id ?? "";
|
|
602
|
+
return { url: upgradeUrl(), anonId: anonId2 };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/index.ts
|
|
606
|
+
var state = ensureState();
|
|
607
|
+
var core = makeCore();
|
|
608
|
+
var subject = process.env.FIDACY_SUBJECT ?? "agent:demo";
|
|
609
|
+
var server = new McpServer({ name: "fidacy", version: "0.1.0" });
|
|
610
|
+
server.registerTool(
|
|
611
|
+
"request_payment",
|
|
612
|
+
{
|
|
16
613
|
title: "Request Payment Authorization",
|
|
17
614
|
description: "Authorize a payment action against the active Fidacy mandate. Returns an ALLOW with a signed grant, or a DENY with the violated rule. The downstream executor MUST require the grant. Call this before any payment; never pay without it.",
|
|
18
615
|
inputSchema: {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.optional(),
|
|
616
|
+
payee: z.string().describe("Payee identifier"),
|
|
617
|
+
amount: z.number().positive().describe("Amount in the mandate currency"),
|
|
618
|
+
currency: z.string().length(3).describe("ISO 4217 currency code"),
|
|
619
|
+
purpose: z.string().describe("Human-readable purpose"),
|
|
620
|
+
category: z.string().describe("Purpose category (must be allowed by the mandate)"),
|
|
621
|
+
idempotencyKey: z.string().describe("Caller-supplied idempotency key"),
|
|
622
|
+
invoiceRef: z.string().describe(
|
|
623
|
+
"Optional invoice identity. When set, Fidacy enforces one payment per invoice: a second request for the same invoiceRef is DENIED, at any amount."
|
|
624
|
+
).optional()
|
|
29
625
|
},
|
|
30
626
|
outputSchema: {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
627
|
+
status: z.enum(["ALLOW", "DENY"]),
|
|
628
|
+
decisionId: z.string(),
|
|
629
|
+
grant: z.string().optional(),
|
|
630
|
+
violatedRule: z.string().optional()
|
|
35
631
|
},
|
|
36
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
37
|
-
},
|
|
632
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
633
|
+
},
|
|
634
|
+
async (args) => {
|
|
38
635
|
const req = args;
|
|
39
636
|
const d = await core.decide(req, subject);
|
|
40
637
|
const out = { status: d.status, decisionId: d.decisionId, grant: d.grant, violatedRule: d.violatedRule };
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// just the decisionId has no grant to present and the executor (correctly)
|
|
44
|
-
// refuses. We print the exact grant the executor expects.
|
|
45
|
-
const human = d.status === "ALLOW"
|
|
46
|
-
? `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:\n${d.grant}`
|
|
47
|
-
: `DENY (decision ${d.decisionId}). Rule violated: ${d.violatedRule}. No grant issued, this payment cannot proceed. The denial itself is recorded immutably in the hash-chained audit: call get_audit_proof with decisionId ${d.decisionId} for the proof of what was blocked.`;
|
|
638
|
+
const human = 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:
|
|
639
|
+
${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.`;
|
|
48
640
|
return { content: [{ type: "text", text: human }], structuredContent: out };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
641
|
+
}
|
|
642
|
+
);
|
|
643
|
+
server.registerTool(
|
|
644
|
+
"verify_mandate",
|
|
645
|
+
{
|
|
53
646
|
title: "Verify Active Mandate",
|
|
54
647
|
description: "Return the active mandate envelope (caps, allowed payees/categories, window, revocation) and Fidacy's Ed25519 public key for grant verification.",
|
|
55
648
|
inputSchema: {},
|
|
56
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
57
|
-
},
|
|
649
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
650
|
+
},
|
|
651
|
+
async () => {
|
|
58
652
|
const m = await core.getMandate(subject);
|
|
59
653
|
const payload = { mandate: m, fidacyPublicKey: core.publicKey() };
|
|
60
654
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], structuredContent: payload };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
655
|
+
}
|
|
656
|
+
);
|
|
657
|
+
server.registerTool(
|
|
658
|
+
"get_audit_proof",
|
|
659
|
+
{
|
|
65
660
|
title: "Get Audit Proof",
|
|
66
|
-
description: "Return the
|
|
661
|
+
description: "Return the tamper-evident, hash-chained proof for a decision id, including whether the chain is intact and the public key it verifies against.",
|
|
67
662
|
inputSchema: { decisionId: z.string().describe("Decision id returned by request_payment") },
|
|
68
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
69
|
-
},
|
|
663
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
664
|
+
},
|
|
665
|
+
async ({ decisionId }) => {
|
|
70
666
|
const proof = await core.getProof(decisionId);
|
|
71
|
-
if (!proof)
|
|
72
|
-
return { content: [{ type: "text", text: `No proof found for ${decisionId}` }], isError: true };
|
|
667
|
+
if (!proof) return { content: [{ type: "text", text: `No proof found for ${decisionId}` }], isError: true };
|
|
73
668
|
return { content: [{ type: "text", text: JSON.stringify(proof, null, 2) }], structuredContent: proof };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// verifiable by anyone via @fidacy/verify against the engine JWKS.
|
|
80
|
-
//
|
|
81
|
-
// Tool registration NEVER requires a key: only CALLING it without one returns a
|
|
82
|
-
// helpful error, so the server still boots with no FIDACY_ENGINE_API_KEY set.
|
|
83
|
-
server.registerTool("assess_action", {
|
|
669
|
+
}
|
|
670
|
+
);
|
|
671
|
+
server.registerTool(
|
|
672
|
+
"assess_action",
|
|
673
|
+
{
|
|
84
674
|
title: "Assess Action (Signed Trust Verdict)",
|
|
85
675
|
description: "Return a SIGNED Fidacy trust verdict from the live engine (default https://api.fidacy.com) for a proposed action. The signed proof is `riskPayloadJws` + `signingKeyId`, verifiable by anyone via @fidacy/verify against the engine JWKS at /.well-known/jwks.json. `kind` is one of ap2_payment, message_send, voice_call, custom, claim_document; `mandate` is the action/mandate object for that kind. This is the verdict (advisory) layer and moves no money; it complements the payment-firewall tools (request_payment et al.) in the same install.",
|
|
86
676
|
inputSchema: {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
spendingMandate: z.record(z.unknown()).optional(),
|
|
94
|
-
a2a: z.object({ task_id: z.string() }).optional(),
|
|
677
|
+
kind: z.enum(["ap2_payment", "message_send", "voice_call", "custom", "claim_document"]).optional(),
|
|
678
|
+
mandate: z.record(z.unknown()),
|
|
679
|
+
mandateType: z.string().optional(),
|
|
680
|
+
idempotencyKey: z.string().optional(),
|
|
681
|
+
spendingMandate: z.record(z.unknown()).optional(),
|
|
682
|
+
a2a: z.object({ task_id: z.string() }).optional()
|
|
95
683
|
},
|
|
96
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
97
|
-
},
|
|
684
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
685
|
+
},
|
|
686
|
+
async ({ kind, mandate, mandateType, idempotencyKey, spendingMandate, a2a }) => {
|
|
98
687
|
const engineUrl = process.env.FIDACY_ENGINE_URL ?? "https://api.fidacy.com";
|
|
99
688
|
const apiKey = (process.env.FIDACY_ENGINE_API_KEY ?? "").trim();
|
|
100
689
|
if (!apiKey) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
690
|
+
return {
|
|
691
|
+
content: [
|
|
692
|
+
{
|
|
693
|
+
type: "text",
|
|
694
|
+
text: "assess_action requires FIDACY_ENGINE_API_KEY (an fky_live_/fky_test_ key with assess:write). Set it to enable signed verdicts."
|
|
695
|
+
}
|
|
696
|
+
],
|
|
697
|
+
isError: true
|
|
698
|
+
};
|
|
110
699
|
}
|
|
111
700
|
try {
|
|
112
|
-
|
|
701
|
+
const r = await assessAction(
|
|
702
|
+
{ kind, mandate, mandateType, idempotencyKey, spendingMandate, a2a },
|
|
703
|
+
{ engineUrl, apiKey }
|
|
704
|
+
);
|
|
705
|
+
return {
|
|
706
|
+
content: [{ type: "text", text: `${r.decision} (score ${r.score}) signed by ${r.signingKeyId}` }],
|
|
707
|
+
structuredContent: r
|
|
708
|
+
};
|
|
709
|
+
} catch (e) {
|
|
710
|
+
if (e instanceof AssessError) {
|
|
711
|
+
const reasons = e.rejection_reasons?.length ? " (" + e.rejection_reasons.map((x) => x.key).join(",") + ")" : "";
|
|
113
712
|
return {
|
|
114
|
-
|
|
115
|
-
|
|
713
|
+
content: [{ type: "text", text: `ASSESS ${e.status}: ${e.type}${reasons}` }],
|
|
714
|
+
isError: true
|
|
116
715
|
};
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
content: [{ type: "text", text: "ASSESS failed: unexpected error" }],
|
|
719
|
+
isError: true
|
|
720
|
+
};
|
|
117
721
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
722
|
+
}
|
|
723
|
+
);
|
|
724
|
+
server.registerTool(
|
|
725
|
+
"upgrade",
|
|
726
|
+
{
|
|
727
|
+
title: "Upgrade to a Fidacy account",
|
|
728
|
+
description: "Start upgrading this local install to a real Fidacy account (server-backed signed verdicts, anchored proof, higher volume). Returns a link to open; your anonymous usage is preserved and migrated to the new account.",
|
|
729
|
+
inputSchema: {},
|
|
730
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
731
|
+
},
|
|
732
|
+
async () => {
|
|
733
|
+
const { url } = requestUpgrade();
|
|
734
|
+
return {
|
|
735
|
+
content: [{ type: "text", text: `To upgrade, open:
|
|
736
|
+
${url}
|
|
737
|
+
|
|
738
|
+
Your local protection keeps working meanwhile; your usage history migrates to the new account on completion.` }],
|
|
739
|
+
structuredContent: { upgradeUrl: url }
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
);
|
|
134
743
|
async function main() {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
744
|
+
const transport = new StdioServerTransport();
|
|
745
|
+
await server.connect(transport);
|
|
746
|
+
if (state.firstRun) recordInstall();
|
|
747
|
+
recordAgentActive();
|
|
748
|
+
process.once("beforeExit", () => {
|
|
749
|
+
void flush();
|
|
750
|
+
});
|
|
751
|
+
void autoProvision();
|
|
752
|
+
const verdictHint = process.env.FIDACY_ENGINE_API_KEY ? "" : " Set FIDACY_ENGINE_API_KEY to enable verdicts (assess_action).";
|
|
753
|
+
const tierHint = state.firstRun ? " First run: free local tier active (deny-unknown payee + cap). Add trusted payees in ~/.fidacy/config.json." : ` Tier: ${state.config.tier} (local-first).`;
|
|
754
|
+
console.error("[fidacy] @fidacy/mcp ready on stdio: signed verdict + payment firewall." + tierHint + verdictHint);
|
|
138
755
|
}
|
|
139
756
|
main().catch((e) => {
|
|
140
|
-
|
|
141
|
-
|
|
757
|
+
console.error("[fidacy] fatal:", e);
|
|
758
|
+
process.exit(1);
|
|
142
759
|
});
|
|
143
|
-
//# sourceMappingURL=index.js.map
|