@decentrys/agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2051 @@
1
+ "use strict";
2
+ var DecentrysAgent = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ AGENT_ACTION_TYPES: () => AGENT_ACTION_TYPES,
25
+ AGENT_MODEL_VERSION: () => AGENT_MODEL_VERSION,
26
+ AGENT_RULES: () => AGENT_RULES,
27
+ AGENT_SDK_VERSION: () => AGENT_SDK_VERSION,
28
+ AgentGuard: () => AgentGuard,
29
+ DEFAULT_AGENT_RISK_POLICY: () => DEFAULT_AGENT_RISK_POLICY,
30
+ SpendLedger: () => SpendLedger,
31
+ describeDuration: () => describeDuration,
32
+ evaluatePolicy: () => evaluatePolicy,
33
+ explainAgentAction: () => explainAgentAction,
34
+ fingerprint: () => fingerprint,
35
+ isReservationLedger: () => isReservationLedger,
36
+ isUnlimitedAllowance: () => isUnlimitedAllowance,
37
+ narrowPolicy: () => narrowPolicy,
38
+ newDecisionId: () => newDecisionId,
39
+ sealPolicy: () => sealPolicy,
40
+ unavailableReason: () => unavailableReason
41
+ });
42
+
43
+ // src/model.ts
44
+ var AGENT_MODEL_VERSION = "agent-1.0.0";
45
+ var AGENT_ACTION_TYPES = [
46
+ "transfer",
47
+ "swap",
48
+ "approve",
49
+ "contract_call",
50
+ "bridge",
51
+ "stake",
52
+ "unstake",
53
+ "deploy",
54
+ "sign_message",
55
+ "unknown"
56
+ ];
57
+ var AGENT_RULES = [
58
+ "AGENT_BINDING",
59
+ "ACTION_TYPE",
60
+ "CHAIN",
61
+ "COUNTERPARTY",
62
+ "CONTRACT",
63
+ "TOKEN",
64
+ "APPROVAL_ALLOWANCE",
65
+ "VALUE_PER_ACTION",
66
+ "CUMULATIVE_SPEND",
67
+ "RATE_LIMIT",
68
+ "RISK_LEVEL",
69
+ "ASSESSMENT_AVAILABILITY",
70
+ "HUMAN_APPROVAL_THRESHOLD"
71
+ ];
72
+
73
+ // ../protect/dist/browser/decentrys-protect.mjs
74
+ var RISK_LEVELS = [
75
+ "NO_CRITICAL_RISK_DETECTED",
76
+ "INFORMATIONAL",
77
+ "CAUTION",
78
+ "ELEVATED_RISK",
79
+ "HIGH_RISK",
80
+ "CRITICAL_THREAT",
81
+ "KNOWN_MALICIOUS"
82
+ ];
83
+ var RISK_LEVEL_MEANING = {
84
+ NO_CRITICAL_RISK_DETECTED: "No known critical threat evidence was identified. This is not an assurance of safety.",
85
+ INFORMATIONAL: "Facts worth knowing before proceeding. Nothing here indicates danger.",
86
+ CAUTION: "A security-sensitive capability or behaviour exists that deserves attention.",
87
+ ELEVATED_RISK: "Several meaningful risk signals are present together.",
88
+ HIGH_RISK: "Strong technical or behavioural evidence of significant danger.",
89
+ CRITICAL_THREAT: "Severe threat supported by concrete evidence.",
90
+ KNOWN_MALICIOUS: "Confirmed malicious infrastructure or behaviour, verified against evidence."
91
+ };
92
+ var HISTORY_STATUS_MEANING = {
93
+ ESTABLISHED: "Substantial on-chain history is available.",
94
+ MODERATE: "Some history is available.",
95
+ LIMITED: "Little history is available yet. This is normal for anything recently deployed and is not a risk finding.",
96
+ NONE: "No history is available. This is not a risk finding."
97
+ };
98
+ var PROTECT_MODEL_VERSION = "protect-1.0.0";
99
+ var RAISING_STATUSES = ["ACTIVE"];
100
+ var MIN_RAISING_CONFIDENCE = 0.5;
101
+ function classify(input) {
102
+ const now = input.now ?? /* @__PURE__ */ new Date();
103
+ const facts = input.facts ?? [];
104
+ const capabilities = input.capabilities ?? [];
105
+ const unknowns = input.unknowns ?? [];
106
+ const historyStatus = input.historyStatus ?? "LIMITED";
107
+ const threatSignals = (input.threatSignals ?? []).map((signal) => applyDecay(signal, now));
108
+ const raising = threatSignals.filter(
109
+ (s) => RAISING_STATUSES.includes(s.status) && s.confidence >= MIN_RAISING_CONFIDENCE
110
+ );
111
+ const explanation = [];
112
+ let level = "NO_CRITICAL_RISK_DETECTED";
113
+ const confirmed = raising.filter(
114
+ (s) => s.severity === "CRITICAL" && s.evidence.some((e) => e.analystVerified)
115
+ );
116
+ const confirmedMalicious = confirmed.length > 0;
117
+ if (confirmedMalicious) {
118
+ level = "KNOWN_MALICIOUS";
119
+ for (const signal of confirmed) {
120
+ explanation.push(`${signal.explanation} (verified by an analyst)`);
121
+ }
122
+ } else {
123
+ const critical = raising.filter((s) => s.severity === "CRITICAL");
124
+ const high = raising.filter((s) => s.severity === "HIGH");
125
+ const medium = raising.filter((s) => s.severity === "MEDIUM");
126
+ if (critical.length > 0) {
127
+ level = "CRITICAL_THREAT";
128
+ for (const s of critical) explanation.push(s.explanation);
129
+ } else if (high.length > 0) {
130
+ level = high.length > 1 ? "HIGH_RISK" : "ELEVATED_RISK";
131
+ for (const s of high) explanation.push(s.explanation);
132
+ } else if (medium.length > 1) {
133
+ level = "ELEVATED_RISK";
134
+ for (const s of medium) explanation.push(s.explanation);
135
+ } else if (medium.length === 1) {
136
+ level = "CAUTION";
137
+ explanation.push(medium[0].explanation);
138
+ }
139
+ const significant = capabilities.filter((c) => c.severity === "SIGNIFICANT");
140
+ if (significant.length > 0 && rank(level) < rank("CAUTION")) {
141
+ level = "CAUTION";
142
+ for (const c of significant) explanation.push(c.statement);
143
+ } else if (rank(level) < rank("INFORMATIONAL") && capabilities.length > 0) {
144
+ level = "INFORMATIONAL";
145
+ for (const c of capabilities.slice(0, 3)) explanation.push(c.statement);
146
+ }
147
+ }
148
+ for (const signal of threatSignals) {
149
+ if (raising.includes(signal)) continue;
150
+ if (signal.status !== "ACTIVE") {
151
+ explanation.push(
152
+ `${signal.explanation} \u2014 this signal is ${signal.status.toLowerCase()} and did not affect the assessment.`
153
+ );
154
+ } else if (signal.confidence < MIN_RAISING_CONFIDENCE) {
155
+ explanation.push(
156
+ `${signal.explanation} \u2014 reported at ${Math.round(signal.confidence * 100)}% confidence, which is too low to raise the risk level on its own.`
157
+ );
158
+ }
159
+ }
160
+ if (level === "NO_CRITICAL_RISK_DETECTED" && facts.length > 0) {
161
+ level = "INFORMATIONAL";
162
+ }
163
+ if (historyStatus === "LIMITED" || historyStatus === "NONE") {
164
+ explanation.push(HISTORY_STATUS_MEANING[historyStatus]);
165
+ }
166
+ if (unknowns.length > 0) {
167
+ explanation.push(
168
+ `${unknowns.length} ${unknowns.length === 1 ? "attribute is" : "attributes are"} unknown. Unknown is reported as unknown; it does not contribute to risk.`
169
+ );
170
+ }
171
+ if (explanation.length === 0) {
172
+ explanation.push(RISK_LEVEL_MEANING[level]);
173
+ }
174
+ return {
175
+ riskLevel: level,
176
+ confirmedMalicious,
177
+ confidence: confidenceFor(level, raising, historyStatus),
178
+ historyStatus,
179
+ facts,
180
+ capabilities,
181
+ threatSignals,
182
+ unknowns,
183
+ components: {
184
+ technicalRisk: technicalRisk(capabilities),
185
+ behavioralRisk: behavioralRisk(raising),
186
+ threatIntelligenceRisk: threatIntelligenceRisk(raising),
187
+ // Coverage, reported separately so it cannot be summed into a risk total.
188
+ historyConfidence: input.historyConfidence ?? historyConfidenceFor(historyStatus)
189
+ },
190
+ explanation,
191
+ modelVersion: PROTECT_MODEL_VERSION,
192
+ assessedAt: now.toISOString()
193
+ };
194
+ }
195
+ function rank(level) {
196
+ return [
197
+ "NO_CRITICAL_RISK_DETECTED",
198
+ "INFORMATIONAL",
199
+ "CAUTION",
200
+ "ELEVATED_RISK",
201
+ "HIGH_RISK",
202
+ "CRITICAL_THREAT",
203
+ "KNOWN_MALICIOUS"
204
+ ].indexOf(level);
205
+ }
206
+ function applyDecay(signal, now) {
207
+ if (signal.status !== "ACTIVE") return signal;
208
+ if (!signal.expiresAt) return signal;
209
+ return Date.parse(signal.expiresAt) <= now.getTime() ? { ...signal, status: "STALE" } : signal;
210
+ }
211
+ function confidenceFor(level, raising, history) {
212
+ if (raising.length > 0) {
213
+ const best = Math.max(...raising.map((s) => s.confidence));
214
+ return Number(best.toFixed(2));
215
+ }
216
+ switch (history) {
217
+ case "ESTABLISHED":
218
+ return 0.85;
219
+ case "MODERATE":
220
+ return 0.7;
221
+ case "LIMITED":
222
+ return 0.5;
223
+ default:
224
+ return 0.35;
225
+ }
226
+ }
227
+ function technicalRisk(capabilities) {
228
+ const weight = { INFO: 4, NOTABLE: 12, SIGNIFICANT: 25 };
229
+ return Math.min(100, capabilities.reduce((sum, c) => sum + weight[c.severity], 0));
230
+ }
231
+ function behavioralRisk(raising) {
232
+ const weight = { LOW: 5, MEDIUM: 20, HIGH: 40, CRITICAL: 70 };
233
+ return Math.min(100, raising.filter((s) => s.hops === 0).reduce((sum, s) => sum + weight[s.severity] * s.confidence, 0));
234
+ }
235
+ function threatIntelligenceRisk(raising) {
236
+ const weight = { LOW: 5, MEDIUM: 15, HIGH: 35, CRITICAL: 60 };
237
+ return Math.min(100, raising.reduce((sum, s) => {
238
+ const decay = Math.pow(0.6, Math.max(0, s.hops));
239
+ return sum + weight[s.severity] * s.confidence * decay;
240
+ }, 0));
241
+ }
242
+ function historyConfidenceFor(status) {
243
+ switch (status) {
244
+ case "ESTABLISHED":
245
+ return 90;
246
+ case "MODERATE":
247
+ return 60;
248
+ case "LIMITED":
249
+ return 25;
250
+ default:
251
+ return 5;
252
+ }
253
+ }
254
+ var DEFAULT_POLICY = {
255
+ NO_CRITICAL_RISK_DETECTED: "allow",
256
+ INFORMATIONAL: "inform",
257
+ CAUTION: "warn",
258
+ ELEVATED_RISK: "warn_strong",
259
+ HIGH_RISK: "require_confirmation",
260
+ CRITICAL_THREAT: "require_confirmation",
261
+ KNOWN_MALICIOUS: "block"
262
+ };
263
+ function applyPolicy(assessment, policy = {}) {
264
+ const action = policy[assessment.riskLevel] ?? DEFAULT_POLICY[assessment.riskLevel];
265
+ return {
266
+ action,
267
+ reason: assessment.explanation[0] ?? RISK_LEVEL_MEANING[assessment.riskLevel]
268
+ };
269
+ }
270
+ function unavailableAssessment(failMode, reason, now = /* @__PURE__ */ new Date()) {
271
+ return {
272
+ riskLevel: "NO_CRITICAL_RISK_DETECTED",
273
+ confirmedMalicious: false,
274
+ confidence: 0,
275
+ historyStatus: "NONE",
276
+ facts: [],
277
+ capabilities: [],
278
+ threatSignals: [],
279
+ unknowns: [{
280
+ field: "assessment",
281
+ reason: "PROVIDER_UNAVAILABLE",
282
+ statement: `Decentrys could not be reached: ${reason}. Nothing was checked.`
283
+ }],
284
+ components: { technicalRisk: 0, behavioralRisk: 0, threatIntelligenceRisk: 0, historyConfidence: 0 },
285
+ explanation: [
286
+ `Decentrys could not be reached: ${reason}.`,
287
+ failMode === "closed" ? "This deployment is configured to refuse unverified transactions." : failMode === "warn" ? "No security check was performed. Proceed with the care you would use without any tool." : "No security check was performed."
288
+ ],
289
+ modelVersion: PROTECT_MODEL_VERSION,
290
+ assessedAt: now.toISOString()
291
+ };
292
+ }
293
+ var COVERAGE_SIGNAL_TYPES = /* @__PURE__ */ new Set([
294
+ "NEW_ADDRESS",
295
+ "NEW_CONTRACT",
296
+ "NEW_DEPLOYMENT",
297
+ "LOW_ACTIVITY",
298
+ "LIMITED_HISTORY",
299
+ "NO_HISTORY",
300
+ "ESTABLISHED_HISTORY",
301
+ "UNVERIFIED_SOURCE",
302
+ "UNVERIFIED_CONTRACT",
303
+ "NO_AUDIT",
304
+ "UNAUDITED",
305
+ "ANONYMOUS_DEPLOYER",
306
+ "ANONYMOUS_TEAM",
307
+ "UNKNOWN_DEPLOYER",
308
+ "LOW_LIQUIDITY",
309
+ "THIN_LIQUIDITY",
310
+ "SMALL_MARKET_CAP",
311
+ "HOLDER_CONCENTRATION",
312
+ "LOW_HOLDER_COUNT",
313
+ "NOT_ON_TOKEN_LIST",
314
+ "UNKNOWN_TOKEN",
315
+ "NO_SOCIAL_PRESENCE"
316
+ ]);
317
+ var NO_HISTORY_TYPES = /* @__PURE__ */ new Set(["NO_HISTORY", "UNKNOWN_TOKEN"]);
318
+ function normalizeEvidence(raw, fallback) {
319
+ const body = isRecord(raw) ? raw : {};
320
+ const demotedSignals = [];
321
+ const facts = asArray(body.facts).map(toFact).filter(isPresent);
322
+ const capabilities = asArray(body.capabilities).map(toCapability).filter(isPresent);
323
+ const unknowns = asArray(body.unknowns).map(toUnknown).filter(isPresent);
324
+ const threatSignals = [];
325
+ let noHistoryObserved = false;
326
+ for (const item of asArray(body.threatSignals)) {
327
+ const signal = toSignal(item);
328
+ if (!signal) continue;
329
+ if (COVERAGE_SIGNAL_TYPES.has(signal.type)) {
330
+ demotedSignals.push(signal.type);
331
+ if (NO_HISTORY_TYPES.has(signal.type)) noHistoryObserved = true;
332
+ facts.push({
333
+ type: signal.type,
334
+ value: null,
335
+ statement: signal.explanation,
336
+ source: signal.evidence[0]?.source ?? "decentrys",
337
+ observedAt: signal.lastSeen
338
+ });
339
+ continue;
340
+ }
341
+ threatSignals.push(signal);
342
+ }
343
+ const declaredHistory = asHistoryStatus(body.historyStatus);
344
+ const historyStatus = declaredHistory ?? (noHistoryObserved ? "NONE" : "LIMITED");
345
+ return {
346
+ subject: toSubject(body.subject, fallback),
347
+ facts,
348
+ capabilities,
349
+ threatSignals,
350
+ unknowns,
351
+ historyStatus,
352
+ historyConfidence: asNumber(body.historyConfidence) ?? void 0,
353
+ producedAt: asString(body.producedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
354
+ demotedSignals
355
+ };
356
+ }
357
+ function toSubject(raw, fallback) {
358
+ if (!isRecord(raw)) return fallback;
359
+ const kind = asString(raw.kind);
360
+ return {
361
+ kind: isSubjectKind(kind) ? kind : fallback.kind,
362
+ chain: asString(raw.chain) ?? fallback.chain,
363
+ identifier: asString(raw.identifier) ?? fallback.identifier
364
+ };
365
+ }
366
+ function toFact(raw) {
367
+ if (!isRecord(raw)) return null;
368
+ const type = asString(raw.type);
369
+ const statement = asString(raw.statement);
370
+ if (!type || !statement) return null;
371
+ return {
372
+ type,
373
+ value: asFactValue(raw.value),
374
+ statement,
375
+ source: asString(raw.source) ?? "decentrys",
376
+ observedAt: asString(raw.observedAt) ?? (/* @__PURE__ */ new Date()).toISOString()
377
+ };
378
+ }
379
+ function toCapability(raw) {
380
+ if (!isRecord(raw)) return null;
381
+ const type = asString(raw.type);
382
+ const statement = asString(raw.statement);
383
+ if (!type || !statement) return null;
384
+ const severity = asString(raw.severity);
385
+ return {
386
+ type,
387
+ // An unrecognised severity becomes INFO, the least consequential value.
388
+ // Guessing upward would let a typo raise someone's risk level.
389
+ severity: severity === "NOTABLE" || severity === "SIGNIFICANT" ? severity : "INFO",
390
+ statement,
391
+ grantedBy: asString(raw.grantedBy) ?? void 0
392
+ };
393
+ }
394
+ function toSignal(raw) {
395
+ if (!isRecord(raw)) return null;
396
+ const type = asString(raw.type);
397
+ const explanation = asString(raw.explanation);
398
+ if (!type || !explanation) return null;
399
+ const evidence = asArray(raw.evidence).map(toEvidence).filter(isPresent);
400
+ const createdAt = asString(raw.createdAt) ?? (/* @__PURE__ */ new Date()).toISOString();
401
+ return {
402
+ type,
403
+ severity: asSeverity(raw.severity),
404
+ confidence: clamp01(asNumber(raw.confidence) ?? 0),
405
+ explanation,
406
+ // A missing hop count means we do not know how far away this is, and an
407
+ // unknown distance is an inference, not a direct observation.
408
+ hops: Math.max(0, Math.trunc(asNumber(raw.hops) ?? 1)),
409
+ evidence,
410
+ status: asSignalStatus(raw.status),
411
+ createdAt,
412
+ lastSeen: asString(raw.lastSeen) ?? createdAt,
413
+ expiresAt: asString(raw.expiresAt) ?? void 0
414
+ };
415
+ }
416
+ function toEvidence(raw) {
417
+ if (!isRecord(raw)) return null;
418
+ const id = asString(raw.id);
419
+ const type = asString(raw.type);
420
+ if (!id || !type) return null;
421
+ return {
422
+ id,
423
+ type,
424
+ source: asString(raw.source) ?? "decentrys",
425
+ chain: asString(raw.chain) ?? void 0,
426
+ txHash: asString(raw.txHash) ?? void 0,
427
+ contract: asString(raw.contract) ?? void 0,
428
+ address: asString(raw.address) ?? void 0,
429
+ observedAt: asString(raw.observedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
430
+ confidence: clamp01(asNumber(raw.confidence) ?? 0),
431
+ // Defaults to false. `analystVerified` is what gates KNOWN_MALICIOUS, so
432
+ // an absent field must never be read as a human having checked.
433
+ analystVerified: raw.analystVerified === true,
434
+ metadata: isRecord(raw.metadata) ? raw.metadata : void 0
435
+ };
436
+ }
437
+ function toUnknown(raw) {
438
+ if (!isRecord(raw)) return null;
439
+ const field = asString(raw.field);
440
+ const statement = asString(raw.statement);
441
+ if (!field || !statement) return null;
442
+ const reason = asString(raw.reason);
443
+ const reasons = ["UNKNOWN", "INSUFFICIENT_DATA", "PROVIDER_UNAVAILABLE", "NOT_APPLICABLE"];
444
+ return {
445
+ field,
446
+ reason: reasons.includes(reason) ? reason : "UNKNOWN",
447
+ statement
448
+ };
449
+ }
450
+ function isRecord(value) {
451
+ return typeof value === "object" && value !== null && !Array.isArray(value);
452
+ }
453
+ function asArray(value) {
454
+ return Array.isArray(value) ? value : [];
455
+ }
456
+ function asString(value) {
457
+ return typeof value === "string" && value.length > 0 ? value : null;
458
+ }
459
+ function asNumber(value) {
460
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
461
+ }
462
+ function asFactValue(value) {
463
+ if (typeof value === "string" || typeof value === "boolean") return value;
464
+ if (typeof value === "number" && Number.isFinite(value)) return value;
465
+ return null;
466
+ }
467
+ function asSeverity(value) {
468
+ const severities = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
469
+ return severities.includes(value) ? value : "LOW";
470
+ }
471
+ function asSignalStatus(value) {
472
+ const statuses = ["ACTIVE", "STALE", "RESOLVED", "DISPUTED_FACT", "REMOVED"];
473
+ return statuses.includes(value) ? value : "ACTIVE";
474
+ }
475
+ function asHistoryStatus(value) {
476
+ const statuses = ["ESTABLISHED", "MODERATE", "LIMITED", "NONE"];
477
+ return statuses.includes(value) ? value : null;
478
+ }
479
+ function isSubjectKind(value) {
480
+ return value === "address" || value === "contract" || value === "token" || value === "transaction" || value === "approval" || value === "dapp";
481
+ }
482
+ function clamp01(value) {
483
+ return Math.min(1, Math.max(0, value));
484
+ }
485
+ function isPresent(value) {
486
+ return value !== null;
487
+ }
488
+ function unavailableSimulation(reason, now = /* @__PURE__ */ new Date()) {
489
+ return {
490
+ outcome: "UNAVAILABLE",
491
+ balanceChanges: [],
492
+ approvalChanges: [],
493
+ contractsCalled: [],
494
+ unavailableReason: reason,
495
+ simulatedAt: now.toISOString()
496
+ };
497
+ }
498
+ var TransportError = class extends Error {
499
+ failure;
500
+ status;
501
+ constructor(failure, message, status) {
502
+ super(message);
503
+ this.name = "TransportError";
504
+ this.failure = failure;
505
+ this.status = status;
506
+ }
507
+ };
508
+ function failureForStatus(status) {
509
+ if (status === 401) return "unauthorized";
510
+ if (status === 403) return "forbidden";
511
+ if (status === 429) return "rate_limited";
512
+ if (status >= 400 && status < 500) return "invalid_request";
513
+ return "server_error";
514
+ }
515
+ var RETRYABLE = ["network", "timeout", "server_error", "rate_limited"];
516
+ var HttpTransport = class {
517
+ constructor(config) {
518
+ this.config = config;
519
+ }
520
+ config;
521
+ async request(options) {
522
+ const attempts = options.idempotent ? this.config.retries + 1 : 1;
523
+ let last = new TransportError("network", "No attempt was made.");
524
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
525
+ try {
526
+ return await this.attempt(options);
527
+ } catch (error) {
528
+ last = error instanceof TransportError ? error : new TransportError("network", error instanceof Error ? error.message : "Unknown error.");
529
+ if (!RETRYABLE.includes(last.failure)) throw last;
530
+ if (attempt === attempts - 1) throw last;
531
+ await delay(120 * (attempt + 1));
532
+ }
533
+ }
534
+ throw last;
535
+ }
536
+ async attempt(options) {
537
+ const controller = new AbortController();
538
+ const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
539
+ const onAbort = () => controller.abort();
540
+ options.signal?.addEventListener("abort", onAbort);
541
+ try {
542
+ const response = await this.config.fetch(joinUrl(this.config.baseUrl, options.path), {
543
+ method: "POST",
544
+ headers: {
545
+ "content-type": "application/json",
546
+ "x-api-key": this.config.apiKey,
547
+ "user-agent": this.config.userAgent
548
+ },
549
+ body: JSON.stringify(options.body ?? {}),
550
+ signal: controller.signal
551
+ });
552
+ const text = await response.text();
553
+ if (!response.ok) {
554
+ throw new TransportError(
555
+ failureForStatus(response.status),
556
+ messageFrom(text) ?? `Decentrys returned HTTP ${response.status}.`,
557
+ response.status
558
+ );
559
+ }
560
+ let parsed;
561
+ try {
562
+ parsed = JSON.parse(text);
563
+ } catch {
564
+ throw new TransportError("malformed_response", "The response was not valid JSON.");
565
+ }
566
+ const envelope = parsed;
567
+ return envelope && typeof envelope === "object" && "data" in envelope ? envelope.data : parsed;
568
+ } catch (error) {
569
+ if (error instanceof TransportError) throw error;
570
+ if (isAbort(error)) {
571
+ throw options.signal?.aborted ? new TransportError("network", "The request was cancelled by the caller.") : new TransportError("timeout", `No response within ${this.config.timeoutMs}ms.`);
572
+ }
573
+ throw new TransportError("network", error instanceof Error ? error.message : "Network request failed.");
574
+ } finally {
575
+ clearTimeout(timer);
576
+ options.signal?.removeEventListener("abort", onAbort);
577
+ }
578
+ }
579
+ };
580
+ function isAbort(error) {
581
+ return typeof error === "object" && error !== null && (error.name === "AbortError" || error.code === "ABORT_ERR");
582
+ }
583
+ function messageFrom(text) {
584
+ try {
585
+ const body = JSON.parse(text);
586
+ if (typeof body.message === "string" && body.message.trim()) return body.message;
587
+ if (typeof body.error === "string" && body.error.trim()) return body.error;
588
+ } catch {
589
+ }
590
+ return null;
591
+ }
592
+ function joinUrl(base, path) {
593
+ return `${base.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
594
+ }
595
+ function delay(ms) {
596
+ return new Promise((resolve) => {
597
+ setTimeout(resolve, ms);
598
+ });
599
+ }
600
+ var TtlCache = class {
601
+ constructor(options) {
602
+ this.options = options;
603
+ this.now = options.now ?? (() => Date.now());
604
+ }
605
+ options;
606
+ entries = /* @__PURE__ */ new Map();
607
+ now;
608
+ get(key) {
609
+ const entry = this.entries.get(key);
610
+ if (!entry) return void 0;
611
+ if (entry.expiresAt <= this.now()) {
612
+ this.entries.delete(key);
613
+ return void 0;
614
+ }
615
+ this.entries.delete(key);
616
+ this.entries.set(key, entry);
617
+ return entry.value;
618
+ }
619
+ set(key, value, ttlMs = this.options.ttlMs) {
620
+ if (ttlMs <= 0 || this.options.maxEntries <= 0) return;
621
+ this.entries.delete(key);
622
+ this.entries.set(key, { value, expiresAt: this.now() + ttlMs });
623
+ while (this.entries.size > this.options.maxEntries) {
624
+ const oldest = this.entries.keys().next();
625
+ if (oldest.done) break;
626
+ this.entries.delete(oldest.value);
627
+ }
628
+ }
629
+ delete(key) {
630
+ this.entries.delete(key);
631
+ }
632
+ clear() {
633
+ this.entries.clear();
634
+ }
635
+ get size() {
636
+ return this.entries.size;
637
+ }
638
+ };
639
+ function cacheKey(kind, parts) {
640
+ return [kind, ...parts.map((p) => p === void 0 || p === null ? "" : String(p))].join("|");
641
+ }
642
+ var SDK_VERSION = "0.1.0";
643
+ var DEFAULT_BASE_URL = "https://api.decentrys.com";
644
+ var DEFAULT_TIMEOUT_MS = 4e3;
645
+ var DEFAULT_CACHE_TTL_MS = 12e4;
646
+ var DEFAULT_CACHE_ENTRIES = 500;
647
+ var Decentrys = class {
648
+ transport;
649
+ failMode;
650
+ policy;
651
+ cache;
652
+ constructor(config) {
653
+ if (!config.apiKey || !config.apiKey.trim()) {
654
+ throw new Error("Decentrys: an apiKey is required. Create one at https://decentrys.com/developers.");
655
+ }
656
+ this.failMode = config.failMode ?? "warn";
657
+ this.policy = config.policy ?? {};
658
+ this.cache = new TtlCache({
659
+ ttlMs: config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,
660
+ maxEntries: config.cacheMaxEntries ?? DEFAULT_CACHE_ENTRIES
661
+ });
662
+ this.transport = config.transport ?? new HttpTransport({
663
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
664
+ apiKey: config.apiKey,
665
+ timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
666
+ retries: config.retries ?? 1,
667
+ fetch: config.fetch ?? resolveFetch(),
668
+ userAgent: `decentrys-protect/${SDK_VERSION}`
669
+ });
670
+ }
671
+ // -------------------------------------------------------------------------
672
+ // Assessments
673
+ // -------------------------------------------------------------------------
674
+ /** Pre-sign analysis of a transaction the user is about to approve. */
675
+ async assessTransaction(tx, options = {}) {
676
+ return this.assess({
677
+ path: "/v1/protect/transaction",
678
+ body: tx,
679
+ subject: { kind: "transaction", chain: tx.chain, identifier: tx.to ?? tx.from },
680
+ // A transaction's assessment depends on its calldata and its moment.
681
+ // Caching one would serve a stale answer for a different transaction.
682
+ cacheable: false,
683
+ idempotent: true,
684
+ options
685
+ });
686
+ }
687
+ /** What a contract *can* do — capabilities, proxy status, admin controls. */
688
+ async scanContract(request, options = {}) {
689
+ return this.assess({
690
+ path: "/v1/protect/contract",
691
+ body: request,
692
+ subject: { kind: "contract", chain: request.chain, identifier: request.address },
693
+ cacheable: true,
694
+ idempotent: true,
695
+ options
696
+ });
697
+ }
698
+ async screenAddress(request, options = {}) {
699
+ return this.assess({
700
+ path: "/v1/protect/address",
701
+ body: request,
702
+ subject: { kind: "address", chain: request.chain, identifier: request.address },
703
+ cacheable: true,
704
+ idempotent: true,
705
+ options
706
+ });
707
+ }
708
+ async screenToken(request, options = {}) {
709
+ return this.assess({
710
+ path: "/v1/protect/token",
711
+ body: request,
712
+ subject: { kind: "token", chain: request.chain, identifier: request.address },
713
+ cacheable: true,
714
+ idempotent: true,
715
+ options
716
+ });
717
+ }
718
+ /**
719
+ * An approval is assessed on the spender and the allowance together.
720
+ *
721
+ * Not cached: the same spender with an unlimited allowance and with a
722
+ * one-off allowance are different decisions, and the amount is the part a
723
+ * user most needs told.
724
+ */
725
+ async screenApproval(request, options = {}) {
726
+ return this.assess({
727
+ path: "/v1/protect/approval",
728
+ body: request,
729
+ subject: { kind: "approval", chain: request.chain, identifier: request.spender },
730
+ cacheable: false,
731
+ idempotent: true,
732
+ options
733
+ });
734
+ }
735
+ async assessDapp(request, options = {}) {
736
+ return this.assess({
737
+ path: "/v1/protect/dapp",
738
+ body: request,
739
+ subject: { kind: "dapp", chain: request.chain ?? "multi", identifier: request.origin },
740
+ cacheable: true,
741
+ idempotent: true,
742
+ options
743
+ });
744
+ }
745
+ /**
746
+ * The threat signals on a subject, without a classification.
747
+ *
748
+ * For integrators building their own presentation. An empty array means no
749
+ * signals were found — which is not the same as safe, and the SDK will not
750
+ * pretend otherwise on their behalf.
751
+ */
752
+ async getThreatSignals(request, options = {}) {
753
+ try {
754
+ const raw = await this.transport.request({
755
+ path: "/v1/protect/signals",
756
+ body: request,
757
+ idempotent: true,
758
+ signal: options.signal
759
+ });
760
+ return normalizeEvidence(raw, {
761
+ kind: "address",
762
+ chain: request.chain,
763
+ identifier: request.address
764
+ }).threatSignals;
765
+ } catch {
766
+ return [];
767
+ }
768
+ }
769
+ // -------------------------------------------------------------------------
770
+ // Decoding and simulation
771
+ // -------------------------------------------------------------------------
772
+ /** What this transaction does, in the words a user would use. */
773
+ async explainTransaction(tx, options = {}) {
774
+ try {
775
+ const raw = await this.transport.request({
776
+ path: "/v1/protect/explain",
777
+ body: tx,
778
+ idempotent: true,
779
+ signal: options.signal
780
+ });
781
+ return {
782
+ summary: typeof raw?.summary === "string" && raw.summary ? raw.summary : "This transaction could not be decoded.",
783
+ actions: stringList(raw?.actions),
784
+ exposure: stringList(raw?.exposure),
785
+ undecoded: stringList(raw?.undecoded)
786
+ };
787
+ } catch (error) {
788
+ return {
789
+ summary: "This transaction could not be decoded.",
790
+ actions: [],
791
+ exposure: [],
792
+ undecoded: [`Decentrys could not be reached: ${describe(error)}.`]
793
+ };
794
+ }
795
+ }
796
+ /** Execute the transaction against a fork and report what would change. */
797
+ async simulateTransaction(tx, options = {}) {
798
+ try {
799
+ const raw = await this.transport.request({
800
+ path: "/v1/protect/simulate",
801
+ body: tx,
802
+ idempotent: true,
803
+ signal: options.signal
804
+ });
805
+ return normalizeSimulation(raw);
806
+ } catch (error) {
807
+ return unavailableSimulation(describe(error));
808
+ }
809
+ }
810
+ // -------------------------------------------------------------------------
811
+ // Cache control
812
+ // -------------------------------------------------------------------------
813
+ /** Drop cached evidence. Call after a user reports a stale result. */
814
+ clearCache() {
815
+ this.cache.clear();
816
+ }
817
+ // -------------------------------------------------------------------------
818
+ // Internals
819
+ // -------------------------------------------------------------------------
820
+ async assess(params) {
821
+ const key = params.cacheable ? cacheKey(params.subject.kind, [params.subject.chain, params.subject.identifier]) : null;
822
+ if (key && !params.options.skipCache) {
823
+ const hit = this.cache.get(key);
824
+ if (hit) return this.finish(hit, true);
825
+ }
826
+ let evidence;
827
+ try {
828
+ const raw = await this.transport.request({
829
+ path: params.path,
830
+ body: params.body,
831
+ idempotent: params.idempotent,
832
+ signal: params.options.signal
833
+ });
834
+ evidence = normalizeEvidence(raw, params.subject);
835
+ } catch (error) {
836
+ return {
837
+ subject: params.subject,
838
+ assessment: unavailableAssessment(this.failMode, describe(error)),
839
+ decision: {
840
+ // `closed` is the only mode where unavailability is itself a stop.
841
+ // The others must not fabricate a risk level to justify blocking.
842
+ action: this.failMode === "closed" ? "block" : "warn",
843
+ reason: `Decentrys could not be reached: ${describe(error)}.`
844
+ },
845
+ cached: false,
846
+ demotedSignals: []
847
+ };
848
+ }
849
+ if (key) this.cache.set(key, evidence);
850
+ return this.finish(evidence, false);
851
+ }
852
+ finish(evidence, cached) {
853
+ const assessment = classify({
854
+ facts: evidence.facts,
855
+ capabilities: evidence.capabilities,
856
+ threatSignals: evidence.threatSignals,
857
+ unknowns: evidence.unknowns,
858
+ historyStatus: evidence.historyStatus,
859
+ historyConfidence: evidence.historyConfidence
860
+ });
861
+ return {
862
+ subject: evidence.subject,
863
+ assessment,
864
+ decision: applyPolicy(assessment, this.policy),
865
+ cached,
866
+ demotedSignals: evidence.demotedSignals
867
+ };
868
+ }
869
+ };
870
+ function resolveFetch() {
871
+ const candidate = globalThis.fetch;
872
+ if (typeof candidate !== "function") {
873
+ throw new Error(
874
+ "Decentrys: no global fetch was found. Pass one via `new Decentrys({ fetch })` (Node 18+, modern browsers and React Native provide one)."
875
+ );
876
+ }
877
+ return candidate.bind(globalThis);
878
+ }
879
+ function describe(error) {
880
+ if (error instanceof TransportError) return error.message;
881
+ if (error instanceof Error) return error.message;
882
+ return "unknown error";
883
+ }
884
+ function stringList(value) {
885
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.length > 0) : [];
886
+ }
887
+ function normalizeSimulation(raw) {
888
+ const body = typeof raw === "object" && raw !== null ? raw : {};
889
+ const outcomes = ["SUCCESS", "REVERT", "NOT_SUPPORTED", "UNAVAILABLE"];
890
+ return {
891
+ // An unrecognised outcome is not a success. Defaulting the other way would
892
+ // let a malformed response read as "this transaction is fine".
893
+ outcome: outcomes.includes(body.outcome) ? body.outcome : "UNAVAILABLE",
894
+ revertReason: typeof body.revertReason === "string" ? body.revertReason : void 0,
895
+ balanceChanges: Array.isArray(body.balanceChanges) ? body.balanceChanges : [],
896
+ approvalChanges: Array.isArray(body.approvalChanges) ? body.approvalChanges : [],
897
+ contractsCalled: stringList(body.contractsCalled),
898
+ gasUsed: typeof body.gasUsed === "string" ? body.gasUsed : void 0,
899
+ unavailableReason: typeof body.unavailableReason === "string" ? body.unavailableReason : void 0,
900
+ simulatedAt: typeof body.simulatedAt === "string" ? body.simulatedAt : (/* @__PURE__ */ new Date()).toISOString()
901
+ };
902
+ }
903
+
904
+ // src/policy.ts
905
+ var DEFAULT_AGENT_RISK_POLICY = {
906
+ NO_CRITICAL_RISK_DETECTED: "allow",
907
+ INFORMATIONAL: "allow",
908
+ CAUTION: "allow",
909
+ ELEVATED_RISK: "require_confirmation",
910
+ HIGH_RISK: "require_confirmation",
911
+ CRITICAL_THREAT: "require_confirmation",
912
+ KNOWN_MALICIOUS: "block"
913
+ };
914
+ var UNLIMITED_ALLOWANCE_THRESHOLD = 1n << 255n;
915
+ var POLICY_ACTION_STRICTNESS = {
916
+ allow: 0,
917
+ inform: 1,
918
+ warn: 2,
919
+ warn_strong: 3,
920
+ require_confirmation: 4,
921
+ block: 5
922
+ };
923
+ var FAIL_MODE_STRICTNESS = { open: 0, escalate: 1, closed: 2 };
924
+ var SEALED_POLICY = /* @__PURE__ */ Symbol("decentrys.agent.sealedPolicy");
925
+ function sealPolicy(policy) {
926
+ const risk = policy.risk ?? {};
927
+ for (const [level, action] of Object.entries(risk)) {
928
+ if (action === "warn" || action === "warn_strong") {
929
+ throw new Error(
930
+ `AgentGuard: '${action}' is not a valid action for an agent policy (at ${level}). There is nobody to warn. Use 'allow' to permit and record it, or 'require_confirmation' to route it to a person.`
931
+ );
932
+ }
933
+ }
934
+ const cloned = clonePolicy(policy);
935
+ const warnings = [];
936
+ const effectiveRisk = { ...DEFAULT_AGENT_RISK_POLICY, ...cloned.risk };
937
+ if (effectiveRisk.KNOWN_MALICIOUS !== "block") {
938
+ warnings.push(
939
+ "This policy does not deny KNOWN_MALICIOUS counterparties. That level requires analyst-verified evidence and is the one classification Decentrys treats as an accusation."
940
+ );
941
+ }
942
+ if (!hasAnyLimit(cloned)) {
943
+ warnings.push(
944
+ "This policy sets no value cap, no cumulative window, no allowlist and no rate ceiling. The agent is bounded only by risk classification."
945
+ );
946
+ }
947
+ return deepFreeze({
948
+ [SEALED_POLICY]: true,
949
+ policy: cloned,
950
+ fingerprint: fingerprint(cloned),
951
+ warnings
952
+ });
953
+ }
954
+ function narrowPolicy(base, restriction) {
955
+ const a = base.policy;
956
+ const b = clonePolicy(restriction);
957
+ if (a.agentId && b.agentId && a.agentId !== b.agentId) {
958
+ throw new Error(
959
+ `AgentGuard: cannot narrow a policy bound to agent '${a.agentId}' with one bound to '${b.agentId}'.`
960
+ );
961
+ }
962
+ return sealPolicy({
963
+ version: [a.version, b.version].filter(Boolean).join("+") || void 0,
964
+ agentId: a.agentId ?? b.agentId,
965
+ risk: strictestRisk(a.risk, b.risk),
966
+ maxRiskLevel: lowerRiskLevel(a.maxRiskLevel, b.maxRiskLevel),
967
+ maxValuePerActionUsd: minDefined(a.maxValuePerActionUsd, b.maxValuePerActionUsd),
968
+ spendWindows: concatWindows(a.spendWindows, b.spendWindows),
969
+ rateWindows: concatWindows(a.rateWindows, b.rateWindows),
970
+ allowedCounterparties: intersectLists(a.allowedCounterparties, b.allowedCounterparties),
971
+ blockedCounterparties: unionLists(a.blockedCounterparties, b.blockedCounterparties),
972
+ allowedContracts: intersectLists(a.allowedContracts, b.allowedContracts),
973
+ allowedTokens: intersectLists(a.allowedTokens, b.allowedTokens),
974
+ allowedActions: intersectLists(a.allowedActions, b.allowedActions),
975
+ deniedActions: unionLists(a.deniedActions, b.deniedActions),
976
+ allowedChains: intersectLists(a.allowedChains, b.allowedChains),
977
+ allowUnlimitedApprovals: (a.allowUnlimitedApprovals ?? false) && (b.allowUnlimitedApprovals ?? false),
978
+ humanApprovalAboveUsd: minDefined(a.humanApprovalAboveUsd, b.humanApprovalAboveUsd),
979
+ humanApprovalAtRiskLevel: lowerRiskLevel(a.humanApprovalAtRiskLevel, b.humanApprovalAtRiskLevel),
980
+ failMode: stricterFailMode(a.failMode, b.failMode)
981
+ });
982
+ }
983
+ function evaluatePolicy(input) {
984
+ const now = input.now ?? /* @__PURE__ */ new Date();
985
+ const nowMs = now.getTime();
986
+ const { action } = input;
987
+ const policy = input.policy.policy;
988
+ const evaluations = [];
989
+ if (policy.agentId) {
990
+ const bound = policy.agentId === action.agentId;
991
+ evaluations.push({
992
+ rule: "AGENT_BINDING",
993
+ outcome: bound ? "pass" : "fail",
994
+ statement: bound ? `This policy is bound to agent '${policy.agentId}', which proposed the action.` : `This policy is bound to agent '${policy.agentId}'; the action was proposed by '${action.agentId}'.`,
995
+ limit: policy.agentId,
996
+ observed: action.agentId
997
+ });
998
+ } else {
999
+ evaluations.push({
1000
+ rule: "AGENT_BINDING",
1001
+ outcome: "not_configured",
1002
+ statement: "This policy is not bound to a specific agent."
1003
+ });
1004
+ }
1005
+ evaluations.push(evaluateActionType(action.type, policy.allowedActions, policy.deniedActions));
1006
+ evaluations.push(evaluateAllowlist(
1007
+ "CHAIN",
1008
+ "chain",
1009
+ action.chain,
1010
+ policy.allowedChains
1011
+ ));
1012
+ evaluations.push(evaluateCounterparty(action, policy));
1013
+ evaluations.push(evaluateContract(action, policy));
1014
+ evaluations.push(evaluateAllowlist("TOKEN", "token", action.token, policy.allowedTokens));
1015
+ evaluations.push(evaluateAllowance(action, policy.allowUnlimitedApprovals ?? false));
1016
+ evaluations.push(evaluatePerActionValue(action, policy.maxValuePerActionUsd));
1017
+ evaluations.push(evaluateCumulativeSpend(action, policy.spendWindows, input.usage, nowMs));
1018
+ evaluations.push(evaluateRate(action, policy.rateWindows, input.usage, nowMs));
1019
+ const effectiveRisk = { ...DEFAULT_AGENT_RISK_POLICY, ...policy.risk };
1020
+ evaluations.push(evaluateAvailability(input.assessment, input.assessmentUnavailable, policy.failMode ?? "escalate"));
1021
+ evaluations.push(evaluateRisk(input.assessment, effectiveRisk, policy.maxRiskLevel));
1022
+ evaluations.push(evaluateHumanApproval(action, policy, input.assessment));
1023
+ const verdict = verdictFrom(evaluations);
1024
+ const reasons = evaluations.filter((e) => e.outcome === "fail" || e.outcome === "escalate").map((e) => e.statement);
1025
+ return {
1026
+ decisionId: input.decisionId ?? newDecisionId(),
1027
+ agentId: action.agentId,
1028
+ verdict,
1029
+ reasons,
1030
+ evaluations,
1031
+ ...input.assessment ? { assessment: input.assessment } : {},
1032
+ ...input.assessmentUnavailable ? { assessmentUnavailable: input.assessmentUnavailable } : {},
1033
+ policyFingerprint: input.policy.fingerprint,
1034
+ ...policy.version ? { policyVersion: policy.version } : {},
1035
+ actionDigest: fingerprint(canonicalAction(action)),
1036
+ modelVersion: AGENT_MODEL_VERSION,
1037
+ decidedAt: now.toISOString()
1038
+ };
1039
+ }
1040
+ function verdictFrom(evaluations) {
1041
+ if (evaluations.some((e) => e.outcome === "fail")) return "deny";
1042
+ if (evaluations.some((e) => e.outcome === "escalate")) return "require_human_approval";
1043
+ return "allow";
1044
+ }
1045
+ function evaluateActionType(type, allowed, denied) {
1046
+ if (denied?.includes(type)) {
1047
+ return {
1048
+ rule: "ACTION_TYPE",
1049
+ outcome: "fail",
1050
+ statement: `Actions of type '${type}' are denied by this policy.`,
1051
+ limit: `denied: ${denied.join(", ")}`,
1052
+ observed: type
1053
+ };
1054
+ }
1055
+ if (allowed) {
1056
+ const ok = allowed.includes(type);
1057
+ return {
1058
+ rule: "ACTION_TYPE",
1059
+ outcome: ok ? "pass" : "fail",
1060
+ statement: ok ? `'${type}' is one of the action types this agent may perform.` : `'${type}' is not among the action types this agent may perform.`,
1061
+ limit: allowed.join(", "),
1062
+ observed: type
1063
+ };
1064
+ }
1065
+ return {
1066
+ rule: "ACTION_TYPE",
1067
+ outcome: "not_configured",
1068
+ statement: "This policy does not restrict which action types the agent may perform.",
1069
+ observed: type
1070
+ };
1071
+ }
1072
+ function evaluateAllowlist(rule, label, value, allowed) {
1073
+ if (!allowed) {
1074
+ return {
1075
+ rule,
1076
+ outcome: "not_configured",
1077
+ statement: `This policy does not restrict the ${label}.`,
1078
+ ...value === void 0 ? {} : { observed: value }
1079
+ };
1080
+ }
1081
+ if (value === void 0) {
1082
+ return {
1083
+ rule,
1084
+ outcome: "not_applicable",
1085
+ statement: `The action carries no ${label}, so the ${label} allowlist does not apply.`,
1086
+ limit: allowed.join(", ")
1087
+ };
1088
+ }
1089
+ const ok = matchesList(value, allowed);
1090
+ return {
1091
+ rule,
1092
+ outcome: ok ? "pass" : "fail",
1093
+ statement: ok ? `The ${label} '${value}' is on the allowlist.` : `The ${label} '${value}' is not on the allowlist.`,
1094
+ limit: allowed.join(", "),
1095
+ observed: value
1096
+ };
1097
+ }
1098
+ var CODE_ADDRESSED = ["contract_call", "approve", "swap", "bridge", "stake", "unstake"];
1099
+ function evaluateCounterparty(action, policy) {
1100
+ const to = action.to;
1101
+ const blocked = policy.blockedCounterparties;
1102
+ const allowed = policy.allowedCounterparties;
1103
+ if (to && blocked && matchesList(to, blocked)) {
1104
+ return {
1105
+ rule: "COUNTERPARTY",
1106
+ outcome: "fail",
1107
+ statement: `The counterparty ${to} is on this policy's blocklist.`,
1108
+ limit: "blocklist",
1109
+ observed: to
1110
+ };
1111
+ }
1112
+ if (!allowed) {
1113
+ return {
1114
+ rule: "COUNTERPARTY",
1115
+ outcome: blocked && blocked.length > 0 ? "pass" : "not_configured",
1116
+ statement: blocked && blocked.length > 0 ? `The counterparty ${to ?? "(none)"} is not on this policy's blocklist.` : "This policy does not restrict counterparties.",
1117
+ ...to === void 0 ? {} : { observed: to }
1118
+ };
1119
+ }
1120
+ if (!to) {
1121
+ return {
1122
+ rule: "COUNTERPARTY",
1123
+ outcome: "fail",
1124
+ statement: "This policy allowlists counterparties, and the action names none. An action with no identifiable recipient cannot be checked against an allowlist.",
1125
+ limit: `${allowed.length} allowlisted`
1126
+ };
1127
+ }
1128
+ const ok = matchesList(to, allowed);
1129
+ return {
1130
+ rule: "COUNTERPARTY",
1131
+ outcome: ok ? "pass" : "fail",
1132
+ statement: ok ? `The counterparty ${to} is on this policy's allowlist.` : `The counterparty ${to} is not on this policy's allowlist.`,
1133
+ limit: `${allowed.length} allowlisted`,
1134
+ observed: to
1135
+ };
1136
+ }
1137
+ function evaluateContract(action, policy) {
1138
+ const allowed = policy.allowedContracts;
1139
+ if (!allowed) {
1140
+ return {
1141
+ rule: "CONTRACT",
1142
+ outcome: "not_configured",
1143
+ statement: "This policy does not restrict which contracts the agent may call."
1144
+ };
1145
+ }
1146
+ if (!CODE_ADDRESSED.includes(action.type)) {
1147
+ return {
1148
+ rule: "CONTRACT",
1149
+ outcome: "not_applicable",
1150
+ statement: `A '${action.type}' action does not call a contract, so the contract allowlist does not apply.`,
1151
+ limit: `${allowed.length} allowlisted`
1152
+ };
1153
+ }
1154
+ if (!action.to) {
1155
+ return {
1156
+ rule: "CONTRACT",
1157
+ outcome: "fail",
1158
+ statement: `A '${action.type}' action must name the contract it calls for the allowlist to be applied.`,
1159
+ limit: `${allowed.length} allowlisted`
1160
+ };
1161
+ }
1162
+ const ok = matchesList(action.to, allowed);
1163
+ return {
1164
+ rule: "CONTRACT",
1165
+ outcome: ok ? "pass" : "fail",
1166
+ statement: ok ? `The contract ${action.to} is on this policy's allowlist.` : `The contract ${action.to} is not on this policy's allowlist.`,
1167
+ limit: `${allowed.length} allowlisted`,
1168
+ observed: action.to
1169
+ };
1170
+ }
1171
+ function evaluateAllowance(action, allowUnlimited) {
1172
+ if (action.type !== "approve") {
1173
+ return {
1174
+ rule: "APPROVAL_ALLOWANCE",
1175
+ outcome: "not_applicable",
1176
+ statement: `A '${action.type}' action grants no allowance.`
1177
+ };
1178
+ }
1179
+ const amount = action.approvalAmount;
1180
+ if (amount === void 0) {
1181
+ return {
1182
+ rule: "APPROVAL_ALLOWANCE",
1183
+ outcome: "fail",
1184
+ statement: "An approval was proposed without stating the allowance. The size of an allowance is the whole of what is being granted, and an unstated one cannot be bounded."
1185
+ };
1186
+ }
1187
+ const unlimited = isUnlimitedAllowance(amount);
1188
+ if (!unlimited) {
1189
+ return {
1190
+ rule: "APPROVAL_ALLOWANCE",
1191
+ outcome: "pass",
1192
+ statement: `The approval grants a bounded allowance of ${amount} base units.`,
1193
+ observed: amount
1194
+ };
1195
+ }
1196
+ return {
1197
+ rule: "APPROVAL_ALLOWANCE",
1198
+ outcome: allowUnlimited ? "pass" : "fail",
1199
+ statement: allowUnlimited ? "The approval is unlimited, which this policy explicitly permits." : "The approval grants an unlimited allowance. This policy does not permit one \u2014 an unlimited allowance turns any later compromise of the spender into an unbounded loss.",
1200
+ limit: allowUnlimited ? "unlimited permitted" : "bounded allowances only",
1201
+ observed: "unlimited"
1202
+ };
1203
+ }
1204
+ function isUnlimitedAllowance(amount) {
1205
+ const trimmed = amount.trim();
1206
+ if (/^unlimited$/i.test(trimmed) || /^max$/i.test(trimmed) || /^infinite$/i.test(trimmed)) return true;
1207
+ try {
1208
+ const parsed = /^0x[0-9a-f]+$/i.test(trimmed) ? BigInt(trimmed) : BigInt(trimmed.replace(/^\+/, ""));
1209
+ return parsed >= UNLIMITED_ALLOWANCE_THRESHOLD;
1210
+ } catch {
1211
+ return false;
1212
+ }
1213
+ }
1214
+ function evaluatePerActionValue(action, cap) {
1215
+ if (cap === void 0) {
1216
+ return {
1217
+ rule: "VALUE_PER_ACTION",
1218
+ outcome: "not_configured",
1219
+ statement: "This policy sets no per-action value cap.",
1220
+ ...action.valueUsd === void 0 ? {} : { observed: action.valueUsd }
1221
+ };
1222
+ }
1223
+ if (action.valueUsd === void 0) {
1224
+ return {
1225
+ rule: "VALUE_PER_ACTION",
1226
+ outcome: "fail",
1227
+ statement: `This policy caps a single action at $${cap.toLocaleString("en-US")}, and the action's value was not stated. An unpriced action cannot be shown to be under a cap. Pass \`valueUsd\` \u2014 use 0 for an action that moves nothing.`,
1228
+ limit: cap,
1229
+ observed: "unknown"
1230
+ };
1231
+ }
1232
+ const ok = action.valueUsd <= cap;
1233
+ return {
1234
+ rule: "VALUE_PER_ACTION",
1235
+ outcome: ok ? "pass" : "fail",
1236
+ statement: ok ? `$${action.valueUsd.toLocaleString("en-US")} is within the $${cap.toLocaleString("en-US")} per-action cap.` : `$${action.valueUsd.toLocaleString("en-US")} exceeds the $${cap.toLocaleString("en-US")} per-action cap.`,
1237
+ limit: cap,
1238
+ observed: action.valueUsd
1239
+ };
1240
+ }
1241
+ function evaluateCumulativeSpend(action, windows, usage, nowMs) {
1242
+ if (!windows || windows.length === 0) {
1243
+ return {
1244
+ rule: "CUMULATIVE_SPEND",
1245
+ outcome: "not_configured",
1246
+ statement: "This policy sets no cumulative spend limit."
1247
+ };
1248
+ }
1249
+ if (action.valueUsd === void 0) {
1250
+ return {
1251
+ rule: "CUMULATIVE_SPEND",
1252
+ outcome: "fail",
1253
+ statement: "This policy limits cumulative spend, and the action's value was not stated. An unpriced action cannot be added to a running total.",
1254
+ limit: windows.map(describeSpendWindow).join("; "),
1255
+ observed: "unknown"
1256
+ };
1257
+ }
1258
+ const breached = [];
1259
+ const lines = [];
1260
+ for (const window of windows) {
1261
+ const spent = usage?.spentUsdSince(action.agentId, nowMs - window.windowMs) ?? 0;
1262
+ const projected = spent + action.valueUsd;
1263
+ lines.push(
1264
+ `${window.label}: $${round2(spent).toLocaleString("en-US")} committed, $${round2(projected).toLocaleString("en-US")} with this action, against $${window.maxValueUsd.toLocaleString("en-US")}`
1265
+ );
1266
+ if (projected > window.maxValueUsd) breached.push(window.label);
1267
+ }
1268
+ return {
1269
+ rule: "CUMULATIVE_SPEND",
1270
+ outcome: breached.length > 0 ? "fail" : "pass",
1271
+ statement: breached.length > 0 ? `This action would take the agent past its ${breached.join(" and ")} spend limit. ${lines.join("; ")}.` : `Within every cumulative spend limit. ${lines.join("; ")}.`,
1272
+ limit: windows.map(describeSpendWindow).join("; "),
1273
+ observed: action.valueUsd
1274
+ };
1275
+ }
1276
+ function evaluateRate(action, windows, usage, nowMs) {
1277
+ if (!windows || windows.length === 0) {
1278
+ return {
1279
+ rule: "RATE_LIMIT",
1280
+ outcome: "not_configured",
1281
+ statement: "This policy sets no rate ceiling. An agent in a loop is bounded only by its value limits."
1282
+ };
1283
+ }
1284
+ const breached = [];
1285
+ const lines = [];
1286
+ for (const window of windows) {
1287
+ const admitted = usage?.actionsSince(action.agentId, nowMs - window.windowMs) ?? 0;
1288
+ lines.push(`${window.label}: ${admitted} admitted, ${admitted + 1} with this action, against ${window.maxActions}`);
1289
+ if (admitted + 1 > window.maxActions) breached.push(window.label);
1290
+ }
1291
+ return {
1292
+ rule: "RATE_LIMIT",
1293
+ outcome: breached.length > 0 ? "fail" : "pass",
1294
+ statement: breached.length > 0 ? `This action would exceed the agent's ${breached.join(" and ")} rate ceiling. ${lines.join("; ")}.` : `Within every rate ceiling. ${lines.join("; ")}.`,
1295
+ limit: windows.map((w) => `${w.maxActions} per ${describeDuration(w.windowMs)} (${w.label})`).join("; ")
1296
+ };
1297
+ }
1298
+ function evaluateAvailability(assessment, unavailable, failMode) {
1299
+ if (assessment) {
1300
+ return {
1301
+ rule: "ASSESSMENT_AVAILABILITY",
1302
+ outcome: "pass",
1303
+ statement: "A Decentrys assessment was obtained for this action.",
1304
+ observed: assessment.riskLevel
1305
+ };
1306
+ }
1307
+ const why = unavailable ?? "no assessment was supplied";
1308
+ const outcome = failMode === "closed" ? "fail" : failMode === "escalate" ? "escalate" : "pass";
1309
+ return {
1310
+ rule: "ASSESSMENT_AVAILABILITY",
1311
+ outcome,
1312
+ statement: failMode === "closed" ? `No risk assessment could be obtained (${why}), and this policy is configured to stop rather than act on unscreened actions.` : failMode === "escalate" ? `No risk assessment could be obtained (${why}). The operator's own limits still applied and still bind; only the external check is missing, so this action needs a person.` : `No risk assessment could be obtained (${why}). This policy is configured to proceed on the operator's own limits alone.`,
1313
+ limit: `failMode: ${failMode}`,
1314
+ observed: "unavailable"
1315
+ };
1316
+ }
1317
+ function evaluateRisk(assessment, risk, maxRiskLevel) {
1318
+ if (!assessment) {
1319
+ return {
1320
+ rule: "RISK_LEVEL",
1321
+ outcome: "not_applicable",
1322
+ statement: "No assessment was available, so no risk level was applied. See ASSESSMENT_AVAILABILITY."
1323
+ };
1324
+ }
1325
+ const level = assessment.riskLevel;
1326
+ if (maxRiskLevel && riskRank(level) > riskRank(maxRiskLevel)) {
1327
+ return {
1328
+ rule: "RISK_LEVEL",
1329
+ outcome: "fail",
1330
+ statement: `The subject classified as ${level}, above this policy's ceiling of ${maxRiskLevel}. ` + (assessment.explanation[0] ?? ""),
1331
+ limit: maxRiskLevel,
1332
+ observed: level
1333
+ };
1334
+ }
1335
+ const decision = applyPolicy(assessment, risk);
1336
+ const outcome = decision.action === "block" ? "fail" : decision.action === "require_confirmation" ? "escalate" : "pass";
1337
+ const statement = decision.action === "block" ? `The subject classified as ${level} and this policy denies at that level. ${decision.reason}` : decision.action === "require_confirmation" ? `The subject classified as ${level}, which this policy routes to a person. ${decision.reason}` : `The subject classified as ${level}, which this policy permits. ${decision.reason}`;
1338
+ return { rule: "RISK_LEVEL", outcome, statement, limit: `${level} \u2192 ${decision.action}`, observed: level };
1339
+ }
1340
+ function evaluateHumanApproval(action, policy, assessment) {
1341
+ const valueThreshold = policy.humanApprovalAboveUsd;
1342
+ const riskThreshold = policy.humanApprovalAtRiskLevel;
1343
+ if (valueThreshold === void 0 && riskThreshold === void 0) {
1344
+ return {
1345
+ rule: "HUMAN_APPROVAL_THRESHOLD",
1346
+ outcome: "not_configured",
1347
+ statement: "This policy sets no threshold above which a person must approve."
1348
+ };
1349
+ }
1350
+ const triggers = [];
1351
+ if (valueThreshold !== void 0) {
1352
+ if (action.valueUsd === void 0) {
1353
+ triggers.push(
1354
+ `a person approves anything above $${valueThreshold.toLocaleString("en-US")} and this action's value was not stated, so it cannot be shown to be below that`
1355
+ );
1356
+ } else if (action.valueUsd > valueThreshold) {
1357
+ triggers.push(
1358
+ `$${action.valueUsd.toLocaleString("en-US")} is above the $${valueThreshold.toLocaleString("en-US")} threshold for human approval`
1359
+ );
1360
+ }
1361
+ }
1362
+ if (riskThreshold && assessment && riskRank(assessment.riskLevel) >= riskRank(riskThreshold)) {
1363
+ triggers.push(`the subject classified as ${assessment.riskLevel}, at or above the ${riskThreshold} threshold`);
1364
+ }
1365
+ if (triggers.length === 0) {
1366
+ return {
1367
+ rule: "HUMAN_APPROVAL_THRESHOLD",
1368
+ outcome: "pass",
1369
+ statement: "Below every threshold that would require a person to approve.",
1370
+ ...valueThreshold === void 0 ? {} : { limit: valueThreshold },
1371
+ ...action.valueUsd === void 0 ? {} : { observed: action.valueUsd }
1372
+ };
1373
+ }
1374
+ return {
1375
+ rule: "HUMAN_APPROVAL_THRESHOLD",
1376
+ outcome: "escalate",
1377
+ statement: `A person must approve this action: ${triggers.join("; ")}.`,
1378
+ ...valueThreshold === void 0 ? {} : { limit: valueThreshold },
1379
+ ...action.valueUsd === void 0 ? {} : { observed: action.valueUsd }
1380
+ };
1381
+ }
1382
+ function riskRank(level) {
1383
+ return RISK_LEVELS.indexOf(level);
1384
+ }
1385
+ function matchesList(value, list) {
1386
+ const needle = value.trim().toLowerCase();
1387
+ return list.some((entry) => entry.trim().toLowerCase() === needle);
1388
+ }
1389
+ function describeSpendWindow(window) {
1390
+ return `$${window.maxValueUsd.toLocaleString("en-US")} per ${describeDuration(window.windowMs)} (${window.label})`;
1391
+ }
1392
+ function describeDuration(ms) {
1393
+ if (ms % 864e5 === 0) return plural(ms / 864e5, "day");
1394
+ if (ms % 36e5 === 0) return plural(ms / 36e5, "hour");
1395
+ if (ms % 6e4 === 0) return plural(ms / 6e4, "minute");
1396
+ if (ms % 1e3 === 0) return plural(ms / 1e3, "second");
1397
+ return `${ms}ms`;
1398
+ }
1399
+ function plural(count, unit) {
1400
+ return count === 1 ? unit : `${count} ${unit}s`;
1401
+ }
1402
+ function round2(value) {
1403
+ return Math.round(value * 100) / 100;
1404
+ }
1405
+ function hasAnyLimit(policy) {
1406
+ return policy.maxValuePerActionUsd !== void 0 || (policy.spendWindows?.length ?? 0) > 0 || (policy.rateWindows?.length ?? 0) > 0 || policy.allowedCounterparties !== void 0 || (policy.blockedCounterparties?.length ?? 0) > 0 || policy.allowedContracts !== void 0 || policy.allowedTokens !== void 0 || policy.allowedActions !== void 0 || (policy.deniedActions?.length ?? 0) > 0 || policy.allowedChains !== void 0 || policy.humanApprovalAboveUsd !== void 0 || policy.humanApprovalAtRiskLevel !== void 0;
1407
+ }
1408
+ function clonePolicy(policy) {
1409
+ const cloned = {};
1410
+ if (policy.version !== void 0) cloned.version = policy.version;
1411
+ if (policy.agentId !== void 0) cloned.agentId = policy.agentId;
1412
+ if (policy.risk !== void 0) cloned.risk = { ...policy.risk };
1413
+ if (policy.maxRiskLevel !== void 0) cloned.maxRiskLevel = policy.maxRiskLevel;
1414
+ if (policy.maxValuePerActionUsd !== void 0) cloned.maxValuePerActionUsd = policy.maxValuePerActionUsd;
1415
+ if (policy.spendWindows !== void 0) cloned.spendWindows = policy.spendWindows.map((w) => ({ ...w }));
1416
+ if (policy.rateWindows !== void 0) cloned.rateWindows = policy.rateWindows.map((w) => ({ ...w }));
1417
+ if (policy.allowedCounterparties !== void 0) cloned.allowedCounterparties = [...policy.allowedCounterparties];
1418
+ if (policy.blockedCounterparties !== void 0) cloned.blockedCounterparties = [...policy.blockedCounterparties];
1419
+ if (policy.allowedContracts !== void 0) cloned.allowedContracts = [...policy.allowedContracts];
1420
+ if (policy.allowedTokens !== void 0) cloned.allowedTokens = [...policy.allowedTokens];
1421
+ if (policy.allowedActions !== void 0) cloned.allowedActions = [...policy.allowedActions];
1422
+ if (policy.deniedActions !== void 0) cloned.deniedActions = [...policy.deniedActions];
1423
+ if (policy.allowedChains !== void 0) cloned.allowedChains = [...policy.allowedChains];
1424
+ if (policy.allowUnlimitedApprovals !== void 0) cloned.allowUnlimitedApprovals = policy.allowUnlimitedApprovals;
1425
+ if (policy.humanApprovalAboveUsd !== void 0) cloned.humanApprovalAboveUsd = policy.humanApprovalAboveUsd;
1426
+ if (policy.humanApprovalAtRiskLevel !== void 0) cloned.humanApprovalAtRiskLevel = policy.humanApprovalAtRiskLevel;
1427
+ if (policy.failMode !== void 0) cloned.failMode = policy.failMode;
1428
+ return cloned;
1429
+ }
1430
+ function deepFreeze(value) {
1431
+ if (value === null || typeof value !== "object") return value;
1432
+ for (const key of Object.getOwnPropertyNames(value)) {
1433
+ deepFreeze(value[key]);
1434
+ }
1435
+ return Object.freeze(value);
1436
+ }
1437
+ function minDefined(a, b) {
1438
+ if (a === void 0) return b;
1439
+ if (b === void 0) return a;
1440
+ return Math.min(a, b);
1441
+ }
1442
+ function lowerRiskLevel(a, b) {
1443
+ if (a === void 0) return b;
1444
+ if (b === void 0) return a;
1445
+ return riskRank(a) <= riskRank(b) ? a : b;
1446
+ }
1447
+ function stricterFailMode(a, b) {
1448
+ if (a === void 0) return b;
1449
+ if (b === void 0) return a;
1450
+ return FAIL_MODE_STRICTNESS[a] >= FAIL_MODE_STRICTNESS[b] ? a : b;
1451
+ }
1452
+ function strictestRisk(a, b) {
1453
+ if (!a) return b;
1454
+ if (!b) return a;
1455
+ const merged = { ...a };
1456
+ for (const level of RISK_LEVELS) {
1457
+ const left = a[level];
1458
+ const right = b[level];
1459
+ if (left === void 0) {
1460
+ if (right !== void 0) merged[level] = right;
1461
+ continue;
1462
+ }
1463
+ if (right === void 0) continue;
1464
+ merged[level] = POLICY_ACTION_STRICTNESS[left] >= POLICY_ACTION_STRICTNESS[right] ? left : right;
1465
+ }
1466
+ return merged;
1467
+ }
1468
+ function intersectLists(a, b) {
1469
+ if (!a) return b ? [...b] : void 0;
1470
+ if (!b) return [...a];
1471
+ const right = new Set(b.map((v) => v.trim().toLowerCase()));
1472
+ return a.filter((v) => right.has(v.trim().toLowerCase()));
1473
+ }
1474
+ function unionLists(a, b) {
1475
+ if (!a && !b) return void 0;
1476
+ const seen = /* @__PURE__ */ new Set();
1477
+ const out = [];
1478
+ for (const v of [...a ?? [], ...b ?? []]) {
1479
+ const key = v.trim().toLowerCase();
1480
+ if (seen.has(key)) continue;
1481
+ seen.add(key);
1482
+ out.push(v);
1483
+ }
1484
+ return out;
1485
+ }
1486
+ function concatWindows(a, b) {
1487
+ if (!a && !b) return void 0;
1488
+ const out = [...a ?? []];
1489
+ for (const window of b ?? []) {
1490
+ out.push(out.some((existing) => existing.label === window.label) ? { ...window, label: `${window.label} (narrowed)` } : window);
1491
+ }
1492
+ return out;
1493
+ }
1494
+ function canonicalAction(action) {
1495
+ return {
1496
+ agentId: action.agentId,
1497
+ chain: action.chain,
1498
+ type: action.type,
1499
+ from: action.from,
1500
+ to: action.to ?? null,
1501
+ token: action.token ?? null,
1502
+ value: action.value ?? null,
1503
+ valueUsd: action.valueUsd ?? null,
1504
+ data: action.data ?? null,
1505
+ raw: action.raw ?? null,
1506
+ origin: action.origin ?? null,
1507
+ approvalAmount: action.approvalAmount ?? null
1508
+ };
1509
+ }
1510
+ function fingerprint(value) {
1511
+ const json = canonicalJson(value);
1512
+ let hash = 2166136261;
1513
+ for (let i = 0; i < json.length; i += 1) {
1514
+ hash ^= json.charCodeAt(i);
1515
+ hash = Math.imul(hash, 16777619) >>> 0;
1516
+ }
1517
+ return hash.toString(16).padStart(8, "0");
1518
+ }
1519
+ function canonicalJson(value) {
1520
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
1521
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
1522
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1523
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
1524
+ }
1525
+ var decisionCounter = 0;
1526
+ function newDecisionId() {
1527
+ const webCrypto = globalThis.crypto;
1528
+ if (typeof webCrypto?.randomUUID === "function") return `ad_${webCrypto.randomUUID()}`;
1529
+ decisionCounter += 1;
1530
+ return `ad_${Date.now().toString(36)}_${decisionCounter.toString(36)}`;
1531
+ }
1532
+
1533
+ // src/ledger.ts
1534
+ function isReservationLedger(usage) {
1535
+ const candidate = usage;
1536
+ return typeof candidate.reserve === "function" && typeof candidate.confirm === "function" && typeof candidate.release === "function";
1537
+ }
1538
+ var DEFAULT_RESERVATION_TTL_MS = 5 * 6e4;
1539
+ var DEFAULT_RETENTION_MS = 8 * 24 * 60 * 6e4;
1540
+ var DEFAULT_MAX_ENTRIES = 5e4;
1541
+ var SpendLedger = class {
1542
+ reservationTtlMs;
1543
+ retentionMs;
1544
+ maxEntries;
1545
+ clock;
1546
+ entries = [];
1547
+ byDecision = /* @__PURE__ */ new Map();
1548
+ byIdempotencyKey = /* @__PURE__ */ new Map();
1549
+ constructor(options = {}) {
1550
+ this.reservationTtlMs = options.reservationTtlMs ?? DEFAULT_RESERVATION_TTL_MS;
1551
+ this.retentionMs = options.retentionMs ?? DEFAULT_RETENTION_MS;
1552
+ this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
1553
+ this.clock = options.clock ?? (() => Date.now());
1554
+ }
1555
+ /**
1556
+ * Hold budget for an admitted decision.
1557
+ *
1558
+ * A repeated `idempotencyKey` returns the existing hold instead of taking a
1559
+ * second one. An agent retrying a call after a timeout is one action, and
1560
+ * counting it twice would tighten its own cap against it for no reason.
1561
+ */
1562
+ reserve(input) {
1563
+ const nowMs = input.now ? input.now.getTime() : this.clock();
1564
+ this.prune(nowMs);
1565
+ if (input.idempotencyKey) {
1566
+ const existing = this.byIdempotencyKey.get(this.keyFor(input.agentId, input.idempotencyKey));
1567
+ if (existing && existing.state !== "released" && existing.state !== "expired") return existing;
1568
+ }
1569
+ const reservation = {
1570
+ decisionId: input.decisionId,
1571
+ agentId: input.agentId,
1572
+ // A negative value would be a credit against the agent's own cap, which
1573
+ // is a way to spend more than the cap allows. Clamped, not trusted.
1574
+ valueUsd: Number.isFinite(input.valueUsd) ? Math.max(0, input.valueUsd) : 0,
1575
+ ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {},
1576
+ state: "held",
1577
+ createdAtMs: nowMs,
1578
+ expiresAtMs: nowMs + this.reservationTtlMs
1579
+ };
1580
+ this.entries.push(reservation);
1581
+ this.byDecision.set(reservation.decisionId, reservation);
1582
+ if (input.idempotencyKey) {
1583
+ this.byIdempotencyKey.set(this.keyFor(input.agentId, input.idempotencyKey), reservation);
1584
+ }
1585
+ this.enforceCeiling();
1586
+ return reservation;
1587
+ }
1588
+ /** The action happened. The hold becomes a permanent commitment. */
1589
+ confirm(decisionId, options = {}) {
1590
+ return this.settle(decisionId, "confirmed", {
1591
+ ...options.txHash === void 0 ? {} : { txHash: options.txHash },
1592
+ ...options.now === void 0 ? {} : { now: options.now }
1593
+ });
1594
+ }
1595
+ /** The action did not happen. The hold is returned to the budget. */
1596
+ release(decisionId, options = {}) {
1597
+ return this.settle(decisionId, "released", {
1598
+ ...options.note === void 0 ? {} : { note: options.note },
1599
+ ...options.now === void 0 ? {} : { now: options.now }
1600
+ });
1601
+ }
1602
+ /**
1603
+ * Value held or committed in the window.
1604
+ *
1605
+ * Released and expired holds do not count. They are actions that did not
1606
+ * happen, and charging an agent for them would shrink a cap the operator
1607
+ * set for real spending.
1608
+ */
1609
+ spentUsdSince(agentId, sinceMs) {
1610
+ this.prune(this.clock());
1611
+ let total = 0;
1612
+ for (const entry of this.entries) {
1613
+ if (entry.agentId !== agentId) continue;
1614
+ if (entry.state === "released" || entry.state === "expired") continue;
1615
+ if (entry.createdAtMs < sinceMs) continue;
1616
+ total += entry.valueUsd;
1617
+ }
1618
+ return total;
1619
+ }
1620
+ /**
1621
+ * Decisions admitted in the window — regardless of how they settled.
1622
+ *
1623
+ * A rate ceiling counts what the guard admitted, not what landed on chain.
1624
+ * An agent that spins through assess-and-abandon at machine speed is exactly
1625
+ * the loop the ceiling exists to catch, and it would be invisible to a
1626
+ * counter that only saw confirmed transactions.
1627
+ */
1628
+ actionsSince(agentId, sinceMs) {
1629
+ this.prune(this.clock());
1630
+ let count = 0;
1631
+ for (const entry of this.entries) {
1632
+ if (entry.agentId !== agentId) continue;
1633
+ if (entry.createdAtMs < sinceMs) continue;
1634
+ count += 1;
1635
+ }
1636
+ return count;
1637
+ }
1638
+ reservation(decisionId) {
1639
+ return this.byDecision.get(decisionId);
1640
+ }
1641
+ /** Snapshot for inspection. Copies, so a caller cannot edit the ledger. */
1642
+ list(agentId) {
1643
+ return this.entries.filter((entry) => !agentId || entry.agentId === agentId).map((entry) => ({ ...entry }));
1644
+ }
1645
+ clear() {
1646
+ this.entries.length = 0;
1647
+ this.byDecision.clear();
1648
+ this.byIdempotencyKey.clear();
1649
+ }
1650
+ // -------------------------------------------------------------------------
1651
+ settle(decisionId, state, options) {
1652
+ const now = options.now ?? new Date(this.clock());
1653
+ const entry = this.byDecision.get(decisionId);
1654
+ if (!entry) return null;
1655
+ const lapsed = entry.state === "expired";
1656
+ entry.state = state;
1657
+ entry.settledAtMs = now.getTime();
1658
+ if (options.txHash !== void 0) entry.txHash = options.txHash;
1659
+ if (options.note !== void 0) entry.note = options.note;
1660
+ return {
1661
+ decisionId,
1662
+ state,
1663
+ ...entry.txHash === void 0 ? {} : { txHash: entry.txHash },
1664
+ ...lapsed ? { note: "The hold had already expired when this was settled; the reservation TTL may be too short." } : entry.note === void 0 ? {} : { note: entry.note },
1665
+ at: now.toISOString()
1666
+ };
1667
+ }
1668
+ prune(nowMs) {
1669
+ for (const entry of this.entries) {
1670
+ if (entry.state === "held" && entry.expiresAtMs <= nowMs) entry.state = "expired";
1671
+ }
1672
+ for (let i = this.entries.length - 1; i >= 0; i -= 1) {
1673
+ const entry = this.entries[i];
1674
+ if (nowMs - entry.createdAtMs <= this.retentionMs) continue;
1675
+ this.entries.splice(i, 1);
1676
+ this.byDecision.delete(entry.decisionId);
1677
+ if (entry.idempotencyKey) this.byIdempotencyKey.delete(this.keyFor(entry.agentId, entry.idempotencyKey));
1678
+ }
1679
+ }
1680
+ enforceCeiling() {
1681
+ while (this.entries.length > this.maxEntries) {
1682
+ const entry = this.entries.shift();
1683
+ if (!entry) break;
1684
+ this.byDecision.delete(entry.decisionId);
1685
+ if (entry.idempotencyKey) this.byIdempotencyKey.delete(this.keyFor(entry.agentId, entry.idempotencyKey));
1686
+ }
1687
+ }
1688
+ /** Idempotency keys are the caller's, so they are namespaced per agent. */
1689
+ keyFor(agentId, idempotencyKey) {
1690
+ return `${agentId}\0${idempotencyKey}`;
1691
+ }
1692
+ };
1693
+
1694
+ // src/explain.ts
1695
+ var VERDICT_HEADLINE = {
1696
+ allow: "Permitted",
1697
+ require_human_approval: "Held for human approval",
1698
+ deny: "Denied"
1699
+ };
1700
+ var REMEDIATION = {
1701
+ AGENT_BINDING: "Use the policy sealed for this agent, or bind this policy to it.",
1702
+ ACTION_TYPE: "Add this action type to `allowedActions`, or remove it from `deniedActions`.",
1703
+ CHAIN: "Add this chain to `allowedChains`.",
1704
+ COUNTERPARTY: "Add this counterparty to `allowedCounterparties`, or remove it from `blockedCounterparties`.",
1705
+ CONTRACT: "Add this contract to `allowedContracts`.",
1706
+ TOKEN: "Add this token to `allowedTokens`.",
1707
+ APPROVAL_ALLOWANCE: "Have the agent request a bounded allowance, or set `allowUnlimitedApprovals` \u2014 which makes any later compromise of the spender unbounded.",
1708
+ VALUE_PER_ACTION: "Raise `maxValuePerActionUsd`, or state the action's `valueUsd`.",
1709
+ CUMULATIVE_SPEND: "Wait for the window to roll over, raise the window's `maxValueUsd`, or state the action's `valueUsd`.",
1710
+ RATE_LIMIT: "Wait for the window to roll over, or raise the window's `maxActions`. A rate ceiling being hit repeatedly is usually the agent looping, not the ceiling being wrong.",
1711
+ RISK_LEVEL: "A person should review the evidence on the decision's assessment before this is permitted.",
1712
+ ASSESSMENT_AVAILABILITY: "Restore connectivity to Decentrys, or accept unscreened actions by setting `failMode: 'open'` \u2014 which signs during precisely the window an attacker would choose.",
1713
+ HUMAN_APPROVAL_THRESHOLD: "A person must approve this action. Raise the threshold only if the operator intends actions of this size to proceed unattended."
1714
+ };
1715
+ function explainAgentAction(decision) {
1716
+ const failing = decision.evaluations.filter((e) => e.outcome === "fail");
1717
+ const escalating = decision.evaluations.filter((e) => e.outcome === "escalate");
1718
+ const blocking = [...failing, ...escalating];
1719
+ const headline = buildHeadline(decision, failing, escalating);
1720
+ const because = blocking.map((e) => e.statement);
1721
+ const unbounded = decision.evaluations.filter((e) => e.outcome === "not_configured").map((e) => e.statement);
1722
+ const toProceed = [...new Set(blocking.map((e) => REMEDIATION[e.rule]))];
1723
+ const explanation = {
1724
+ decisionId: decision.decisionId,
1725
+ agentId: decision.agentId,
1726
+ verdict: decision.verdict,
1727
+ headline,
1728
+ because,
1729
+ rulesApplied: decision.evaluations,
1730
+ unbounded,
1731
+ toProceed,
1732
+ provenance: {
1733
+ policyFingerprint: decision.policyFingerprint,
1734
+ ...decision.policyVersion === void 0 ? {} : { policyVersion: decision.policyVersion },
1735
+ actionDigest: decision.actionDigest,
1736
+ modelVersion: decision.modelVersion,
1737
+ ...decision.assessment === void 0 ? {} : {
1738
+ riskModelVersion: decision.assessment.modelVersion,
1739
+ riskLevel: decision.assessment.riskLevel
1740
+ },
1741
+ decidedAt: decision.decidedAt
1742
+ },
1743
+ text: ""
1744
+ };
1745
+ explanation.text = render(decision, explanation);
1746
+ return explanation;
1747
+ }
1748
+ function buildHeadline(decision, failing, escalating) {
1749
+ const prefix = VERDICT_HEADLINE[decision.verdict];
1750
+ if (decision.verdict === "allow") {
1751
+ const checked = decision.evaluations.filter((e) => e.outcome === "pass").length;
1752
+ const unconfigured = decision.evaluations.filter((e) => e.outcome === "not_configured").length;
1753
+ return `${prefix}: ${checked} ${checked === 1 ? "rule" : "rules"} were applied and passed` + (unconfigured > 0 ? `, and ${unconfigured} ${unconfigured === 1 ? "axis was" : "axes were"} left unbounded by this policy.` : ".");
1754
+ }
1755
+ const driving = failing[0] ?? escalating[0];
1756
+ return driving ? `${prefix}: ${driving.statement}` : `${prefix}.`;
1757
+ }
1758
+ function render(decision, explanation) {
1759
+ const lines = [
1760
+ `${explanation.headline}`,
1761
+ "",
1762
+ `Agent: ${decision.agentId}`,
1763
+ `Decision: ${decision.decisionId} at ${decision.decidedAt}`,
1764
+ `Policy: ${decision.policyVersion ? `${decision.policyVersion} ` : ""}#${decision.policyFingerprint}`,
1765
+ `Action: #${decision.actionDigest}`
1766
+ ];
1767
+ if (decision.assessment) {
1768
+ lines.push(
1769
+ `Risk: ${decision.assessment.riskLevel} (confidence ${decision.assessment.confidence}) \u2014 ${RISK_LEVEL_MEANING[decision.assessment.riskLevel]}`
1770
+ );
1771
+ } else if (decision.assessmentUnavailable) {
1772
+ lines.push(`Risk: not assessed \u2014 ${decision.assessmentUnavailable}`);
1773
+ }
1774
+ lines.push("", "Rules applied:");
1775
+ for (const rule of decision.evaluations) {
1776
+ const bound = rule.limit === void 0 ? "" : ` [limit ${rule.limit}]`;
1777
+ const seen = rule.observed === void 0 ? "" : ` [observed ${rule.observed}]`;
1778
+ lines.push(` ${symbolFor(rule.outcome)} ${rule.rule}: ${rule.statement}${bound}${seen}`);
1779
+ }
1780
+ if (explanation.unbounded.length > 0) {
1781
+ lines.push("", "Left unbounded by this policy:");
1782
+ for (const line of explanation.unbounded) lines.push(` - ${line}`);
1783
+ }
1784
+ if (explanation.toProceed.length > 0) {
1785
+ lines.push("", "For this to proceed, the operator would have to:");
1786
+ for (const line of explanation.toProceed) lines.push(` - ${line}`);
1787
+ }
1788
+ return lines.join("\n");
1789
+ }
1790
+ function symbolFor(outcome) {
1791
+ switch (outcome) {
1792
+ case "pass":
1793
+ return "PASS ";
1794
+ case "fail":
1795
+ return "FAIL ";
1796
+ case "escalate":
1797
+ return "HOLD ";
1798
+ case "not_applicable":
1799
+ return "n/a ";
1800
+ case "not_configured":
1801
+ return "unset";
1802
+ }
1803
+ }
1804
+
1805
+ // src/client.ts
1806
+ var AGENT_SDK_VERSION = "0.1.0";
1807
+ var DEFAULT_AUDIT_LOG_ENTRIES = 1e3;
1808
+ var AgentGuard = class _AgentGuard {
1809
+ protect;
1810
+ sealed;
1811
+ ledgerImpl;
1812
+ reserving;
1813
+ auditEntries = [];
1814
+ auditLimit;
1815
+ onDecision;
1816
+ constructor(config) {
1817
+ if (!config.policy || typeof config.policy !== "object" || typeof config.policy.fingerprint !== "string") {
1818
+ throw new Error(
1819
+ "AgentGuard: a sealed policy is required. Build one with sealPolicy({ ... }) at wiring time, from a human-owned configuration \u2014 not from anything the agent produces."
1820
+ );
1821
+ }
1822
+ if (config.protect) {
1823
+ this.protect = config.protect;
1824
+ } else if (config.apiKey || config.transport) {
1825
+ const protectConfig = {
1826
+ apiKey: config.apiKey ?? "transport-provided",
1827
+ // Protect's own failMode only shapes the wording of an unavailable
1828
+ // assessment; AgentGuard's `failMode` decides what actually happens.
1829
+ // Pinning it to `open` keeps the two from being applied twice.
1830
+ failMode: "open",
1831
+ ...config.baseUrl === void 0 ? {} : { baseUrl: config.baseUrl },
1832
+ ...config.timeoutMs === void 0 ? {} : { timeoutMs: config.timeoutMs },
1833
+ ...config.retries === void 0 ? {} : { retries: config.retries },
1834
+ ...config.fetch === void 0 ? {} : { fetch: config.fetch },
1835
+ ...config.transport === void 0 ? {} : { transport: config.transport }
1836
+ };
1837
+ this.protect = new Decentrys(protectConfig);
1838
+ } else {
1839
+ throw new Error(
1840
+ "AgentGuard: pass an apiKey or an existing Decentrys client. Screening a counterparty is the part of this that no local policy can do \u2014 an allowlist cannot tell you the address on it was labelled malicious yesterday."
1841
+ );
1842
+ }
1843
+ this.sealed = config.policy;
1844
+ this.ledgerImpl = config.ledger ?? new SpendLedger();
1845
+ this.reserving = isReservationLedger(this.ledgerImpl) ? this.ledgerImpl : null;
1846
+ this.auditLimit = config.auditLogEntries ?? DEFAULT_AUDIT_LOG_ENTRIES;
1847
+ this.onDecision = config.onDecision;
1848
+ }
1849
+ /** The policy in force. Frozen — reading it is safe, changing it is impossible. */
1850
+ get policy() {
1851
+ return this.sealed;
1852
+ }
1853
+ /** Configurations that are legal but worth an operator's attention. */
1854
+ get policyWarnings() {
1855
+ return this.sealed.warnings;
1856
+ }
1857
+ /**
1858
+ * Whether admitted actions hold budget before they execute.
1859
+ *
1860
+ * False when the guard was given a read-only usage source. Cumulative caps
1861
+ * then count settled history only, so two actions that each fit under the
1862
+ * cap can both be admitted and together exceed it. Exposed rather than
1863
+ * assumed, because the difference is invisible until it costs something.
1864
+ */
1865
+ get reservesBudget() {
1866
+ return this.reserving !== null;
1867
+ }
1868
+ // -------------------------------------------------------------------------
1869
+ // The main path
1870
+ // -------------------------------------------------------------------------
1871
+ /**
1872
+ * Assess a proposed action and decide whether the agent may sign it.
1873
+ *
1874
+ * Screening happens first and always — including when a local rule has
1875
+ * already failed. A record saying an action exceeded its value cap, while
1876
+ * omitting that its counterparty was confirmed malicious, would send an
1877
+ * operator to raise the cap and ship the same hole.
1878
+ *
1879
+ * On `allow` or `require_human_approval` the action's value and its slot in
1880
+ * the rate window are held immediately; call `confirm()` when it lands or
1881
+ * `release()` when it does not. The hold is taken synchronously in the same
1882
+ * turn as the evaluation, so two concurrent calls in one process cannot both
1883
+ * pass a cap that only one of them fits under. Across processes they can —
1884
+ * see `ledger.ts`, which says so plainly.
1885
+ */
1886
+ async assessAgentTransaction(action, options = {}) {
1887
+ const policy = options.restrict ? narrowPolicy(this.sealed, options.restrict) : this.sealed;
1888
+ const decisionId = newDecisionId();
1889
+ let assessment = options.assessment;
1890
+ if (!assessment) {
1891
+ assessment = await this.screen(action, options);
1892
+ }
1893
+ const unavailable = unavailableReason(assessment);
1894
+ const decision = evaluatePolicy({
1895
+ action,
1896
+ policy,
1897
+ assessment: unavailable ? null : assessment,
1898
+ ...unavailable === void 0 ? {} : { assessmentUnavailable: unavailable },
1899
+ usage: this.ledgerImpl,
1900
+ decisionId,
1901
+ ...options.now === void 0 ? {} : { now: options.now }
1902
+ });
1903
+ if (decision.verdict !== "deny" && this.reserving) {
1904
+ const reservation = this.reserving.reserve({
1905
+ decisionId,
1906
+ agentId: action.agentId,
1907
+ // Reaching here with no stated value means no value-based rule was
1908
+ // configured; if one had been, this would already be a denial.
1909
+ valueUsd: action.valueUsd ?? 0,
1910
+ ...action.idempotencyKey === void 0 ? {} : { idempotencyKey: action.idempotencyKey },
1911
+ ...options.now === void 0 ? {} : { now: options.now }
1912
+ });
1913
+ decision.reservationId = reservation.decisionId;
1914
+ }
1915
+ this.record(decision);
1916
+ return decision;
1917
+ }
1918
+ /**
1919
+ * Decide without going to the network.
1920
+ *
1921
+ * For an operator replaying a decision against a different policy, and for
1922
+ * an agent runtime that already holds an assessment. It applies the same
1923
+ * rules; what it cannot do is discover that a counterparty was labelled
1924
+ * since the assessment was taken.
1925
+ */
1926
+ evaluate(action, options = {}) {
1927
+ const policy = options.restrict ? narrowPolicy(this.sealed, options.restrict) : this.sealed;
1928
+ const decision = evaluatePolicy({
1929
+ action,
1930
+ policy,
1931
+ assessment: options.assessment ?? null,
1932
+ ...options.assessment ? {} : { assessmentUnavailable: options.assessmentUnavailable ?? "no assessment was supplied to evaluate()" },
1933
+ usage: this.ledgerImpl,
1934
+ ...options.now === void 0 ? {} : { now: options.now }
1935
+ });
1936
+ this.record(decision);
1937
+ return decision;
1938
+ }
1939
+ // -------------------------------------------------------------------------
1940
+ // Settling
1941
+ // -------------------------------------------------------------------------
1942
+ /** The agent signed and broadcast it. The held value becomes committed. */
1943
+ confirm(decisionId, options = {}) {
1944
+ const outcome = this.reserving?.confirm(decisionId, options) ?? null;
1945
+ if (outcome) this.attachOutcome(outcome);
1946
+ return outcome;
1947
+ }
1948
+ /** The agent did not proceed. The held value returns to the budget. */
1949
+ release(decisionId, options = {}) {
1950
+ const outcome = this.reserving?.release(decisionId, options) ?? null;
1951
+ if (outcome) this.attachOutcome(outcome);
1952
+ return outcome;
1953
+ }
1954
+ // -------------------------------------------------------------------------
1955
+ // The trail
1956
+ // -------------------------------------------------------------------------
1957
+ /**
1958
+ * Explain a decision, by id or by value.
1959
+ *
1960
+ * Returns `null` for an id this process never made or no longer holds —
1961
+ * stated rather than reconstructed, because an explanation assembled from a
1962
+ * decision that is no longer in hand is a guess wearing a record's clothes.
1963
+ */
1964
+ explain(decision) {
1965
+ if (typeof decision !== "string") return explainAgentAction(decision);
1966
+ const entry = this.auditEntries.find((e) => e.decision.decisionId === decision);
1967
+ return entry ? explainAgentAction(entry.decision) : null;
1968
+ }
1969
+ /**
1970
+ * Recent decisions, newest last.
1971
+ *
1972
+ * In memory and bounded; it does not survive a restart. Use `onDecision` for
1973
+ * a trail that does. Saying which of the two this is matters — an operator
1974
+ * who believes this is the audit log will discover otherwise at the worst
1975
+ * possible moment.
1976
+ */
1977
+ auditLog(filter = {}) {
1978
+ return this.auditEntries.filter((e) => !filter.agentId || e.decision.agentId === filter.agentId).filter((e) => !filter.verdict || e.decision.verdict === filter.verdict).map((e) => ({ decision: e.decision, ...e.outcome ? { outcome: e.outcome } : {} }));
1979
+ }
1980
+ /**
1981
+ * A guard for a narrower task, sharing this one's ledger and client.
1982
+ *
1983
+ * The shared ledger is the point: a sub-task that spends against a tighter
1984
+ * per-task cap still spends against the agent's daily one.
1985
+ */
1986
+ withPolicy(restriction) {
1987
+ return new _AgentGuard({
1988
+ policy: narrowPolicy(this.sealed, restriction),
1989
+ protect: this.protect,
1990
+ ledger: this.ledgerImpl,
1991
+ auditLogEntries: this.auditLimit,
1992
+ ...this.onDecision === void 0 ? {} : { onDecision: this.onDecision }
1993
+ });
1994
+ }
1995
+ // -------------------------------------------------------------------------
1996
+ // Internals
1997
+ // -------------------------------------------------------------------------
1998
+ /**
1999
+ * Route the action to the right Protect endpoint.
2000
+ *
2001
+ * An approval is assessed on its spender *and its allowance together*,
2002
+ * which is a different question from "is this transaction dangerous" and
2003
+ * the one an unlimited approval turns on.
2004
+ */
2005
+ async screen(action, options) {
2006
+ const call = {
2007
+ ...options.signal === void 0 ? {} : { signal: options.signal },
2008
+ ...options.skipCache === void 0 ? {} : { skipCache: options.skipCache }
2009
+ };
2010
+ if (action.type === "approve" && action.token && action.to) {
2011
+ const result2 = await this.protect.screenApproval({
2012
+ chain: action.chain,
2013
+ owner: action.from,
2014
+ spender: action.to,
2015
+ token: action.token,
2016
+ ...action.approvalAmount === void 0 ? {} : { amount: action.approvalAmount }
2017
+ }, call);
2018
+ return result2.assessment;
2019
+ }
2020
+ const result = await this.protect.assessTransaction({
2021
+ chain: action.chain,
2022
+ from: action.from,
2023
+ ...action.to === void 0 ? {} : { to: action.to },
2024
+ ...action.value === void 0 ? {} : { value: action.value },
2025
+ ...action.data === void 0 ? {} : { data: action.data },
2026
+ ...action.raw === void 0 ? {} : { raw: action.raw },
2027
+ ...action.origin === void 0 ? {} : { origin: action.origin }
2028
+ }, call);
2029
+ return result.assessment;
2030
+ }
2031
+ record(decision) {
2032
+ this.auditEntries.push({ decision });
2033
+ while (this.auditEntries.length > this.auditLimit) this.auditEntries.shift();
2034
+ if (!this.onDecision) return;
2035
+ try {
2036
+ this.onDecision(decision);
2037
+ } catch {
2038
+ }
2039
+ }
2040
+ attachOutcome(outcome) {
2041
+ const entry = this.auditEntries.find((e) => e.decision.decisionId === outcome.decisionId);
2042
+ if (entry) entry.outcome = outcome;
2043
+ }
2044
+ };
2045
+ function unavailableReason(assessment) {
2046
+ if (!assessment) return "no assessment was obtained";
2047
+ const unknown = assessment.unknowns.find((u) => u.reason === "PROVIDER_UNAVAILABLE");
2048
+ return unknown ? unknown.statement : void 0;
2049
+ }
2050
+ return __toCommonJS(index_exports);
2051
+ })();