@gethelio/proxy 0.4.0 → 0.6.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.
@@ -6,8 +6,8 @@
6
6
  <meta name="referrer" content="no-referrer" />
7
7
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
8
8
  <title>Helio Dashboard</title>
9
- <script type="module" crossorigin src="/assets/index-CAgnN6wV.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-DG3h7Cvn.css">
9
+ <script type="module" crossorigin src="/assets/index-DgywE2WQ.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-DZKoV0Vx.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
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,33 @@ 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>>;
84
119
  on_tool_drift: z.ZodOptional<z.ZodEnum<{
85
120
  require_approval: "require_approval";
86
121
  log: "log";
@@ -164,6 +199,12 @@ declare const helioConfigSchema: z.ZodObject<{
164
199
  regex: z.ZodOptional<z.ZodString>;
165
200
  }, z.core.$strict>>>;
166
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>]>>>;
167
208
  }, z.core.$strict>;
168
209
  action: z.ZodEnum<{
169
210
  allow: "allow";
@@ -191,6 +232,7 @@ declare const helioConfigSchema: z.ZodObject<{
191
232
  tool: "tool";
192
233
  agent: "agent";
193
234
  session: "session";
235
+ sender_id: "sender_id";
194
236
  }>>;
195
237
  max_spend: z.ZodOptional<z.ZodObject<{
196
238
  field: z.ZodString;
@@ -201,6 +243,7 @@ declare const helioConfigSchema: z.ZodObject<{
201
243
  tool: "tool";
202
244
  agent: "agent";
203
245
  session: "session";
246
+ sender_id: "sender_id";
204
247
  }>>;
205
248
  }, z.core.$strict>>;
206
249
  }, z.core.$strict>>;
@@ -209,6 +252,33 @@ declare const helioConfigSchema: z.ZodObject<{
209
252
  suggestion: z.ZodOptional<z.ZodString>;
210
253
  }, z.core.$strict>>;
211
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>>;
212
282
  on_tool_drift: z.ZodOptional<z.ZodEnum<{
213
283
  require_approval: "require_approval";
214
284
  log: "log";
@@ -250,6 +320,7 @@ declare const helioConfigSchema: z.ZodObject<{
250
320
  enabled: z.ZodDefault<z.ZodBoolean>;
251
321
  port: z.ZodDefault<z.ZodNumber>;
252
322
  host: z.ZodDefault<z.ZodString>;
323
+ evaluation_ttl: z.ZodDefault<z.ZodString>;
253
324
  }, z.core.$strip>>;
254
325
  }, z.core.$strip>;
255
326
  /** Fully validated and defaulted Helio configuration. */
@@ -313,6 +384,22 @@ interface InputCondition {
313
384
  /** Pre-compiled RegExp when operator is 'regex'. */
314
385
  readonly regex?: RegExp;
315
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
+ }
316
403
  /** Compiled match block for a policy rule. */
317
404
  interface CompiledMatch {
318
405
  readonly tool?: ToolMatcher;
@@ -320,6 +407,8 @@ interface CompiledMatch {
320
407
  /** Flattened list of input conditions (one entry per path+operator pair). */
321
408
  readonly input?: readonly InputCondition[];
322
409
  readonly environment?: string;
410
+ /** Flattened list of metadata conditions (one entry per key+operator pair). */
411
+ readonly metadata?: readonly MetadataCondition[];
323
412
  }
324
413
  /** Compiled approval configuration with durations as milliseconds. */
325
414
  interface CompiledApproval {
@@ -334,13 +423,13 @@ interface CompiledSpendLimit {
334
423
  readonly limit: number;
335
424
  readonly currency: string;
336
425
  readonly windowMs: number;
337
- readonly key?: 'tool' | 'agent' | 'session';
426
+ readonly key?: 'tool' | 'agent' | 'session' | 'sender_id';
338
427
  }
339
428
  /** Compiled rate/spend limit configuration. */
340
429
  interface CompiledLimits {
341
430
  readonly maxCalls?: number;
342
431
  readonly windowMs?: number;
343
- readonly key?: 'tool' | 'agent' | 'session';
432
+ readonly key?: 'tool' | 'agent' | 'session' | 'sender_id';
344
433
  readonly maxSpend?: CompiledSpendLimit;
345
434
  }
346
435
  /** Policy action types. */
@@ -371,6 +460,31 @@ interface CompiledPolicyRule {
371
460
  readonly suggestion?: string;
372
461
  };
373
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
+ }
374
488
  /** Top-level compiled policy — what the engine consumes. */
375
489
  interface CompiledPolicy {
376
490
  readonly defaultAction: 'allow' | 'deny';
@@ -382,6 +496,8 @@ interface CompiledPolicy {
382
496
  */
383
497
  readonly onToolDrift?: 'block' | 'require_approval' | 'log';
384
498
  readonly rules: readonly CompiledPolicyRule[];
499
+ /** Install-time policy (issue #13 — deny_install). Undefined ⇒ observational. */
500
+ readonly install?: CompiledInstallPolicy;
385
501
  }
386
502
  /** A non-fatal warning produced during policy compilation. */
387
503
  interface PolicyParseWarning {
@@ -421,6 +537,13 @@ interface MatchContext {
421
537
  readonly toolArguments?: Readonly<Record<string, unknown>>;
422
538
  /** The configured environment label (e.g. "production", "staging"). */
423
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>>;
424
547
  }
425
548
 
426
549
  /** A JSON-RPC 2.0 request object. */
@@ -737,6 +860,27 @@ interface AuditRecord {
737
860
  readonly flagged_destructive: boolean;
738
861
  /** Whether this record was produced in dry-run mode. */
739
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;
740
884
  /** ISO 8601 timestamp of when the record was persisted. */
741
885
  readonly created_at: string;
742
886
  }
@@ -762,6 +906,14 @@ interface AuditQueryFilters {
762
906
  readonly flagged_destructive?: boolean;
763
907
  /** Include only dry-run records (true) or non-dry-run records (false). */
764
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;
765
917
  /** Include only records where upstream HTTP status is >= this value. */
766
918
  readonly upstream_status_min?: number;
767
919
  /** Include only records where upstream HTTP status is <= this value. */
@@ -932,7 +1084,7 @@ declare class AuditWriter {
932
1084
  * is scheduled. This keeps request-path latency bounded even under bursty
933
1085
  * write load.
934
1086
  */
935
- push(record: Omit<AuditRecord, 'id' | 'created_at'>): void;
1087
+ push(record: Omit<AuditRecord, 'id' | 'created_at'>, id?: string): void;
936
1088
  /**
937
1089
  * Push a record and schedule a high-priority async flush.
938
1090
  *
@@ -941,7 +1093,7 @@ declare class AuditWriter {
941
1093
  * A fatal-process crash still invokes the crash-drain hook, which calls
942
1094
  * `flush()` synchronously before exit.
943
1095
  */
944
- pushImmediate(record: Omit<AuditRecord, 'id' | 'created_at'>): void;
1096
+ pushImmediate(record: Omit<AuditRecord, 'id' | 'created_at'>, id?: string): void;
945
1097
  /**
946
1098
  * Schedule a flush on the next tick, coalescing multiple calls into one.
947
1099
  */
@@ -1125,7 +1277,14 @@ declare class EvidenceStore {
1125
1277
  }
1126
1278
 
1127
1279
  /** Possible states of an approval ticket. */
1128
- 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';
1129
1288
  /** A failed attempt to deliver an approval notification. */
1130
1289
  interface ApprovalNotificationFailure {
1131
1290
  readonly channel: string;
@@ -1248,7 +1407,7 @@ declare class ApprovalQueue {
1248
1407
  * @returns `true` if the ticket was resolved, `false` if not found or
1249
1408
  * already resolved (no double-resolution).
1250
1409
  */
1251
- 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?: {
1252
1411
  denial_reason?: string;
1253
1412
  break_glass_reason?: string;
1254
1413
  }): boolean;
@@ -1291,6 +1450,19 @@ interface ApprovalSubmitParams {
1291
1450
  readonly matched_rule: CompiledPolicyRule | undefined;
1292
1451
  readonly session_id: string | null;
1293
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
+ }
1294
1466
  declare class ApprovalRouter {
1295
1467
  private readonly defaultTimeoutMs;
1296
1468
  readonly defaultOnTimeout: 'allow' | 'deny';
@@ -1311,6 +1483,36 @@ declare class ApprovalRouter {
1311
1483
  * is held open.
1312
1484
  */
1313
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;
1314
1516
  /**
1315
1517
  * Approve a pending ticket. Resolves the held Promise so the governed
1316
1518
  * forwarder can forward the request upstream.
@@ -1335,6 +1537,8 @@ declare class ApprovalRouter {
1335
1537
  * already resolved.
1336
1538
  */
1337
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;
1338
1542
  /** Clean up all pending timers and resolve all pending promises. */
1339
1543
  close(): void;
1340
1544
  private finalizePending;
@@ -1405,6 +1609,21 @@ declare class RateLimiter {
1405
1609
  * - At/over limit: does NOT record (blocked calls don't consume a slot)
1406
1610
  */
1407
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;
1408
1627
  /**
1409
1628
  * Check the rate limit without recording the call (non-destructive).
1410
1629
  *
@@ -1514,6 +1733,20 @@ declare class SpendLimiter {
1514
1733
  * - Would exceed: does NOT record (rejected spends don't consume budget)
1515
1734
  */
1516
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;
1517
1750
  /**
1518
1751
  * Check the spend limit without recording the spend (non-destructive).
1519
1752
  *
@@ -1602,6 +1835,7 @@ declare class GovernedForwarder implements McpForwarder {
1602
1835
  private readonly spendLimiter;
1603
1836
  private readonly annotationCache;
1604
1837
  private agentKeyWarned;
1838
+ private senderKeyWarned;
1605
1839
  constructor(inner: McpForwarder, policy: CompiledPolicy, options?: GovernedForwarderOptions);
1606
1840
  /**
1607
1841
  * Swap the compiled policy atomically and reconcile limit bucket state
@@ -1665,15 +1899,193 @@ declare class GovernedForwarder implements McpForwarder {
1665
1899
  private makeClientDisconnectedBlockResult;
1666
1900
  }
1667
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 AuditEvidenceInput {
1936
+ readonly evidence_key: string;
1937
+ readonly evidence_data: unknown;
1938
+ readonly ttl_seconds?: number;
1939
+ }
1940
+ interface AuditInput {
1941
+ readonly evaluation_id: string;
1942
+ readonly status: 'success' | 'error' | 'not_executed';
1943
+ readonly error?: string;
1944
+ readonly duration_ms?: number;
1945
+ readonly result?: unknown;
1946
+ readonly actual_amount?: number;
1947
+ /**
1948
+ * Optional evidence to populate on a successfully-audited call (issue #11). Adapter-scoped, single-token evidence write: bound to the pending
1949
+ * evaluation's session/tool, success-only, first-finalize-only. Every
1950
+ * per-entry failure is soft (reported, never request-fatal) — see audit().
1951
+ */
1952
+ readonly evidence?: ReadonlyArray<AuditEvidenceInput>;
1953
+ }
1954
+ interface ResolveApprovalInput {
1955
+ readonly resolution: 'approved' | 'denied' | 'timeout' | 'cancelled';
1956
+ readonly resolved_by?: string;
1957
+ readonly reason?: string;
1958
+ readonly scope?: 'once' | 'always';
1959
+ }
1960
+ /** A service result: HTTP status + JSON body, adapted verbatim by the route. */
1961
+ interface ServiceResult {
1962
+ readonly status: number;
1963
+ readonly body: Record<string, unknown>;
1964
+ }
1965
+ interface GovernanceServiceOptions {
1966
+ readonly policy: CompiledPolicy;
1967
+ readonly environment?: string;
1968
+ readonly evidenceStore?: EvidenceStore;
1969
+ readonly approvalRouter?: ApprovalRouter;
1970
+ readonly rateLimiter?: RateLimiter;
1971
+ readonly spendLimiter?: SpendLimiter;
1972
+ readonly auditWriter?: AuditWriter;
1973
+ /** Default approval timeout (ms) when a rule sets none. */
1974
+ readonly approvalTimeoutMs?: number;
1975
+ /** Pending-evaluation TTL (ms). Default 10 minutes. */
1976
+ readonly ttlMs?: number;
1977
+ /** Clock for testable time. Defaults to Date.now. */
1978
+ readonly now?: () => number;
1979
+ /** Sweep interval (ms). 0 disables the periodic GC backstop. Default 30s. */
1980
+ readonly sweepIntervalMs?: number;
1981
+ /** Max pending evaluations (count). Default 10,000. Overridable for tests. */
1982
+ readonly maxPending?: number;
1983
+ /** Max pending-evaluation footprint (serialized bytes). Default 64 MiB. */
1984
+ readonly maxPendingBytes?: number;
1985
+ /** Max distinct sender_id limit keys (issue #13). Default 50,000. Overridable for tests. */
1986
+ readonly maxSenderKeys?: number;
1987
+ }
1988
+ declare class GovernanceService {
1989
+ private policy;
1990
+ private readonly environment;
1991
+ private readonly evidenceStore;
1992
+ private readonly approvalRouter;
1993
+ private readonly rateLimiter;
1994
+ private readonly spendLimiter;
1995
+ private readonly auditWriter;
1996
+ private readonly approvalTimeoutMs;
1997
+ private readonly ttlMs;
1998
+ private readonly now;
1999
+ private readonly maxPending;
2000
+ private readonly maxPendingBytes;
2001
+ private readonly maxSenderKeys;
2002
+ /** Distinct sender_id limit keys with live state (reservation registry, issue #13). */
2003
+ private readonly senderKeys;
2004
+ private readonly pending;
2005
+ private readonly tombstones;
2006
+ private readonly caches;
2007
+ /** Native approval ticket id → its pending evaluation id, for on-access
2008
+ * deadline enforcement on the resolve path. */
2009
+ private readonly ticketToEvaluation;
2010
+ private pendingBytes;
2011
+ private sweepTimer;
2012
+ private closed;
2013
+ constructor(options: GovernanceServiceOptions);
2014
+ /** Swap the compiled policy on hot-reload (mirrors GovernedForwarder). */
2015
+ updatePolicy(policy: CompiledPolicy): void;
2016
+ evaluate(req: EvaluateInput): ServiceResult;
2017
+ audit(req: AuditInput, payloadHash: string): ServiceResult;
2018
+ /**
2019
+ * Write the optional `/audit` evidence entries for a successful call
2020
+ * (issue #11), returning a per-entry outcome list — or `undefined`
2021
+ * when there is nothing to report (non-success status, or no evidence
2022
+ * supplied). Caps are enforced here, NOT in route validation, so an over-cap
2023
+ * entry soft-drops without discarding the audit row: entries past
2024
+ * `MAX_EVIDENCE_ENTRIES` → `too_many`; oversized `evidence_data` →
2025
+ * `too_large`; no evidence store on the service → `evidence_unavailable`;
2026
+ * a sessionless evaluation → `no_session`; the store's own rejections
2027
+ * (`key_not_in_policy_allowlist`, `closed`) pass through as the per-entry
2028
+ * reason. None of these fail the audit.
2029
+ */
2030
+ private populateEvidence;
2031
+ installScan(req: InstallScanInput): ServiceResult;
2032
+ /** First-match-wins evaluation of the compiled install policy (issue #13). */
2033
+ private evaluateInstall;
2034
+ resolveApproval(ticketId: string, req: ResolveApprovalInput): ServiceResult;
2035
+ sweep(): void;
2036
+ /**
2037
+ * Reserve a cardinality slot for a sender-keyed limit (issue #13).
2038
+ *
2039
+ * Only `sender:*` keys are gated — tool/session families are bounded by upstream
2040
+ * cardinality, and the MCP path never reaches here, so structural traffic cannot
2041
+ * be starved. A key already backed by live state (registry or a live limiter
2042
+ * bucket) costs no new slot. At capacity we lazily prune dead keys before failing
2043
+ * closed, so an emptied bucket frees its slot without waiting for the sweep.
2044
+ */
2045
+ private reserveSenderKey;
2046
+ /** Drop registry keys with no pending evaluation AND no live limiter bucket. */
2047
+ private pruneSenderKeys;
2048
+ /**
2049
+ * Whether either limiter still holds a live bucket for `key`. Uses the public
2050
+ * `getKeyState()` — never the limiters' private maps — and its lazy eviction of
2051
+ * an emptied bucket IS the prune-on-touch mechanism.
2052
+ */
2053
+ private hasLiveBucket;
2054
+ close(): void;
2055
+ /** Apply crossed deadlines to one pending entry. Returns its post-state. */
2056
+ private enforceDeadlines;
2057
+ private cacheFor;
2058
+ private discardPending;
2059
+ private getTicketStatus;
2060
+ private planRate;
2061
+ private planSpend;
2062
+ /** Commit a limit plan at /audit time and return the evidence_chain block. */
2063
+ private commitLimit;
2064
+ private writeAudit;
2065
+ private assertApprovalRouter;
2066
+ }
2067
+
1668
2068
  /** Options for constructing the SDK sideband Hono app. */
1669
2069
  interface SidebandAppOptions {
1670
2070
  /**
1671
- * Bearer token that every request must carry in its `Authorization`
1672
- * header (except `GET /healthz`). When omitted or empty, the sideband
1673
- * runs open useful for local development when the operator has
1674
- * explicitly disabled the per-boot token via env.
2071
+ * Bearer token for the evidence/context/session routes (the SDK scope).
2072
+ * When omitted or empty, those routes run open — useful for local
2073
+ * development when the operator has disabled the per-boot token via env.
1675
2074
  */
1676
2075
  readonly token?: string;
2076
+ /**
2077
+ * Bearer token for the governance routes (the adapter scope, issue #12/F6).
2078
+ * Distinct from `token` so an SDK client cannot drive policy decisions and
2079
+ * an adapter cannot write evidence it was not granted. When omitted, the
2080
+ * governance routes run open (and only mount if `governance` is provided).
2081
+ */
2082
+ readonly adapterToken?: string;
2083
+ /**
2084
+ * The governance service backing `/evaluate`, `/audit`, `/install-scan`, and
2085
+ * `/approval/:id/resolve`. When omitted, those routes return 503
2086
+ * `governance_unavailable` (evidence-only deployments, and existing tests).
2087
+ */
2088
+ readonly governance?: GovernanceService;
1677
2089
  }
1678
2090
  /**
1679
2091
  * Create a Hono app for the SDK sideband API.
@@ -1698,6 +2110,20 @@ interface SidebandAppOptions {
1698
2110
  */
1699
2111
  declare function createSidebandApp(store: EvidenceStore, options?: SidebandAppOptions): Hono;
1700
2112
 
2113
+ /**
2114
+ * Error thrown when the GovernanceService is wired in a way that could fail
2115
+ * open — for example, an approval-capable policy with no ApprovalRouter to
2116
+ * route `require_approval` decisions through (issue #12). Surfaced at
2117
+ * construction and on hot-reload so a misconfiguration crashes loudly rather
2118
+ * than silently degrading an approval into an unenforced allow.
2119
+ *
2120
+ * Direct embedders of GovernanceService can catch this distinctly; the bundled
2121
+ * CLI always provides a router, so it never fires in the shipped path.
2122
+ */
2123
+ declare class GovernanceConfigError extends Error {
2124
+ constructor(message: string);
2125
+ }
2126
+
1701
2127
  /** @internal Exported for testing only. */
1702
2128
  declare class QueueChannel implements ApprovalChannel {
1703
2129
  readonly type = "dashboard";
@@ -1834,6 +2260,8 @@ interface ActionEvent {
1834
2260
  readonly dry_run: boolean;
1835
2261
  readonly matched_rule: string | null;
1836
2262
  readonly matched_rule_index: number | null;
2263
+ readonly record_kind: AuditRecord['record_kind'];
2264
+ readonly origin: string;
1837
2265
  }
1838
2266
  /** Payload for an approval_requested event. */
1839
2267
  interface ApprovalRequestedEvent {
@@ -1928,4 +2356,4 @@ interface DashboardAppOptions {
1928
2356
  */
1929
2357
  declare function createDashboardApp(deps: DashboardAppDeps, options?: DashboardAppOptions): Hono;
1930
2358
 
1931
- 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 };
2359
+ 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 };