@reinconsole/policy-engine 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.
package/dist/server.js ADDED
@@ -0,0 +1,829 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server.ts
4
+ import { fileURLToPath } from "url";
5
+ import { realpathSync } from "fs";
6
+ import Fastify from "fastify";
7
+ import { z as z3 } from "zod";
8
+
9
+ // ../../packages/core/dist/index.js
10
+ import { z } from "zod";
11
+ var Chain = z.enum(["base", "solana", "polygon", "bnb"]);
12
+ var Asset = z.enum(["USDC", "USDT", "EURC"]);
13
+ var DecimalString = z.string().regex(/^\d+(\.\d+)?$/, 'must be a non-negative decimal string, e.g. "5.00"');
14
+ var DECIMAL_RE = /^\d+(\.\d+)?$/;
15
+ function isValidDecimal(value) {
16
+ return DECIMAL_RE.test(value);
17
+ }
18
+ function assertDecimal(value) {
19
+ if (!isValidDecimal(value)) {
20
+ throw new TypeError(`invalid decimal string: ${JSON.stringify(value)}`);
21
+ }
22
+ }
23
+ function fractionLength(value) {
24
+ const dot = value.indexOf(".");
25
+ return dot === -1 ? 0 : value.length - dot - 1;
26
+ }
27
+ function toScaled(value, scale) {
28
+ const dot = value.indexOf(".");
29
+ const intPart = dot === -1 ? value : value.slice(0, dot);
30
+ const fracPart = dot === -1 ? "" : value.slice(dot + 1);
31
+ const paddedFrac = (fracPart + "0".repeat(scale)).slice(0, scale);
32
+ return BigInt(intPart + paddedFrac);
33
+ }
34
+ function fromScaled(scaled, scale) {
35
+ if (scale === 0) return scaled.toString();
36
+ const digits = scaled.toString().padStart(scale + 1, "0");
37
+ const intPart = digits.slice(0, digits.length - scale);
38
+ const fracPart = digits.slice(digits.length - scale).replace(/0+$/, "");
39
+ return fracPart.length > 0 ? `${intPart}.${fracPart}` : intPart;
40
+ }
41
+ function compareDecimal(a, b) {
42
+ assertDecimal(a);
43
+ assertDecimal(b);
44
+ const scale = Math.max(fractionLength(a), fractionLength(b));
45
+ const av = toScaled(a, scale);
46
+ const bv = toScaled(b, scale);
47
+ return av < bv ? -1 : av > bv ? 1 : 0;
48
+ }
49
+ function sumDecimal(values) {
50
+ if (values.length === 0) return "0";
51
+ let scale = 0;
52
+ for (const v of values) {
53
+ assertDecimal(v);
54
+ scale = Math.max(scale, fractionLength(v));
55
+ }
56
+ let total = 0n;
57
+ for (const v of values) total += toScaled(v, scale);
58
+ return fromScaled(total, scale);
59
+ }
60
+ function mulDecimal(a, b) {
61
+ assertDecimal(a);
62
+ assertDecimal(b);
63
+ const scaleA = fractionLength(a);
64
+ const scaleB = fractionLength(b);
65
+ const product = toScaled(a, scaleA) * toScaled(b, scaleB);
66
+ return fromScaled(product, scaleA + scaleB);
67
+ }
68
+ function gt(a, b) {
69
+ return compareDecimal(a, b) === 1;
70
+ }
71
+ function globMatch(pattern, value) {
72
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
73
+ const withWildcard = escaped.replace(/\\\*/g, ".*");
74
+ return new RegExp(`^${withWildcard}$`).test(value);
75
+ }
76
+ function globMatchAny(patterns, value) {
77
+ return patterns.some((p) => globMatch(p, value));
78
+ }
79
+ var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
80
+ var TIME_LEN = 10;
81
+ var RANDOM_LEN = 16;
82
+ function randomBytes(length) {
83
+ const bytes = new Uint8Array(length);
84
+ globalThis.crypto.getRandomValues(bytes);
85
+ return bytes;
86
+ }
87
+ function encodeTime(now) {
88
+ let time = now;
89
+ let out = "";
90
+ for (let i = TIME_LEN - 1; i >= 0; i--) {
91
+ out = ENCODING.charAt(time % 32) + out;
92
+ time = Math.floor(time / 32);
93
+ }
94
+ return out;
95
+ }
96
+ function encodeRandom() {
97
+ let out = "";
98
+ for (const byte of randomBytes(RANDOM_LEN)) {
99
+ out += ENCODING.charAt(byte % 32);
100
+ }
101
+ return out;
102
+ }
103
+ function ulid(seedTime = Date.now()) {
104
+ return encodeTime(seedTime) + encodeRandom();
105
+ }
106
+ function newId(prefix) {
107
+ return `${prefix}_${ulid()}`;
108
+ }
109
+ var ULID_BODY = "[0-9A-HJKMNP-TV-Z]{26}";
110
+ function prefixedId(prefix) {
111
+ return z.string().regex(new RegExp(`^${prefix}_${ULID_BODY}$`), `expected a "${prefix}_" prefixed id`);
112
+ }
113
+ var OrgId = prefixedId("org");
114
+ var AgentId = prefixedId("agt");
115
+ var PolicyId = prefixedId("pol");
116
+ var IntentId = prefixedId("int");
117
+ var DecisionId = prefixedId("dec");
118
+ var ReceiptId = prefixedId("rcp");
119
+ var SessionId = prefixedId("ses");
120
+ var GateReceiptId = prefixedId("grc");
121
+ var EnforcementMode = z.enum(["observed", "sdk", "session-key"]);
122
+ var AgentWallet = z.object({
123
+ chain: Chain,
124
+ address: z.string().min(1),
125
+ mode: EnforcementMode
126
+ });
127
+ var AgentStatus = z.enum(["active", "frozen"]);
128
+ var Agent = z.object({
129
+ id: AgentId,
130
+ orgId: OrgId,
131
+ name: z.string().min(1).max(200),
132
+ /** On-chain ERC-8004 identity, if the agent is registered. */
133
+ erc8004Id: z.string().optional(),
134
+ wallets: z.array(AgentWallet).default([]),
135
+ status: AgentStatus.default("active"),
136
+ createdAt: z.coerce.date()
137
+ });
138
+ var ID_PATTERN = /^eip155:(\d+):(0x[0-9a-fA-F]{40})\/(\d+)$/;
139
+ function parseErc8004Id(value) {
140
+ const match = ID_PATTERN.exec(value);
141
+ if (!match) return void 0;
142
+ const chainId = Number(match[1]);
143
+ if (!Number.isSafeInteger(chainId) || chainId < 1) return void 0;
144
+ return {
145
+ chainId,
146
+ registry: match[2].toLowerCase(),
147
+ tokenId: BigInt(match[3])
148
+ };
149
+ }
150
+ var Erc8004Id = z.string().refine((value) => parseErc8004Id(value) !== void 0, {
151
+ message: 'expected "eip155:{chainId}:{registry}/{tokenId}"'
152
+ });
153
+ var Vendor = z.object({
154
+ host: z.string().min(1),
155
+ address: z.string().min(1),
156
+ erc8004Id: z.string().optional()
157
+ });
158
+ var TaskContext = z.object({
159
+ taskId: z.string().optional(),
160
+ parentRunId: z.string().optional(),
161
+ purpose: z.string().max(500).optional()
162
+ });
163
+ var PaymentIntent = z.object({
164
+ id: IntentId,
165
+ agentId: AgentId,
166
+ vendor: Vendor,
167
+ resource: z.string(),
168
+ amount: DecimalString,
169
+ asset: Asset,
170
+ chain: Chain,
171
+ taskContext: TaskContext.default({}),
172
+ nonce: z.string().min(1),
173
+ createdAt: z.coerce.date()
174
+ });
175
+ var DecisionOutcome = z.enum(["allow", "deny", "escalate"]);
176
+ var Decision = z.object({
177
+ id: DecisionId,
178
+ intentId: IntentId,
179
+ /**
180
+ * sha256 of the intent's canonical content (see canonical.ts). Binds the
181
+ * decision to the exact transfer it judged — amount, recipient, asset,
182
+ * chain — so a signer can verify an {intent, decision} pair offline as a
183
+ * self-contained spend voucher, not just a reference by id.
184
+ */
185
+ intentHash: z.string(),
186
+ outcome: DecisionOutcome,
187
+ /** Ids of the rules that fired, for explainability. */
188
+ matchedRules: z.array(z.string()).default([]),
189
+ /** Human-readable explanation surfaced in the dashboard. */
190
+ reason: z.string().optional(),
191
+ policyId: z.string(),
192
+ policyVersion: z.string(),
193
+ /** Hash of the previous decision in the chain (tamper-evident log). */
194
+ prevHash: z.string(),
195
+ /** Hash of this decision's canonical content. */
196
+ hash: z.string(),
197
+ /** Service signing-key signature over `hash`. */
198
+ signature: z.string(),
199
+ latencyMs: z.number().nonnegative(),
200
+ decidedAt: z.coerce.date()
201
+ });
202
+ function canonicalIntent(intent) {
203
+ return JSON.stringify({
204
+ id: intent.id,
205
+ agentId: intent.agentId,
206
+ vendor: {
207
+ host: intent.vendor.host,
208
+ address: intent.vendor.address,
209
+ erc8004Id: intent.vendor.erc8004Id ?? null
210
+ },
211
+ resource: intent.resource,
212
+ amount: intent.amount,
213
+ asset: intent.asset,
214
+ chain: intent.chain,
215
+ taskContext: {
216
+ taskId: intent.taskContext.taskId ?? null,
217
+ parentRunId: intent.taskContext.parentRunId ?? null,
218
+ purpose: intent.taskContext.purpose ?? null
219
+ },
220
+ nonce: intent.nonce,
221
+ createdAt: intent.createdAt.toISOString()
222
+ });
223
+ }
224
+ function canonicalDecision(d) {
225
+ return JSON.stringify({
226
+ intentId: d.intentId,
227
+ intentHash: d.intentHash,
228
+ outcome: d.outcome,
229
+ matchedRules: d.matchedRules,
230
+ policyId: d.policyId,
231
+ policyVersion: d.policyVersion,
232
+ prevHash: d.prevHash,
233
+ decidedAt: d.decidedAt.toISOString()
234
+ });
235
+ }
236
+ var Session = z.object({
237
+ id: SessionId,
238
+ agentId: AgentId,
239
+ /** sha256 hex of the bearer token. */
240
+ tokenHash: z.string(),
241
+ /** Cumulative ceiling across the session's lifetime. Absent = uncapped. */
242
+ capAmount: DecimalString.optional(),
243
+ /** Ceiling per individual signature. Absent = uncapped. */
244
+ maxPerPayment: DecimalString.optional(),
245
+ expiresAt: z.coerce.date(),
246
+ createdAt: z.coerce.date(),
247
+ revokedAt: z.coerce.date().optional()
248
+ });
249
+ var SettledPayment = z.object({
250
+ intentId: IntentId,
251
+ txHash: z.string(),
252
+ chain: Chain,
253
+ blockNumber: z.coerce.bigint(),
254
+ facilitator: z.string().optional(),
255
+ feePaid: DecimalString.optional(),
256
+ confirmedAt: z.coerce.date()
257
+ });
258
+ var ReceiptSettlement = z.object({
259
+ txHash: z.string().optional(),
260
+ networkId: z.string().optional(),
261
+ /** Raw header payload, kept for forensics until the indexer confirms. */
262
+ raw: z.string().optional()
263
+ });
264
+ var Receipt = z.object({
265
+ id: ReceiptId,
266
+ agentId: AgentId,
267
+ intentId: IntentId,
268
+ decisionId: DecisionId,
269
+ outcome: DecisionOutcome,
270
+ /** The URL the agent actually fetched (the paywalled resource). */
271
+ url: z.string(),
272
+ method: z.string().default("GET"),
273
+ vendorHost: z.string(),
274
+ amount: DecimalString,
275
+ asset: Asset,
276
+ chain: Chain,
277
+ taskContext: TaskContext.default({}),
278
+ /** Human-readable explanation copied from the decision. */
279
+ reason: z.string().optional(),
280
+ /** Present only once a payment was made and the vendor confirmed it. */
281
+ settlement: ReceiptSettlement.optional(),
282
+ createdAt: z.coerce.date()
283
+ });
284
+ var GateReceipt = z.object({
285
+ id: GateReceiptId,
286
+ at: z.coerce.date(),
287
+ /** The route pattern that priced this request, e.g. "/api/reports/*". */
288
+ route: z.string().min(1),
289
+ /** The concrete resource paid for (URL path actually requested). */
290
+ resource: z.string().min(1),
291
+ method: z.string().min(1),
292
+ /** The paying wallet address, as asserted by the settled payment. */
293
+ payer: z.string().min(1),
294
+ payTo: z.string().min(1),
295
+ /** Human-unit decimal amount, e.g. "0.05". */
296
+ amount: DecimalString,
297
+ /** The same amount in the asset's atomic units, e.g. "50000". */
298
+ amountAtomic: z.string().regex(/^\d+$/, "atomic amount must be an integer string"),
299
+ /** Token symbol or contract address, exactly as quoted in the requirement. */
300
+ asset: z.string().min(1),
301
+ network: z.string().min(1),
302
+ /** Settlement transaction hash (mock ledger or on-chain). */
303
+ transaction: z.string().min(1)
304
+ });
305
+ var Window = z.string().regex(/^\d+[smhd]$/, 'window must be like "30s", "15m", "1h", or "7d"');
306
+ var Multiplier = z.string().regex(/^\d+(\.\d+)?x$/, 'multiplier must be like "3x"');
307
+ var Condition = z.object({
308
+ /** Per-transaction amount exceeds this value. */
309
+ amountGt: DecimalString.optional(),
310
+ /** Sum of spend in a rolling window exceeds `gt`. */
311
+ rollingSum: z.object({ window: Window, gt: DecimalString }).optional(),
312
+ /** Transaction count in a rolling window exceeds `gt`. */
313
+ txCount: z.object({ window: Window, gt: z.number().int().nonnegative() }).optional(),
314
+ /** Vendor host matches one of these patterns (supports `*` globs). */
315
+ vendorHostIn: z.array(z.string()).optional(),
316
+ /** First time Rein has seen this vendor for the agent. */
317
+ vendorFirstSeen: z.boolean().optional(),
318
+ /** Vendor reputation score is below this threshold (Phase 3 hook). */
319
+ vendorReputationLt: z.number().min(0).max(100).optional(),
320
+ /** Amount is more than `gt`-times the observed median for this resource. */
321
+ amountVsResourceMedian: z.object({ gt: Multiplier }).optional()
322
+ }).refine((c) => Object.values(c).some((v) => v !== void 0), {
323
+ message: "a condition must specify at least one predicate"
324
+ });
325
+ var Rule = z.object({
326
+ id: z.string().min(1),
327
+ allow: Condition.optional(),
328
+ deny: Condition.optional(),
329
+ escalate: Condition.optional()
330
+ }).refine((r) => [r.allow, r.deny, r.escalate].filter((v) => v !== void 0).length === 1, {
331
+ message: "a rule must specify exactly one of allow / deny / escalate"
332
+ });
333
+ var PolicyDefault = z.enum(["allow", "deny"]);
334
+ var AppliesTo = z.object({
335
+ /** Agent id patterns (supports `*` globs, e.g. "agt_research_*"). */
336
+ agents: z.array(z.string()).optional(),
337
+ chains: z.array(Chain).optional()
338
+ });
339
+ var Escalation = z.object({
340
+ approvers: z.array(z.string()).default([]),
341
+ timeoutAction: PolicyDefault.default("deny"),
342
+ timeoutMin: z.number().int().positive().default(15)
343
+ });
344
+ var Policy = z.object({
345
+ policyId: z.string().min(1),
346
+ version: z.string().default("1"),
347
+ appliesTo: AppliesTo.default({}),
348
+ rules: z.array(Rule).default([]),
349
+ default: PolicyDefault.default("deny"),
350
+ /** Below this amount, fail-open is permitted during a policy-service outage. */
351
+ denyFloor: DecimalString.default("0.05"),
352
+ escalation: Escalation.optional()
353
+ });
354
+ var ReputationSubject = z.object({
355
+ kind: z.enum(["agent", "vendor"]),
356
+ id: z.string()
357
+ });
358
+ var ReputationComponents = z.object({
359
+ volume: z.number(),
360
+ longevity: z.number(),
361
+ disputeRate: z.number(),
362
+ counterpartyQuality: z.number(),
363
+ settlementReliability: z.number()
364
+ });
365
+ var ReputationScore = z.object({
366
+ subject: ReputationSubject,
367
+ score: z.number().min(0).max(100),
368
+ components: ReputationComponents,
369
+ confidence: z.number().min(0).max(1),
370
+ asOf: z.coerce.date(),
371
+ /** Hash-anchored attestation, optionally published on-chain (ERC-8004). */
372
+ evidenceUri: z.string().optional()
373
+ });
374
+ var ReinEvent = z.discriminatedUnion("type", [
375
+ z.object({ type: z.literal("intent.created"), at: z.coerce.date(), intent: PaymentIntent }),
376
+ z.object({ type: z.literal("decision.made"), at: z.coerce.date(), decision: Decision }),
377
+ z.object({ type: z.literal("payment.settled"), at: z.coerce.date(), payment: SettledPayment }),
378
+ z.object({
379
+ type: z.literal("shadow.spend"),
380
+ at: z.coerce.date(),
381
+ agentId: AgentId,
382
+ txHash: z.string(),
383
+ chain: Chain,
384
+ amount: DecimalString
385
+ }),
386
+ z.object({
387
+ type: z.literal("signature.released"),
388
+ at: z.coerce.date(),
389
+ sessionId: SessionId,
390
+ agentId: AgentId,
391
+ intentId: IntentId,
392
+ decisionId: DecisionId,
393
+ amount: DecimalString
394
+ }),
395
+ z.object({
396
+ type: z.literal("signature.refused"),
397
+ at: z.coerce.date(),
398
+ /** Refusal code, e.g. "decision_replayed" (see @reinconsole/signer). */
399
+ code: z.string(),
400
+ reason: z.string(),
401
+ sessionId: SessionId.optional(),
402
+ agentId: AgentId.optional(),
403
+ intentId: IntentId.optional()
404
+ }),
405
+ z.object({
406
+ type: z.literal("gate.quoted"),
407
+ at: z.coerce.date(),
408
+ resource: z.string(),
409
+ method: z.string(),
410
+ amount: DecimalString,
411
+ asset: z.string(),
412
+ network: z.string()
413
+ }),
414
+ z.object({ type: z.literal("gate.settled"), at: z.coerce.date(), receipt: GateReceipt }),
415
+ z.object({
416
+ type: z.literal("gate.refused"),
417
+ at: z.coerce.date(),
418
+ /** Refusal code, e.g. "payment_replayed" (see @reinconsole/gate). */
419
+ code: z.string(),
420
+ reason: z.string(),
421
+ resource: z.string(),
422
+ /** Known only when the payment header decoded far enough to name a payer. */
423
+ payer: z.string().optional()
424
+ })
425
+ ]);
426
+
427
+ // src/engine.ts
428
+ import { EventEmitter } from "events";
429
+ import { createHash as createHash2 } from "crypto";
430
+ import { performance } from "perf_hooks";
431
+ import { z as z2 } from "zod";
432
+
433
+ // src/evaluator.ts
434
+ function policyApplies(policy, intent) {
435
+ const { agents, chains } = policy.appliesTo;
436
+ if (chains && !chains.includes(intent.chain)) return false;
437
+ if (agents && agents.length > 0 && !globMatchAny(agents, intent.agentId)) return false;
438
+ return true;
439
+ }
440
+ function conditionMatches(cond, intent, ctx) {
441
+ if (cond.amountGt !== void 0 && !gt(intent.amount, cond.amountGt)) return false;
442
+ if (cond.rollingSum) {
443
+ const prospective = sumDecimal([ctx.rollingSum(cond.rollingSum.window), intent.amount]);
444
+ if (!gt(prospective, cond.rollingSum.gt)) return false;
445
+ }
446
+ if (cond.txCount) {
447
+ const prospective = ctx.txCount(cond.txCount.window) + 1;
448
+ if (prospective <= cond.txCount.gt) return false;
449
+ }
450
+ if (cond.vendorHostIn && !globMatchAny(cond.vendorHostIn, intent.vendor.host)) return false;
451
+ if (cond.vendorFirstSeen !== void 0 && ctx.isVendorFirstSeen(intent.vendor.host) !== cond.vendorFirstSeen) {
452
+ return false;
453
+ }
454
+ if (cond.vendorReputationLt !== void 0) {
455
+ const rep = ctx.vendorReputation(intent.vendor.host);
456
+ if (rep === void 0 || !(rep < cond.vendorReputationLt)) return false;
457
+ }
458
+ if (cond.amountVsResourceMedian) {
459
+ const med = ctx.resourceMedian(intent.resource);
460
+ if (med === void 0) return false;
461
+ const factor = cond.amountVsResourceMedian.gt.replace(/x$/, "");
462
+ if (!gt(intent.amount, mulDecimal(med, factor))) return false;
463
+ }
464
+ return true;
465
+ }
466
+ function actionOf(rule) {
467
+ if (rule.deny) return { action: "deny", cond: rule.deny };
468
+ if (rule.escalate) return { action: "escalate", cond: rule.escalate };
469
+ return { action: "allow", cond: rule.allow };
470
+ }
471
+ function evaluate(intent, policies, ctx) {
472
+ const policy = policies.find((p) => policyApplies(p, intent));
473
+ if (!policy) {
474
+ return {
475
+ outcome: "deny",
476
+ matchedRules: [],
477
+ reason: "no applicable policy (fail-closed default)",
478
+ policyId: "none",
479
+ policyVersion: "0"
480
+ };
481
+ }
482
+ const denies = [];
483
+ const escalates = [];
484
+ const allows = [];
485
+ for (const rule of policy.rules) {
486
+ const { action, cond } = actionOf(rule);
487
+ if (!conditionMatches(cond, intent, ctx)) continue;
488
+ if (action === "deny") denies.push(rule.id);
489
+ else if (action === "escalate") escalates.push(rule.id);
490
+ else allows.push(rule.id);
491
+ }
492
+ const base = { policyId: policy.policyId, policyVersion: policy.version };
493
+ if (denies.length > 0) {
494
+ return { outcome: "deny", matchedRules: denies, reason: `denied by: ${denies.join(", ")}`, ...base };
495
+ }
496
+ if (escalates.length > 0) {
497
+ return {
498
+ outcome: "escalate",
499
+ matchedRules: escalates,
500
+ reason: `escalated by: ${escalates.join(", ")}`,
501
+ ...base
502
+ };
503
+ }
504
+ if (allows.length > 0) {
505
+ return { outcome: "allow", matchedRules: allows, reason: `allowed by: ${allows.join(", ")}`, ...base };
506
+ }
507
+ return {
508
+ outcome: policy.default,
509
+ matchedRules: [],
510
+ reason: `no rule matched; policy default = ${policy.default}`,
511
+ ...base
512
+ };
513
+ }
514
+
515
+ // src/stores.ts
516
+ var WINDOW_MS = { s: 1e3, m: 6e4, h: 36e5, d: 864e5 };
517
+ function parseWindowMs(window) {
518
+ const match = /^(\d+)([smhd])$/.exec(window);
519
+ if (!match) throw new TypeError(`invalid window: ${window}`);
520
+ const unit = WINDOW_MS[match[2]];
521
+ return Number(match[1]) * (unit ?? 0);
522
+ }
523
+ function within(records, window, now) {
524
+ const cutoff = now - parseWindowMs(window);
525
+ return records.filter((r) => r.at >= cutoff);
526
+ }
527
+ function median(values) {
528
+ if (values.length === 0) return void 0;
529
+ const sorted = [...values].sort(compareDecimal);
530
+ return sorted[Math.floor(sorted.length / 2)];
531
+ }
532
+ var InMemorySpendStore = class {
533
+ records = [];
534
+ reputations = /* @__PURE__ */ new Map();
535
+ record(rec) {
536
+ this.records.push(rec);
537
+ }
538
+ setVendorReputation(host, score) {
539
+ this.reputations.set(host, score);
540
+ }
541
+ /** Resolve a point-in-time spend context for one agent (prior activity only). */
542
+ contextFor(agentId, now = Date.now()) {
543
+ const mine = this.records.filter((r) => r.agentId === agentId);
544
+ const reputations = this.reputations;
545
+ const all = this.records;
546
+ return {
547
+ rollingSum: (window) => sumDecimal(within(mine, window, now).map((r) => r.amount)),
548
+ txCount: (window) => within(mine, window, now).length,
549
+ isVendorFirstSeen: (host) => !mine.some((r) => r.host === host),
550
+ vendorReputation: (host) => reputations.get(host),
551
+ resourceMedian: (resource) => median(all.filter((r) => r.resource === resource).map((r) => r.amount))
552
+ };
553
+ }
554
+ };
555
+ var InMemoryPolicyStore = class {
556
+ policies = [];
557
+ /** Upsert by policyId (a new version replaces the prior one). */
558
+ add(policy) {
559
+ this.policies = this.policies.filter((p) => p.policyId !== policy.policyId);
560
+ this.policies.push(policy);
561
+ }
562
+ list() {
563
+ return this.policies;
564
+ }
565
+ get(policyId) {
566
+ return this.policies.find((p) => p.policyId === policyId);
567
+ }
568
+ applicableFor(intent) {
569
+ return this.policies.filter((p) => policyApplies(p, intent));
570
+ }
571
+ };
572
+ var InMemoryAgentRegistry = class {
573
+ agents = /* @__PURE__ */ new Map();
574
+ frozen = /* @__PURE__ */ new Set();
575
+ register(agent) {
576
+ this.agents.set(agent.id, agent);
577
+ if (agent.status === "frozen") this.frozen.add(agent.id);
578
+ }
579
+ get(id) {
580
+ return this.agents.get(id);
581
+ }
582
+ list() {
583
+ return [...this.agents.values()];
584
+ }
585
+ freeze(id) {
586
+ this.frozen.add(id);
587
+ }
588
+ unfreeze(id) {
589
+ this.frozen.delete(id);
590
+ }
591
+ isFrozen(id) {
592
+ return this.frozen.has(id);
593
+ }
594
+ };
595
+
596
+ // src/decision-log.ts
597
+ import {
598
+ createHash,
599
+ generateKeyPairSync,
600
+ sign as edSign,
601
+ verify as edVerify,
602
+ createPublicKey
603
+ } from "crypto";
604
+ var DecisionLog = class {
605
+ prevHash;
606
+ privateKey;
607
+ publicKeyPem;
608
+ chain;
609
+ persist;
610
+ tail = Promise.resolve();
611
+ constructor(options = {}) {
612
+ const kp = options.keyPair ?? generateKeyPairSync("ed25519");
613
+ this.privateKey = kp.privateKey;
614
+ this.publicKeyPem = kp.publicKey.export({ type: "spki", format: "pem" }).toString();
615
+ this.chain = [...options.resume ?? []];
616
+ this.prevHash = this.chain.at(-1)?.hash ?? "genesis";
617
+ this.persist = options.persist;
618
+ }
619
+ append(input) {
620
+ const run = this.tail.then(() => this.appendSerialized(input));
621
+ this.tail = run.catch(() => void 0);
622
+ return run;
623
+ }
624
+ async appendSerialized(input) {
625
+ const decidedAt = /* @__PURE__ */ new Date();
626
+ const hash = createHash("sha256").update(canonicalDecision({ ...input, prevHash: this.prevHash, decidedAt })).digest("hex");
627
+ const signature = edSign(null, Buffer.from(hash), this.privateKey).toString("base64");
628
+ const decision = {
629
+ id: newId("dec"),
630
+ intentId: input.intentId,
631
+ intentHash: input.intentHash,
632
+ outcome: input.outcome,
633
+ matchedRules: input.matchedRules,
634
+ reason: input.reason,
635
+ policyId: input.policyId,
636
+ policyVersion: input.policyVersion,
637
+ prevHash: this.prevHash,
638
+ hash,
639
+ signature,
640
+ latencyMs: input.latencyMs,
641
+ decidedAt
642
+ };
643
+ if (this.persist) await this.persist(decision);
644
+ this.prevHash = hash;
645
+ this.chain.push(decision);
646
+ return decision;
647
+ }
648
+ all() {
649
+ return this.chain;
650
+ }
651
+ };
652
+
653
+ // src/engine.ts
654
+ var IntentInput = z2.object({
655
+ id: IntentId.optional(),
656
+ agentId: AgentId,
657
+ vendor: Vendor,
658
+ resource: z2.string(),
659
+ amount: DecimalString,
660
+ asset: Asset,
661
+ chain: Chain,
662
+ taskContext: TaskContext.optional(),
663
+ nonce: z2.string().min(1).optional(),
664
+ createdAt: z2.coerce.date().optional()
665
+ });
666
+ var PolicyEngine = class {
667
+ spend;
668
+ policies;
669
+ agents;
670
+ log;
671
+ bus = new EventEmitter();
672
+ tail = Promise.resolve();
673
+ constructor(stores = {}) {
674
+ this.spend = stores.spend ?? new InMemorySpendStore();
675
+ this.policies = stores.policies ?? new InMemoryPolicyStore();
676
+ this.agents = stores.agents ?? new InMemoryAgentRegistry();
677
+ this.log = stores.log ?? new DecisionLog();
678
+ }
679
+ get publicKeyPem() {
680
+ return this.log.publicKeyPem;
681
+ }
682
+ onEvent(handler) {
683
+ this.bus.on("event", handler);
684
+ }
685
+ emit(event) {
686
+ this.bus.emit("event", event);
687
+ }
688
+ async registerAgent(input) {
689
+ const agent = Agent.parse(input);
690
+ await this.agents.register(agent);
691
+ return agent;
692
+ }
693
+ async addPolicy(input) {
694
+ const policy = Policy.parse(input);
695
+ await this.policies.add(policy);
696
+ return policy;
697
+ }
698
+ async freeze(agentId) {
699
+ await this.agents.freeze(agentId);
700
+ }
701
+ async unfreeze(agentId) {
702
+ await this.agents.unfreeze(agentId);
703
+ }
704
+ normalize(input) {
705
+ return PaymentIntent.parse({
706
+ id: input.id ?? newId("int"),
707
+ agentId: input.agentId,
708
+ vendor: input.vendor,
709
+ resource: input.resource,
710
+ amount: input.amount,
711
+ asset: input.asset,
712
+ chain: input.chain,
713
+ taskContext: input.taskContext ?? {},
714
+ nonce: input.nonce ?? newId("non"),
715
+ createdAt: input.createdAt ?? /* @__PURE__ */ new Date()
716
+ });
717
+ }
718
+ evaluateIntent(input) {
719
+ const run = this.tail.then(() => this.evaluateSerialized(input));
720
+ this.tail = run.catch(() => void 0);
721
+ return run;
722
+ }
723
+ async evaluateSerialized(input) {
724
+ const start = performance.now();
725
+ const intent = this.normalize(input);
726
+ this.emit({ type: "intent.created", at: /* @__PURE__ */ new Date(), intent });
727
+ let result;
728
+ if (this.agents.isFrozen(intent.agentId)) {
729
+ result = {
730
+ outcome: "deny",
731
+ matchedRules: ["agent-frozen"],
732
+ reason: "agent is frozen (kill switch)",
733
+ policyId: "system",
734
+ policyVersion: "0"
735
+ };
736
+ } else {
737
+ const ctx = this.spend.contextFor(intent.agentId, intent.createdAt.getTime());
738
+ result = evaluate(intent, this.policies.list(), ctx);
739
+ }
740
+ const latencyMs = performance.now() - start;
741
+ const intentHash = createHash2("sha256").update(canonicalIntent(intent)).digest("hex");
742
+ const decision = await this.log.append({
743
+ ...result,
744
+ intentId: intent.id,
745
+ intentHash,
746
+ latencyMs
747
+ });
748
+ this.emit({ type: "decision.made", at: /* @__PURE__ */ new Date(), decision });
749
+ if (decision.outcome === "allow") {
750
+ await this.spend.record({
751
+ agentId: intent.agentId,
752
+ host: intent.vendor.host,
753
+ resource: intent.resource,
754
+ amount: intent.amount,
755
+ at: intent.createdAt.getTime()
756
+ });
757
+ }
758
+ return { intent, decision };
759
+ }
760
+ decisions() {
761
+ return this.log.all();
762
+ }
763
+ };
764
+
765
+ // src/server.ts
766
+ var AgentInput = z3.object({
767
+ orgId: OrgId,
768
+ name: z3.string().min(1).max(200),
769
+ erc8004Id: z3.string().optional(),
770
+ wallets: Agent.shape.wallets.optional()
771
+ });
772
+ function buildServer(engine = new PolicyEngine()) {
773
+ const app = Fastify({ logger: false });
774
+ app.setErrorHandler((err, _req, reply) => {
775
+ if (err instanceof z3.ZodError) {
776
+ return reply.status(400).send({ error: "validation_error", issues: err.issues });
777
+ }
778
+ const message = err instanceof Error ? err.message : String(err);
779
+ return reply.status(500).send({ error: "internal_error", message });
780
+ });
781
+ app.get("/health", () => ({ status: "ok", publicKey: engine.publicKeyPem }));
782
+ app.post("/v1/agents", (req) => {
783
+ const input = AgentInput.parse(req.body);
784
+ return engine.registerAgent({
785
+ id: newId("agt"),
786
+ orgId: input.orgId,
787
+ name: input.name,
788
+ erc8004Id: input.erc8004Id,
789
+ wallets: input.wallets ?? [],
790
+ status: "active",
791
+ createdAt: /* @__PURE__ */ new Date()
792
+ });
793
+ });
794
+ app.get("/v1/agents", () => engine.agents.list());
795
+ app.post("/v1/agents/:id/freeze", async (req, reply) => {
796
+ await engine.freeze(req.params.id);
797
+ return reply.status(204).send();
798
+ });
799
+ app.post("/v1/agents/:id/unfreeze", async (req, reply) => {
800
+ await engine.unfreeze(req.params.id);
801
+ return reply.status(204).send();
802
+ });
803
+ app.post("/v1/policies", (req) => engine.addPolicy(Policy.parse(req.body)));
804
+ app.get("/v1/policies", () => engine.policies.list());
805
+ app.post("/v1/evaluate", (req) => engine.evaluateIntent(IntentInput.parse(req.body)));
806
+ app.get("/v1/decisions", () => engine.decisions());
807
+ return app;
808
+ }
809
+ function isMainModule() {
810
+ if (!process.argv[1]) return false;
811
+ try {
812
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
813
+ } catch {
814
+ return false;
815
+ }
816
+ }
817
+ if (isMainModule()) {
818
+ const port = Number(process.env.PORT ?? 8787);
819
+ const host = process.env.HOST ?? "0.0.0.0";
820
+ const app = buildServer();
821
+ app.listen({ port, host }).then(() => console.log(`[rein] policy-engine listening on http://${host}:${port}`)).catch((err) => {
822
+ console.error(err);
823
+ process.exit(1);
824
+ });
825
+ }
826
+ export {
827
+ buildServer
828
+ };
829
+ //# sourceMappingURL=server.js.map