@gethelio/proxy 0.3.0 → 0.5.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
@@ -36,6 +36,12 @@ declare const policiesSchema: z.ZodObject<{
36
36
  regex: z.ZodOptional<z.ZodString>;
37
37
  }, z.core.$strict>>>;
38
38
  environment: z.ZodOptional<z.ZodString>;
39
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
40
+ eq: z.ZodOptional<z.ZodString>;
41
+ neq: z.ZodOptional<z.ZodString>;
42
+ contains: z.ZodOptional<z.ZodString>;
43
+ regex: z.ZodOptional<z.ZodString>;
44
+ }, z.core.$strict>]>>>;
39
45
  }, z.core.$strict>;
40
46
  action: z.ZodEnum<{
41
47
  allow: "allow";
@@ -63,6 +69,7 @@ declare const policiesSchema: z.ZodObject<{
63
69
  tool: "tool";
64
70
  agent: "agent";
65
71
  session: "session";
72
+ sender_id: "sender_id";
66
73
  }>>;
67
74
  max_spend: z.ZodOptional<z.ZodObject<{
68
75
  field: z.ZodString;
@@ -73,6 +80,7 @@ declare const policiesSchema: z.ZodObject<{
73
80
  tool: "tool";
74
81
  agent: "agent";
75
82
  session: "session";
83
+ sender_id: "sender_id";
76
84
  }>>;
77
85
  }, z.core.$strict>>;
78
86
  }, z.core.$strict>>;
@@ -81,6 +89,38 @@ declare const policiesSchema: z.ZodObject<{
81
89
  suggestion: z.ZodOptional<z.ZodString>;
82
90
  }, z.core.$strict>>;
83
91
  }, z.core.$strict>>>;
92
+ install: z.ZodOptional<z.ZodObject<{
93
+ default: z.ZodDefault<z.ZodEnum<{
94
+ allow: "allow";
95
+ deny: "deny";
96
+ }>>;
97
+ rules: z.ZodDefault<z.ZodArray<z.ZodObject<{
98
+ name: z.ZodOptional<z.ZodString>;
99
+ match: z.ZodObject<{
100
+ name: z.ZodOptional<z.ZodString>;
101
+ source: z.ZodOptional<z.ZodString>;
102
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
103
+ eq: z.ZodOptional<z.ZodString>;
104
+ neq: z.ZodOptional<z.ZodString>;
105
+ contains: z.ZodOptional<z.ZodString>;
106
+ regex: z.ZodOptional<z.ZodString>;
107
+ }, z.core.$strict>]>>>;
108
+ }, z.core.$strict>;
109
+ action: z.ZodEnum<{
110
+ allow: "allow";
111
+ deny_install: "deny_install";
112
+ }>;
113
+ feedback: z.ZodOptional<z.ZodObject<{
114
+ message: z.ZodString;
115
+ suggestion: z.ZodOptional<z.ZodString>;
116
+ }, z.core.$strict>>;
117
+ }, z.core.$strict>>>;
118
+ }, z.core.$strict>>;
119
+ on_tool_drift: z.ZodOptional<z.ZodEnum<{
120
+ require_approval: "require_approval";
121
+ log: "log";
122
+ block: "block";
123
+ }>>;
84
124
  hot_reload: z.ZodOptional<z.ZodBoolean>;
85
125
  }, z.core.$strict>;
86
126
  declare const approvalChannelSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -159,6 +199,12 @@ declare const helioConfigSchema: z.ZodObject<{
159
199
  regex: z.ZodOptional<z.ZodString>;
160
200
  }, z.core.$strict>>>;
161
201
  environment: z.ZodOptional<z.ZodString>;
202
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
203
+ eq: z.ZodOptional<z.ZodString>;
204
+ neq: z.ZodOptional<z.ZodString>;
205
+ contains: z.ZodOptional<z.ZodString>;
206
+ regex: z.ZodOptional<z.ZodString>;
207
+ }, z.core.$strict>]>>>;
162
208
  }, z.core.$strict>;
163
209
  action: z.ZodEnum<{
164
210
  allow: "allow";
@@ -186,6 +232,7 @@ declare const helioConfigSchema: z.ZodObject<{
186
232
  tool: "tool";
187
233
  agent: "agent";
188
234
  session: "session";
235
+ sender_id: "sender_id";
189
236
  }>>;
190
237
  max_spend: z.ZodOptional<z.ZodObject<{
191
238
  field: z.ZodString;
@@ -196,6 +243,7 @@ declare const helioConfigSchema: z.ZodObject<{
196
243
  tool: "tool";
197
244
  agent: "agent";
198
245
  session: "session";
246
+ sender_id: "sender_id";
199
247
  }>>;
200
248
  }, z.core.$strict>>;
201
249
  }, z.core.$strict>>;
@@ -204,6 +252,38 @@ declare const helioConfigSchema: z.ZodObject<{
204
252
  suggestion: z.ZodOptional<z.ZodString>;
205
253
  }, z.core.$strict>>;
206
254
  }, z.core.$strict>>>;
255
+ install: z.ZodOptional<z.ZodObject<{
256
+ default: z.ZodDefault<z.ZodEnum<{
257
+ allow: "allow";
258
+ deny: "deny";
259
+ }>>;
260
+ rules: z.ZodDefault<z.ZodArray<z.ZodObject<{
261
+ name: z.ZodOptional<z.ZodString>;
262
+ match: z.ZodObject<{
263
+ name: z.ZodOptional<z.ZodString>;
264
+ source: z.ZodOptional<z.ZodString>;
265
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
266
+ eq: z.ZodOptional<z.ZodString>;
267
+ neq: z.ZodOptional<z.ZodString>;
268
+ contains: z.ZodOptional<z.ZodString>;
269
+ regex: z.ZodOptional<z.ZodString>;
270
+ }, z.core.$strict>]>>>;
271
+ }, z.core.$strict>;
272
+ action: z.ZodEnum<{
273
+ allow: "allow";
274
+ deny_install: "deny_install";
275
+ }>;
276
+ feedback: z.ZodOptional<z.ZodObject<{
277
+ message: z.ZodString;
278
+ suggestion: z.ZodOptional<z.ZodString>;
279
+ }, z.core.$strict>>;
280
+ }, z.core.$strict>>>;
281
+ }, z.core.$strict>>;
282
+ on_tool_drift: z.ZodOptional<z.ZodEnum<{
283
+ require_approval: "require_approval";
284
+ log: "log";
285
+ block: "block";
286
+ }>>;
207
287
  hot_reload: z.ZodOptional<z.ZodBoolean>;
208
288
  }, z.core.$strict>>;
209
289
  approval: z.ZodPrefault<z.ZodObject<{
@@ -240,6 +320,7 @@ declare const helioConfigSchema: z.ZodObject<{
240
320
  enabled: z.ZodDefault<z.ZodBoolean>;
241
321
  port: z.ZodDefault<z.ZodNumber>;
242
322
  host: z.ZodDefault<z.ZodString>;
323
+ evaluation_ttl: z.ZodDefault<z.ZodString>;
243
324
  }, z.core.$strip>>;
244
325
  }, z.core.$strip>;
245
326
  /** Fully validated and defaulted Helio configuration. */
@@ -303,6 +384,22 @@ interface InputCondition {
303
384
  /** Pre-compiled RegExp when operator is 'regex'. */
304
385
  readonly regex?: RegExp;
305
386
  }
387
+ /**
388
+ * A single flattened metadata condition (issue #13 — `match.metadata.*`).
389
+ *
390
+ * Mirrors InputCondition but matches a flat top-level key of the adapter-supplied
391
+ * context object (no JSONPath traversal), and is restricted to the string-friendly
392
+ * operator subset — metadata values (channel_id, sender_id, …) are strings, so the
393
+ * numeric comparators are deliberately excluded.
394
+ */
395
+ interface MetadataCondition {
396
+ /** The metadata key to read (e.g. "channel_id", "sender_id", or virtual "agent_id"). */
397
+ readonly key: string;
398
+ readonly operator: 'eq' | 'neq' | 'contains' | 'regex';
399
+ readonly value: unknown;
400
+ /** Pre-compiled RegExp when operator is 'regex'. */
401
+ readonly regex?: RegExp;
402
+ }
306
403
  /** Compiled match block for a policy rule. */
307
404
  interface CompiledMatch {
308
405
  readonly tool?: ToolMatcher;
@@ -310,6 +407,8 @@ interface CompiledMatch {
310
407
  /** Flattened list of input conditions (one entry per path+operator pair). */
311
408
  readonly input?: readonly InputCondition[];
312
409
  readonly environment?: string;
410
+ /** Flattened list of metadata conditions (one entry per key+operator pair). */
411
+ readonly metadata?: readonly MetadataCondition[];
313
412
  }
314
413
  /** Compiled approval configuration with durations as milliseconds. */
315
414
  interface CompiledApproval {
@@ -324,13 +423,13 @@ interface CompiledSpendLimit {
324
423
  readonly limit: number;
325
424
  readonly currency: string;
326
425
  readonly windowMs: number;
327
- readonly key?: 'tool' | 'agent' | 'session';
426
+ readonly key?: 'tool' | 'agent' | 'session' | 'sender_id';
328
427
  }
329
428
  /** Compiled rate/spend limit configuration. */
330
429
  interface CompiledLimits {
331
430
  readonly maxCalls?: number;
332
431
  readonly windowMs?: number;
333
- readonly key?: 'tool' | 'agent' | 'session';
432
+ readonly key?: 'tool' | 'agent' | 'session' | 'sender_id';
334
433
  readonly maxSpend?: CompiledSpendLimit;
335
434
  }
336
435
  /** Policy action types. */
@@ -361,12 +460,44 @@ interface CompiledPolicyRule {
361
460
  readonly suggestion?: string;
362
461
  };
363
462
  }
463
+ /** Compiled match block for an install-time rule (issue #13). */
464
+ interface CompiledInstallMatch {
465
+ /** Glob matcher on the package name. */
466
+ readonly name?: ToolMatcher;
467
+ /** Exact ecosystem/source match (npm | pip | …). */
468
+ readonly source?: string;
469
+ /** Flattened metadata conditions (sender/channel-gated installs). */
470
+ readonly metadata?: readonly MetadataCondition[];
471
+ }
472
+ /** A compiled install-time rule. */
473
+ interface CompiledInstallRule {
474
+ readonly index: number;
475
+ readonly name?: string;
476
+ readonly match: CompiledInstallMatch;
477
+ readonly action: 'deny_install' | 'allow';
478
+ readonly feedback?: {
479
+ readonly message: string;
480
+ readonly suggestion?: string;
481
+ };
482
+ }
483
+ /** Compiled install-time policy (issue #13). */
484
+ interface CompiledInstallPolicy {
485
+ readonly defaultAction: 'allow' | 'deny';
486
+ readonly rules: readonly CompiledInstallRule[];
487
+ }
364
488
  /** Top-level compiled policy — what the engine consumes. */
365
489
  interface CompiledPolicy {
366
490
  readonly defaultAction: 'allow' | 'deny';
367
491
  readonly flagDestructive?: 'log' | 'require_approval';
368
492
  readonly dryRun?: boolean;
493
+ /**
494
+ * Response to tool definition drift (issue #25). Undefined means "block"
495
+ * at the use site — conservative by default.
496
+ */
497
+ readonly onToolDrift?: 'block' | 'require_approval' | 'log';
369
498
  readonly rules: readonly CompiledPolicyRule[];
499
+ /** Install-time policy (issue #13 — deny_install). Undefined ⇒ observational. */
500
+ readonly install?: CompiledInstallPolicy;
370
501
  }
371
502
  /** A non-fatal warning produced during policy compilation. */
372
503
  interface PolicyParseWarning {
@@ -406,6 +537,13 @@ interface MatchContext {
406
537
  readonly toolArguments?: Readonly<Record<string, unknown>>;
407
538
  /** The configured environment label (e.g. "production", "staging"). */
408
539
  readonly environment?: string;
540
+ /**
541
+ * Adapter-supplied context for `match.metadata.*` (issue #13). Present only on
542
+ * the sideband (host-enforced) path; always absent on the MCP path, so metadata
543
+ * rules are inert there by construction. The virtual `agent_id` key (from the
544
+ * request column) is merged in by the decision pipeline, not stored here twice.
545
+ */
546
+ readonly metadata?: Readonly<Record<string, unknown>>;
409
547
  }
410
548
 
411
549
  /** A JSON-RPC 2.0 request object. */
@@ -722,6 +860,27 @@ interface AuditRecord {
722
860
  readonly flagged_destructive: boolean;
723
861
  /** Whether this record was produced in dry-run mode. */
724
862
  readonly dry_run: boolean;
863
+ /**
864
+ * Record category discriminator (issues #12/#16). `'tool_call'` is the
865
+ * default for governed tool calls; `'drift_event'` for tool-definition drift
866
+ * records; `'install_scan'` for sideband install evaluations; and
867
+ * `'evaluation_expired'` for sideband evaluations whose `/audit` never
868
+ * arrived (the bypass/tamper signal — block_reason stays null so they do not
869
+ * count as enforcement blocks).
870
+ */
871
+ readonly record_kind: 'tool_call' | 'drift_event' | 'install_scan' | 'evaluation_expired';
872
+ /**
873
+ * Enforcement origin: `'mcp'` for the proxy path, or an adapter-supplied
874
+ * origin string (e.g. `'openclaw'`) for sideband-governed calls. Surfaces
875
+ * the enforcement-grade ladder (structural vs host-enforced) per record.
876
+ */
877
+ readonly origin: string;
878
+ /**
879
+ * Adapter-supplied context object (reserved keys: `channel_id`, `sender_id`,
880
+ * `sender_name`, `conversation_id`). Null for MCP-origin records. Backs
881
+ * `match.metadata.*` (#13) and the dashboard metadata columns (#16).
882
+ */
883
+ readonly metadata: Record<string, unknown> | null;
725
884
  /** ISO 8601 timestamp of when the record was persisted. */
726
885
  readonly created_at: string;
727
886
  }
@@ -747,6 +906,14 @@ interface AuditQueryFilters {
747
906
  readonly flagged_destructive?: boolean;
748
907
  /** Include only dry-run records (true) or non-dry-run records (false). */
749
908
  readonly dry_run?: boolean;
909
+ /** Filter by record kind (tool_call / drift_event / install_scan / evaluation_expired). */
910
+ readonly record_kind?: string;
911
+ /** Filter by enforcement origin (e.g. 'mcp', 'openclaw'). */
912
+ readonly origin?: string;
913
+ /** Filter by metadata.channel_id (adapter-supplied; JSON-extracted, substring match). */
914
+ readonly channel_id?: string;
915
+ /** Filter by metadata.sender_id (adapter-supplied; JSON-extracted, substring match). */
916
+ readonly sender_id?: string;
750
917
  /** Include only records where upstream HTTP status is >= this value. */
751
918
  readonly upstream_status_min?: number;
752
919
  /** Include only records where upstream HTTP status is <= this value. */
@@ -917,7 +1084,7 @@ declare class AuditWriter {
917
1084
  * is scheduled. This keeps request-path latency bounded even under bursty
918
1085
  * write load.
919
1086
  */
920
- push(record: Omit<AuditRecord, 'id' | 'created_at'>): void;
1087
+ push(record: Omit<AuditRecord, 'id' | 'created_at'>, id?: string): void;
921
1088
  /**
922
1089
  * Push a record and schedule a high-priority async flush.
923
1090
  *
@@ -926,7 +1093,7 @@ declare class AuditWriter {
926
1093
  * A fatal-process crash still invokes the crash-drain hook, which calls
927
1094
  * `flush()` synchronously before exit.
928
1095
  */
929
- pushImmediate(record: Omit<AuditRecord, 'id' | 'created_at'>): void;
1096
+ pushImmediate(record: Omit<AuditRecord, 'id' | 'created_at'>, id?: string): void;
930
1097
  /**
931
1098
  * Schedule a flush on the next tick, coalescing multiple calls into one.
932
1099
  */
@@ -1110,7 +1277,14 @@ declare class EvidenceStore {
1110
1277
  }
1111
1278
 
1112
1279
  /** Possible states of an approval ticket. */
1113
- type ApprovalStatus = 'pending' | 'approved' | 'denied' | 'timeout' | 'break_glass' | 'client_disconnected' | 'shutdown_cancelled';
1280
+ type ApprovalStatus = 'pending' | 'approved' | 'denied' | 'timeout' | 'break_glass' | 'client_disconnected' | 'shutdown_cancelled'
1281
+ /**
1282
+ * The approver's native UI cancelled the request (e.g. an OpenClaw
1283
+ * `/approve` dialog dismissed without a decision). Distinct from
1284
+ * `client_disconnected`, which the router reserves for the requesting agent
1285
+ * aborting the held MCP request. Sideband (native) approvals only. (#12.)
1286
+ */
1287
+ | 'cancelled';
1114
1288
  /** A failed attempt to deliver an approval notification. */
1115
1289
  interface ApprovalNotificationFailure {
1116
1290
  readonly channel: string;
@@ -1233,7 +1407,7 @@ declare class ApprovalQueue {
1233
1407
  * @returns `true` if the ticket was resolved, `false` if not found or
1234
1408
  * already resolved (no double-resolution).
1235
1409
  */
1236
- resolve(id: string, status: 'approved' | 'denied' | 'timeout' | 'break_glass' | 'client_disconnected' | 'shutdown_cancelled', resolvedBy?: string, options?: {
1410
+ resolve(id: string, status: 'approved' | 'denied' | 'timeout' | 'break_glass' | 'client_disconnected' | 'shutdown_cancelled' | 'cancelled', resolvedBy?: string, options?: {
1237
1411
  denial_reason?: string;
1238
1412
  break_glass_reason?: string;
1239
1413
  }): boolean;
@@ -1276,6 +1450,19 @@ interface ApprovalSubmitParams {
1276
1450
  readonly matched_rule: CompiledPolicyRule | undefined;
1277
1451
  readonly session_id: string | null;
1278
1452
  }
1453
+ /** Resolution statuses a native (sideband) ticket can be moved to. */
1454
+ type NativeResolution = 'approved' | 'denied' | 'timeout' | 'cancelled';
1455
+ /** Parameters for creating a native (adapter-owned) approval ticket. */
1456
+ interface NativeTicketParams {
1457
+ readonly tool_name: string;
1458
+ readonly tool_input: Record<string, unknown>;
1459
+ readonly matched_rule: CompiledPolicyRule | undefined;
1460
+ readonly session_id: string | null;
1461
+ /** Adapter origin (e.g. "openclaw"); stored as channel_name `native:<origin>`. */
1462
+ readonly origin: string;
1463
+ /** Ticket timeout in ms (rule timeout, else the router default). */
1464
+ readonly timeout_ms?: number;
1465
+ }
1279
1466
  declare class ApprovalRouter {
1280
1467
  private readonly defaultTimeoutMs;
1281
1468
  readonly defaultOnTimeout: 'allow' | 'deny';
@@ -1296,6 +1483,36 @@ declare class ApprovalRouter {
1296
1483
  * is held open.
1297
1484
  */
1298
1485
  submit(params: ApprovalSubmitParams, abortSignal?: AbortSignal): Promise<ApprovalOutcome>;
1486
+ /**
1487
+ * Create a native (adapter-owned) approval ticket without holding a Promise.
1488
+ *
1489
+ * Used by the sideband governance path (issue #12, D10): when `/evaluate`
1490
+ * yields `require_approval`, the adapter runs the approval in its own UI
1491
+ * (e.g. OpenClaw's Telegram dialog), so Helio must NOT block, start
1492
+ * timeout/escalation timers, or notify a channel — doing so would
1493
+ * double-notify. We still create the queue ticket and fire `onSubmit` so the
1494
+ * dashboard's `approval_requested` SSE event flows and the ticket is visible.
1495
+ *
1496
+ * The ticket's `channel_name` is `native:<origin>`, which marks it as
1497
+ * adapter-owned: the dashboard approve/deny endpoints refuse it (it can only
1498
+ * be resolved through the adapter), and {@link resolveNativeTicket} is the
1499
+ * resolution path.
1500
+ */
1501
+ createNativeTicket(params: NativeTicketParams): ApprovalTicket;
1502
+ /**
1503
+ * Resolve a native ticket created by {@link createNativeTicket}.
1504
+ *
1505
+ * Resolves the queue ticket and fires `onResolve` (→ `approval_resolved`
1506
+ * SSE), with no held Promise to settle. Refuses tickets that have a pending
1507
+ * router Promise (those are MCP-path tickets; resolving them here would leave
1508
+ * the held request hanging) and tickets that are not `native:`-prefixed.
1509
+ *
1510
+ * @returns `true` if resolved, `false` if not found, already resolved, not a
1511
+ * native ticket, or router-managed.
1512
+ */
1513
+ resolveNativeTicket(ticketId: string, status: NativeResolution, resolvedBy?: string, options?: {
1514
+ denial_reason?: string;
1515
+ }): boolean;
1299
1516
  /**
1300
1517
  * Approve a pending ticket. Resolves the held Promise so the governed
1301
1518
  * forwarder can forward the request upstream.
@@ -1320,6 +1537,8 @@ declare class ApprovalRouter {
1320
1537
  * already resolved.
1321
1538
  */
1322
1539
  breakGlass(ticketId: string, resolvedBy: string, reason: string): boolean;
1540
+ /** Look up a ticket by id (delegates to the queue). */
1541
+ getTicket(ticketId: string): ApprovalTicket | undefined;
1323
1542
  /** Clean up all pending timers and resolve all pending promises. */
1324
1543
  close(): void;
1325
1544
  private finalizePending;
@@ -1390,6 +1609,21 @@ declare class RateLimiter {
1390
1609
  * - At/over limit: does NOT record (blocked calls don't consume a slot)
1391
1610
  */
1392
1611
  check(params: RateLimitCheckParams): RateLimitResult;
1612
+ /**
1613
+ * Unconditionally record a call against the rate limit.
1614
+ *
1615
+ * Unlike check(), this always appends the timestamp — even when the bucket
1616
+ * is already at/over the limit — because the call it represents has already
1617
+ * executed. The sideband splits decision from execution: /evaluate peeks
1618
+ * (non-destructive), and /audit calls record() once the external call ran,
1619
+ * so refusing to record at the limit (as check() does) would let real calls
1620
+ * escape accounting and under-count subsequent peeks. (issue #12, D3.)
1621
+ *
1622
+ * Warnings fire only while the post-append count stays within the limit —
1623
+ * exact parity with check(), which never warns on its over-limit path — so a
1624
+ * burst of over-limit audits cannot flood the dashboard's limit_warning feed.
1625
+ */
1626
+ record(params: RateLimitCheckParams): RateLimitResult;
1393
1627
  /**
1394
1628
  * Check the rate limit without recording the call (non-destructive).
1395
1629
  *
@@ -1499,6 +1733,20 @@ declare class SpendLimiter {
1499
1733
  * - Would exceed: does NOT record (rejected spends don't consume budget)
1500
1734
  */
1501
1735
  check(params: SpendLimitCheckParams): SpendLimitResult;
1736
+ /**
1737
+ * Unconditionally record a spend against the limit.
1738
+ *
1739
+ * Unlike check(), this always appends the amount — even when it pushes the
1740
+ * window past the limit — because the spend it represents has already been
1741
+ * incurred. The sideband peeks at /evaluate and commits here at /audit once
1742
+ * the external call ran (issue #12, D3).
1743
+ *
1744
+ * Throws on a negative or non-finite amount: such amounts are rejected at
1745
+ * /evaluate, so one reaching record() is a logic bug we surface loudly rather
1746
+ * than silently corrupt the sliding-window sum. Warnings fire only while the
1747
+ * post-append spend stays within the limit (parity with check()).
1748
+ */
1749
+ record(params: SpendLimitCheckParams): SpendLimitResult;
1502
1750
  /**
1503
1751
  * Check the spend limit without recording the spend (non-destructive).
1504
1752
  *
@@ -1587,6 +1835,7 @@ declare class GovernedForwarder implements McpForwarder {
1587
1835
  private readonly spendLimiter;
1588
1836
  private readonly annotationCache;
1589
1837
  private agentKeyWarned;
1838
+ private senderKeyWarned;
1590
1839
  constructor(inner: McpForwarder, policy: CompiledPolicy, options?: GovernedForwarderOptions);
1591
1840
  /**
1592
1841
  * Swap the compiled policy atomically and reconcile limit bucket state
@@ -1617,6 +1866,15 @@ declare class GovernedForwarder implements McpForwarder {
1617
1866
  */
1618
1867
  primeAnnotationCache(): Promise<AnnotationCachePrimeResult>;
1619
1868
  forward(request: McpRequest): Promise<ForwardResult>;
1869
+ /**
1870
+ * Apply a tools/list response to the definition cache and surface any
1871
+ * drift: console warning + immediate audit record per event. Single entry
1872
+ * point for both runtime tools/list responses and startup priming, so the
1873
+ * cache is updated exactly once per response.
1874
+ */
1875
+ private applyToolDefinitionUpdate;
1876
+ /** Write an immediate audit record for a drift event (not a tool call). */
1877
+ private writeDriftAuditRecord;
1620
1878
  private handleToolsCall;
1621
1879
  private handleApproval;
1622
1880
  private handleRateLimit;
@@ -1631,6 +1889,7 @@ declare class GovernedForwarder implements McpForwarder {
1631
1889
  /** Determine if the request was actually forwarded to the upstream MCP server. */
1632
1890
  private wasForwardedUpstream;
1633
1891
  private writeAuditRecord;
1892
+ private makeDriftBlockResult;
1634
1893
  private makeDenyResult;
1635
1894
  private makePolicyMisconfiguredResult;
1636
1895
  private makeUnsupportedResult;
@@ -1640,15 +1899,169 @@ declare class GovernedForwarder implements McpForwarder {
1640
1899
  private makeClientDisconnectedBlockResult;
1641
1900
  }
1642
1901
 
1902
+ /** The outcome vocabulary adapters branch on — never internal rule actions. */
1903
+ type WireDecision = 'allow' | 'deny' | 'require_approval' | 'rate_limited' | 'spend_limited' | 'dry_run';
1904
+ /** Tool definition carried by /evaluate (optional; enables the drift guard). */
1905
+ interface WireToolDefinition {
1906
+ readonly name: string;
1907
+ readonly description?: string;
1908
+ readonly input_schema?: unknown;
1909
+ readonly output_schema?: unknown;
1910
+ readonly title?: string;
1911
+ readonly annotations?: Record<string, unknown>;
1912
+ }
1913
+ interface EvaluateInput {
1914
+ readonly origin: string;
1915
+ readonly adapter_version?: string;
1916
+ readonly agent_id: string | null;
1917
+ readonly session_id: string | null;
1918
+ readonly tool: WireToolDefinition;
1919
+ readonly arguments: Record<string, unknown> | undefined;
1920
+ readonly metadata: Record<string, unknown> | null;
1921
+ }
1922
+ interface InstallScanInput {
1923
+ readonly origin: string;
1924
+ readonly agent_id: string | null;
1925
+ readonly session_id: string | null;
1926
+ readonly package: {
1927
+ readonly name: string;
1928
+ readonly version?: string;
1929
+ readonly source?: string;
1930
+ readonly spec?: string;
1931
+ readonly url?: string;
1932
+ };
1933
+ readonly metadata: Record<string, unknown> | null;
1934
+ }
1935
+ interface AuditInput {
1936
+ readonly evaluation_id: string;
1937
+ readonly status: 'success' | 'error' | 'not_executed';
1938
+ readonly error?: string;
1939
+ readonly duration_ms?: number;
1940
+ readonly result?: unknown;
1941
+ readonly actual_amount?: number;
1942
+ }
1943
+ interface ResolveApprovalInput {
1944
+ readonly resolution: 'approved' | 'denied' | 'timeout' | 'cancelled';
1945
+ readonly resolved_by?: string;
1946
+ readonly reason?: string;
1947
+ readonly scope?: 'once' | 'always';
1948
+ }
1949
+ /** A service result: HTTP status + JSON body, adapted verbatim by the route. */
1950
+ interface ServiceResult {
1951
+ readonly status: number;
1952
+ readonly body: Record<string, unknown>;
1953
+ }
1954
+ interface GovernanceServiceOptions {
1955
+ readonly policy: CompiledPolicy;
1956
+ readonly environment?: string;
1957
+ readonly evidenceStore?: EvidenceStore;
1958
+ readonly approvalRouter?: ApprovalRouter;
1959
+ readonly rateLimiter?: RateLimiter;
1960
+ readonly spendLimiter?: SpendLimiter;
1961
+ readonly auditWriter?: AuditWriter;
1962
+ /** Default approval timeout (ms) when a rule sets none. */
1963
+ readonly approvalTimeoutMs?: number;
1964
+ /** Pending-evaluation TTL (ms). Default 10 minutes. */
1965
+ readonly ttlMs?: number;
1966
+ /** Clock for testable time. Defaults to Date.now. */
1967
+ readonly now?: () => number;
1968
+ /** Sweep interval (ms). 0 disables the periodic GC backstop. Default 30s. */
1969
+ readonly sweepIntervalMs?: number;
1970
+ /** Max pending evaluations (count). Default 10,000. Overridable for tests. */
1971
+ readonly maxPending?: number;
1972
+ /** Max pending-evaluation footprint (serialized bytes). Default 64 MiB. */
1973
+ readonly maxPendingBytes?: number;
1974
+ /** Max distinct sender_id limit keys (issue #13). Default 50,000. Overridable for tests. */
1975
+ readonly maxSenderKeys?: number;
1976
+ }
1977
+ declare class GovernanceService {
1978
+ private policy;
1979
+ private readonly environment;
1980
+ private readonly evidenceStore;
1981
+ private readonly approvalRouter;
1982
+ private readonly rateLimiter;
1983
+ private readonly spendLimiter;
1984
+ private readonly auditWriter;
1985
+ private readonly approvalTimeoutMs;
1986
+ private readonly ttlMs;
1987
+ private readonly now;
1988
+ private readonly maxPending;
1989
+ private readonly maxPendingBytes;
1990
+ private readonly maxSenderKeys;
1991
+ /** Distinct sender_id limit keys with live state (reservation registry, issue #13). */
1992
+ private readonly senderKeys;
1993
+ private readonly pending;
1994
+ private readonly tombstones;
1995
+ private readonly caches;
1996
+ /** Native approval ticket id → its pending evaluation id, for on-access
1997
+ * deadline enforcement on the resolve path. */
1998
+ private readonly ticketToEvaluation;
1999
+ private pendingBytes;
2000
+ private sweepTimer;
2001
+ private closed;
2002
+ constructor(options: GovernanceServiceOptions);
2003
+ /** Swap the compiled policy on hot-reload (mirrors GovernedForwarder). */
2004
+ updatePolicy(policy: CompiledPolicy): void;
2005
+ evaluate(req: EvaluateInput): ServiceResult;
2006
+ audit(req: AuditInput, payloadHash: string): ServiceResult;
2007
+ installScan(req: InstallScanInput): ServiceResult;
2008
+ /** First-match-wins evaluation of the compiled install policy (issue #13). */
2009
+ private evaluateInstall;
2010
+ resolveApproval(ticketId: string, req: ResolveApprovalInput): ServiceResult;
2011
+ sweep(): void;
2012
+ /**
2013
+ * Reserve a cardinality slot for a sender-keyed limit (issue #13).
2014
+ *
2015
+ * Only `sender:*` keys are gated — tool/session families are bounded by upstream
2016
+ * cardinality, and the MCP path never reaches here, so structural traffic cannot
2017
+ * be starved. A key already backed by live state (registry or a live limiter
2018
+ * bucket) costs no new slot. At capacity we lazily prune dead keys before failing
2019
+ * closed, so an emptied bucket frees its slot without waiting for the sweep.
2020
+ */
2021
+ private reserveSenderKey;
2022
+ /** Drop registry keys with no pending evaluation AND no live limiter bucket. */
2023
+ private pruneSenderKeys;
2024
+ /**
2025
+ * Whether either limiter still holds a live bucket for `key`. Uses the public
2026
+ * `getKeyState()` — never the limiters' private maps — and its lazy eviction of
2027
+ * an emptied bucket IS the prune-on-touch mechanism.
2028
+ */
2029
+ private hasLiveBucket;
2030
+ close(): void;
2031
+ /** Apply crossed deadlines to one pending entry. Returns its post-state. */
2032
+ private enforceDeadlines;
2033
+ private cacheFor;
2034
+ private discardPending;
2035
+ private getTicketStatus;
2036
+ private planRate;
2037
+ private planSpend;
2038
+ /** Commit a limit plan at /audit time and return the evidence_chain block. */
2039
+ private commitLimit;
2040
+ private writeAudit;
2041
+ private assertApprovalRouter;
2042
+ }
2043
+
1643
2044
  /** Options for constructing the SDK sideband Hono app. */
1644
2045
  interface SidebandAppOptions {
1645
2046
  /**
1646
- * Bearer token that every request must carry in its `Authorization`
1647
- * header (except `GET /healthz`). When omitted or empty, the sideband
1648
- * runs open useful for local development when the operator has
1649
- * explicitly disabled the per-boot token via env.
2047
+ * Bearer token for the evidence/context/session routes (the SDK scope).
2048
+ * When omitted or empty, those routes run open — useful for local
2049
+ * development when the operator has disabled the per-boot token via env.
1650
2050
  */
1651
2051
  readonly token?: string;
2052
+ /**
2053
+ * Bearer token for the governance routes (the adapter scope, issue #12/F6).
2054
+ * Distinct from `token` so an SDK client cannot drive policy decisions and
2055
+ * an adapter cannot write evidence it was not granted. When omitted, the
2056
+ * governance routes run open (and only mount if `governance` is provided).
2057
+ */
2058
+ readonly adapterToken?: string;
2059
+ /**
2060
+ * The governance service backing `/evaluate`, `/audit`, `/install-scan`, and
2061
+ * `/approval/:id/resolve`. When omitted, those routes return 503
2062
+ * `governance_unavailable` (evidence-only deployments, and existing tests).
2063
+ */
2064
+ readonly governance?: GovernanceService;
1652
2065
  }
1653
2066
  /**
1654
2067
  * Create a Hono app for the SDK sideband API.
@@ -1673,6 +2086,20 @@ interface SidebandAppOptions {
1673
2086
  */
1674
2087
  declare function createSidebandApp(store: EvidenceStore, options?: SidebandAppOptions): Hono;
1675
2088
 
2089
+ /**
2090
+ * Error thrown when the GovernanceService is wired in a way that could fail
2091
+ * open — for example, an approval-capable policy with no ApprovalRouter to
2092
+ * route `require_approval` decisions through (issue #12). Surfaced at
2093
+ * construction and on hot-reload so a misconfiguration crashes loudly rather
2094
+ * than silently degrading an approval into an unenforced allow.
2095
+ *
2096
+ * Direct embedders of GovernanceService can catch this distinctly; the bundled
2097
+ * CLI always provides a router, so it never fires in the shipped path.
2098
+ */
2099
+ declare class GovernanceConfigError extends Error {
2100
+ constructor(message: string);
2101
+ }
2102
+
1676
2103
  /** @internal Exported for testing only. */
1677
2104
  declare class QueueChannel implements ApprovalChannel {
1678
2105
  readonly type = "dashboard";
@@ -1809,6 +2236,8 @@ interface ActionEvent {
1809
2236
  readonly dry_run: boolean;
1810
2237
  readonly matched_rule: string | null;
1811
2238
  readonly matched_rule_index: number | null;
2239
+ readonly record_kind: AuditRecord['record_kind'];
2240
+ readonly origin: string;
1812
2241
  }
1813
2242
  /** Payload for an approval_requested event. */
1814
2243
  interface ApprovalRequestedEvent {
@@ -1903,4 +2332,4 @@ interface DashboardAppOptions {
1903
2332
  */
1904
2333
  declare function createDashboardApp(deps: DashboardAppDeps, options?: DashboardAppOptions): Hono;
1905
2334
 
1906
- export { type ApprovalAppOptions, type ApprovalChannel, type ApprovalOutcome, ApprovalQueue, type ApprovalQueueOptions, ApprovalRouter, type ApprovalRouterOptions, type ApprovalStatus, type ApprovalTicket, type AuditAggregateStats, 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, type EvidenceEntry, EvidenceStore, type EvidenceStoreOptions, GovernedForwarder, type GovernedForwarderOptions, type HelioConfig, type MatchContext, type PolicyDecision, PolicyParseError, QueueChannel, type RateLimitCheckParams, type RateLimitKeyState, type RateLimitResult, RateLimiter, type RateLimiterOptions, 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, compilePolicies, createApp, createApprovalApp, createChannels, createDashboardApp, createSidebandApp, createSlackActionApp, evaluatePolicy, loadConfig, matchRule, startServer, startSidebandServer };
2335
+ export { 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, type EvaluateInput, type EvidenceEntry, EvidenceStore, type EvidenceStoreOptions, GovernanceConfigError, GovernanceService, type GovernanceServiceOptions, GovernedForwarder, type GovernedForwarderOptions, type HelioConfig, type InstallScanInput, 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 };