@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 CHANGED
@@ -461,7 +461,7 @@ function resolveMandateRules(cfg) {
461
461
  }
462
462
 
463
463
  // src/telemetry.ts
464
- var CLIENT_VERSION = true ? "0.2.3" : "dev";
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 ts = (/* @__PURE__ */ new Date()).toISOString();
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 ts = decision.ts;
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}|${ts}`);
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.3" : "dev";
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.3" : "dev";
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.3" : "dev";
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 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:
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 ts = (/* @__PURE__ */ new Date()).toISOString();
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 ts = decision.ts;
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}|${ts}`);
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.3" : "dev";
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.3" : "dev";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fidacy/mcp",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
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",