@gethelio/proxy 0.9.0 → 0.11.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/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { Hono } from 'hono';
3
3
  import { ServerType } from '@hono/node-server';
4
+ import { Database } from 'better-sqlite3';
4
5
  import { KnownBlock } from '@slack/web-api';
5
6
 
6
7
  declare const VERSION: string;
@@ -123,23 +124,61 @@ declare const policiesSchema: z.ZodObject<{
123
124
  }>>;
124
125
  hot_reload: z.ZodOptional<z.ZodBoolean>;
125
126
  }, z.core.$strict>;
127
+ declare const budgetSchema: z.ZodObject<{
128
+ name: z.ZodString;
129
+ limit: z.ZodNumber;
130
+ currency: z.ZodString;
131
+ window: z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<"session">]>;
132
+ key: z.ZodDefault<z.ZodEnum<{
133
+ session: "session";
134
+ sender_id: "sender_id";
135
+ global: "global";
136
+ }>>;
137
+ on_exceed: z.ZodDefault<z.ZodEnum<{
138
+ deny: "deny";
139
+ require_approval: "require_approval";
140
+ }>>;
141
+ approval: z.ZodOptional<z.ZodObject<{
142
+ channel: z.ZodString;
143
+ timeout: z.ZodOptional<z.ZodString>;
144
+ delegates: z.ZodOptional<z.ZodArray<z.ZodString>>;
145
+ escalation_after: z.ZodOptional<z.ZodString>;
146
+ }, z.core.$strict>>;
147
+ idle_ttl: z.ZodOptional<z.ZodString>;
148
+ contributors: z.ZodArray<z.ZodPipe<z.ZodUnknown, z.ZodObject<{
149
+ match: z.ZodObject<{
150
+ tool: z.ZodString;
151
+ input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
152
+ eq: z.ZodOptional<z.ZodUnknown>;
153
+ neq: z.ZodOptional<z.ZodUnknown>;
154
+ gt: z.ZodOptional<z.ZodNumber>;
155
+ gte: z.ZodOptional<z.ZodNumber>;
156
+ lt: z.ZodOptional<z.ZodNumber>;
157
+ lte: z.ZodOptional<z.ZodNumber>;
158
+ contains: z.ZodOptional<z.ZodString>;
159
+ regex: z.ZodOptional<z.ZodString>;
160
+ }, z.core.$strict>>>;
161
+ }, z.core.$strict>;
162
+ field: z.ZodString;
163
+ }, z.core.$strict>>>;
164
+ }, z.core.$strict>;
126
165
  declare const approvalChannelSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
127
166
  type: z.ZodLiteral<"slack">;
128
167
  name: z.ZodOptional<z.ZodString>;
129
168
  bot_token: z.ZodString;
130
169
  signing_secret: z.ZodString;
131
170
  channel: z.ZodString;
132
- }, z.core.$strip>, z.ZodObject<{
171
+ }, z.core.$strict>, z.ZodObject<{
133
172
  type: z.ZodLiteral<"webhook">;
134
173
  name: z.ZodOptional<z.ZodString>;
135
174
  url: z.ZodString;
136
175
  secret: z.ZodOptional<z.ZodString>;
137
- }, z.core.$strip>, z.ZodObject<{
176
+ }, z.core.$strict>, z.ZodObject<{
138
177
  type: z.ZodLiteral<"dashboard">;
139
178
  name: z.ZodOptional<z.ZodString>;
140
- }, z.core.$strip>], "type">;
179
+ }, z.core.$strict>], "type">;
141
180
  /** Zod schema for the complete `helio.yaml` configuration file. */
142
- declare const helioConfigSchema: z.ZodObject<{
181
+ declare const helioConfigSchema: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodObject<{
143
182
  version: z.ZodLiteral<"1">;
144
183
  upstream: z.ZodObject<{
145
184
  url: z.ZodString;
@@ -154,19 +193,11 @@ declare const helioConfigSchema: z.ZodObject<{
154
193
  request_timeout: z.ZodDefault<z.ZodString>;
155
194
  forward_headers: z.ZodDefault<z.ZodArray<z.ZodString>>;
156
195
  headers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
157
- }, z.core.$strip>;
196
+ }, z.core.$strict>;
158
197
  listen: z.ZodPrefault<z.ZodObject<{
159
198
  port: z.ZodDefault<z.ZodNumber>;
160
199
  host: z.ZodDefault<z.ZodString>;
161
- }, z.core.$strip>>;
162
- dashboard: z.ZodPrefault<z.ZodObject<{
163
- enabled: z.ZodDefault<z.ZodBoolean>;
164
- port: z.ZodDefault<z.ZodNumber>;
165
- host: z.ZodDefault<z.ZodString>;
166
- api_secret: z.ZodOptional<z.ZodString>;
167
- allow_open_mode: z.ZodDefault<z.ZodBoolean>;
168
- sse_heartbeat_interval: z.ZodDefault<z.ZodString>;
169
- }, z.core.$strip>>;
200
+ }, z.core.$strict>>;
170
201
  environment: z.ZodOptional<z.ZodString>;
171
202
  policies: z.ZodPrefault<z.ZodObject<{
172
203
  default: z.ZodDefault<z.ZodEnum<{
@@ -286,6 +317,44 @@ declare const helioConfigSchema: z.ZodObject<{
286
317
  }>>;
287
318
  hot_reload: z.ZodOptional<z.ZodBoolean>;
288
319
  }, z.core.$strict>>;
320
+ budgets: z.ZodDefault<z.ZodArray<z.ZodObject<{
321
+ name: z.ZodString;
322
+ limit: z.ZodNumber;
323
+ currency: z.ZodString;
324
+ window: z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<"session">]>;
325
+ key: z.ZodDefault<z.ZodEnum<{
326
+ session: "session";
327
+ sender_id: "sender_id";
328
+ global: "global";
329
+ }>>;
330
+ on_exceed: z.ZodDefault<z.ZodEnum<{
331
+ deny: "deny";
332
+ require_approval: "require_approval";
333
+ }>>;
334
+ approval: z.ZodOptional<z.ZodObject<{
335
+ channel: z.ZodString;
336
+ timeout: z.ZodOptional<z.ZodString>;
337
+ delegates: z.ZodOptional<z.ZodArray<z.ZodString>>;
338
+ escalation_after: z.ZodOptional<z.ZodString>;
339
+ }, z.core.$strict>>;
340
+ idle_ttl: z.ZodOptional<z.ZodString>;
341
+ contributors: z.ZodArray<z.ZodPipe<z.ZodUnknown, z.ZodObject<{
342
+ match: z.ZodObject<{
343
+ tool: z.ZodString;
344
+ input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
345
+ eq: z.ZodOptional<z.ZodUnknown>;
346
+ neq: z.ZodOptional<z.ZodUnknown>;
347
+ gt: z.ZodOptional<z.ZodNumber>;
348
+ gte: z.ZodOptional<z.ZodNumber>;
349
+ lt: z.ZodOptional<z.ZodNumber>;
350
+ lte: z.ZodOptional<z.ZodNumber>;
351
+ contains: z.ZodOptional<z.ZodString>;
352
+ regex: z.ZodOptional<z.ZodString>;
353
+ }, z.core.$strict>>>;
354
+ }, z.core.$strict>;
355
+ field: z.ZodString;
356
+ }, z.core.$strict>>>;
357
+ }, z.core.$strict>>>;
289
358
  approval: z.ZodPrefault<z.ZodObject<{
290
359
  timeout: z.ZodDefault<z.ZodString>;
291
360
  default_on_timeout: z.ZodDefault<z.ZodEnum<{
@@ -298,16 +367,16 @@ declare const helioConfigSchema: z.ZodObject<{
298
367
  bot_token: z.ZodString;
299
368
  signing_secret: z.ZodString;
300
369
  channel: z.ZodString;
301
- }, z.core.$strip>, z.ZodObject<{
370
+ }, z.core.$strict>, z.ZodObject<{
302
371
  type: z.ZodLiteral<"webhook">;
303
372
  name: z.ZodOptional<z.ZodString>;
304
373
  url: z.ZodString;
305
374
  secret: z.ZodOptional<z.ZodString>;
306
- }, z.core.$strip>, z.ZodObject<{
375
+ }, z.core.$strict>, z.ZodObject<{
307
376
  type: z.ZodLiteral<"dashboard">;
308
377
  name: z.ZodOptional<z.ZodString>;
309
- }, z.core.$strip>], "type">>>;
310
- }, z.core.$strip>>;
378
+ }, z.core.$strict>], "type">>>;
379
+ }, z.core.$strict>>;
311
380
  audit: z.ZodPrefault<z.ZodObject<{
312
381
  storage: z.ZodDefault<z.ZodEnum<{
313
382
  sqlite: "sqlite";
@@ -315,20 +384,32 @@ declare const helioConfigSchema: z.ZodObject<{
315
384
  path: z.ZodDefault<z.ZodString>;
316
385
  retention: z.ZodDefault<z.ZodString>;
317
386
  include_responses: z.ZodDefault<z.ZodBoolean>;
318
- }, z.core.$strip>>;
387
+ }, z.core.$strict>>;
388
+ dashboard: z.ZodPrefault<z.ZodObject<{
389
+ enabled: z.ZodDefault<z.ZodBoolean>;
390
+ port: z.ZodDefault<z.ZodNumber>;
391
+ host: z.ZodDefault<z.ZodString>;
392
+ api_secret: z.ZodOptional<z.ZodString>;
393
+ allow_open_mode: z.ZodDefault<z.ZodBoolean>;
394
+ sse_heartbeat_interval: z.ZodDefault<z.ZodString>;
395
+ }, z.core.$strict>>;
319
396
  sdk: z.ZodPrefault<z.ZodObject<{
320
397
  enabled: z.ZodDefault<z.ZodBoolean>;
321
398
  port: z.ZodDefault<z.ZodNumber>;
322
399
  host: z.ZodDefault<z.ZodString>;
323
400
  evaluation_ttl: z.ZodDefault<z.ZodString>;
324
- }, z.core.$strip>>;
325
- }, z.core.$strip>;
401
+ }, z.core.$strict>>;
402
+ }, z.core.$strict>>;
326
403
  /** Fully validated and defaulted Helio configuration. */
327
404
  type HelioConfig = z.infer<typeof helioConfigSchema>;
328
405
  /** An approval channel configuration (slack, webhook, or dashboard). */
329
406
  type ApprovalChannel$1 = z.infer<typeof approvalChannelSchema>;
330
407
  /** The policies section of the config. */
331
408
  type PoliciesConfig = z.infer<typeof policiesSchema>;
409
+ /** A single named budget from the `budgets` array (issue #14). */
410
+ type BudgetConfig = z.infer<typeof budgetSchema>;
411
+ /** The `budgets` section of the config. */
412
+ type BudgetsConfig = readonly BudgetConfig[];
332
413
 
333
414
  /** Structured error for configuration loading failures. */
334
415
  declare class ConfigError extends Error {
@@ -546,6 +627,51 @@ interface MatchContext {
546
627
  readonly metadata?: Readonly<Record<string, unknown>>;
547
628
  }
548
629
 
630
+ /** One compiled contributor: which calls feed the budget, and from which field. */
631
+ interface CompiledBudgetContributor {
632
+ readonly match: {
633
+ readonly tool: ToolMatcher;
634
+ /** Flattened conditions (one per path+operator); absent when unconditioned. */
635
+ readonly input?: readonly InputCondition[];
636
+ };
637
+ /** Dot-path into the tool arguments (e.g. "$.amount"), resolved per call. */
638
+ readonly field: string;
639
+ }
640
+ /**
641
+ * A budget's replenishment semantics.
642
+ *
643
+ * - `duration`: sliding window; spend ages out after `windowMs`.
644
+ * - `session`: a depleting pot per session key that never replenishes on a
645
+ * timer; idle pots are garbage-collected after `idleTtlMs` because neither
646
+ * door has an authoritative session-end signal.
647
+ */
648
+ type CompiledBudgetWindow = {
649
+ readonly kind: 'duration';
650
+ readonly windowMs: number;
651
+ } | {
652
+ readonly kind: 'session';
653
+ readonly idleTtlMs: number;
654
+ };
655
+ /** A fully compiled named budget, ready for the engine. */
656
+ interface CompiledBudget {
657
+ readonly name: string;
658
+ readonly limit: number;
659
+ readonly currency: string;
660
+ readonly window: CompiledBudgetWindow;
661
+ /** The raw config window string ("1h" | "session") for wire/docs surfaces. */
662
+ readonly windowRaw: string;
663
+ readonly key: 'global' | 'session' | 'sender_id';
664
+ /** What a breach does: deny the call, or raise a break-glass ticket. */
665
+ readonly onExceed: 'deny' | 'require_approval';
666
+ /**
667
+ * Break-glass ticket routing (`on_exceed: require_approval` only). Absent
668
+ * means the dashboard channel and the router's default timeout. Budget
669
+ * tickets never consult `default_on_timeout` — timeout fails closed.
670
+ */
671
+ readonly approval?: CompiledApproval;
672
+ readonly contributors: readonly CompiledBudgetContributor[];
673
+ }
674
+
549
675
  /** A JSON-RPC 2.0 request object. */
550
676
  interface JsonRpcRequest {
551
677
  jsonrpc: '2.0';
@@ -997,6 +1123,15 @@ declare const EXPORT_MAX_RECORDS = 10000;
997
1123
  * audit route schema for the same reason as {@link EXPORT_MAX_RECORDS}.
998
1124
  */
999
1125
  declare const LIST_MAX_PAGE_SIZE = 1000;
1126
+ /**
1127
+ * The retention cutoff of one sweep, computed once and shared with every
1128
+ * registered hook: `iso` for `created_at`-style string comparisons, `ms`
1129
+ * for epoch-millisecond columns.
1130
+ */
1131
+ interface RetentionSweepCutoff {
1132
+ readonly iso: string;
1133
+ readonly ms: number;
1134
+ }
1000
1135
  /**
1001
1136
  * SQLite-backed audit record store.
1002
1137
  *
@@ -1008,8 +1143,33 @@ declare class AuditStore {
1008
1143
  private readonly insertStmt;
1009
1144
  private readonly retentionMs;
1010
1145
  private readonly includeResponses;
1146
+ private readonly retentionSweepHooks;
1011
1147
  private cleanupTimer;
1012
1148
  constructor(options: AuditStoreOptions);
1149
+ /**
1150
+ * Register a hook to run on every retention sweep, receiving the sweep's
1151
+ * cutoff. This is how co-resident tables (the budget ledger) join the
1152
+ * store's single sweep schedule instead of running their own timers.
1153
+ * Hooks registered after construction miss the constructor's initial
1154
+ * purge — call {@link runRetentionSweep} once after registering to cover
1155
+ * rows that aged out while the process was down.
1156
+ */
1157
+ onRetentionSweep(fn: (cutoff: RetentionSweepCutoff) => void): void;
1158
+ /**
1159
+ * One full retention sweep: purge expired audit records, then fire every
1160
+ * registered hook with the sweep's cutoff. Hook failures degrade to a
1161
+ * logged error — a broken co-resident purge must not stop the audit
1162
+ * table's own retention.
1163
+ */
1164
+ runRetentionSweep(): void;
1165
+ /**
1166
+ * Package-internal: the store's open database handle, for components that
1167
+ * co-locate their tables in the audit db (the budget ledger). Sharing the
1168
+ * handle keeps one connection, one WAL domain, and one file-permission
1169
+ * hardening pass. Not part of the public embedding API — do not re-export
1170
+ * anything built on this from the package root.
1171
+ */
1172
+ get database(): Database;
1013
1173
  /**
1014
1174
  * Validate that the on-disk audit schema contains all required canonical columns.
1015
1175
  *
@@ -1059,6 +1219,7 @@ declare class AuditStore {
1059
1219
  aggregate(from?: string, to?: string): AuditAggregateStats;
1060
1220
  /** Delete records older than the retention period. Returns the count of deleted records. */
1061
1221
  purgeExpired(): number;
1222
+ private purgeBefore;
1062
1223
  /** Close the database and stop the cleanup timer. */
1063
1224
  close(): void;
1064
1225
  }
@@ -1304,6 +1465,21 @@ type ApprovalStatus = 'pending' | 'approved' | 'denied' | 'timeout' | 'break_gla
1304
1465
  * aborting the held MCP request. Sideband (native) approvals only. (#12.)
1305
1466
  */
1306
1467
  | 'cancelled';
1468
+ /**
1469
+ * One breached budget's context on a break-glass approval ticket (issue #14).
1470
+ *
1471
+ * DTO: snake_case because it rides {@link ApprovalTicket}, which is emitted
1472
+ * verbatim over REST and webhooks. `spent` is the accrued spend BEFORE the
1473
+ * attempted charge; `window` is the raw config string ("1h" | "session").
1474
+ */
1475
+ interface BudgetBreachContext {
1476
+ readonly name: string;
1477
+ readonly limit: number;
1478
+ readonly spent: number;
1479
+ readonly attempted_amount: number;
1480
+ readonly currency: string;
1481
+ readonly window: string;
1482
+ }
1307
1483
  /** A failed attempt to deliver an approval notification. */
1308
1484
  interface ApprovalNotificationFailure {
1309
1485
  readonly channel: string;
@@ -1331,6 +1507,14 @@ interface ApprovalTicket {
1331
1507
  readonly requested_at: string;
1332
1508
  readonly timeout_at: string;
1333
1509
  readonly timeout_ms: number;
1510
+ /**
1511
+ * Every budget the call breached, when this is a break-glass (budget) or
1512
+ * merged rule+budget ticket (issue #14). Its presence marks the ticket as
1513
+ * budget-context: one approval covers every listed overage, the approval is
1514
+ * scope-once by definition (issue #127 interlock — a `scope: "always"`
1515
+ * resolution grants nothing beyond this call), and timeout fails closed.
1516
+ */
1517
+ readonly breached_budgets?: readonly BudgetBreachContext[];
1334
1518
  status: ApprovalStatus;
1335
1519
  resolved_at?: string;
1336
1520
  resolved_by?: string;
@@ -1413,6 +1597,8 @@ declare class ApprovalQueue {
1413
1597
  channel_name: string;
1414
1598
  session_id: string | null;
1415
1599
  timeout_ms: number;
1600
+ /** Breached budget context on break-glass / merged tickets (issue #14). */
1601
+ breached_budgets?: readonly BudgetBreachContext[];
1416
1602
  }): ApprovalTicket;
1417
1603
  /** Get a ticket by ID. Returns undefined if not found. */
1418
1604
  get(id: string): ApprovalTicket | undefined;
@@ -1468,6 +1654,17 @@ interface ApprovalSubmitParams {
1468
1654
  readonly tool_input: Record<string, unknown>;
1469
1655
  readonly matched_rule: CompiledPolicyRule | undefined;
1470
1656
  readonly session_id: string | null;
1657
+ /** Breached budget context; marks the ticket as break-glass (issue #14). */
1658
+ readonly breached_budgets?: readonly BudgetBreachContext[];
1659
+ /**
1660
+ * Total approval-config override. When set, channel/timeout/delegates/
1661
+ * escalation come from HERE and the matched rule's approval config is
1662
+ * ignored entirely — budget tickets are routed by the breached BUDGET's
1663
+ * config (the matched rule may be an allow rule whose approval block, if
1664
+ * any, has no authority over the money gate). Fields the override omits
1665
+ * fall back to the router defaults, never to the rule.
1666
+ */
1667
+ readonly approval?: CompiledApproval;
1471
1668
  }
1472
1669
  /** Resolution statuses a native (sideband) ticket can be moved to. */
1473
1670
  type NativeResolution = 'approved' | 'denied' | 'timeout' | 'cancelled';
@@ -1481,6 +1678,8 @@ interface NativeTicketParams {
1481
1678
  readonly origin: string;
1482
1679
  /** Ticket timeout in ms (rule timeout, else the router default). */
1483
1680
  readonly timeout_ms?: number;
1681
+ /** Breached budget context; marks the ticket as break-glass (issue #14). */
1682
+ readonly breached_budgets?: readonly BudgetBreachContext[];
1484
1683
  }
1485
1684
  declare class ApprovalRouter {
1486
1685
  private readonly defaultTimeoutMs;
@@ -1677,6 +1876,13 @@ declare class RateLimiter {
1677
1876
  windowMs: number;
1678
1877
  }>): void;
1679
1878
  /** Stop the cleanup timer and mark as closed. */
1879
+ /**
1880
+ * Invoke the warning callback without letting a subscriber throw into the
1881
+ * limiter's caller: a warning fires after state has already mutated, and a
1882
+ * governed call must not be blocked (or double-charged on retry) by an
1883
+ * observability bug.
1884
+ */
1885
+ private safeWarn;
1680
1886
  close(): void;
1681
1887
  }
1682
1888
 
@@ -1796,6 +2002,13 @@ declare class SpendLimiter {
1796
2002
  * whose config is gone (rule changed or removed) are evicted so the next
1797
2003
  * check lazy-creates a fresh bucket under the new config.
1798
2004
  *
2005
+ * Keys built by {@link spendBucketKey} carry the owning rule's index, and
2006
+ * for those the tuple must match at THAT index (`config.ruleIndex`): a
2007
+ * reorder that shifts a spend rule's index evicts its old-index bucket
2008
+ * instead of leaving an orphan no rule reads again — or worse, letting
2009
+ * whatever rule now sits at that index adopt another rule's accrued spend.
2010
+ * Un-suffixed keys keep the tuple-anywhere match.
2011
+ *
1799
2012
  * Currency is part of the tuple because a USD→EUR switch is a meaningful
1800
2013
  * policy change — the same numeric limit buys a different amount of real
1801
2014
  * spend, so the bucket must reset. This replaces the old `reset()` call
@@ -1806,11 +2019,316 @@ declare class SpendLimiter {
1806
2019
  limit: number;
1807
2020
  currency: string;
1808
2021
  windowMs: number;
2022
+ ruleIndex?: number;
1809
2023
  }>): void;
1810
2024
  /** Stop the cleanup timer and mark as closed. */
2025
+ /**
2026
+ * Invoke the warning callback without letting a subscriber throw into the
2027
+ * limiter's caller: a warning fires after state has already mutated, and a
2028
+ * governed call must not be blocked (or double-charged on retry) by an
2029
+ * observability bug.
2030
+ */
2031
+ private safeWarn;
1811
2032
  close(): void;
1812
2033
  }
1813
2034
 
2035
+ /** Everything the engine needs about one tool call to resolve its charges. */
2036
+ interface BudgetChargeContext {
2037
+ readonly toolName: string;
2038
+ readonly toolArguments: Record<string, unknown> | undefined;
2039
+ readonly sessionId: string | null;
2040
+ /** Adapter-supplied sender id (sideband only); null on the MCP path. */
2041
+ readonly senderId: string | null;
2042
+ }
2043
+ /** One budget's share of a call: which bucket, how much. */
2044
+ interface BudgetCharge {
2045
+ readonly budget: CompiledBudget;
2046
+ readonly bucketKey: string;
2047
+ readonly amount: number;
2048
+ /**
2049
+ * The budget's config generation at peek time. A tuple-changing reload
2050
+ * bumps the generation and resets the pot; a charge frozen before the bump
2051
+ * (sideband /evaluate → /audit, or an MCP approval wait) is stale and MUST
2052
+ * NOT repopulate the new pot with old-config spend — recordAll skips it.
2053
+ */
2054
+ readonly generation: number;
2055
+ }
2056
+ /** A budget whose contributor matched but whose amount was unusable. */
2057
+ interface BudgetChargeFailure {
2058
+ readonly budget: CompiledBudget;
2059
+ readonly bucketKey: string;
2060
+ readonly reason: 'invalid_amount';
2061
+ /** REAL accrued spend on the bucket the charge would have hit. */
2062
+ readonly spent: number;
2063
+ readonly remaining: number;
2064
+ /** Epoch ms when the oldest entry ages out (duration); null for session pots. */
2065
+ readonly resetAtMs: number | null;
2066
+ }
2067
+ /** Snapshot of one budget's state relative to a charge. */
2068
+ interface BudgetPeekEntry {
2069
+ readonly budget: CompiledBudget;
2070
+ readonly bucketKey: string;
2071
+ readonly amount: number;
2072
+ readonly allowed: boolean;
2073
+ /** Spend accrued before this charge. */
2074
+ readonly spent: number;
2075
+ /** Headroom before this charge: max(0, limit - spent). */
2076
+ readonly remaining: number;
2077
+ /** Epoch ms when the oldest entry ages out (duration); null for session pots. */
2078
+ readonly resetAtMs: number | null;
2079
+ /**
2080
+ * Set on recordAll snapshots for charges frozen before a tuple-changing
2081
+ * reload: the executed spend was ledgered under its evaluate-time
2082
+ * generation, but the reset pot was not touched.
2083
+ */
2084
+ readonly stale?: true;
2085
+ }
2086
+ /** Metadata recorded with every committed charge of one call. */
2087
+ interface BudgetCommitMeta {
2088
+ /** Default kind for every charge of the call. */
2089
+ readonly kind: 'spend' | 'approved_overage';
2090
+ /**
2091
+ * Per-budget overrides by budget name (break-glass): one approved call can
2092
+ * mix kinds — breached budgets commit as `approved_overage` while
2093
+ * unbreached ones stay `spend` — and all rows must still land in ONE
2094
+ * ledger transaction, so the split is expressed here, not via two calls.
2095
+ */
2096
+ readonly kinds?: ReadonlyMap<string, 'spend' | 'approved_overage'>;
2097
+ readonly auditRecordId: string;
2098
+ readonly origin: string;
2099
+ readonly toolName: string;
2100
+ readonly timestampIso: string;
2101
+ }
2102
+ /**
2103
+ * One durable ledger row. DTO: snake_case, matching the `budget_events`
2104
+ * table columns the persistence layer writes.
2105
+ */
2106
+ interface BudgetLedgerRow {
2107
+ readonly budget_name: string;
2108
+ readonly bucket_key: string;
2109
+ readonly kind: 'spend' | 'approved_overage';
2110
+ readonly amount: number;
2111
+ readonly currency: string;
2112
+ readonly tool_name: string;
2113
+ readonly origin: string;
2114
+ readonly audit_record_id: string;
2115
+ readonly timestamp: string;
2116
+ readonly timestamp_ms: number;
2117
+ /**
2118
+ * The charge's config generation at evaluate time. Rows from a stale
2119
+ * generation are historical accounting for money that really moved; live
2120
+ * replay only ever reads the current generation (the epoch of PR 2).
2121
+ */
2122
+ readonly generation: number;
2123
+ }
2124
+ /**
2125
+ * Durable sink for committed charges. `commitAll` MUST be transactional:
2126
+ * either every row of the batch persists or none does (a throw means none).
2127
+ * The in-memory default is a no-op; the SQLite ledger implements this.
2128
+ */
2129
+ interface BudgetLedgerSink {
2130
+ commitAll(rows: readonly BudgetLedgerRow[]): void;
2131
+ }
2132
+ /** Payload for the per-charge commit callback (dashboard event bus). */
2133
+ interface BudgetCommitEvent {
2134
+ readonly name: string;
2135
+ readonly bucket_key: string;
2136
+ readonly kind: 'spend' | 'approved_overage';
2137
+ readonly amount: number;
2138
+ readonly spent: number;
2139
+ readonly remaining: number;
2140
+ readonly limit: number;
2141
+ readonly currency: string;
2142
+ readonly utilization: number;
2143
+ }
2144
+ /**
2145
+ * Payload for the per-budget breach callback (dashboard event bus). Fired
2146
+ * via {@link BudgetEngine.reportBreaches} by the DOORS at the moment a peek
2147
+ * actually denies a call or raises the composite break-glass ticket — never
2148
+ * by `peekAll` itself, which is pure and also runs for dry-run.
2149
+ */
2150
+ interface BudgetBreachEvent {
2151
+ readonly name: string;
2152
+ readonly bucket_key: string;
2153
+ /** The budget's configured posture, even when the outcome was a deny. */
2154
+ readonly on_exceed: 'deny' | 'require_approval';
2155
+ readonly attempted_amount: number;
2156
+ readonly spent: number;
2157
+ readonly limit: number;
2158
+ readonly currency: string;
2159
+ }
2160
+ /** Wire-ready bucket state for `GET /api/budgets` (snake_case DTO). */
2161
+ interface BudgetBucketState {
2162
+ readonly bucket_key: string;
2163
+ readonly spent: number;
2164
+ readonly remaining: number;
2165
+ readonly reset_at_ms: number | null;
2166
+ readonly last_activity_ms: number;
2167
+ }
2168
+ /** Wire-ready budget state for `GET /api/budgets` (snake_case DTO). */
2169
+ interface BudgetState {
2170
+ readonly name: string;
2171
+ readonly limit: number;
2172
+ readonly currency: string;
2173
+ readonly window: string;
2174
+ readonly key: 'global' | 'session' | 'sender_id';
2175
+ readonly on_exceed: 'deny' | 'require_approval';
2176
+ readonly buckets: readonly BudgetBucketState[];
2177
+ }
2178
+ interface BudgetEngineOptions {
2179
+ readonly budgets?: readonly CompiledBudget[];
2180
+ /** Clock function for testable time. Defaults to `Date.now`. */
2181
+ readonly now?: () => number;
2182
+ /** Interval (ms) between GC sweeps. 0 disables the timer. Default: 60000. */
2183
+ readonly cleanupIntervalMs?: number;
2184
+ /** Durable sink; defaults to a no-op (state resets on restart). */
2185
+ readonly ledger?: BudgetLedgerSink;
2186
+ /** Fired once per committed charge with post-record numbers. */
2187
+ readonly onCommit?: (event: BudgetCommitEvent) => void;
2188
+ /** Fired once per breached budget when a door denies or raises a ticket. */
2189
+ readonly onBreach?: (event: BudgetBreachEvent) => void;
2190
+ }
2191
+ declare class BudgetEngine {
2192
+ private budgets;
2193
+ /** budget name → bucket key → bucket. */
2194
+ private readonly state;
2195
+ /** budget name → config generation; bumped whenever the pot resets. */
2196
+ private readonly generations;
2197
+ private readonly now;
2198
+ private readonly ledger;
2199
+ /** The sink again, when it carries the full persistence contract. */
2200
+ private readonly persistence;
2201
+ private readonly onCommit;
2202
+ private readonly onBreach;
2203
+ private timer;
2204
+ private closed;
2205
+ private hydrated;
2206
+ constructor(options?: BudgetEngineOptions);
2207
+ /**
2208
+ * Resolve which budgets a call feeds and how much it charges each.
2209
+ *
2210
+ * A contributor participates when its tool glob matches the tool name AND
2211
+ * every `match.input` condition holds (absent conditions means the glob
2212
+ * alone decides); the FIRST participating contributor (config order, over
2213
+ * that combined predicate) supplies the amount field. A call that matches
2214
+ * the glob but not the conditions simply does not feed the budget — no
2215
+ * charge, no failure. Once a contributor is selected, a missing,
2216
+ * non-numeric, negative, or non-finite amount fails closed as a `failures`
2217
+ * entry — the caller must deny the call.
2218
+ */
2219
+ resolveCharges(ctx: BudgetChargeContext): {
2220
+ charges: BudgetCharge[];
2221
+ failures: BudgetChargeFailure[];
2222
+ };
2223
+ /** Check every charge without mutating. All-or-nothing: one deny flips `allowed`. */
2224
+ peekAll(charges: readonly BudgetCharge[]): {
2225
+ allowed: boolean;
2226
+ entries: BudgetPeekEntry[];
2227
+ };
2228
+ /**
2229
+ * Commit every charge of one call: ledger first (one atomic batch), then
2230
+ * in-memory state, then the commit events. A sink throw propagates and
2231
+ * leaves ALL in-memory buckets untouched — no partial commit, ever.
2232
+ * Recording is unconditional past the sink (an approved overage
2233
+ * legitimately pushes a bucket past its limit).
2234
+ */
2235
+ recordAll(charges: readonly BudgetCharge[], meta: BudgetCommitMeta): BudgetPeekEntry[];
2236
+ /**
2237
+ * Fire one `onBreach` event per breached entry. Called by the doors at the
2238
+ * moment a peek outcome actually denies the call or raises the composite
2239
+ * break-glass ticket (never for dry-run peeks, never for invalid-amount
2240
+ * failures — those are input errors, not breaches). Subscriber throws are
2241
+ * isolated: a dashboard bug must never affect a gate outcome.
2242
+ */
2243
+ reportBreaches(entries: readonly BudgetPeekEntry[]): void;
2244
+ /**
2245
+ * Rebuild in-memory state from the ledger. Call once at startup, after
2246
+ * construction and before serving traffic; a no-op when the configured
2247
+ * sink does not carry the persistence contract (in-memory mode).
2248
+ *
2249
+ * Per configured budget, `budget_meta` decides:
2250
+ * - no row → first boot for this name: mint epoch 1, nothing to replay;
2251
+ * - a different `{limit, currency, window, key}` tuple → the config
2252
+ * changed while down: bump the epoch, replay nothing (the same reset a
2253
+ * live tuple-changing reload performs, extended across restarts). Old
2254
+ * rows keep their epoch — history stays queryable, replay ignores it;
2255
+ * - a matching tuple → replay at the meta epoch: duration windows rebuild
2256
+ * entry lists from a window lookback (bit-equivalent to never having
2257
+ * restarted), session windows rebuild still-live pots (idle-TTL bound)
2258
+ * from their post-GC-watermark lifetime sums.
2259
+ *
2260
+ * Meta writes here propagate failures: a ledger that cannot record epochs
2261
+ * at startup must fail the boot loudly, the same posture as the audit
2262
+ * store's schema assertion.
2263
+ */
2264
+ hydrate(): void;
2265
+ /**
2266
+ * Swap budget configs on hot-reload. Identity is the NAME: removed names
2267
+ * drop their live buckets; a changed `{limit, currency, window, key}` tuple
2268
+ * resets the budget's buckets (a different pool or scope structure);
2269
+ * everything else — contributors, on_exceed — applies to the accrued state
2270
+ * as-is, because those edits do not change what was already spent.
2271
+ *
2272
+ * Persist-before-swap: every epoch this reload mints lands in
2273
+ * `budget_meta` in ONE transaction BEFORE any memory changes. A throw
2274
+ * rejects the whole reload — the caller keeps the previous config — so
2275
+ * disk and memory can never diverge; a failed reload simply never
2276
+ * happened, and no later restart can misread it. (A swallow-and-continue
2277
+ * posture here would let an A→B reload with a failed flush resurrect the
2278
+ * retired A pot after a revert-and-restart.)
2279
+ *
2280
+ * Removed names mint too: an in-flight charge frozen before the removal
2281
+ * must go stale, or its commit would recreate hidden bucket state for a
2282
+ * budget that no longer exists — and without the on-disk tombstone, a
2283
+ * restart with the budget back in the config would resurrect the
2284
+ * pre-removal pot that the removal had reset. Generations for removed
2285
+ * names are kept (not deleted) so a later re-add keeps counting up.
2286
+ *
2287
+ * @throws When the epoch flush fails; the engine is unchanged.
2288
+ */
2289
+ reconcile(next: readonly CompiledBudget[]): void;
2290
+ /** Sweep: collect idle session pots, evict expired duration entries. */
2291
+ gc(): void;
2292
+ /**
2293
+ * Wire-ready state for `GET /api/budgets`. Configured budgets appear even
2294
+ * with zero live buckets, so the dashboard shows every pot at headroom.
2295
+ */
2296
+ listStates(): BudgetState[];
2297
+ /** Whether any budget holds a live bucket under `key` (cardinality probes). */
2298
+ hasBucket(key: string): boolean;
2299
+ close(): void;
2300
+ /**
2301
+ * The next epoch for a name: one past the highest that memory, the meta
2302
+ * row, or the rows themselves have seen. Pure — the caller applies it to
2303
+ * `generations` only after the mint is durable. The meta consult matters
2304
+ * for names this process has no memory of (a hot-reload re-add after a
2305
+ * restart); the rows consult is a backstop against historical divergence
2306
+ * (rows at an epoch no meta row records) — minting from memory or meta
2307
+ * alone could collide into an epoch that already has rows and replay them
2308
+ * into a different pot.
2309
+ */
2310
+ private nextEpoch;
2311
+ /**
2312
+ * The key format is part of the ON-DISK contract: hydrate rebuilds buckets
2313
+ * from `budget_events.bucket_key` verbatim, so renaming any segment here
2314
+ * would strand every persisted bucket of an unchanged tuple as an
2315
+ * unreachable ghost (displayed, never charged). Changing the format
2316
+ * requires folding a format version into the epoch decision.
2317
+ */
2318
+ private bucketKey;
2319
+ private bucketFor;
2320
+ private evictExpired;
2321
+ /**
2322
+ * Fetch a bucket for reading, evicting expired duration entries first and
2323
+ * pruning the bucket if nothing is left. Reads must never see (or keep
2324
+ * alive, via `hasBucket`-driven capacity slots) state the window has
2325
+ * already expired — expiry is lazy on read, not just on the sweep timer.
2326
+ */
2327
+ private liveBucket;
2328
+ private spentOf;
2329
+ private snapshot;
2330
+ }
2331
+
1814
2332
  /** Options for constructing a GovernedForwarder. */
1815
2333
  interface GovernedForwarderOptions {
1816
2334
  /** The current environment label (e.g. "production", "staging"). */
@@ -1825,6 +2343,8 @@ interface GovernedForwarderOptions {
1825
2343
  rateLimiter?: RateLimiter;
1826
2344
  /** Spend limiter for handling spend_limit decisions. */
1827
2345
  spendLimiter?: SpendLimiter;
2346
+ /** Budget engine for named cross-tool budgets (issue #14). */
2347
+ budgetEngine?: BudgetEngine;
1828
2348
  }
1829
2349
  /** Result of attempting to prime the tool annotation cache. */
1830
2350
  interface AnnotationCachePrimeResult {
@@ -1852,6 +2372,7 @@ declare class GovernedForwarder implements McpForwarder {
1852
2372
  private readonly approvalRouter;
1853
2373
  private readonly rateLimiter;
1854
2374
  private readonly spendLimiter;
2375
+ private readonly budgetEngine;
1855
2376
  private readonly annotationCache;
1856
2377
  private agentKeyWarned;
1857
2378
  private senderKeyWarned;
@@ -1895,6 +2416,51 @@ declare class GovernedForwarder implements McpForwarder {
1895
2416
  /** Write an immediate audit record for a drift event (not a tool call). */
1896
2417
  private writeDriftAuditRecord;
1897
2418
  private handleToolsCall;
2419
+ /**
2420
+ * Await the composite break-glass ticket for a budget overage (issue #14).
2421
+ *
2422
+ * The ticket is routed by the BUDGET's approval config (first breached
2423
+ * budget in config order), never the matched rule's. Deviation from rule
2424
+ * approvals, by design: timeout ALWAYS fails closed — `default_on_timeout:
2425
+ * allow` would forward an unapproved overage, and recording it as
2426
+ * `approved_overage` would be a lie while not recording it would corrupt
2427
+ * the pot. Money gates do not fail open.
2428
+ */
2429
+ private handleBudgetApproval;
2430
+ /** writeAuditRecord, isolated: an audit-writer bug must not reject the response. */
2431
+ private writeAuditRecordSafely;
2432
+ /**
2433
+ * Everything the non-approval action branches decide, minus the forward
2434
+ * itself. Deliberately SYNCHRONOUS: the caller must reach the phase-3
2435
+ * commits without yielding to the microtask queue, or concurrent calls
2436
+ * could double-spend a peeked limiter slot. The approval branch (the only
2437
+ * one that genuinely waits) is dispatched by the caller directly.
2438
+ */
2439
+ private resolveActionGate;
2440
+ /**
2441
+ * Phase 2: check every budget the call feeds, all-or-nothing (issue #14).
2442
+ *
2443
+ * Any `on_exceed: deny` breach (or invalid amount) denies and records
2444
+ * NOTHING on any budget — rejected calls never consume budget anywhere.
2445
+ * Breaches that are all `on_exceed: require_approval` yield the `approval`
2446
+ * variant: one composite break-glass ticket per call, and only an explicit
2447
+ * approval commits (breached budgets as `approved_overage`). On proceed,
2448
+ * the returned `commit` records every charge together (ledger rows first,
2449
+ * atomically, referencing the pre-generated audit id).
2450
+ */
2451
+ private gateBudgets;
2452
+ /**
2453
+ * Reject a `tools/call` that carries no usable tool name and record it.
2454
+ *
2455
+ * The rejection is its own audit shape, not a governed decision: no rule was
2456
+ * evaluated, so it is written directly (like {@link writeDriftAuditRecord})
2457
+ * rather than threaded through {@link writeAuditRecord}, whose
2458
+ * `PolicyDecision.action` union has no `rejected` member and whose
2459
+ * forwarded-upstream logic does not apply. The raw `params` are preserved in
2460
+ * `tool_input` so an investigator can see exactly what a lenient upstream
2461
+ * could have keyed off.
2462
+ */
2463
+ private rejectNamelessToolsCall;
1898
2464
  private handleApproval;
1899
2465
  private handleRateLimit;
1900
2466
  private handleSpendLimit;
@@ -1905,8 +2471,12 @@ declare class GovernedForwarder implements McpForwarder {
1905
2471
  private handleDryRun;
1906
2472
  /** Construct a limit bucket key based on the configured key type. */
1907
2473
  private buildLimitKey;
1908
- /** Determine if the request was actually forwarded to the upstream MCP server. */
1909
- private wasForwardedUpstream;
2474
+ /**
2475
+ * Construct a spend bucket key via the shared {@link spendBucketKey}
2476
+ * composer — see its doc for why spend buckets are rule-discriminated.
2477
+ * Rate buckets keep the undiscriminated keys.
2478
+ */
2479
+ private buildSpendLimitKey;
1910
2480
  private writeAuditRecord;
1911
2481
  private makeDriftBlockResult;
1912
2482
  private makeDenyResult;
@@ -1918,8 +2488,48 @@ declare class GovernedForwarder implements McpForwarder {
1918
2488
  private makeClientDisconnectedBlockResult;
1919
2489
  }
1920
2490
 
2491
+ /** A budget failed to compile (invalid contributor glob). */
2492
+ declare class BudgetParseError extends Error {
2493
+ readonly budgetName: string;
2494
+ constructor(message: string, budgetName: string);
2495
+ }
2496
+ /**
2497
+ * Compile validated budget configs into engine-ready form.
2498
+ *
2499
+ * Contributor globs use the same picomatch engine as `match.tool` so a
2500
+ * pattern behaves identically whether it gates a rule or feeds a budget.
2501
+ *
2502
+ * @throws {BudgetParseError} On an invalid contributor glob.
2503
+ */
2504
+ declare function compileBudgets(budgets: BudgetsConfig): CompiledBudget[];
2505
+
2506
+ /**
2507
+ * One `budget_events` row as listed by `GET /api/budgets/:name/events` —
2508
+ * the table columns minus `epoch` (internal replay bookkeeping), snake_case
2509
+ * verbatim. `budget_name` stays in the row so a page is self-describing.
2510
+ */
2511
+ interface BudgetEventRecord {
2512
+ readonly id: string;
2513
+ readonly budget_name: string;
2514
+ readonly bucket_key: string;
2515
+ readonly kind: 'spend' | 'approved_overage';
2516
+ readonly amount: number;
2517
+ readonly currency: string;
2518
+ readonly tool_name: string;
2519
+ readonly origin: string;
2520
+ readonly audit_record_id: string | null;
2521
+ readonly timestamp: string;
2522
+ readonly timestamp_ms: number;
2523
+ readonly created_at: string;
2524
+ }
2525
+ /** One page of a budget's event history plus the unpaginated total. */
2526
+ interface BudgetEventsPage {
2527
+ readonly events: readonly BudgetEventRecord[];
2528
+ readonly total: number;
2529
+ }
2530
+
1921
2531
  /** The outcome vocabulary adapters branch on — never internal rule actions. */
1922
- type WireDecision = 'allow' | 'deny' | 'require_approval' | 'rate_limited' | 'spend_limited' | 'dry_run';
2532
+ type WireDecision = 'allow' | 'deny' | 'require_approval' | 'rate_limited' | 'spend_limited' | 'budget_exceeded' | 'dry_run';
1923
2533
  /** Tool definition carried by /evaluate (optional; enables the drift guard). */
1924
2534
  interface WireToolDefinition {
1925
2535
  readonly name: string;
@@ -1999,6 +2609,8 @@ interface GovernanceServiceOptions {
1999
2609
  readonly approvalRouter?: ApprovalRouter;
2000
2610
  readonly rateLimiter?: RateLimiter;
2001
2611
  readonly spendLimiter?: SpendLimiter;
2612
+ /** Budget engine for named cross-tool budgets (issue #14). */
2613
+ readonly budgetEngine?: BudgetEngine;
2002
2614
  readonly auditWriter?: AuditWriter;
2003
2615
  /** Default approval timeout (ms) when a rule sets none. */
2004
2616
  readonly approvalTimeoutMs?: number;
@@ -2022,6 +2634,7 @@ declare class GovernanceService {
2022
2634
  private readonly approvalRouter;
2023
2635
  private readonly rateLimiter;
2024
2636
  private readonly spendLimiter;
2637
+ private readonly budgetEngine;
2025
2638
  private readonly auditWriter;
2026
2639
  private readonly approvalTimeoutMs;
2027
2640
  private readonly ttlMs;
@@ -2107,10 +2720,12 @@ declare class GovernanceService {
2107
2720
  private cacheFor;
2108
2721
  private discardPending;
2109
2722
  private getTicketStatus;
2723
+ /** Latch the entry's ticket resolution while the ticket still exists. */
2724
+ private snapshotTicketResolution;
2110
2725
  private planRate;
2111
2726
  private planSpend;
2112
- /** Commit a limit plan at /audit time and return the evidence_chain block. */
2113
- private commitLimit;
2727
+ /** Commit every plan of one call at /audit time; returns the chain blocks. */
2728
+ private commitPlans;
2114
2729
  private writeAudit;
2115
2730
  private assertApprovalRouter;
2116
2731
  }
@@ -2342,6 +2957,19 @@ interface ApprovalNotificationFailedEvent {
2342
2957
  readonly phase: 'initial' | 'escalation';
2343
2958
  readonly error: string;
2344
2959
  }
2960
+ /**
2961
+ * Payload for a budget_update event: one committed charge with post-record
2962
+ * numbers (issue #14). The engine's commit-event DTO is already snake_case
2963
+ * wire shape, so it is emitted verbatim. `utilization` drives dashboard
2964
+ * thresholds — there is no separate budget warning event.
2965
+ */
2966
+ type BudgetUpdateEvent = BudgetCommitEvent;
2967
+ /**
2968
+ * Payload for a budget_breached event: a peek denied the call or raised the
2969
+ * composite break-glass ticket (issue #14). Emitted verbatim from the
2970
+ * engine's breach-event DTO.
2971
+ */
2972
+ type BudgetBreachedEvent = BudgetBreachEvent;
2345
2973
  /** Map of event type names to their payload types. */
2346
2974
  interface DashboardEvents {
2347
2975
  action: ActionEvent;
@@ -2349,6 +2977,8 @@ interface DashboardEvents {
2349
2977
  approval_resolved: ApprovalResolvedEvent;
2350
2978
  limit_warning: LimitWarningEvent;
2351
2979
  approval_notification_failed: ApprovalNotificationFailedEvent;
2980
+ budget_update: BudgetUpdateEvent;
2981
+ budget_breached: BudgetBreachedEvent;
2352
2982
  }
2353
2983
  /** Union of all dashboard event type names. */
2354
2984
  type DashboardEventType = keyof DashboardEvents;
@@ -2396,6 +3026,23 @@ interface DashboardAppDeps {
2396
3026
  readonly adapterLiveness?: {
2397
3027
  listAdapters(): AdapterLivenessEntry[];
2398
3028
  };
3029
+ /**
3030
+ * Budget read surface for `GET /api/budgets`,
3031
+ * `GET /api/budgets/:name/events`, and
3032
+ * `GET /api/budgets/:name/events/export` (issues #14, #155) — narrow
3033
+ * views of the BudgetEngine (live pot states) and BudgetLedger (spend
3034
+ * history). Optional on the adapterLiveness pattern: absent (direct
3035
+ * embedders), the listings serve empty lists and the export serves an
3036
+ * empty artifact, all with 200.
3037
+ */
3038
+ readonly budgets?: {
3039
+ listStates(): BudgetState[];
3040
+ listEvents(name: string, page: {
3041
+ limit: number;
3042
+ offset: number;
3043
+ }): BudgetEventsPage;
3044
+ listEventsForExport(name: string, limit: number): BudgetEventsPage;
3045
+ };
2399
3046
  }
2400
3047
  /** Options for the dashboard API. */
2401
3048
  interface DashboardAppOptions {
@@ -2414,4 +3061,4 @@ interface DashboardAppOptions {
2414
3061
  */
2415
3062
  declare function createDashboardApp(deps: DashboardAppDeps, options?: DashboardAppOptions): Hono;
2416
3063
 
2417
- export { type AdapterLivenessEntry, type ApprovalAppOptions, type ApprovalChannel, type ApprovalOutcome, ApprovalQueue, type ApprovalQueueOptions, ApprovalRouter, type ApprovalRouterOptions, type ApprovalStatus, type ApprovalTicket, type AuditAggregateStats, type AuditInput, type AuditListResult, type AuditPaginationOptions, type AuditQueryFilters, type AuditRecord, AuditStore, type AuditStoreOptions, type AuditTimeBucket, AuditWriter, type AuditWriterOptions, type CompilePoliciesResult, type CompiledPolicy, type CompiledPolicyRule, ConfigError, type CreateAppOptions, type DashboardAppDeps, type DashboardAppOptions, DashboardEventBus, type DashboardEventType, type DashboardEvents, EXPORT_MAX_RECORDS, type EvaluateInput, type EvidenceEntry, EvidenceStore, type EvidenceStoreOptions, GovernanceConfigError, GovernanceService, type GovernanceServiceOptions, GovernedForwarder, type GovernedForwarderOptions, type HelioConfig, type InstallScanInput, LIST_MAX_PAGE_SIZE, type MatchContext, type PolicyDecision, PolicyParseError, QueueChannel, type RateLimitCheckParams, type RateLimitKeyState, type RateLimitResult, RateLimiter, type RateLimiterOptions, type ResolveApprovalInput, type ServerHandle, type SessionState, type SlackActionAppOptions, SlackChannel, type SlackChannelOptions, type SpendLimitCheckParams, type SpendLimitKeyState, type SpendLimitResult, SpendLimiter, type SpendLimiterOptions, SseUpstreamForwarder, type SseUpstreamForwarderOptions, StdioForwarder, type StdioForwarderOptions, StreamableHttpForwarder, type StreamableHttpForwarderOptions, UpstreamForwarder, type UpstreamForwarderOptions, VERSION, WebhookChannel, type WebhookChannelOptions, type WireDecision, compilePolicies, createApp, createApprovalApp, createChannels, createDashboardApp, createSidebandApp, createSlackActionApp, evaluatePolicy, loadConfig, matchRule, startServer, startSidebandServer };
3064
+ export { type AdapterLivenessEntry, type ApprovalAppOptions, type ApprovalChannel, type ApprovalOutcome, ApprovalQueue, type ApprovalQueueOptions, ApprovalRouter, type ApprovalRouterOptions, type ApprovalStatus, type ApprovalTicket, type AuditAggregateStats, type AuditInput, type AuditListResult, type AuditPaginationOptions, type AuditQueryFilters, type AuditRecord, AuditStore, type AuditStoreOptions, type AuditTimeBucket, AuditWriter, type AuditWriterOptions, type BudgetBreachContext, type BudgetBreachEvent, type BudgetBucketState, type BudgetCommitEvent, BudgetEngine, type BudgetEventRecord, type BudgetEventsPage, type BudgetLedgerRow, type BudgetLedgerSink, BudgetParseError, type BudgetState, type CompilePoliciesResult, type CompiledBudget, type CompiledBudgetContributor, type CompiledBudgetWindow, type CompiledPolicy, type CompiledPolicyRule, ConfigError, type CreateAppOptions, type DashboardAppDeps, type DashboardAppOptions, DashboardEventBus, type DashboardEventType, type DashboardEvents, EXPORT_MAX_RECORDS, type EvaluateInput, type EvidenceEntry, EvidenceStore, type EvidenceStoreOptions, GovernanceConfigError, GovernanceService, type GovernanceServiceOptions, GovernedForwarder, type GovernedForwarderOptions, type HelioConfig, type InstallScanInput, LIST_MAX_PAGE_SIZE, type MatchContext, type PolicyDecision, PolicyParseError, QueueChannel, type RateLimitCheckParams, type RateLimitKeyState, type RateLimitResult, RateLimiter, type RateLimiterOptions, type ResolveApprovalInput, type ServerHandle, type SessionState, type SlackActionAppOptions, SlackChannel, type SlackChannelOptions, type SpendLimitCheckParams, type SpendLimitKeyState, type SpendLimitResult, SpendLimiter, type SpendLimiterOptions, SseUpstreamForwarder, type SseUpstreamForwarderOptions, StdioForwarder, type StdioForwarderOptions, StreamableHttpForwarder, type StreamableHttpForwarderOptions, UpstreamForwarder, type UpstreamForwarderOptions, VERSION, WebhookChannel, type WebhookChannelOptions, type WireDecision, compileBudgets, compilePolicies, createApp, createApprovalApp, createChannels, createDashboardApp, createSidebandApp, createSlackActionApp, evaluatePolicy, loadConfig, matchRule, startServer, startSidebandServer };