@fidacy/mcp 0.2.3 → 0.2.4
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/core.js +1 -1
- package/dist/index.js +122 -12
- package/dist/lib.js +97 -10
- package/package.json +1 -1
package/dist/core.js
CHANGED
|
@@ -461,7 +461,7 @@ function resolveMandateRules(cfg) {
|
|
|
461
461
|
}
|
|
462
462
|
|
|
463
463
|
// src/telemetry.ts
|
|
464
|
-
var CLIENT_VERSION = true ? "0.2.
|
|
464
|
+
var CLIENT_VERSION = true ? "0.2.4" : "dev";
|
|
465
465
|
function bandOf(amount) {
|
|
466
466
|
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
|
|
467
467
|
if (amount < 10) return "lt10";
|
package/dist/index.js
CHANGED
|
@@ -217,17 +217,17 @@ var DevFidacyCore = class {
|
|
|
217
217
|
}
|
|
218
218
|
async decide(req, subject2) {
|
|
219
219
|
const decisionId = randomUUID();
|
|
220
|
-
const
|
|
220
|
+
const ts2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
221
221
|
const invalid = validateRequest(req);
|
|
222
222
|
if (invalid) {
|
|
223
|
-
const decision2 = { decisionId, status: "DENY", subject: subject2, mandateId: this.mandate.id, request: req, violatedRule: invalid, ts };
|
|
223
|
+
const decision2 = { decisionId, status: "DENY", subject: subject2, mandateId: this.mandate.id, request: req, violatedRule: invalid, ts: ts2 };
|
|
224
224
|
this.store.append(decision2);
|
|
225
225
|
this.onDecision?.(decision2);
|
|
226
226
|
return decision2;
|
|
227
227
|
}
|
|
228
228
|
const violated = evaluate(this.mandate, req, this.spent);
|
|
229
229
|
if (violated) {
|
|
230
|
-
const decision2 = { decisionId, status: "DENY", subject: subject2, mandateId: this.mandate.id, request: req, violatedRule: violated, ts };
|
|
230
|
+
const decision2 = { decisionId, status: "DENY", subject: subject2, mandateId: this.mandate.id, request: req, violatedRule: violated, ts: ts2 };
|
|
231
231
|
this.store.append(decision2);
|
|
232
232
|
this.onDecision?.(decision2);
|
|
233
233
|
return decision2;
|
|
@@ -236,7 +236,7 @@ var DevFidacyCore = class {
|
|
|
236
236
|
if (invoice) {
|
|
237
237
|
const k = `${subject2}|${invoice}`;
|
|
238
238
|
if (this.claimedInvoices.has(k)) {
|
|
239
|
-
const decision2 = { decisionId, status: "DENY", subject: subject2, mandateId: this.mandate.id, request: req, violatedRule: `duplicate_invoice:${invoice}`, ts };
|
|
239
|
+
const decision2 = { decisionId, status: "DENY", subject: subject2, mandateId: this.mandate.id, request: req, violatedRule: `duplicate_invoice:${invoice}`, ts: ts2 };
|
|
240
240
|
this.store.append(decision2);
|
|
241
241
|
this.onDecision?.(decision2);
|
|
242
242
|
return decision2;
|
|
@@ -246,7 +246,7 @@ var DevFidacyCore = class {
|
|
|
246
246
|
const grantPayload = { decisionId, subject: subject2, payee: req.payee, amount: req.amount, currency: req.currency, exp: Date.now() + 12e4, ...req.invoiceRef ? { invoiceRef: req.invoiceRef } : {} };
|
|
247
247
|
const grantBody = Buffer.from(stableStringify(grantPayload), "utf8").toString("base64url");
|
|
248
248
|
const grant = `${grantBody}.${sign(this.priv, grantBody)}`;
|
|
249
|
-
const decision = { decisionId, status: "ALLOW", subject: subject2, mandateId: this.mandate.id, request: req, grant, ts };
|
|
249
|
+
const decision = { decisionId, status: "ALLOW", subject: subject2, mandateId: this.mandate.id, request: req, grant, ts: ts2 };
|
|
250
250
|
this.spent += req.amount;
|
|
251
251
|
this.store.append(decision);
|
|
252
252
|
this.onDecision?.(decision);
|
|
@@ -358,10 +358,10 @@ var FileAuditStore = class {
|
|
|
358
358
|
append(decision) {
|
|
359
359
|
const prevHash = this.head();
|
|
360
360
|
const seq = this.chain.length;
|
|
361
|
-
const
|
|
361
|
+
const ts2 = decision.ts;
|
|
362
362
|
const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
|
|
363
|
-
const hash = sha256(`${prevHash}|${digest}|${seq}|${
|
|
364
|
-
const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts };
|
|
363
|
+
const hash = sha256(`${prevHash}|${digest}|${seq}|${ts2}`);
|
|
364
|
+
const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts: ts2 };
|
|
365
365
|
if (typeof decision.request?.purpose === "string" && decision.request.purpose.trim()) {
|
|
366
366
|
record2.purpose = decision.request.purpose.slice(0, 500);
|
|
367
367
|
}
|
|
@@ -490,7 +490,7 @@ function resolveMandateRules(cfg) {
|
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
// src/telemetry.ts
|
|
493
|
-
var CLIENT_VERSION = true ? "0.2.
|
|
493
|
+
var CLIENT_VERSION = true ? "0.2.4" : "dev";
|
|
494
494
|
function bandOf(amount) {
|
|
495
495
|
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
|
|
496
496
|
if (amount < 10) return "lt10";
|
|
@@ -834,7 +834,7 @@ async function findArtifacts(sha2562, cfg) {
|
|
|
834
834
|
}
|
|
835
835
|
|
|
836
836
|
// src/provision.ts
|
|
837
|
-
var CLIENT_VERSION2 = true ? "0.2.
|
|
837
|
+
var CLIENT_VERSION2 = true ? "0.2.4" : "dev";
|
|
838
838
|
function provisionEnabled() {
|
|
839
839
|
const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
|
|
840
840
|
return !(v === "1" || v === "true" || v === "yes");
|
|
@@ -1118,11 +1118,94 @@ function explainRule(rule, payee) {
|
|
|
1118
1118
|
return `The mandate rule "${rule}" was violated.`;
|
|
1119
1119
|
}
|
|
1120
1120
|
|
|
1121
|
+
// src/sentinel.ts
|
|
1122
|
+
var SENTINEL = {
|
|
1123
|
+
/** amount >= SPIKE_FACTOR x the historical max ALLOW to the same payee. */
|
|
1124
|
+
SPIKE_FACTOR: 3,
|
|
1125
|
+
/** minimum prior ALLOWs to a payee before a spike is judged (else first_payee covers it). */
|
|
1126
|
+
SPIKE_MIN_HISTORY: 2,
|
|
1127
|
+
/** decisions within BURST_WINDOW_MS to trip velocity_burst. */
|
|
1128
|
+
BURST_COUNT: 5,
|
|
1129
|
+
BURST_WINDOW_MS: 10 * 6e4,
|
|
1130
|
+
/** fraction of perTxMax that counts as riding the ceiling. */
|
|
1131
|
+
CAP_CREEP_RATIO: 0.8,
|
|
1132
|
+
/** a DENY for the same payee within this window makes a new attempt a retry. */
|
|
1133
|
+
RETRY_WINDOW_MS: 30 * 6e4
|
|
1134
|
+
};
|
|
1135
|
+
var ts = (r) => {
|
|
1136
|
+
const t = Date.parse(r.ts);
|
|
1137
|
+
return Number.isFinite(t) ? t : 0;
|
|
1138
|
+
};
|
|
1139
|
+
function assessPattern(records, req, opts = {}) {
|
|
1140
|
+
const now = opts.now ?? Date.now();
|
|
1141
|
+
const alerts = [];
|
|
1142
|
+
const toPayee = records.filter((r) => r.payee === req.payee);
|
|
1143
|
+
const allowedToPayee = toPayee.filter((r) => r.status === "ALLOW");
|
|
1144
|
+
if (req.payee && allowedToPayee.length === 0) {
|
|
1145
|
+
alerts.push({
|
|
1146
|
+
rule: "first_payee",
|
|
1147
|
+
severity: "warning",
|
|
1148
|
+
message: `First payment ever from this agent to "${req.payee}". A swapped or lookalike vendor always looks like a first payee; worth a human glance before anything settles.`
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
if (allowedToPayee.length >= SENTINEL.SPIKE_MIN_HISTORY && typeof req.amount === "number") {
|
|
1152
|
+
const maxSeen = Math.max(...allowedToPayee.map((r) => typeof r.amount === "number" ? r.amount : 0));
|
|
1153
|
+
if (maxSeen > 0 && req.amount >= maxSeen * SENTINEL.SPIKE_FACTOR) {
|
|
1154
|
+
alerts.push({
|
|
1155
|
+
rule: "amount_spike",
|
|
1156
|
+
severity: "warning",
|
|
1157
|
+
message: `${req.amount.toLocaleString("en-US")} is ${(req.amount / maxSeen).toFixed(1)}x the largest amount this agent has ever paid "${req.payee}" (${maxSeen.toLocaleString("en-US")}). Inflated re-presentations of a known vendor look exactly like this.`
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
const recent = records.filter((r) => now - ts(r) <= SENTINEL.BURST_WINDOW_MS);
|
|
1162
|
+
if (recent.length + 1 >= SENTINEL.BURST_COUNT) {
|
|
1163
|
+
alerts.push({
|
|
1164
|
+
rule: "velocity_burst",
|
|
1165
|
+
severity: "warning",
|
|
1166
|
+
message: `${recent.length + 1} payment decisions inside ${Math.round(SENTINEL.BURST_WINDOW_MS / 6e4)} minutes. Runaway loops and drain attempts announce themselves as bursts before they announce themselves as losses.`
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
if (typeof opts.perTxMax === "number" && opts.perTxMax > 0 && typeof req.amount === "number") {
|
|
1170
|
+
if (req.amount >= opts.perTxMax * SENTINEL.CAP_CREEP_RATIO && req.amount <= opts.perTxMax) {
|
|
1171
|
+
alerts.push({
|
|
1172
|
+
rule: "cap_creep",
|
|
1173
|
+
severity: "notice",
|
|
1174
|
+
message: `${req.amount.toLocaleString("en-US")} is ${Math.round(req.amount / opts.perTxMax * 100)}% of the per-transaction ceiling. An attacker probing a mandate pays just under the cap; a human authorizing a real bill usually doesn't land this close.`
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
const recentDeny = toPayee.find((r) => r.status === "DENY" && now - ts(r) <= SENTINEL.RETRY_WINDOW_MS);
|
|
1179
|
+
if (recentDeny) {
|
|
1180
|
+
alerts.push({
|
|
1181
|
+
rule: "retry_after_deny",
|
|
1182
|
+
severity: "warning",
|
|
1183
|
+
message: `This agent was DENIED a payment to "${req.payee}" ${Math.max(1, Math.round((now - ts(recentDeny)) / 6e4))} minute(s) ago (${recentDeny.violatedRule ?? "rule on record"}) and is now trying again. Our public Model Watch benchmark measures exactly this behavior; in an autonomous agent it is the pattern that precedes a loss.`
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
return alerts;
|
|
1187
|
+
}
|
|
1188
|
+
function sweep(records, opts = {}) {
|
|
1189
|
+
const out = [];
|
|
1190
|
+
for (let i = 0; i < records.length; i++) {
|
|
1191
|
+
const r = records[i];
|
|
1192
|
+
if (!r.payee) continue;
|
|
1193
|
+
const alerts = assessPattern(records.slice(0, i), { payee: r.payee, amount: r.amount }, { perTxMax: opts.perTxMax, now: ts(r) });
|
|
1194
|
+
if (alerts.length) out.push({ decisionId: r.decisionId, ts: r.ts, status: r.status, payee: r.payee, alerts });
|
|
1195
|
+
}
|
|
1196
|
+
return out.slice(-(opts.limit ?? 20)).reverse();
|
|
1197
|
+
}
|
|
1198
|
+
function renderAlertLine(alerts) {
|
|
1199
|
+
if (!alerts.length) return "";
|
|
1200
|
+
const worst = alerts.some((a) => a.severity === "warning") ? "SENTINEL WARNING" : "SENTINEL NOTICE";
|
|
1201
|
+
return ` [${worst}: ${alerts.map((a) => a.message).join(" Also: ")}]`;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1121
1204
|
// src/index.ts
|
|
1122
1205
|
var state = ensureState();
|
|
1123
1206
|
var core = makeCore();
|
|
1124
1207
|
var subject = process.env.FIDACY_SUBJECT ?? "agent:demo";
|
|
1125
|
-
var SERVER_VERSION = true ? "0.2.
|
|
1208
|
+
var SERVER_VERSION = true ? "0.2.4" : "dev";
|
|
1126
1209
|
var bootClaim = claimUrl();
|
|
1127
1210
|
var server = new McpServer(
|
|
1128
1211
|
{ name: "fidacy", version: SERVER_VERSION },
|
|
@@ -1163,9 +1246,16 @@ server.registerTool(
|
|
|
1163
1246
|
return { content: [{ type: "text", text: gate.message }], structuredContent: out2 };
|
|
1164
1247
|
}
|
|
1165
1248
|
const req = args;
|
|
1249
|
+
let sentinelAlerts = [];
|
|
1250
|
+
try {
|
|
1251
|
+
const [before, mandate] = await Promise.all([core.history(5e3), core.getMandate(subject)]);
|
|
1252
|
+
sentinelAlerts = assessPattern(before, { payee: req.payee, amount: req.amount }, { perTxMax: mandate?.allow?.perTxMax });
|
|
1253
|
+
} catch {
|
|
1254
|
+
}
|
|
1166
1255
|
const d = await core.decide(req, subject);
|
|
1167
1256
|
const out = { status: d.status, decisionId: d.decisionId, grant: d.grant, violatedRule: d.violatedRule };
|
|
1168
|
-
const
|
|
1257
|
+
const sentinelLine = d.status === "ALLOW" ? renderAlertLine(sentinelAlerts) : "";
|
|
1258
|
+
const base = d.status === "ALLOW" ? `ALLOW (decision ${d.decisionId})${req.invoiceRef ? ` for invoice ${req.invoiceRef}` : ""}.${sentinelLine} To settle, call execute_payment with the SAME payee, amount, currency, and idempotencyKey, and set "grant" to EXACTLY this signed value:
|
|
1169
1259
|
${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.` : d.violatedRule?.startsWith("category_not_allowed") ? ` If the user considers "${req.category}" a legitimate purpose for this agent, add it to mandate.categories in ~/.fidacy/config.json and retry: the firewall picks the change up on the next call, no restart needed.` : d.violatedRule?.startsWith("currency_not_allowed") ? ` The mandate is single-currency and set to another one (mandate.currency in ~/.fidacy/config.json, default USD). If this agent legitimately pays in ${req.currency}, update it there and retry: hot-reloaded, no restart.` : ""}`;
|
|
1170
1260
|
const nudge = decisionNudge(d.status, d.violatedRule, "upgrade");
|
|
1171
1261
|
const countdown = trialCountdownLine();
|
|
@@ -1284,6 +1374,26 @@ Proof: entry #${record2.seq} in the hash-chained audit, chain currently ${proof.
|
|
|
1284
1374
|
return { content: [{ type: "text", text: head + why + said + integrity }], structuredContent: payload };
|
|
1285
1375
|
}
|
|
1286
1376
|
);
|
|
1377
|
+
server.registerTool(
|
|
1378
|
+
"sentinel_alerts",
|
|
1379
|
+
{
|
|
1380
|
+
title: "Sentinel Alerts (allowed, but outside this agent's pattern)",
|
|
1381
|
+
description: "Predictive pattern alerts from the local audit chain: first-ever payee, amount spikes vs this agent's own history, velocity bursts, payments riding the mandate ceiling, and retries after a denial (the behavior our Model Watch benchmark measures). Deterministic and explainable, no model. Use to answer 'has anything unusual happened' before it becomes a loss.",
|
|
1382
|
+
inputSchema: {
|
|
1383
|
+
limit: z.number().int().positive().max(100).optional().describe("How many flagged decisions to return. Default 20.")
|
|
1384
|
+
},
|
|
1385
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
1386
|
+
},
|
|
1387
|
+
async ({ limit }) => {
|
|
1388
|
+
const [records, mandate] = await Promise.all([core.history(5e3), core.getMandate(subject).catch(() => null)]);
|
|
1389
|
+
const flagged = sweep(records, { perTxMax: mandate?.allow?.perTxMax, limit: limit ?? 20 });
|
|
1390
|
+
const text = flagged.length ? flagged.map((f) => `#${f.decisionId.slice(0, 8)} ${f.ts} ${f.status} to ${f.payee}: ${f.alerts.map((a) => `[${a.rule}] ${a.message}`).join(" ")}`).join("\n") : "No pattern alerts on this chain. Every decision so far sits inside this agent's own historical pattern.";
|
|
1391
|
+
return {
|
|
1392
|
+
content: [{ type: "text", text }],
|
|
1393
|
+
structuredContent: { count: flagged.length, thresholds: SENTINEL, flagged }
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
);
|
|
1287
1397
|
server.registerTool(
|
|
1288
1398
|
"assess_action",
|
|
1289
1399
|
{
|
package/dist/lib.js
CHANGED
|
@@ -248,17 +248,17 @@ var DevFidacyCore = class {
|
|
|
248
248
|
}
|
|
249
249
|
async decide(req, subject) {
|
|
250
250
|
const decisionId = randomUUID();
|
|
251
|
-
const
|
|
251
|
+
const ts2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
252
252
|
const invalid = validateRequest(req);
|
|
253
253
|
if (invalid) {
|
|
254
|
-
const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: invalid, ts };
|
|
254
|
+
const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: invalid, ts: ts2 };
|
|
255
255
|
this.store.append(decision2);
|
|
256
256
|
this.onDecision?.(decision2);
|
|
257
257
|
return decision2;
|
|
258
258
|
}
|
|
259
259
|
const violated = evaluate(this.mandate, req, this.spent);
|
|
260
260
|
if (violated) {
|
|
261
|
-
const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: violated, ts };
|
|
261
|
+
const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: violated, ts: ts2 };
|
|
262
262
|
this.store.append(decision2);
|
|
263
263
|
this.onDecision?.(decision2);
|
|
264
264
|
return decision2;
|
|
@@ -267,7 +267,7 @@ var DevFidacyCore = class {
|
|
|
267
267
|
if (invoice) {
|
|
268
268
|
const k = `${subject}|${invoice}`;
|
|
269
269
|
if (this.claimedInvoices.has(k)) {
|
|
270
|
-
const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: `duplicate_invoice:${invoice}`, ts };
|
|
270
|
+
const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: `duplicate_invoice:${invoice}`, ts: ts2 };
|
|
271
271
|
this.store.append(decision2);
|
|
272
272
|
this.onDecision?.(decision2);
|
|
273
273
|
return decision2;
|
|
@@ -277,7 +277,7 @@ var DevFidacyCore = class {
|
|
|
277
277
|
const grantPayload = { decisionId, subject, payee: req.payee, amount: req.amount, currency: req.currency, exp: Date.now() + 12e4, ...req.invoiceRef ? { invoiceRef: req.invoiceRef } : {} };
|
|
278
278
|
const grantBody = Buffer.from(stableStringify(grantPayload), "utf8").toString("base64url");
|
|
279
279
|
const grant = `${grantBody}.${sign(this.priv, grantBody)}`;
|
|
280
|
-
const decision = { decisionId, status: "ALLOW", subject, mandateId: this.mandate.id, request: req, grant, ts };
|
|
280
|
+
const decision = { decisionId, status: "ALLOW", subject, mandateId: this.mandate.id, request: req, grant, ts: ts2 };
|
|
281
281
|
this.spent += req.amount;
|
|
282
282
|
this.store.append(decision);
|
|
283
283
|
this.onDecision?.(decision);
|
|
@@ -389,10 +389,10 @@ var FileAuditStore = class {
|
|
|
389
389
|
append(decision) {
|
|
390
390
|
const prevHash = this.head();
|
|
391
391
|
const seq = this.chain.length;
|
|
392
|
-
const
|
|
392
|
+
const ts2 = decision.ts;
|
|
393
393
|
const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
|
|
394
|
-
const hash = sha256(`${prevHash}|${digest}|${seq}|${
|
|
395
|
-
const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts };
|
|
394
|
+
const hash = sha256(`${prevHash}|${digest}|${seq}|${ts2}`);
|
|
395
|
+
const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts: ts2 };
|
|
396
396
|
if (typeof decision.request?.purpose === "string" && decision.request.purpose.trim()) {
|
|
397
397
|
record2.purpose = decision.request.purpose.slice(0, 500);
|
|
398
398
|
}
|
|
@@ -613,7 +613,7 @@ function resolveMandateRules(cfg) {
|
|
|
613
613
|
}
|
|
614
614
|
|
|
615
615
|
// src/telemetry.ts
|
|
616
|
-
var CLIENT_VERSION = true ? "0.2.
|
|
616
|
+
var CLIENT_VERSION = true ? "0.2.4" : "dev";
|
|
617
617
|
function bandOf(amount) {
|
|
618
618
|
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
|
|
619
619
|
if (amount < 10) return "lt10";
|
|
@@ -791,7 +791,7 @@ function requestUpgrade() {
|
|
|
791
791
|
}
|
|
792
792
|
|
|
793
793
|
// src/provision.ts
|
|
794
|
-
var CLIENT_VERSION2 = true ? "0.2.
|
|
794
|
+
var CLIENT_VERSION2 = true ? "0.2.4" : "dev";
|
|
795
795
|
function provisionEnabled() {
|
|
796
796
|
const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
|
|
797
797
|
return !(v === "1" || v === "true" || v === "yes");
|
|
@@ -1053,6 +1053,89 @@ function explainRule(rule, payee) {
|
|
|
1053
1053
|
return "The anonymous trial had been used up, so the firewall failed closed. No money can move until the install is activated with a free API key.";
|
|
1054
1054
|
return `The mandate rule "${rule}" was violated.`;
|
|
1055
1055
|
}
|
|
1056
|
+
|
|
1057
|
+
// src/sentinel.ts
|
|
1058
|
+
var SENTINEL = {
|
|
1059
|
+
/** amount >= SPIKE_FACTOR x the historical max ALLOW to the same payee. */
|
|
1060
|
+
SPIKE_FACTOR: 3,
|
|
1061
|
+
/** minimum prior ALLOWs to a payee before a spike is judged (else first_payee covers it). */
|
|
1062
|
+
SPIKE_MIN_HISTORY: 2,
|
|
1063
|
+
/** decisions within BURST_WINDOW_MS to trip velocity_burst. */
|
|
1064
|
+
BURST_COUNT: 5,
|
|
1065
|
+
BURST_WINDOW_MS: 10 * 6e4,
|
|
1066
|
+
/** fraction of perTxMax that counts as riding the ceiling. */
|
|
1067
|
+
CAP_CREEP_RATIO: 0.8,
|
|
1068
|
+
/** a DENY for the same payee within this window makes a new attempt a retry. */
|
|
1069
|
+
RETRY_WINDOW_MS: 30 * 6e4
|
|
1070
|
+
};
|
|
1071
|
+
var ts = (r) => {
|
|
1072
|
+
const t = Date.parse(r.ts);
|
|
1073
|
+
return Number.isFinite(t) ? t : 0;
|
|
1074
|
+
};
|
|
1075
|
+
function assessPattern(records, req, opts = {}) {
|
|
1076
|
+
const now = opts.now ?? Date.now();
|
|
1077
|
+
const alerts = [];
|
|
1078
|
+
const toPayee = records.filter((r) => r.payee === req.payee);
|
|
1079
|
+
const allowedToPayee = toPayee.filter((r) => r.status === "ALLOW");
|
|
1080
|
+
if (req.payee && allowedToPayee.length === 0) {
|
|
1081
|
+
alerts.push({
|
|
1082
|
+
rule: "first_payee",
|
|
1083
|
+
severity: "warning",
|
|
1084
|
+
message: `First payment ever from this agent to "${req.payee}". A swapped or lookalike vendor always looks like a first payee; worth a human glance before anything settles.`
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
if (allowedToPayee.length >= SENTINEL.SPIKE_MIN_HISTORY && typeof req.amount === "number") {
|
|
1088
|
+
const maxSeen = Math.max(...allowedToPayee.map((r) => typeof r.amount === "number" ? r.amount : 0));
|
|
1089
|
+
if (maxSeen > 0 && req.amount >= maxSeen * SENTINEL.SPIKE_FACTOR) {
|
|
1090
|
+
alerts.push({
|
|
1091
|
+
rule: "amount_spike",
|
|
1092
|
+
severity: "warning",
|
|
1093
|
+
message: `${req.amount.toLocaleString("en-US")} is ${(req.amount / maxSeen).toFixed(1)}x the largest amount this agent has ever paid "${req.payee}" (${maxSeen.toLocaleString("en-US")}). Inflated re-presentations of a known vendor look exactly like this.`
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
const recent = records.filter((r) => now - ts(r) <= SENTINEL.BURST_WINDOW_MS);
|
|
1098
|
+
if (recent.length + 1 >= SENTINEL.BURST_COUNT) {
|
|
1099
|
+
alerts.push({
|
|
1100
|
+
rule: "velocity_burst",
|
|
1101
|
+
severity: "warning",
|
|
1102
|
+
message: `${recent.length + 1} payment decisions inside ${Math.round(SENTINEL.BURST_WINDOW_MS / 6e4)} minutes. Runaway loops and drain attempts announce themselves as bursts before they announce themselves as losses.`
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
if (typeof opts.perTxMax === "number" && opts.perTxMax > 0 && typeof req.amount === "number") {
|
|
1106
|
+
if (req.amount >= opts.perTxMax * SENTINEL.CAP_CREEP_RATIO && req.amount <= opts.perTxMax) {
|
|
1107
|
+
alerts.push({
|
|
1108
|
+
rule: "cap_creep",
|
|
1109
|
+
severity: "notice",
|
|
1110
|
+
message: `${req.amount.toLocaleString("en-US")} is ${Math.round(req.amount / opts.perTxMax * 100)}% of the per-transaction ceiling. An attacker probing a mandate pays just under the cap; a human authorizing a real bill usually doesn't land this close.`
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
const recentDeny = toPayee.find((r) => r.status === "DENY" && now - ts(r) <= SENTINEL.RETRY_WINDOW_MS);
|
|
1115
|
+
if (recentDeny) {
|
|
1116
|
+
alerts.push({
|
|
1117
|
+
rule: "retry_after_deny",
|
|
1118
|
+
severity: "warning",
|
|
1119
|
+
message: `This agent was DENIED a payment to "${req.payee}" ${Math.max(1, Math.round((now - ts(recentDeny)) / 6e4))} minute(s) ago (${recentDeny.violatedRule ?? "rule on record"}) and is now trying again. Our public Model Watch benchmark measures exactly this behavior; in an autonomous agent it is the pattern that precedes a loss.`
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
return alerts;
|
|
1123
|
+
}
|
|
1124
|
+
function sweep(records, opts = {}) {
|
|
1125
|
+
const out = [];
|
|
1126
|
+
for (let i = 0; i < records.length; i++) {
|
|
1127
|
+
const r = records[i];
|
|
1128
|
+
if (!r.payee) continue;
|
|
1129
|
+
const alerts = assessPattern(records.slice(0, i), { payee: r.payee, amount: r.amount }, { perTxMax: opts.perTxMax, now: ts(r) });
|
|
1130
|
+
if (alerts.length) out.push({ decisionId: r.decisionId, ts: r.ts, status: r.status, payee: r.payee, alerts });
|
|
1131
|
+
}
|
|
1132
|
+
return out.slice(-(opts.limit ?? 20)).reverse();
|
|
1133
|
+
}
|
|
1134
|
+
function renderAlertLine(alerts) {
|
|
1135
|
+
if (!alerts.length) return "";
|
|
1136
|
+
const worst = alerts.some((a) => a.severity === "warning") ? "SENTINEL WARNING" : "SENTINEL NOTICE";
|
|
1137
|
+
return ` [${worst}: ${alerts.map((a) => a.message).join(" Also: ")}]`;
|
|
1138
|
+
}
|
|
1056
1139
|
export {
|
|
1057
1140
|
DevFidacyCore,
|
|
1058
1141
|
FREE_DECISIONS,
|
|
@@ -1060,8 +1143,10 @@ export {
|
|
|
1060
1143
|
GrantEnforcingExecutor,
|
|
1061
1144
|
HttpFidacyCore,
|
|
1062
1145
|
ReferenceRail,
|
|
1146
|
+
SENTINEL,
|
|
1063
1147
|
activationBootLine,
|
|
1064
1148
|
activationGate,
|
|
1149
|
+
assessPattern,
|
|
1065
1150
|
autoProvision,
|
|
1066
1151
|
canonInvoice,
|
|
1067
1152
|
claimUrl,
|
|
@@ -1082,6 +1167,7 @@ export {
|
|
|
1082
1167
|
recordAgentActive,
|
|
1083
1168
|
recordInstall,
|
|
1084
1169
|
remainingFree,
|
|
1170
|
+
renderAlertLine,
|
|
1085
1171
|
renderRows,
|
|
1086
1172
|
renderSummary,
|
|
1087
1173
|
requestUpgrade,
|
|
@@ -1091,6 +1177,7 @@ export {
|
|
|
1091
1177
|
sign,
|
|
1092
1178
|
stableStringify,
|
|
1093
1179
|
summarize,
|
|
1180
|
+
sweep,
|
|
1094
1181
|
toRows,
|
|
1095
1182
|
trialCountdownLine,
|
|
1096
1183
|
upgradeUrl,
|
package/package.json
CHANGED