@gethelio/proxy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +311 -0
- package/dist/cli.js +6420 -0
- package/dist/dashboard-assets/assets/index-DG3h7Cvn.css +1 -0
- package/dist/dashboard-assets/assets/index-Yyo92yXC.js +128 -0
- package/dist/dashboard-assets/favicon.svg +16 -0
- package/dist/dashboard-assets/index.html +15 -0
- package/dist/dashboard-assets/logo.svg +16 -0
- package/dist/index.d.ts +1874 -0
- package/dist/index.js +5672 -0
- package/package.json +69 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1874 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { ServerType } from '@hono/node-server';
|
|
4
|
+
import { KnownBlock } from '@slack/web-api';
|
|
5
|
+
|
|
6
|
+
declare const VERSION: string;
|
|
7
|
+
|
|
8
|
+
declare const policiesSchema: z.ZodObject<{
|
|
9
|
+
default: z.ZodDefault<z.ZodEnum<{
|
|
10
|
+
allow: "allow";
|
|
11
|
+
deny: "deny";
|
|
12
|
+
}>>;
|
|
13
|
+
flag_destructive: z.ZodOptional<z.ZodEnum<{
|
|
14
|
+
require_approval: "require_approval";
|
|
15
|
+
log: "log";
|
|
16
|
+
}>>;
|
|
17
|
+
dry_run: z.ZodDefault<z.ZodBoolean>;
|
|
18
|
+
rules: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
19
|
+
name: z.ZodOptional<z.ZodString>;
|
|
20
|
+
match: z.ZodObject<{
|
|
21
|
+
tool: z.ZodOptional<z.ZodString>;
|
|
22
|
+
annotations: z.ZodOptional<z.ZodObject<{
|
|
23
|
+
readOnlyHint: z.ZodOptional<z.ZodBoolean>;
|
|
24
|
+
destructiveHint: z.ZodOptional<z.ZodBoolean>;
|
|
25
|
+
idempotentHint: z.ZodOptional<z.ZodBoolean>;
|
|
26
|
+
openWorldHint: z.ZodOptional<z.ZodBoolean>;
|
|
27
|
+
}, z.core.$strict>>;
|
|
28
|
+
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
29
|
+
eq: z.ZodOptional<z.ZodUnknown>;
|
|
30
|
+
neq: z.ZodOptional<z.ZodUnknown>;
|
|
31
|
+
gt: z.ZodOptional<z.ZodNumber>;
|
|
32
|
+
gte: z.ZodOptional<z.ZodNumber>;
|
|
33
|
+
lt: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
lte: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
contains: z.ZodOptional<z.ZodString>;
|
|
36
|
+
regex: z.ZodOptional<z.ZodString>;
|
|
37
|
+
}, z.core.$strict>>>;
|
|
38
|
+
environment: z.ZodOptional<z.ZodString>;
|
|
39
|
+
}, z.core.$strict>;
|
|
40
|
+
action: z.ZodEnum<{
|
|
41
|
+
allow: "allow";
|
|
42
|
+
deny: "deny";
|
|
43
|
+
require_approval: "require_approval";
|
|
44
|
+
rate_limit: "rate_limit";
|
|
45
|
+
spend_limit: "spend_limit";
|
|
46
|
+
dry_run: "dry_run";
|
|
47
|
+
}>;
|
|
48
|
+
approval: z.ZodOptional<z.ZodObject<{
|
|
49
|
+
channel: z.ZodString;
|
|
50
|
+
timeout: z.ZodOptional<z.ZodString>;
|
|
51
|
+
delegates: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
52
|
+
escalation_after: z.ZodOptional<z.ZodString>;
|
|
53
|
+
}, z.core.$strict>>;
|
|
54
|
+
evidence: z.ZodOptional<z.ZodObject<{
|
|
55
|
+
requires: z.ZodArray<z.ZodString>;
|
|
56
|
+
}, z.core.$strict>>;
|
|
57
|
+
requires: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
58
|
+
requires_success: z.ZodOptional<z.ZodBoolean>;
|
|
59
|
+
limits: z.ZodOptional<z.ZodObject<{
|
|
60
|
+
max_calls: z.ZodOptional<z.ZodNumber>;
|
|
61
|
+
window: z.ZodOptional<z.ZodString>;
|
|
62
|
+
key: z.ZodOptional<z.ZodEnum<{
|
|
63
|
+
tool: "tool";
|
|
64
|
+
agent: "agent";
|
|
65
|
+
session: "session";
|
|
66
|
+
}>>;
|
|
67
|
+
max_spend: z.ZodOptional<z.ZodObject<{
|
|
68
|
+
field: z.ZodString;
|
|
69
|
+
limit: z.ZodNumber;
|
|
70
|
+
currency: z.ZodString;
|
|
71
|
+
window: z.ZodString;
|
|
72
|
+
key: z.ZodOptional<z.ZodEnum<{
|
|
73
|
+
tool: "tool";
|
|
74
|
+
agent: "agent";
|
|
75
|
+
session: "session";
|
|
76
|
+
}>>;
|
|
77
|
+
}, z.core.$strict>>;
|
|
78
|
+
}, z.core.$strict>>;
|
|
79
|
+
feedback: z.ZodOptional<z.ZodObject<{
|
|
80
|
+
message: z.ZodString;
|
|
81
|
+
suggestion: z.ZodOptional<z.ZodString>;
|
|
82
|
+
}, z.core.$strict>>;
|
|
83
|
+
}, z.core.$strict>>>;
|
|
84
|
+
hot_reload: z.ZodOptional<z.ZodBoolean>;
|
|
85
|
+
}, z.core.$strict>;
|
|
86
|
+
declare const approvalChannelSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
87
|
+
type: z.ZodLiteral<"slack">;
|
|
88
|
+
name: z.ZodOptional<z.ZodString>;
|
|
89
|
+
bot_token: z.ZodString;
|
|
90
|
+
signing_secret: z.ZodString;
|
|
91
|
+
channel: z.ZodString;
|
|
92
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
93
|
+
type: z.ZodLiteral<"webhook">;
|
|
94
|
+
name: z.ZodOptional<z.ZodString>;
|
|
95
|
+
url: z.ZodString;
|
|
96
|
+
secret: z.ZodOptional<z.ZodString>;
|
|
97
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
98
|
+
type: z.ZodLiteral<"dashboard">;
|
|
99
|
+
name: z.ZodOptional<z.ZodString>;
|
|
100
|
+
}, z.core.$strip>], "type">;
|
|
101
|
+
/** Zod schema for the complete `helio.yaml` configuration file. */
|
|
102
|
+
declare const helioConfigSchema: z.ZodObject<{
|
|
103
|
+
version: z.ZodLiteral<"1">;
|
|
104
|
+
upstream: z.ZodObject<{
|
|
105
|
+
url: z.ZodString;
|
|
106
|
+
transport: z.ZodDefault<z.ZodEnum<{
|
|
107
|
+
"streamable-http": "streamable-http";
|
|
108
|
+
sse: "sse";
|
|
109
|
+
stdio: "stdio";
|
|
110
|
+
}>>;
|
|
111
|
+
command: z.ZodOptional<z.ZodString>;
|
|
112
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
113
|
+
connect_timeout: z.ZodDefault<z.ZodString>;
|
|
114
|
+
request_timeout: z.ZodDefault<z.ZodString>;
|
|
115
|
+
forward_headers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
116
|
+
}, z.core.$strip>;
|
|
117
|
+
listen: z.ZodPrefault<z.ZodObject<{
|
|
118
|
+
port: z.ZodDefault<z.ZodNumber>;
|
|
119
|
+
host: z.ZodDefault<z.ZodString>;
|
|
120
|
+
}, z.core.$strip>>;
|
|
121
|
+
dashboard: z.ZodPrefault<z.ZodObject<{
|
|
122
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
123
|
+
port: z.ZodDefault<z.ZodNumber>;
|
|
124
|
+
host: z.ZodDefault<z.ZodString>;
|
|
125
|
+
api_secret: z.ZodOptional<z.ZodString>;
|
|
126
|
+
allow_open_mode: z.ZodDefault<z.ZodBoolean>;
|
|
127
|
+
sse_heartbeat_interval: z.ZodDefault<z.ZodString>;
|
|
128
|
+
}, z.core.$strip>>;
|
|
129
|
+
environment: z.ZodOptional<z.ZodString>;
|
|
130
|
+
policies: z.ZodPrefault<z.ZodObject<{
|
|
131
|
+
default: z.ZodDefault<z.ZodEnum<{
|
|
132
|
+
allow: "allow";
|
|
133
|
+
deny: "deny";
|
|
134
|
+
}>>;
|
|
135
|
+
flag_destructive: z.ZodOptional<z.ZodEnum<{
|
|
136
|
+
require_approval: "require_approval";
|
|
137
|
+
log: "log";
|
|
138
|
+
}>>;
|
|
139
|
+
dry_run: z.ZodDefault<z.ZodBoolean>;
|
|
140
|
+
rules: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
141
|
+
name: z.ZodOptional<z.ZodString>;
|
|
142
|
+
match: z.ZodObject<{
|
|
143
|
+
tool: z.ZodOptional<z.ZodString>;
|
|
144
|
+
annotations: z.ZodOptional<z.ZodObject<{
|
|
145
|
+
readOnlyHint: z.ZodOptional<z.ZodBoolean>;
|
|
146
|
+
destructiveHint: z.ZodOptional<z.ZodBoolean>;
|
|
147
|
+
idempotentHint: z.ZodOptional<z.ZodBoolean>;
|
|
148
|
+
openWorldHint: z.ZodOptional<z.ZodBoolean>;
|
|
149
|
+
}, z.core.$strict>>;
|
|
150
|
+
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
151
|
+
eq: z.ZodOptional<z.ZodUnknown>;
|
|
152
|
+
neq: z.ZodOptional<z.ZodUnknown>;
|
|
153
|
+
gt: z.ZodOptional<z.ZodNumber>;
|
|
154
|
+
gte: z.ZodOptional<z.ZodNumber>;
|
|
155
|
+
lt: z.ZodOptional<z.ZodNumber>;
|
|
156
|
+
lte: z.ZodOptional<z.ZodNumber>;
|
|
157
|
+
contains: z.ZodOptional<z.ZodString>;
|
|
158
|
+
regex: z.ZodOptional<z.ZodString>;
|
|
159
|
+
}, z.core.$strict>>>;
|
|
160
|
+
environment: z.ZodOptional<z.ZodString>;
|
|
161
|
+
}, z.core.$strict>;
|
|
162
|
+
action: z.ZodEnum<{
|
|
163
|
+
allow: "allow";
|
|
164
|
+
deny: "deny";
|
|
165
|
+
require_approval: "require_approval";
|
|
166
|
+
rate_limit: "rate_limit";
|
|
167
|
+
spend_limit: "spend_limit";
|
|
168
|
+
dry_run: "dry_run";
|
|
169
|
+
}>;
|
|
170
|
+
approval: z.ZodOptional<z.ZodObject<{
|
|
171
|
+
channel: z.ZodString;
|
|
172
|
+
timeout: z.ZodOptional<z.ZodString>;
|
|
173
|
+
delegates: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
174
|
+
escalation_after: z.ZodOptional<z.ZodString>;
|
|
175
|
+
}, z.core.$strict>>;
|
|
176
|
+
evidence: z.ZodOptional<z.ZodObject<{
|
|
177
|
+
requires: z.ZodArray<z.ZodString>;
|
|
178
|
+
}, z.core.$strict>>;
|
|
179
|
+
requires: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
180
|
+
requires_success: z.ZodOptional<z.ZodBoolean>;
|
|
181
|
+
limits: z.ZodOptional<z.ZodObject<{
|
|
182
|
+
max_calls: z.ZodOptional<z.ZodNumber>;
|
|
183
|
+
window: z.ZodOptional<z.ZodString>;
|
|
184
|
+
key: z.ZodOptional<z.ZodEnum<{
|
|
185
|
+
tool: "tool";
|
|
186
|
+
agent: "agent";
|
|
187
|
+
session: "session";
|
|
188
|
+
}>>;
|
|
189
|
+
max_spend: z.ZodOptional<z.ZodObject<{
|
|
190
|
+
field: z.ZodString;
|
|
191
|
+
limit: z.ZodNumber;
|
|
192
|
+
currency: z.ZodString;
|
|
193
|
+
window: z.ZodString;
|
|
194
|
+
key: z.ZodOptional<z.ZodEnum<{
|
|
195
|
+
tool: "tool";
|
|
196
|
+
agent: "agent";
|
|
197
|
+
session: "session";
|
|
198
|
+
}>>;
|
|
199
|
+
}, z.core.$strict>>;
|
|
200
|
+
}, z.core.$strict>>;
|
|
201
|
+
feedback: z.ZodOptional<z.ZodObject<{
|
|
202
|
+
message: z.ZodString;
|
|
203
|
+
suggestion: z.ZodOptional<z.ZodString>;
|
|
204
|
+
}, z.core.$strict>>;
|
|
205
|
+
}, z.core.$strict>>>;
|
|
206
|
+
hot_reload: z.ZodOptional<z.ZodBoolean>;
|
|
207
|
+
}, z.core.$strict>>;
|
|
208
|
+
approval: z.ZodPrefault<z.ZodObject<{
|
|
209
|
+
timeout: z.ZodDefault<z.ZodString>;
|
|
210
|
+
default_on_timeout: z.ZodDefault<z.ZodEnum<{
|
|
211
|
+
allow: "allow";
|
|
212
|
+
deny: "deny";
|
|
213
|
+
}>>;
|
|
214
|
+
channels: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
215
|
+
type: z.ZodLiteral<"slack">;
|
|
216
|
+
name: z.ZodOptional<z.ZodString>;
|
|
217
|
+
bot_token: z.ZodString;
|
|
218
|
+
signing_secret: z.ZodString;
|
|
219
|
+
channel: z.ZodString;
|
|
220
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
221
|
+
type: z.ZodLiteral<"webhook">;
|
|
222
|
+
name: z.ZodOptional<z.ZodString>;
|
|
223
|
+
url: z.ZodString;
|
|
224
|
+
secret: z.ZodOptional<z.ZodString>;
|
|
225
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
226
|
+
type: z.ZodLiteral<"dashboard">;
|
|
227
|
+
name: z.ZodOptional<z.ZodString>;
|
|
228
|
+
}, z.core.$strip>], "type">>>;
|
|
229
|
+
}, z.core.$strip>>;
|
|
230
|
+
audit: z.ZodPrefault<z.ZodObject<{
|
|
231
|
+
storage: z.ZodDefault<z.ZodEnum<{
|
|
232
|
+
sqlite: "sqlite";
|
|
233
|
+
}>>;
|
|
234
|
+
path: z.ZodDefault<z.ZodString>;
|
|
235
|
+
retention: z.ZodDefault<z.ZodString>;
|
|
236
|
+
include_responses: z.ZodDefault<z.ZodBoolean>;
|
|
237
|
+
}, z.core.$strip>>;
|
|
238
|
+
sdk: z.ZodPrefault<z.ZodObject<{
|
|
239
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
240
|
+
port: z.ZodDefault<z.ZodNumber>;
|
|
241
|
+
host: z.ZodDefault<z.ZodString>;
|
|
242
|
+
}, z.core.$strip>>;
|
|
243
|
+
}, z.core.$strip>;
|
|
244
|
+
/** Fully validated and defaulted Helio configuration. */
|
|
245
|
+
type HelioConfig = z.infer<typeof helioConfigSchema>;
|
|
246
|
+
/** An approval channel configuration (slack, webhook, or dashboard). */
|
|
247
|
+
type ApprovalChannel$1 = z.infer<typeof approvalChannelSchema>;
|
|
248
|
+
/** The policies section of the config. */
|
|
249
|
+
type PoliciesConfig = z.infer<typeof policiesSchema>;
|
|
250
|
+
|
|
251
|
+
/** Structured error for configuration loading failures. */
|
|
252
|
+
declare class ConfigError extends Error {
|
|
253
|
+
readonly details?: ReadonlyArray<{
|
|
254
|
+
path: string;
|
|
255
|
+
message: string;
|
|
256
|
+
}> | undefined;
|
|
257
|
+
constructor(message: string, details?: ReadonlyArray<{
|
|
258
|
+
path: string;
|
|
259
|
+
message: string;
|
|
260
|
+
}> | undefined);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Load and validate a `helio.yaml` configuration file.
|
|
264
|
+
*
|
|
265
|
+
* Pipeline: read file → parse YAML → interpolate env vars → validate with Zod.
|
|
266
|
+
*
|
|
267
|
+
* @param filePath - Path to the YAML configuration file.
|
|
268
|
+
* @param env - Optional environment variables for `${VAR}` interpolation.
|
|
269
|
+
* @returns The fully validated and defaulted configuration object.
|
|
270
|
+
* @throws {ConfigError} On file read error, YAML parse error, missing env var, or validation failure.
|
|
271
|
+
*/
|
|
272
|
+
declare function loadConfig(filePath: string, env?: Record<string, string | undefined>): Promise<HelioConfig>;
|
|
273
|
+
|
|
274
|
+
/** Pre-compiled glob matcher for tool names. */
|
|
275
|
+
interface ToolMatcher {
|
|
276
|
+
/** Original glob pattern string (e.g. "send_*") for audit/logging. */
|
|
277
|
+
readonly pattern: string;
|
|
278
|
+
/** Compiled test function — returns true if the tool name matches. */
|
|
279
|
+
readonly test: (toolName: string) => boolean;
|
|
280
|
+
}
|
|
281
|
+
/** Compiled annotation match conditions. */
|
|
282
|
+
interface AnnotationMatch {
|
|
283
|
+
readonly readOnlyHint?: boolean;
|
|
284
|
+
readonly destructiveHint?: boolean;
|
|
285
|
+
readonly idempotentHint?: boolean;
|
|
286
|
+
readonly openWorldHint?: boolean;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* A single flattened input condition.
|
|
290
|
+
*
|
|
291
|
+
* Config format `{ '$.amount': { gt: 10, lt: 100 } }` is flattened into
|
|
292
|
+
* two separate InputCondition entries — one per operator — to simplify
|
|
293
|
+
* the engine's evaluation loop.
|
|
294
|
+
*/
|
|
295
|
+
interface InputCondition {
|
|
296
|
+
/** JSONPath string (e.g. "$.amount"). */
|
|
297
|
+
readonly path: string;
|
|
298
|
+
/** Comparison operator. */
|
|
299
|
+
readonly operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'regex';
|
|
300
|
+
/** The comparison value. */
|
|
301
|
+
readonly value: unknown;
|
|
302
|
+
/** Pre-compiled RegExp when operator is 'regex'. */
|
|
303
|
+
readonly regex?: RegExp;
|
|
304
|
+
}
|
|
305
|
+
/** Compiled match block for a policy rule. */
|
|
306
|
+
interface CompiledMatch {
|
|
307
|
+
readonly tool?: ToolMatcher;
|
|
308
|
+
readonly annotations?: AnnotationMatch;
|
|
309
|
+
/** Flattened list of input conditions (one entry per path+operator pair). */
|
|
310
|
+
readonly input?: readonly InputCondition[];
|
|
311
|
+
readonly environment?: string;
|
|
312
|
+
}
|
|
313
|
+
/** Compiled approval configuration with durations as milliseconds. */
|
|
314
|
+
interface CompiledApproval {
|
|
315
|
+
readonly channel: string;
|
|
316
|
+
readonly timeoutMs?: number;
|
|
317
|
+
readonly delegates?: readonly string[];
|
|
318
|
+
readonly escalationAfterMs?: number;
|
|
319
|
+
}
|
|
320
|
+
/** Compiled spend limit with window as milliseconds. */
|
|
321
|
+
interface CompiledSpendLimit {
|
|
322
|
+
readonly field: string;
|
|
323
|
+
readonly limit: number;
|
|
324
|
+
readonly currency: string;
|
|
325
|
+
readonly windowMs: number;
|
|
326
|
+
readonly key?: 'tool' | 'agent' | 'session';
|
|
327
|
+
}
|
|
328
|
+
/** Compiled rate/spend limit configuration. */
|
|
329
|
+
interface CompiledLimits {
|
|
330
|
+
readonly maxCalls?: number;
|
|
331
|
+
readonly windowMs?: number;
|
|
332
|
+
readonly key?: 'tool' | 'agent' | 'session';
|
|
333
|
+
readonly maxSpend?: CompiledSpendLimit;
|
|
334
|
+
}
|
|
335
|
+
/** Policy action types. */
|
|
336
|
+
type PolicyAction = 'allow' | 'deny' | 'require_approval' | 'rate_limit' | 'spend_limit' | 'dry_run';
|
|
337
|
+
/** A fully compiled policy rule ready for engine evaluation. */
|
|
338
|
+
interface CompiledPolicyRule {
|
|
339
|
+
/** Original position in the rules array (for audit/debugging). */
|
|
340
|
+
readonly index: number;
|
|
341
|
+
readonly name?: string;
|
|
342
|
+
readonly match: CompiledMatch;
|
|
343
|
+
readonly action: PolicyAction;
|
|
344
|
+
readonly approval?: CompiledApproval;
|
|
345
|
+
readonly evidence?: {
|
|
346
|
+
readonly requires: readonly string[];
|
|
347
|
+
};
|
|
348
|
+
readonly requires?: readonly string[];
|
|
349
|
+
/**
|
|
350
|
+
* When `requires` is set, controls whether dependency calls must have
|
|
351
|
+
* succeeded upstream. Undefined defaults to `true` at the check site — a
|
|
352
|
+
* failed call does NOT satisfy the dependency by default. Set to `false`
|
|
353
|
+
* in config (`requires_success: false`) to opt out of the outcome check
|
|
354
|
+
* and treat any attempted call as satisfying the dependency.
|
|
355
|
+
*/
|
|
356
|
+
readonly requiresSuccess?: boolean;
|
|
357
|
+
readonly limits?: CompiledLimits;
|
|
358
|
+
readonly feedback?: {
|
|
359
|
+
readonly message: string;
|
|
360
|
+
readonly suggestion?: string;
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
/** Top-level compiled policy — what the engine consumes. */
|
|
364
|
+
interface CompiledPolicy {
|
|
365
|
+
readonly defaultAction: 'allow' | 'deny';
|
|
366
|
+
readonly flagDestructive?: 'log' | 'require_approval';
|
|
367
|
+
readonly dryRun?: boolean;
|
|
368
|
+
readonly rules: readonly CompiledPolicyRule[];
|
|
369
|
+
}
|
|
370
|
+
/** A non-fatal warning produced during policy compilation. */
|
|
371
|
+
interface PolicyParseWarning {
|
|
372
|
+
readonly ruleIndex: number;
|
|
373
|
+
readonly ruleName?: string;
|
|
374
|
+
readonly message: string;
|
|
375
|
+
}
|
|
376
|
+
/** Result of compiling a PoliciesConfig into engine-ready form. */
|
|
377
|
+
interface CompilePoliciesResult {
|
|
378
|
+
readonly policy: CompiledPolicy;
|
|
379
|
+
readonly warnings: readonly PolicyParseWarning[];
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Tool annotation hints as reported by the MCP server's tools/list response.
|
|
383
|
+
*
|
|
384
|
+
* Mirrors the MCP spec's annotation boolean fields. Defined locally to avoid
|
|
385
|
+
* a runtime dependency on `@modelcontextprotocol/sdk`.
|
|
386
|
+
*/
|
|
387
|
+
interface ToolAnnotationHints {
|
|
388
|
+
readonly readOnlyHint?: boolean;
|
|
389
|
+
readonly destructiveHint?: boolean;
|
|
390
|
+
readonly idempotentHint?: boolean;
|
|
391
|
+
readonly openWorldHint?: boolean;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Context passed to matchers when evaluating a policy rule against a request.
|
|
395
|
+
*
|
|
396
|
+
* Built by the engine from the MCP request and cached tool metadata.
|
|
397
|
+
* All fields are optional — not every MCP method involves a tool call.
|
|
398
|
+
*/
|
|
399
|
+
interface MatchContext {
|
|
400
|
+
/** The tool name from the tools/call request params. */
|
|
401
|
+
readonly toolName?: string;
|
|
402
|
+
/** Annotations from the MCP tool's metadata (tools/list cache). */
|
|
403
|
+
readonly annotations?: ToolAnnotationHints;
|
|
404
|
+
/** The `arguments` object from the tools/call request params. */
|
|
405
|
+
readonly toolArguments?: Readonly<Record<string, unknown>>;
|
|
406
|
+
/** The configured environment label (e.g. "production", "staging"). */
|
|
407
|
+
readonly environment?: string;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** A JSON-RPC 2.0 request object. */
|
|
411
|
+
interface JsonRpcRequest {
|
|
412
|
+
jsonrpc: '2.0';
|
|
413
|
+
id?: string | number | null;
|
|
414
|
+
method: string;
|
|
415
|
+
params?: unknown;
|
|
416
|
+
}
|
|
417
|
+
/** A parsed MCP request enriched with session context. */
|
|
418
|
+
interface McpRequest extends JsonRpcRequest {
|
|
419
|
+
/** MCP session ID extracted from the `Mcp-Session-Id` header. */
|
|
420
|
+
sessionId?: string;
|
|
421
|
+
/** Per-request headers to forward to upstream (e.g. Authorization, X-* headers). */
|
|
422
|
+
headers?: Record<string, string>;
|
|
423
|
+
/** Abort signal tied to the downstream client request lifecycle. */
|
|
424
|
+
signal?: AbortSignal;
|
|
425
|
+
}
|
|
426
|
+
/** The response returned by an MCP forwarder. */
|
|
427
|
+
interface McpResponse {
|
|
428
|
+
status: number;
|
|
429
|
+
headers: Record<string, string>;
|
|
430
|
+
body: unknown;
|
|
431
|
+
}
|
|
432
|
+
/** The result of forwarding an MCP request, including timing metadata. */
|
|
433
|
+
interface ForwardResult {
|
|
434
|
+
response: McpResponse;
|
|
435
|
+
/** Time in milliseconds the upstream request took. */
|
|
436
|
+
durationMs: number;
|
|
437
|
+
}
|
|
438
|
+
/** Interface for forwarding MCP requests to an upstream server. */
|
|
439
|
+
interface McpForwarder {
|
|
440
|
+
forward(request: McpRequest): Promise<ForwardResult>;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Handle returned by `startServer` for lifecycle management. */
|
|
444
|
+
interface ServerHandle {
|
|
445
|
+
server: ServerType;
|
|
446
|
+
close: () => Promise<void>;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Optional sub-apps to mount on the main proxy server.
|
|
450
|
+
*
|
|
451
|
+
* The main MCP port is the agent-facing edge and must stay minimal. Operator
|
|
452
|
+
* read APIs (approval REST, rate/spend limit status) deliberately live on
|
|
453
|
+
* the dashboard sideband instead, so that an agent speaking `/mcp` cannot
|
|
454
|
+
* enumerate budget state to time attacks or self-approve its own pending
|
|
455
|
+
* tickets on the same origin. Only Slack interactive action callbacks
|
|
456
|
+
* belong here — they are public inbound webhooks from Slack's servers, not
|
|
457
|
+
* operator read endpoints.
|
|
458
|
+
*/
|
|
459
|
+
interface CreateAppOptions {
|
|
460
|
+
/** Slack interactive action handler (mounted at /slack/actions). */
|
|
461
|
+
slackActionApp?: Hono;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Create a Hono app configured with the MCP proxy routes.
|
|
465
|
+
*
|
|
466
|
+
* @param _config - The validated Helio configuration.
|
|
467
|
+
* @param forwarder - The MCP forwarder to delegate requests to.
|
|
468
|
+
* @param options - Optional sub-apps to mount.
|
|
469
|
+
*/
|
|
470
|
+
declare function createApp(config: HelioConfig, forwarder: McpForwarder, options?: CreateAppOptions): Hono;
|
|
471
|
+
/**
|
|
472
|
+
* Start the HTTP server on the configured host and port.
|
|
473
|
+
*
|
|
474
|
+
* @param app - The Hono app to serve.
|
|
475
|
+
* @param config - The validated Helio configuration (uses `listen.port` and `listen.host`).
|
|
476
|
+
* @returns A handle with the underlying server and a `close()` method for graceful shutdown.
|
|
477
|
+
*/
|
|
478
|
+
declare function startServer(app: Hono, config: HelioConfig): ServerHandle;
|
|
479
|
+
/**
|
|
480
|
+
* Start the sideband HTTP server for the SDK API.
|
|
481
|
+
*
|
|
482
|
+
* Binds to 127.0.0.1 by default (local-only) on the configured SDK port.
|
|
483
|
+
*/
|
|
484
|
+
declare function startSidebandServer(app: Hono, port: number, host?: string): ServerHandle;
|
|
485
|
+
|
|
486
|
+
/** Options for constructing an UpstreamForwarder. */
|
|
487
|
+
interface UpstreamForwarderOptions {
|
|
488
|
+
/** The upstream MCP server URL (e.g. "http://localhost:8080/mcp"). */
|
|
489
|
+
url: string;
|
|
490
|
+
/** Static headers to include on every upstream request (e.g. API keys). */
|
|
491
|
+
headers?: Record<string, string>;
|
|
492
|
+
/** Maximum time to wait for an upstream request before aborting. */
|
|
493
|
+
requestTimeoutMs?: number;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Forward MCP requests to an upstream server via HTTP.
|
|
497
|
+
*
|
|
498
|
+
* Sends JSON-RPC POST requests to the configured URL, passes through
|
|
499
|
+
* session IDs and per-request headers, and captures request timing.
|
|
500
|
+
*/
|
|
501
|
+
declare class UpstreamForwarder implements McpForwarder {
|
|
502
|
+
private readonly url;
|
|
503
|
+
private readonly staticHeaders;
|
|
504
|
+
private readonly requestTimeoutMs;
|
|
505
|
+
constructor(options: UpstreamForwarderOptions);
|
|
506
|
+
forward(request: McpRequest): Promise<ForwardResult>;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Options for constructing an SseUpstreamForwarder. */
|
|
510
|
+
interface SseUpstreamForwarderOptions {
|
|
511
|
+
/** The upstream SSE server URL (e.g. "http://localhost:8080/sse"). */
|
|
512
|
+
url: string;
|
|
513
|
+
/** Static headers to include on every request. */
|
|
514
|
+
headers?: Record<string, string>;
|
|
515
|
+
/** Timeout in milliseconds for individual requests. */
|
|
516
|
+
requestTimeoutMs?: number;
|
|
517
|
+
/** Timeout in milliseconds while establishing the SSE connection. */
|
|
518
|
+
connectTimeoutMs?: number;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Forward MCP requests to an upstream server that speaks SSE transport.
|
|
522
|
+
*
|
|
523
|
+
* Connects via GET to establish an SSE stream, learns the POST endpoint
|
|
524
|
+
* from the `endpoint` event, and correlates responses via JSON-RPC `id`.
|
|
525
|
+
*/
|
|
526
|
+
declare class SseUpstreamForwarder implements McpForwarder {
|
|
527
|
+
private readonly url;
|
|
528
|
+
private readonly staticHeaders;
|
|
529
|
+
private readonly pending;
|
|
530
|
+
private readonly requestTimeoutMs;
|
|
531
|
+
private readonly connectTimeoutMs;
|
|
532
|
+
private postUrl;
|
|
533
|
+
private abortController;
|
|
534
|
+
private connected;
|
|
535
|
+
constructor(options: SseUpstreamForwarderOptions);
|
|
536
|
+
/** Connect to the upstream SSE server and learn the POST endpoint. */
|
|
537
|
+
connect(): Promise<void>;
|
|
538
|
+
forward(request: McpRequest): Promise<ForwardResult>;
|
|
539
|
+
/** Close the SSE connection and reject all pending requests. */
|
|
540
|
+
close(): Promise<void>;
|
|
541
|
+
/** Resolve a potentially relative endpoint URL against the upstream base URL. */
|
|
542
|
+
private resolveEndpointUrl;
|
|
543
|
+
/** Consume the SSE ReadableStream, parsing events. */
|
|
544
|
+
private consumeStream;
|
|
545
|
+
/** Handle a `message` event from the SSE stream. */
|
|
546
|
+
private onMessage;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Options for constructing a StdioForwarder. */
|
|
550
|
+
interface StdioForwarderOptions {
|
|
551
|
+
/** The command to spawn (e.g. "node", "python"). */
|
|
552
|
+
command: string;
|
|
553
|
+
/** Arguments to pass to the command. */
|
|
554
|
+
args?: string[];
|
|
555
|
+
/** Maximum number of auto-restart attempts on crash. */
|
|
556
|
+
maxRetries?: number;
|
|
557
|
+
/** Delay in milliseconds between restart attempts. */
|
|
558
|
+
retryDelayMs?: number;
|
|
559
|
+
/** Timeout in milliseconds for individual requests. */
|
|
560
|
+
requestTimeoutMs?: number;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Forward MCP requests to an upstream server via stdio.
|
|
564
|
+
*
|
|
565
|
+
* Spawns a child process and communicates using newline-delimited
|
|
566
|
+
* JSON-RPC on stdin/stdout.
|
|
567
|
+
*/
|
|
568
|
+
declare class StdioForwarder implements McpForwarder {
|
|
569
|
+
private readonly command;
|
|
570
|
+
private readonly args;
|
|
571
|
+
private readonly maxRetries;
|
|
572
|
+
private readonly retryDelayMs;
|
|
573
|
+
private readonly pending;
|
|
574
|
+
private child;
|
|
575
|
+
private buffer;
|
|
576
|
+
private retryCount;
|
|
577
|
+
private dead;
|
|
578
|
+
private closing;
|
|
579
|
+
constructor(options: StdioForwarderOptions);
|
|
580
|
+
/** Spawn the child process and set up event handlers. */
|
|
581
|
+
start(): Promise<void>;
|
|
582
|
+
forward(request: McpRequest): Promise<ForwardResult>;
|
|
583
|
+
/** Gracefully close the child process. */
|
|
584
|
+
close(): Promise<void>;
|
|
585
|
+
/** Handle incoming data from child stdout, buffering partial lines. */
|
|
586
|
+
private onData;
|
|
587
|
+
/** Process a complete line from stdout. */
|
|
588
|
+
private onLine;
|
|
589
|
+
/** Handle unexpected child process exit (crash). */
|
|
590
|
+
private onUnexpectedExit;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Compile a validated PoliciesConfig into engine-ready form.
|
|
595
|
+
*
|
|
596
|
+
* Transforms raw config types into compiled types with pre-built glob
|
|
597
|
+
* matchers, pre-compiled regexes, and pre-parsed durations. Returns
|
|
598
|
+
* warnings for semantic issues (e.g. missing approval config).
|
|
599
|
+
*
|
|
600
|
+
* @throws {PolicyParseError} On invalid glob patterns or regex strings.
|
|
601
|
+
*/
|
|
602
|
+
declare function compilePolicies(config: PoliciesConfig): CompilePoliciesResult;
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Error thrown when policy compilation fails due to an invalid rule.
|
|
606
|
+
*
|
|
607
|
+
* Includes the rule index and optional name for clear error reporting.
|
|
608
|
+
*/
|
|
609
|
+
declare class PolicyParseError extends Error {
|
|
610
|
+
readonly ruleIndex: number;
|
|
611
|
+
readonly ruleName?: string;
|
|
612
|
+
constructor(message: string, ruleIndex: number, ruleName?: string);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Evaluate whether a compiled policy rule matches the given context.
|
|
617
|
+
*
|
|
618
|
+
* All present conditions in the rule's match block are AND'd together.
|
|
619
|
+
* A condition that is absent/undefined in the CompiledMatch is treated
|
|
620
|
+
* as "don't care" — it always passes. Short-circuits on the first false.
|
|
621
|
+
*/
|
|
622
|
+
declare function matchRule(rule: CompiledPolicyRule, ctx: MatchContext): boolean;
|
|
623
|
+
|
|
624
|
+
/** The result of evaluating a tools/call request against the compiled policy. */
|
|
625
|
+
interface PolicyDecision {
|
|
626
|
+
/** The action dictated by the matched rule (or the default policy). */
|
|
627
|
+
readonly action: PolicyAction;
|
|
628
|
+
/** The rule that matched, if any. Undefined when the default policy was applied. */
|
|
629
|
+
readonly matchedRule?: CompiledPolicyRule;
|
|
630
|
+
/** Human-readable explanation of the decision. */
|
|
631
|
+
readonly reason: string;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Evaluate a tools/call request against the compiled policy.
|
|
635
|
+
*
|
|
636
|
+
* Iterates the policy rules in definition order and returns the decision from
|
|
637
|
+
* the first matching rule (first-match-wins). If no rule matches, the policy's
|
|
638
|
+
* default action is applied.
|
|
639
|
+
*
|
|
640
|
+
* This function is pure and synchronous — no I/O, no side effects. It is the
|
|
641
|
+
* hot path and must complete in well under 1ms for any reasonable rule count.
|
|
642
|
+
*/
|
|
643
|
+
declare function evaluatePolicy(policy: CompiledPolicy, ctx: MatchContext): PolicyDecision;
|
|
644
|
+
|
|
645
|
+
/** A single audit log entry recording a tool call and its governance outcome. */
|
|
646
|
+
interface AuditRecord {
|
|
647
|
+
/** Unique record identifier (UUID v4). */
|
|
648
|
+
readonly id: string;
|
|
649
|
+
/** ISO 8601 timestamp of when the tool call was received by the proxy. */
|
|
650
|
+
readonly timestamp: string;
|
|
651
|
+
/** MCP session ID from the Mcp-Session-Id header, if present. */
|
|
652
|
+
readonly session_id: string | null;
|
|
653
|
+
/** Agent identifier from config or request header, if present. */
|
|
654
|
+
readonly agent_id: string | null;
|
|
655
|
+
/** Runtime environment label configured on proxy startup, if set. */
|
|
656
|
+
readonly environment: string | null;
|
|
657
|
+
/** The name of the tool that was called. */
|
|
658
|
+
readonly tool_name: string;
|
|
659
|
+
/** The arguments passed to the tool call. */
|
|
660
|
+
readonly tool_input: Record<string, unknown>;
|
|
661
|
+
/** The policy engine's decision: allow, deny, require_approval, etc. */
|
|
662
|
+
readonly policy_decision: string;
|
|
663
|
+
/** Structured block reason (when blocked), e.g. evidence_expired. */
|
|
664
|
+
readonly block_reason: string | null;
|
|
665
|
+
/** Name of the policy rule that matched, if any. */
|
|
666
|
+
readonly matched_rule: string | null;
|
|
667
|
+
/** Index of the matched rule in config order, if any. */
|
|
668
|
+
readonly matched_rule_index: number | null;
|
|
669
|
+
/** Evidence context from the evidence grounding system. */
|
|
670
|
+
readonly evidence_chain: Record<string, unknown> | null;
|
|
671
|
+
/** Approval workflow status: pending, approved, denied, timeout. */
|
|
672
|
+
readonly approval_status: string | null;
|
|
673
|
+
/** Identity of the approver, if approval was granted. */
|
|
674
|
+
readonly approved_by: string | null;
|
|
675
|
+
/**
|
|
676
|
+
* Upstream MCP server response. When `audit.include_responses` is true,
|
|
677
|
+
* this is the full JSON-RPC response body. When false, a {@link ResponseSummary}
|
|
678
|
+
* with only success/error status and content types. Null for denied calls
|
|
679
|
+
* (no upstream request was made).
|
|
680
|
+
*/
|
|
681
|
+
readonly upstream_response: unknown;
|
|
682
|
+
/** Error message from upstream, if the call failed. */
|
|
683
|
+
readonly upstream_error: string | null;
|
|
684
|
+
/** HTTP status code returned by upstream, if a response was received. */
|
|
685
|
+
readonly upstream_http_status: number | null;
|
|
686
|
+
/** Time in milliseconds the upstream request took. */
|
|
687
|
+
readonly upstream_latency_ms: number | null;
|
|
688
|
+
/** End-to-end time from request receipt to final response. */
|
|
689
|
+
readonly total_duration_ms: number;
|
|
690
|
+
/** Time spent waiting in the approval queue, if applicable. */
|
|
691
|
+
readonly approval_wait_ms: number;
|
|
692
|
+
/** Proxy compute time excluding approval waits and upstream processing. */
|
|
693
|
+
readonly proxy_compute_ms: number;
|
|
694
|
+
/** Whether the tool was flagged as potentially destructive (destructiveHint). */
|
|
695
|
+
readonly flagged_destructive: boolean;
|
|
696
|
+
/** Whether this record was produced in dry-run mode. */
|
|
697
|
+
readonly dry_run: boolean;
|
|
698
|
+
/** ISO 8601 timestamp of when the record was persisted. */
|
|
699
|
+
readonly created_at: string;
|
|
700
|
+
}
|
|
701
|
+
/** Fields available for filtering audit record queries. */
|
|
702
|
+
interface AuditQueryFilters {
|
|
703
|
+
/** Filter by tool name (exact match). */
|
|
704
|
+
readonly tool_name?: string;
|
|
705
|
+
/** Filter by policy decision (exact match). */
|
|
706
|
+
readonly policy_decision?: string;
|
|
707
|
+
/** Filter by block reason (exact match). */
|
|
708
|
+
readonly block_reason?: string;
|
|
709
|
+
/** Filter by whether a call was blocked (block_reason non-null). */
|
|
710
|
+
readonly blocked?: boolean;
|
|
711
|
+
/** Filter by session ID (exact match). */
|
|
712
|
+
readonly session_id?: string;
|
|
713
|
+
/** Filter by agent ID (exact match). */
|
|
714
|
+
readonly agent_id?: string;
|
|
715
|
+
/** Include only records created at or after this ISO 8601 timestamp. */
|
|
716
|
+
readonly from?: string;
|
|
717
|
+
/** Include only records created at or before this ISO 8601 timestamp. */
|
|
718
|
+
readonly to?: string;
|
|
719
|
+
/** Include only records flagged as destructive (true) or not (false). */
|
|
720
|
+
readonly flagged_destructive?: boolean;
|
|
721
|
+
/** Include only dry-run records (true) or non-dry-run records (false). */
|
|
722
|
+
readonly dry_run?: boolean;
|
|
723
|
+
/** Include only records where upstream HTTP status is >= this value. */
|
|
724
|
+
readonly upstream_status_min?: number;
|
|
725
|
+
/** Include only records where upstream HTTP status is <= this value. */
|
|
726
|
+
readonly upstream_status_max?: number;
|
|
727
|
+
}
|
|
728
|
+
/** Pagination options for list queries. */
|
|
729
|
+
interface AuditPaginationOptions {
|
|
730
|
+
/** Maximum number of records to return (default: 50, max: 1000). */
|
|
731
|
+
readonly limit?: number;
|
|
732
|
+
/** Number of records to skip (default: 0). */
|
|
733
|
+
readonly offset?: number;
|
|
734
|
+
/** Sort order by created_at (default: 'desc'). */
|
|
735
|
+
readonly order?: 'asc' | 'desc';
|
|
736
|
+
}
|
|
737
|
+
/** Paginated result set from a list query. */
|
|
738
|
+
interface AuditListResult {
|
|
739
|
+
readonly records: readonly AuditRecord[];
|
|
740
|
+
readonly total: number;
|
|
741
|
+
readonly limit: number;
|
|
742
|
+
readonly offset: number;
|
|
743
|
+
}
|
|
744
|
+
/** Time-bucket aggregation for dashboard charts. */
|
|
745
|
+
interface AuditTimeBucket {
|
|
746
|
+
/** ISO 8601 timestamp for the start of the bucket. */
|
|
747
|
+
readonly bucket: string;
|
|
748
|
+
/** Number of records in this bucket. */
|
|
749
|
+
readonly count: number;
|
|
750
|
+
}
|
|
751
|
+
/** Aggregated statistics for the dashboard. */
|
|
752
|
+
interface AuditAggregateStats {
|
|
753
|
+
/** Total number of records in the time range. */
|
|
754
|
+
readonly total: number;
|
|
755
|
+
/** Total records that resolved without a block (`block_reason IS NULL`). */
|
|
756
|
+
readonly allowed_total: number;
|
|
757
|
+
/** Total records that resolved with a block (`block_reason IS NOT NULL`). */
|
|
758
|
+
readonly blocked_total: number;
|
|
759
|
+
/** Total records produced in dry-run mode (`dry_run = 1`). */
|
|
760
|
+
readonly dry_run_total: number;
|
|
761
|
+
/** Total records produced in applied mode (`dry_run = 0`). */
|
|
762
|
+
readonly applied_total: number;
|
|
763
|
+
/** Counts grouped by policy decision. */
|
|
764
|
+
readonly by_decision: ReadonlyArray<{
|
|
765
|
+
readonly decision: string;
|
|
766
|
+
readonly count: number;
|
|
767
|
+
}>;
|
|
768
|
+
/** Counts grouped by block reason for blocked records only. */
|
|
769
|
+
readonly by_block_reason: ReadonlyArray<{
|
|
770
|
+
readonly reason: string;
|
|
771
|
+
readonly count: number;
|
|
772
|
+
}>;
|
|
773
|
+
/** Top tools by call count (max 10). */
|
|
774
|
+
readonly top_tools: ReadonlyArray<{
|
|
775
|
+
readonly tool_name: string;
|
|
776
|
+
readonly count: number;
|
|
777
|
+
}>;
|
|
778
|
+
/** Approval rate (approved / total require_approval decisions), or null if none. */
|
|
779
|
+
readonly approval_rate: number | null;
|
|
780
|
+
/** Records per hour over the time range. */
|
|
781
|
+
readonly per_hour: readonly AuditTimeBucket[];
|
|
782
|
+
}
|
|
783
|
+
/** Options for constructing an AuditStore. */
|
|
784
|
+
interface AuditStoreOptions {
|
|
785
|
+
/** Path to the SQLite database file (use ':memory:' for in-memory). */
|
|
786
|
+
readonly path: string;
|
|
787
|
+
/** Retention duration string (e.g. "90d"). Records older than this are purged. */
|
|
788
|
+
readonly retention: string;
|
|
789
|
+
/** Whether to store upstream response bodies. */
|
|
790
|
+
readonly includeResponses: boolean;
|
|
791
|
+
/** Interval in milliseconds for retention cleanup (default: 86_400_000 = 24h). Set to 0 to disable. */
|
|
792
|
+
readonly cleanupIntervalMs?: number;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* SQLite-backed audit record store.
|
|
797
|
+
*
|
|
798
|
+
* All read/write operations are synchronous (better-sqlite3 design).
|
|
799
|
+
* The AuditWriter wraps this with a batching/buffering layer.
|
|
800
|
+
*/
|
|
801
|
+
declare class AuditStore {
|
|
802
|
+
private readonly db;
|
|
803
|
+
private readonly insertStmt;
|
|
804
|
+
private readonly retentionMs;
|
|
805
|
+
private readonly includeResponses;
|
|
806
|
+
private cleanupTimer;
|
|
807
|
+
constructor(options: AuditStoreOptions);
|
|
808
|
+
/**
|
|
809
|
+
* Validate that the on-disk audit schema contains all required canonical columns.
|
|
810
|
+
*
|
|
811
|
+
* This keeps the clean-break migration policy while surfacing an actionable
|
|
812
|
+
* error for stale local databases instead of a low-level SQLite failure.
|
|
813
|
+
*/
|
|
814
|
+
private assertRequiredSchema;
|
|
815
|
+
/**
|
|
816
|
+
* Insert a single audit record. Returns the generated record ID.
|
|
817
|
+
*
|
|
818
|
+
* @param record - The record to insert (id and created_at are auto-generated).
|
|
819
|
+
* @param createdAt - Optional override for the created_at timestamp (for testing).
|
|
820
|
+
* @param id - Optional pre-generated ID (used by AuditWriter to share ID with SSE event bus).
|
|
821
|
+
*/
|
|
822
|
+
insert(record: Omit<AuditRecord, 'id' | 'created_at'>, createdAt?: string, id?: string): string;
|
|
823
|
+
/**
|
|
824
|
+
* Insert multiple audit records in a single SQLite transaction.
|
|
825
|
+
*
|
|
826
|
+
* Significantly faster than calling `insert()` in a loop because SQLite
|
|
827
|
+
* only syncs the WAL once per transaction. Records that fail to insert
|
|
828
|
+
* are skipped (the error callback is invoked) without aborting the batch.
|
|
829
|
+
*
|
|
830
|
+
* @param records - The records to insert.
|
|
831
|
+
* @param onError - Optional callback for per-record insert failures.
|
|
832
|
+
* @param onPersist - Optional callback for per-record successful inserts.
|
|
833
|
+
* @param ids - Optional pre-generated IDs (one per record). When provided,
|
|
834
|
+
* each ID is passed to `insert()` so the same UUID can be shared with
|
|
835
|
+
* the SSE event bus before the record is persisted.
|
|
836
|
+
* @returns The number of records successfully inserted.
|
|
837
|
+
*/
|
|
838
|
+
insertBatch(records: ReadonlyArray<Omit<AuditRecord, 'id' | 'created_at'>>, onError?: (record: Omit<AuditRecord, 'id' | 'created_at'>, err: unknown) => void, ids?: ReadonlyArray<string>, onPersist?: (record: Omit<AuditRecord, 'id' | 'created_at'>, id: string) => void): number;
|
|
839
|
+
/** Get a single record by ID, or undefined if not found. */
|
|
840
|
+
get(id: string): AuditRecord | undefined;
|
|
841
|
+
/** Query records with filters and pagination. */
|
|
842
|
+
list(filters?: AuditQueryFilters, pagination?: AuditPaginationOptions): AuditListResult;
|
|
843
|
+
/** Count records matching the given filters. */
|
|
844
|
+
count(filters?: AuditQueryFilters): number;
|
|
845
|
+
/** Get aggregate statistics for a time range. */
|
|
846
|
+
aggregate(from?: string, to?: string): AuditAggregateStats;
|
|
847
|
+
/** Delete records older than the retention period. Returns the count of deleted records. */
|
|
848
|
+
purgeExpired(): number;
|
|
849
|
+
/** Close the database and stop the cleanup timer. */
|
|
850
|
+
close(): void;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** Options for constructing an AuditWriter. */
|
|
854
|
+
interface AuditWriterOptions {
|
|
855
|
+
/** The underlying synchronous store to flush records into. */
|
|
856
|
+
readonly store: AuditStore;
|
|
857
|
+
/** Max records before a flush is triggered (default: 50). */
|
|
858
|
+
readonly bufferSize?: number;
|
|
859
|
+
/** Max milliseconds between flushes (default: 100). */
|
|
860
|
+
readonly flushIntervalMs?: number;
|
|
861
|
+
/** Optional callback invoked when a record enters the in-memory buffer. */
|
|
862
|
+
readonly onPush?: (record: Omit<AuditRecord, 'id' | 'created_at'>, id: string) => void;
|
|
863
|
+
/** Optional callback invoked after a record is successfully persisted. */
|
|
864
|
+
readonly onPersist?: (record: Omit<AuditRecord, 'id' | 'created_at'>, id: string) => void;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Async buffered audit record writer.
|
|
868
|
+
*
|
|
869
|
+
* Records are pushed into an in-memory buffer and flushed to the AuditStore
|
|
870
|
+
* in batches — either when the buffer reaches `bufferSize` or every
|
|
871
|
+
* `flushIntervalMs`, whichever comes first.
|
|
872
|
+
*/
|
|
873
|
+
declare class AuditWriter {
|
|
874
|
+
private readonly store;
|
|
875
|
+
private readonly bufferSize;
|
|
876
|
+
private readonly onPush;
|
|
877
|
+
private readonly onPersist;
|
|
878
|
+
private buffer;
|
|
879
|
+
private timer;
|
|
880
|
+
private flushSoonTimer;
|
|
881
|
+
private closed;
|
|
882
|
+
constructor(options: AuditWriterOptions);
|
|
883
|
+
/**
|
|
884
|
+
* Push an audit record into the buffer. Returns immediately.
|
|
885
|
+
*
|
|
886
|
+
* A UUID is pre-generated so it can be shared with the SSE event bus
|
|
887
|
+
* (via the onPush callback) and later reused at insert time.
|
|
888
|
+
*
|
|
889
|
+
* If the buffer reaches the configured threshold, a near-term async flush
|
|
890
|
+
* is scheduled. This keeps request-path latency bounded even under bursty
|
|
891
|
+
* write load.
|
|
892
|
+
*/
|
|
893
|
+
push(record: Omit<AuditRecord, 'id' | 'created_at'>): void;
|
|
894
|
+
/**
|
|
895
|
+
* Push a record and schedule a high-priority async flush.
|
|
896
|
+
*
|
|
897
|
+
* Security-critical records (deny, break-glass, rate/spend blocks) use this
|
|
898
|
+
* path so they are persisted on the next tick without blocking the request.
|
|
899
|
+
* A fatal-process crash still invokes the crash-drain hook, which calls
|
|
900
|
+
* `flush()` synchronously before exit.
|
|
901
|
+
*/
|
|
902
|
+
pushImmediate(record: Omit<AuditRecord, 'id' | 'created_at'>): void;
|
|
903
|
+
/**
|
|
904
|
+
* Schedule a flush on the next tick, coalescing multiple calls into one.
|
|
905
|
+
*/
|
|
906
|
+
private scheduleFlushSoon;
|
|
907
|
+
/** Flush all buffered records to the store in a single transaction. */
|
|
908
|
+
flush(): void;
|
|
909
|
+
/** Flush remaining records, stop the timer, and close the underlying store. */
|
|
910
|
+
close(): void;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* A single evidence entry recorded by the SDK.
|
|
915
|
+
*
|
|
916
|
+
* DTO: snake_case because it's emitted directly over `/api/evidence/:session_id`
|
|
917
|
+
* and the SDK sideband's `/session/:id/state`. `JSON.stringify(entry)` produces
|
|
918
|
+
* the wire shape without a mapping layer.
|
|
919
|
+
*/
|
|
920
|
+
interface EvidenceEntry {
|
|
921
|
+
readonly evidence_key: string;
|
|
922
|
+
readonly data: unknown;
|
|
923
|
+
readonly tool_name: string;
|
|
924
|
+
readonly timestamp: string;
|
|
925
|
+
readonly expires_at: number;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* A record that a tool was invoked in a session.
|
|
929
|
+
*
|
|
930
|
+
* DTO: snake_case because it appears inside `SessionState.completed_tools`.
|
|
931
|
+
*/
|
|
932
|
+
interface CompletedTool {
|
|
933
|
+
readonly tool_name: string;
|
|
934
|
+
readonly timestamp: string;
|
|
935
|
+
readonly succeeded: boolean;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Combined read-only state for a single session.
|
|
939
|
+
*
|
|
940
|
+
* DTO: snake_case because this is the response body of
|
|
941
|
+
* `/api/evidence/:session_id` and `/session/:session_id/state`.
|
|
942
|
+
*/
|
|
943
|
+
interface SessionState {
|
|
944
|
+
readonly session_id: string;
|
|
945
|
+
readonly evidence: Record<string, EvidenceEntry>;
|
|
946
|
+
readonly context: Record<string, unknown>;
|
|
947
|
+
readonly completed_tools: readonly CompletedTool[];
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Diagnostics preview of the current evidence-key allowlist.
|
|
951
|
+
*
|
|
952
|
+
* Internal helper type used when sideband writes are rejected because an
|
|
953
|
+
* evidence key is not configured in policy.
|
|
954
|
+
*/
|
|
955
|
+
interface EvidenceKeyAllowlistPreview {
|
|
956
|
+
readonly allowedKeys: readonly string[];
|
|
957
|
+
readonly allowedKeyCount: number;
|
|
958
|
+
readonly truncated: boolean;
|
|
959
|
+
}
|
|
960
|
+
/** Result of attempting to store evidence via putEvidence(). */
|
|
961
|
+
type PutEvidenceResult = {
|
|
962
|
+
readonly stored: true;
|
|
963
|
+
} | {
|
|
964
|
+
readonly stored: false;
|
|
965
|
+
readonly reason: 'closed';
|
|
966
|
+
} | {
|
|
967
|
+
readonly stored: false;
|
|
968
|
+
readonly reason: 'key_not_in_policy_allowlist';
|
|
969
|
+
readonly rejectedKey: string;
|
|
970
|
+
readonly allowlist: EvidenceKeyAllowlistPreview;
|
|
971
|
+
};
|
|
972
|
+
/** Result of attempting to write context via putContext(). */
|
|
973
|
+
type PutContextResult = {
|
|
974
|
+
readonly stored: true;
|
|
975
|
+
} | {
|
|
976
|
+
readonly stored: false;
|
|
977
|
+
readonly reason: 'closed';
|
|
978
|
+
};
|
|
979
|
+
/** Options for constructing an EvidenceStore. */
|
|
980
|
+
interface EvidenceStoreOptions {
|
|
981
|
+
/** Default TTL in seconds when not specified per-entry (default: 300). */
|
|
982
|
+
readonly defaultTtlSeconds?: number;
|
|
983
|
+
/** Cleanup sweep interval in milliseconds (default: 60_000). Set to 0 to disable. */
|
|
984
|
+
readonly cleanupIntervalMs?: number;
|
|
985
|
+
/**
|
|
986
|
+
* Inactivity TTL for sessions that have no live evidence entries but still
|
|
987
|
+
* contain context and/or completed tool history (default: 1 hour).
|
|
988
|
+
*/
|
|
989
|
+
readonly sessionInactivityMs?: number;
|
|
990
|
+
/**
|
|
991
|
+
* Optional allowlist of evidence keys accepted by putEvidence().
|
|
992
|
+
*
|
|
993
|
+
* When omitted, all keys are accepted. When provided, unknown keys are
|
|
994
|
+
* rejected to prevent high-cardinality session growth from dynamic keys.
|
|
995
|
+
*/
|
|
996
|
+
readonly allowedEvidenceKeys?: readonly string[];
|
|
997
|
+
/** Injectable clock for testing (default: Date.now). */
|
|
998
|
+
readonly now?: () => number;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/** Input for putEvidence (omits computed fields). */
|
|
1002
|
+
interface PutEvidenceInput {
|
|
1003
|
+
readonly evidence_key: string;
|
|
1004
|
+
readonly data: unknown;
|
|
1005
|
+
readonly tool_name: string;
|
|
1006
|
+
readonly ttl_seconds?: number;
|
|
1007
|
+
}
|
|
1008
|
+
declare class EvidenceStore {
|
|
1009
|
+
private static readonly EVIDENCE_ALLOWLIST_PREVIEW_LIMIT;
|
|
1010
|
+
private static readonly MAX_UNIQUE_REJECTION_WARNINGS;
|
|
1011
|
+
private static readonly REJECTION_WARNING_SUMMARY_INTERVAL;
|
|
1012
|
+
private readonly sessions;
|
|
1013
|
+
private readonly defaultTtlSeconds;
|
|
1014
|
+
private readonly sessionInactivityMs;
|
|
1015
|
+
private readonly now;
|
|
1016
|
+
private readonly warnedRejectedEvidenceKeys;
|
|
1017
|
+
private allowedEvidenceKeys;
|
|
1018
|
+
private suppressedRejectionWarningCount;
|
|
1019
|
+
private timer;
|
|
1020
|
+
private closed;
|
|
1021
|
+
constructor(options?: EvidenceStoreOptions);
|
|
1022
|
+
/** Insert or update an evidence entry for a session. */
|
|
1023
|
+
putEvidence(sessionId: string, input: PutEvidenceInput): PutEvidenceResult;
|
|
1024
|
+
/** Set an arbitrary context value for a session. */
|
|
1025
|
+
putContext(sessionId: string, key: string, value: unknown): PutContextResult;
|
|
1026
|
+
/** Get a single evidence entry, or undefined if missing/expired. */
|
|
1027
|
+
getEvidence(sessionId: string, evidenceKey: string): EvidenceEntry | undefined;
|
|
1028
|
+
/** Check whether valid (non-expired) evidence exists. */
|
|
1029
|
+
hasEvidence(sessionId: string, evidenceKey: string): boolean;
|
|
1030
|
+
/**
|
|
1031
|
+
* Check whether evidence with this key has ever been seen in the session.
|
|
1032
|
+
*
|
|
1033
|
+
* Distinct from hasEvidence(): a key may be "seen" but currently expired.
|
|
1034
|
+
*/
|
|
1035
|
+
hasSeenEvidence(sessionId: string, evidenceKey: string): boolean;
|
|
1036
|
+
/** Get a single context value, or undefined if missing. */
|
|
1037
|
+
getContext(sessionId: string, key: string): unknown;
|
|
1038
|
+
/**
|
|
1039
|
+
* Record that a tool was invoked in a session (for dependency chains).
|
|
1040
|
+
*
|
|
1041
|
+
* Dependency-chain success is *sticky*: once a tool has successfully
|
|
1042
|
+
* completed in a session, a later failed retry of the same tool must not
|
|
1043
|
+
* revoke the dependency. Otherwise an agent could legitimately satisfy a
|
|
1044
|
+
* dependency, get mid-session credentials, then lose access by accidentally
|
|
1045
|
+
* re-calling the same tool with a bad argument. The stored entry therefore
|
|
1046
|
+
* keeps `succeeded: true` once set; the timestamp refreshes on every call
|
|
1047
|
+
* so operators still see the most recent invocation time.
|
|
1048
|
+
*/
|
|
1049
|
+
recordToolCall(sessionId: string, toolName: string, succeeded: boolean): void;
|
|
1050
|
+
/** Check whether a tool was called in a session (regardless of outcome). */
|
|
1051
|
+
hasCompletedTool(sessionId: string, toolName: string): boolean;
|
|
1052
|
+
/** Check whether a tool was called AND succeeded in a session. */
|
|
1053
|
+
hasSuccessfulTool(sessionId: string, toolName: string): boolean;
|
|
1054
|
+
/**
|
|
1055
|
+
* Get a raw evidence entry WITHOUT lazy eviction.
|
|
1056
|
+
* Used by the grounding layer to distinguish "never stored" from "stored but expired".
|
|
1057
|
+
*/
|
|
1058
|
+
peekEvidence(sessionId: string, evidenceKey: string): EvidenceEntry | undefined;
|
|
1059
|
+
/** Get the full combined state for a session (evidence + context + completed tools). */
|
|
1060
|
+
getSessionState(sessionId: string): SessionState;
|
|
1061
|
+
/** Sweep all sessions, removing expired evidence and empty sessions. */
|
|
1062
|
+
cleanup(): void;
|
|
1063
|
+
/** Stop the cleanup timer and clear all state. Idempotent. */
|
|
1064
|
+
close(): void;
|
|
1065
|
+
/** Number of tracked sessions (for testing/debugging). */
|
|
1066
|
+
get sessionCount(): number;
|
|
1067
|
+
/** Number of evidence entries for a session (including expired, pre-eviction). */
|
|
1068
|
+
evidenceCount(sessionId: string): number;
|
|
1069
|
+
/** Number of evidence keys ever seen in a session. */
|
|
1070
|
+
seenEvidenceCount(sessionId: string): number;
|
|
1071
|
+
/**
|
|
1072
|
+
* Replace the evidence key allowlist.
|
|
1073
|
+
*
|
|
1074
|
+
* Unknown keys submitted through putEvidence() are ignored once an allowlist
|
|
1075
|
+
* is configured.
|
|
1076
|
+
*/
|
|
1077
|
+
setAllowedEvidenceKeys(keys: readonly string[]): void;
|
|
1078
|
+
private ensureSession;
|
|
1079
|
+
private touchSession;
|
|
1080
|
+
private isEvidenceKeyAllowed;
|
|
1081
|
+
private buildAllowlistPreview;
|
|
1082
|
+
private warnRejectedEvidenceKey;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/** Possible states of an approval ticket. */
|
|
1086
|
+
type ApprovalStatus = 'pending' | 'approved' | 'denied' | 'timeout' | 'break_glass' | 'client_disconnected' | 'shutdown_cancelled';
|
|
1087
|
+
/** A failed attempt to deliver an approval notification. */
|
|
1088
|
+
interface ApprovalNotificationFailure {
|
|
1089
|
+
readonly channel: string;
|
|
1090
|
+
readonly phase: 'initial' | 'escalation';
|
|
1091
|
+
readonly error: string;
|
|
1092
|
+
readonly failed_at: string;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* An approval ticket representing a held tools/call request waiting for a
|
|
1096
|
+
* human decision.
|
|
1097
|
+
*
|
|
1098
|
+
* DTO: field names are snake_case because this type is emitted directly
|
|
1099
|
+
* over REST `/api/approvals` and webhook payloads. `JSON.stringify(ticket)`
|
|
1100
|
+
* produces the wire shape without a mapping layer. Strictly internal types
|
|
1101
|
+
* in this file (e.g. `ApprovalOutcome`) remain idiomatic camelCase.
|
|
1102
|
+
*/
|
|
1103
|
+
interface ApprovalTicket {
|
|
1104
|
+
readonly id: string;
|
|
1105
|
+
readonly tool_name: string;
|
|
1106
|
+
readonly tool_input: Record<string, unknown>;
|
|
1107
|
+
readonly matched_rule: string | null;
|
|
1108
|
+
readonly rule_index: number | null;
|
|
1109
|
+
readonly channel_name: string;
|
|
1110
|
+
readonly session_id: string | null;
|
|
1111
|
+
readonly requested_at: string;
|
|
1112
|
+
readonly timeout_at: string;
|
|
1113
|
+
readonly timeout_ms: number;
|
|
1114
|
+
status: ApprovalStatus;
|
|
1115
|
+
resolved_at?: string;
|
|
1116
|
+
resolved_by?: string;
|
|
1117
|
+
denial_reason?: string;
|
|
1118
|
+
break_glass_reason?: string;
|
|
1119
|
+
escalated_at?: string;
|
|
1120
|
+
escalated_to?: string[];
|
|
1121
|
+
notification_failures?: ApprovalNotificationFailure[];
|
|
1122
|
+
}
|
|
1123
|
+
/** What the approval router returns to the governed forwarder. */
|
|
1124
|
+
type ApprovalOutcome = {
|
|
1125
|
+
readonly status: 'approved';
|
|
1126
|
+
readonly resolvedBy: string;
|
|
1127
|
+
readonly ticketId: string;
|
|
1128
|
+
} | {
|
|
1129
|
+
readonly status: 'denied';
|
|
1130
|
+
readonly resolvedBy: string;
|
|
1131
|
+
readonly reason?: string;
|
|
1132
|
+
readonly ticketId: string;
|
|
1133
|
+
} | {
|
|
1134
|
+
readonly status: 'timeout';
|
|
1135
|
+
readonly ticketId: string;
|
|
1136
|
+
readonly timeoutMs: number;
|
|
1137
|
+
} | {
|
|
1138
|
+
readonly status: 'client_disconnected';
|
|
1139
|
+
readonly ticketId: string;
|
|
1140
|
+
} | {
|
|
1141
|
+
readonly status: 'shutdown_cancelled';
|
|
1142
|
+
readonly ticketId: string;
|
|
1143
|
+
} | {
|
|
1144
|
+
readonly status: 'break_glass';
|
|
1145
|
+
readonly resolvedBy: string;
|
|
1146
|
+
readonly reason: string;
|
|
1147
|
+
readonly ticketId: string;
|
|
1148
|
+
};
|
|
1149
|
+
/**
|
|
1150
|
+
* Channel interface for approval notifications.
|
|
1151
|
+
*
|
|
1152
|
+
* Channels are responsible for notifying approvers when a new ticket
|
|
1153
|
+
* is created. The actual resolution happens via the REST API or the
|
|
1154
|
+
* channel's own callback mechanism.
|
|
1155
|
+
*/
|
|
1156
|
+
interface ApprovalChannel {
|
|
1157
|
+
/** Channel type identifier (e.g. 'dashboard', 'webhook', 'slack'). */
|
|
1158
|
+
readonly type: string;
|
|
1159
|
+
/** Send a notification about a new approval request. Fire-and-forget. */
|
|
1160
|
+
notify(ticket: ApprovalTicket): Promise<void>;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/** Options for constructing an ApprovalQueue. */
|
|
1164
|
+
interface ApprovalQueueOptions {
|
|
1165
|
+
/** Clock function for testable time. Defaults to `Date.now`. */
|
|
1166
|
+
readonly now?: () => number;
|
|
1167
|
+
/** Interval (ms) between cleanup sweeps. 0 disables cleanup. Default: 60000. */
|
|
1168
|
+
readonly cleanupIntervalMs?: number;
|
|
1169
|
+
/** How long (ms) to keep resolved tickets before cleanup. Default: 3600000 (1 hour). */
|
|
1170
|
+
readonly resolvedRetentionMs?: number;
|
|
1171
|
+
}
|
|
1172
|
+
/** Filter options for listing tickets. */
|
|
1173
|
+
interface ApprovalListFilter {
|
|
1174
|
+
readonly status?: ApprovalStatus;
|
|
1175
|
+
}
|
|
1176
|
+
declare class ApprovalQueue {
|
|
1177
|
+
private readonly tickets;
|
|
1178
|
+
private readonly now;
|
|
1179
|
+
private readonly resolvedRetentionMs;
|
|
1180
|
+
private timer;
|
|
1181
|
+
private closed;
|
|
1182
|
+
constructor(options?: ApprovalQueueOptions);
|
|
1183
|
+
/**
|
|
1184
|
+
* Create and store a new approval ticket.
|
|
1185
|
+
*
|
|
1186
|
+
* @returns the created ticket (with generated ID and timestamps).
|
|
1187
|
+
*/
|
|
1188
|
+
add(params: {
|
|
1189
|
+
tool_name: string;
|
|
1190
|
+
tool_input: Record<string, unknown>;
|
|
1191
|
+
matched_rule: string | null;
|
|
1192
|
+
rule_index: number | null;
|
|
1193
|
+
channel_name: string;
|
|
1194
|
+
session_id: string | null;
|
|
1195
|
+
timeout_ms: number;
|
|
1196
|
+
}): ApprovalTicket;
|
|
1197
|
+
/** Get a ticket by ID. Returns undefined if not found. */
|
|
1198
|
+
get(id: string): ApprovalTicket | undefined;
|
|
1199
|
+
/** List tickets, optionally filtered by status. */
|
|
1200
|
+
list(filter?: ApprovalListFilter): ApprovalTicket[];
|
|
1201
|
+
/** Shortcut: list only pending tickets. */
|
|
1202
|
+
listPending(): ApprovalTicket[];
|
|
1203
|
+
/**
|
|
1204
|
+
* Resolve a pending ticket.
|
|
1205
|
+
*
|
|
1206
|
+
* @returns `true` if the ticket was resolved, `false` if not found or
|
|
1207
|
+
* already resolved (no double-resolution).
|
|
1208
|
+
*/
|
|
1209
|
+
resolve(id: string, status: 'approved' | 'denied' | 'timeout' | 'break_glass' | 'client_disconnected' | 'shutdown_cancelled', resolvedBy?: string, options?: {
|
|
1210
|
+
denial_reason?: string;
|
|
1211
|
+
break_glass_reason?: string;
|
|
1212
|
+
}): boolean;
|
|
1213
|
+
/** Remove resolved tickets older than the retention period. */
|
|
1214
|
+
cleanup(): void;
|
|
1215
|
+
/** Clear the cleanup timer and mark the queue as closed. */
|
|
1216
|
+
close(): void;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/** Options for constructing an ApprovalRouter. */
|
|
1220
|
+
interface ApprovalRouterOptions {
|
|
1221
|
+
/** Default timeout (ms) when no rule-level timeout is specified. */
|
|
1222
|
+
readonly defaultTimeoutMs: number;
|
|
1223
|
+
/** What to do when a timeout fires: 'allow' forwards upstream, 'deny' blocks. */
|
|
1224
|
+
readonly defaultOnTimeout: 'allow' | 'deny';
|
|
1225
|
+
/** Map of channel-type → channel implementation. */
|
|
1226
|
+
readonly channels: Map<string, ApprovalChannel>;
|
|
1227
|
+
/** The approval queue for ticket storage. */
|
|
1228
|
+
readonly queue: ApprovalQueue;
|
|
1229
|
+
/** Clock function for testable time. Defaults to `Date.now`. */
|
|
1230
|
+
readonly now?: () => number;
|
|
1231
|
+
/** Optional callback when a new approval ticket is submitted. */
|
|
1232
|
+
readonly onSubmit?: (ticket: ApprovalTicket) => void;
|
|
1233
|
+
/** Optional callback when a ticket is resolved (approved, denied, break_glass, timeout, client_disconnected, shutdown_cancelled). */
|
|
1234
|
+
readonly onResolve?: (ticket: ApprovalTicket) => void;
|
|
1235
|
+
/** Optional callback when channel notification delivery fails. */
|
|
1236
|
+
readonly onNotifyFailure?: (event: ApprovalNotifyFailureEvent) => void;
|
|
1237
|
+
}
|
|
1238
|
+
/** Payload emitted when approval-channel notification delivery fails. */
|
|
1239
|
+
interface ApprovalNotifyFailureEvent {
|
|
1240
|
+
readonly ticket_id: string;
|
|
1241
|
+
readonly channel: string;
|
|
1242
|
+
readonly phase: 'initial' | 'escalation';
|
|
1243
|
+
readonly error: string;
|
|
1244
|
+
}
|
|
1245
|
+
/** Parameters for submitting an approval request. */
|
|
1246
|
+
interface ApprovalSubmitParams {
|
|
1247
|
+
readonly tool_name: string;
|
|
1248
|
+
readonly tool_input: Record<string, unknown>;
|
|
1249
|
+
readonly matched_rule: CompiledPolicyRule | undefined;
|
|
1250
|
+
readonly session_id: string | null;
|
|
1251
|
+
}
|
|
1252
|
+
declare class ApprovalRouter {
|
|
1253
|
+
private readonly defaultTimeoutMs;
|
|
1254
|
+
readonly defaultOnTimeout: 'allow' | 'deny';
|
|
1255
|
+
private readonly channels;
|
|
1256
|
+
private readonly queue;
|
|
1257
|
+
private readonly now;
|
|
1258
|
+
private readonly onSubmit;
|
|
1259
|
+
private readonly onResolve;
|
|
1260
|
+
private readonly onNotifyFailure;
|
|
1261
|
+
private readonly pending;
|
|
1262
|
+
private closed;
|
|
1263
|
+
constructor(options: ApprovalRouterOptions);
|
|
1264
|
+
/**
|
|
1265
|
+
* Submit a tool call for approval. Returns a Promise that resolves
|
|
1266
|
+
* when the approval is granted, denied, or times out.
|
|
1267
|
+
*
|
|
1268
|
+
* The governed forwarder `await`s this Promise while the HTTP request
|
|
1269
|
+
* is held open.
|
|
1270
|
+
*/
|
|
1271
|
+
submit(params: ApprovalSubmitParams, abortSignal?: AbortSignal): Promise<ApprovalOutcome>;
|
|
1272
|
+
/**
|
|
1273
|
+
* Approve a pending ticket. Resolves the held Promise so the governed
|
|
1274
|
+
* forwarder can forward the request upstream.
|
|
1275
|
+
*
|
|
1276
|
+
* @returns `true` if the ticket was approved, `false` if not found or
|
|
1277
|
+
* already resolved.
|
|
1278
|
+
*/
|
|
1279
|
+
approve(ticketId: string, approvedBy: string): boolean;
|
|
1280
|
+
/**
|
|
1281
|
+
* Deny a pending ticket. Resolves the held Promise so the governed
|
|
1282
|
+
* forwarder can return structured denial feedback.
|
|
1283
|
+
*
|
|
1284
|
+
* @returns `true` if the ticket was denied, `false` if not found or
|
|
1285
|
+
* already resolved.
|
|
1286
|
+
*/
|
|
1287
|
+
deny(ticketId: string, deniedBy: string, reason?: string): boolean;
|
|
1288
|
+
/**
|
|
1289
|
+
* Break-glass override: force-approve a pending ticket, bypassing the
|
|
1290
|
+
* normal approval flow. The event is prominently flagged in the audit trail.
|
|
1291
|
+
*
|
|
1292
|
+
* @returns `true` if the ticket was resolved, `false` if not found or
|
|
1293
|
+
* already resolved.
|
|
1294
|
+
*/
|
|
1295
|
+
breakGlass(ticketId: string, resolvedBy: string, reason: string): boolean;
|
|
1296
|
+
/** Clean up all pending timers and resolve all pending promises. */
|
|
1297
|
+
close(): void;
|
|
1298
|
+
private finalizePending;
|
|
1299
|
+
private reportNotifyFailure;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/** Options for constructing a RateLimiter. */
|
|
1303
|
+
interface RateLimiterOptions {
|
|
1304
|
+
/** Clock function for testable time. Defaults to `Date.now`. */
|
|
1305
|
+
readonly now?: () => number;
|
|
1306
|
+
/** Interval (ms) between cleanup sweeps. 0 disables cleanup. Default: 60000. */
|
|
1307
|
+
readonly cleanupIntervalMs?: number;
|
|
1308
|
+
/** Optional callback when utilization exceeds the warning threshold. */
|
|
1309
|
+
readonly onWarning?: (state: RateLimitKeyState) => void;
|
|
1310
|
+
/** Utilization ratio (0-1) that triggers the warning callback. Default: 0.8. */
|
|
1311
|
+
readonly warningThreshold?: number;
|
|
1312
|
+
}
|
|
1313
|
+
/** Parameters for a rate limit check. */
|
|
1314
|
+
interface RateLimitCheckParams {
|
|
1315
|
+
/** The bucket key, constructed by the caller (e.g. "tool:send_email"). */
|
|
1316
|
+
readonly key: string;
|
|
1317
|
+
/** Maximum calls allowed within the window. */
|
|
1318
|
+
readonly maxCalls: number;
|
|
1319
|
+
/** Sliding window duration in milliseconds. */
|
|
1320
|
+
readonly windowMs: number;
|
|
1321
|
+
}
|
|
1322
|
+
/** Result of a rate limit check. */
|
|
1323
|
+
interface RateLimitResult {
|
|
1324
|
+
/** Whether the call was allowed (under the limit). */
|
|
1325
|
+
readonly allowed: boolean;
|
|
1326
|
+
/** Number of calls in the current window (after this check). */
|
|
1327
|
+
readonly current: number;
|
|
1328
|
+
/** The configured max calls. */
|
|
1329
|
+
readonly limit: number;
|
|
1330
|
+
/** The configured window in milliseconds. */
|
|
1331
|
+
readonly windowMs: number;
|
|
1332
|
+
/** Timestamp (ms) when the oldest entry in the window expires. 0 if no entries. */
|
|
1333
|
+
readonly resetAtMs: number;
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Read-only snapshot of a rate limit key's current state (for dashboard).
|
|
1337
|
+
*
|
|
1338
|
+
* DTO: field names are snake_case because this type is emitted directly over
|
|
1339
|
+
* `/api/limits`. `JSON.stringify(state)` produces the wire shape without a
|
|
1340
|
+
* mapping layer. Strictly internal types in this file (e.g. `RateLimitResult`,
|
|
1341
|
+
* `RateLimitCheckParams`) remain idiomatic camelCase.
|
|
1342
|
+
*/
|
|
1343
|
+
interface RateLimitKeyState {
|
|
1344
|
+
readonly key: string;
|
|
1345
|
+
readonly current: number;
|
|
1346
|
+
readonly limit: number;
|
|
1347
|
+
readonly window_ms: number;
|
|
1348
|
+
readonly reset_at_ms: number;
|
|
1349
|
+
}
|
|
1350
|
+
declare class RateLimiter {
|
|
1351
|
+
private readonly buckets;
|
|
1352
|
+
private readonly now;
|
|
1353
|
+
private readonly onWarning;
|
|
1354
|
+
private readonly warningThreshold;
|
|
1355
|
+
private timer;
|
|
1356
|
+
private closed;
|
|
1357
|
+
constructor(options?: RateLimiterOptions);
|
|
1358
|
+
/**
|
|
1359
|
+
* Check and optionally record a call against the rate limit.
|
|
1360
|
+
*
|
|
1361
|
+
* Evicts expired timestamps, then checks the count:
|
|
1362
|
+
* - Under limit: records the timestamp and returns `allowed: true`
|
|
1363
|
+
* - At/over limit: does NOT record (blocked calls don't consume a slot)
|
|
1364
|
+
*/
|
|
1365
|
+
check(params: RateLimitCheckParams): RateLimitResult;
|
|
1366
|
+
/**
|
|
1367
|
+
* Check the rate limit without recording the call (non-destructive).
|
|
1368
|
+
*
|
|
1369
|
+
* Used by dry-run mode to determine what would happen without consuming
|
|
1370
|
+
* a slot in the bucket.
|
|
1371
|
+
*/
|
|
1372
|
+
peek(params: RateLimitCheckParams): RateLimitResult;
|
|
1373
|
+
/** Get the current state of a single key. Returns undefined if not tracked. */
|
|
1374
|
+
getKeyState(key: string): RateLimitKeyState | undefined;
|
|
1375
|
+
/** List all tracked keys with their current state. */
|
|
1376
|
+
listKeyStates(): RateLimitKeyState[];
|
|
1377
|
+
/** Sweep all buckets: remove expired timestamps, delete empty buckets. */
|
|
1378
|
+
cleanup(): void;
|
|
1379
|
+
/** Clear all rate limit state. Called on policy hot-reload. */
|
|
1380
|
+
reset(): void;
|
|
1381
|
+
/**
|
|
1382
|
+
* Reconcile bucket state against a new policy's limit configuration.
|
|
1383
|
+
*
|
|
1384
|
+
* Walks every existing bucket and checks whether its last-seen
|
|
1385
|
+
* `{ maxCalls, windowMs }` tuple still appears anywhere in `validConfigs`.
|
|
1386
|
+
* Buckets whose config is still present are left untouched — counters and
|
|
1387
|
+
* elapsed-window progress are preserved across hot-reloads. Buckets whose
|
|
1388
|
+
* config is gone (rule changed or removed) are evicted so the next check
|
|
1389
|
+
* lazy-creates a fresh bucket under the new config.
|
|
1390
|
+
*
|
|
1391
|
+
* This is the compare-and-evict semantic that replaces the old `reset()`
|
|
1392
|
+
* call on every hot-reload, which wiped all state even when the matching
|
|
1393
|
+
* rule was unchanged.
|
|
1394
|
+
*/
|
|
1395
|
+
reconcile(validConfigs: Iterable<{
|
|
1396
|
+
maxCalls: number;
|
|
1397
|
+
windowMs: number;
|
|
1398
|
+
}>): void;
|
|
1399
|
+
/** Stop the cleanup timer and mark as closed. */
|
|
1400
|
+
close(): void;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/** Options for constructing a SpendLimiter. */
|
|
1404
|
+
interface SpendLimiterOptions {
|
|
1405
|
+
/** Clock function for testable time. Defaults to `Date.now`. */
|
|
1406
|
+
readonly now?: () => number;
|
|
1407
|
+
/** Interval (ms) between cleanup sweeps. 0 disables cleanup. Default: 60000. */
|
|
1408
|
+
readonly cleanupIntervalMs?: number;
|
|
1409
|
+
/** Optional callback when utilization exceeds the warning threshold. */
|
|
1410
|
+
readonly onWarning?: (state: SpendLimitKeyState) => void;
|
|
1411
|
+
/** Utilization ratio (0-1) that triggers the warning callback. Default: 0.8. */
|
|
1412
|
+
readonly warningThreshold?: number;
|
|
1413
|
+
}
|
|
1414
|
+
/** Parameters for a spend limit check. */
|
|
1415
|
+
interface SpendLimitCheckParams {
|
|
1416
|
+
/** The bucket key, constructed by the caller (e.g. "tool:create_payment"). */
|
|
1417
|
+
readonly key: string;
|
|
1418
|
+
/** The monetary amount of this call. */
|
|
1419
|
+
readonly amount: number;
|
|
1420
|
+
/** Maximum cumulative spend allowed within the window. */
|
|
1421
|
+
readonly limit: number;
|
|
1422
|
+
/** Sliding window duration in milliseconds. */
|
|
1423
|
+
readonly windowMs: number;
|
|
1424
|
+
}
|
|
1425
|
+
/** Result of a spend limit check. */
|
|
1426
|
+
interface SpendLimitResult {
|
|
1427
|
+
/** Whether the spend was allowed (under the limit). */
|
|
1428
|
+
readonly allowed: boolean;
|
|
1429
|
+
/** Cumulative spend in the current window (after this check). */
|
|
1430
|
+
readonly currentSpend: number;
|
|
1431
|
+
/** The configured spend limit. */
|
|
1432
|
+
readonly limit: number;
|
|
1433
|
+
/** The configured window in milliseconds. */
|
|
1434
|
+
readonly windowMs: number;
|
|
1435
|
+
/** Timestamp (ms) when the oldest entry in the window expires. 0 if no entries. */
|
|
1436
|
+
readonly resetAtMs: number;
|
|
1437
|
+
/**
|
|
1438
|
+
* Set when the call was rejected for a reason other than "would exceed limit".
|
|
1439
|
+
* `'invalid_amount'` indicates the amount was negative, NaN, or non-finite.
|
|
1440
|
+
*/
|
|
1441
|
+
readonly reason?: 'invalid_amount';
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Read-only snapshot of a spend limit key's current state (for dashboard).
|
|
1445
|
+
*
|
|
1446
|
+
* DTO: field names are snake_case because this type is emitted directly over
|
|
1447
|
+
* `/api/limits`. `JSON.stringify(state)` produces the wire shape without a
|
|
1448
|
+
* mapping layer. Strictly internal types in this file (e.g. `SpendLimitResult`,
|
|
1449
|
+
* `SpendLimitCheckParams`) remain idiomatic camelCase.
|
|
1450
|
+
*/
|
|
1451
|
+
interface SpendLimitKeyState {
|
|
1452
|
+
readonly key: string;
|
|
1453
|
+
readonly current_spend: number;
|
|
1454
|
+
readonly limit: number;
|
|
1455
|
+
readonly currency: string;
|
|
1456
|
+
readonly window_ms: number;
|
|
1457
|
+
readonly reset_at_ms: number;
|
|
1458
|
+
}
|
|
1459
|
+
declare class SpendLimiter {
|
|
1460
|
+
private readonly buckets;
|
|
1461
|
+
private readonly now;
|
|
1462
|
+
private readonly onWarning;
|
|
1463
|
+
private readonly warningThreshold;
|
|
1464
|
+
private timer;
|
|
1465
|
+
private closed;
|
|
1466
|
+
constructor(options?: SpendLimiterOptions);
|
|
1467
|
+
/**
|
|
1468
|
+
* Check and optionally record a spend against the limit.
|
|
1469
|
+
*
|
|
1470
|
+
* Evicts expired entries, sums remaining amounts, then checks:
|
|
1471
|
+
* - Under limit (currentSpend + amount <= limit): records and returns `allowed: true`
|
|
1472
|
+
* - Would exceed: does NOT record (rejected spends don't consume budget)
|
|
1473
|
+
*/
|
|
1474
|
+
check(params: SpendLimitCheckParams): SpendLimitResult;
|
|
1475
|
+
/**
|
|
1476
|
+
* Check the spend limit without recording the spend (non-destructive).
|
|
1477
|
+
*
|
|
1478
|
+
* Used by dry-run mode to determine what would happen without consuming
|
|
1479
|
+
* budget in the bucket.
|
|
1480
|
+
*/
|
|
1481
|
+
peek(params: SpendLimitCheckParams): SpendLimitResult;
|
|
1482
|
+
/**
|
|
1483
|
+
* Set the display currency for a key. Called by the governed forwarder
|
|
1484
|
+
* after check() so dashboard reads include the currency label.
|
|
1485
|
+
*/
|
|
1486
|
+
setCurrency(key: string, currency: string): void;
|
|
1487
|
+
/** Get the current state of a single key. Returns undefined if not tracked. */
|
|
1488
|
+
getKeyState(key: string): SpendLimitKeyState | undefined;
|
|
1489
|
+
/** List all tracked keys with their current state. */
|
|
1490
|
+
listKeyStates(): SpendLimitKeyState[];
|
|
1491
|
+
/** Sweep all buckets: remove expired entries, delete empty buckets. */
|
|
1492
|
+
cleanup(): void;
|
|
1493
|
+
/** Clear all spend limit state. Called on policy hot-reload. */
|
|
1494
|
+
reset(): void;
|
|
1495
|
+
/**
|
|
1496
|
+
* Reconcile bucket state against a new policy's spend configuration.
|
|
1497
|
+
*
|
|
1498
|
+
* Walks every existing bucket and checks whether its last-seen
|
|
1499
|
+
* `{ limit, currency, windowMs }` tuple still appears in `validConfigs`.
|
|
1500
|
+
* Buckets whose config is unchanged are left untouched — cumulative spend
|
|
1501
|
+
* and elapsed-window progress are preserved across hot-reloads. Buckets
|
|
1502
|
+
* whose config is gone (rule changed or removed) are evicted so the next
|
|
1503
|
+
* check lazy-creates a fresh bucket under the new config.
|
|
1504
|
+
*
|
|
1505
|
+
* Currency is part of the tuple because a USD→EUR switch is a meaningful
|
|
1506
|
+
* policy change — the same numeric limit buys a different amount of real
|
|
1507
|
+
* spend, so the bucket must reset. This replaces the old `reset()` call
|
|
1508
|
+
* on every hot-reload, which wiped all state even when the matching rule
|
|
1509
|
+
* was unchanged.
|
|
1510
|
+
*/
|
|
1511
|
+
reconcile(validConfigs: Iterable<{
|
|
1512
|
+
limit: number;
|
|
1513
|
+
currency: string;
|
|
1514
|
+
windowMs: number;
|
|
1515
|
+
}>): void;
|
|
1516
|
+
/** Stop the cleanup timer and mark as closed. */
|
|
1517
|
+
close(): void;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/** Options for constructing a GovernedForwarder. */
|
|
1521
|
+
interface GovernedForwarderOptions {
|
|
1522
|
+
/** The current environment label (e.g. "production", "staging"). */
|
|
1523
|
+
environment?: string;
|
|
1524
|
+
/** Async audit writer for recording tool call decisions. */
|
|
1525
|
+
auditWriter?: AuditWriter;
|
|
1526
|
+
/** In-memory evidence store for evidence grounding. */
|
|
1527
|
+
evidenceStore?: EvidenceStore;
|
|
1528
|
+
/** Approval router for handling require_approval decisions. */
|
|
1529
|
+
approvalRouter?: ApprovalRouter;
|
|
1530
|
+
/** Rate limiter for handling rate_limit decisions. */
|
|
1531
|
+
rateLimiter?: RateLimiter;
|
|
1532
|
+
/** Spend limiter for handling spend_limit decisions. */
|
|
1533
|
+
spendLimiter?: SpendLimiter;
|
|
1534
|
+
}
|
|
1535
|
+
/** Result of attempting to prime the tool annotation cache. */
|
|
1536
|
+
interface AnnotationCachePrimeResult {
|
|
1537
|
+
/** True when cache update succeeded from a valid tools/list response shape. */
|
|
1538
|
+
readonly success: boolean;
|
|
1539
|
+
/** Number of tools currently cached after the attempt. */
|
|
1540
|
+
readonly toolsCached: number;
|
|
1541
|
+
/** Failure reason when success is false. */
|
|
1542
|
+
readonly reason?: string;
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* A McpForwarder decorator that evaluates policy rules before forwarding
|
|
1546
|
+
* tools/call requests to the upstream MCP server.
|
|
1547
|
+
*
|
|
1548
|
+
* - tools/call: evaluate policy → allow, deny, or reject (unimplemented action)
|
|
1549
|
+
* - tools/list: forward, then cache tool annotations for future evaluations
|
|
1550
|
+
* - All other methods: pass through directly
|
|
1551
|
+
*/
|
|
1552
|
+
declare class GovernedForwarder implements McpForwarder {
|
|
1553
|
+
private readonly inner;
|
|
1554
|
+
private policy;
|
|
1555
|
+
private readonly environment;
|
|
1556
|
+
private readonly auditWriter;
|
|
1557
|
+
private readonly evidenceStore;
|
|
1558
|
+
private readonly approvalRouter;
|
|
1559
|
+
private readonly rateLimiter;
|
|
1560
|
+
private readonly spendLimiter;
|
|
1561
|
+
private readonly annotationCache;
|
|
1562
|
+
private agentKeyWarned;
|
|
1563
|
+
constructor(inner: McpForwarder, policy: CompiledPolicy, options?: GovernedForwarderOptions);
|
|
1564
|
+
/**
|
|
1565
|
+
* Swap the compiled policy atomically and reconcile limit bucket state
|
|
1566
|
+
* against the new configuration.
|
|
1567
|
+
*
|
|
1568
|
+
* Rate and spend limit buckets are preserved when their underlying rule
|
|
1569
|
+
* config is unchanged — this is what makes a benign hot-reload (e.g. a
|
|
1570
|
+
* `vim :w` with no real edits, or a whitespace-only config change) safe:
|
|
1571
|
+
* operators do not get a surprise zero of their live rate/spend state
|
|
1572
|
+
* mid-window. Buckets whose config changed or whose rule was removed are
|
|
1573
|
+
* evicted by the limiters' `reconcile()` methods, so the next check
|
|
1574
|
+
* lazy-creates a fresh bucket under the new config.
|
|
1575
|
+
*
|
|
1576
|
+
* See `packages/proxy/src/policy/rate-limiter.ts` and `spend-limiter.ts`
|
|
1577
|
+
* for the per-bucket compare-and-evict semantics.
|
|
1578
|
+
*/
|
|
1579
|
+
updatePolicy(policy: CompiledPolicy): void;
|
|
1580
|
+
/**
|
|
1581
|
+
* Prime the annotation cache by fetching tools/list directly from upstream.
|
|
1582
|
+
*
|
|
1583
|
+
* This path is intended for startup warm-up and intentionally bypasses policy
|
|
1584
|
+
* and audit handling. Runtime tools/list requests still flow through forward().
|
|
1585
|
+
*/
|
|
1586
|
+
primeAnnotationCache(): Promise<AnnotationCachePrimeResult>;
|
|
1587
|
+
forward(request: McpRequest): Promise<ForwardResult>;
|
|
1588
|
+
private handleToolsCall;
|
|
1589
|
+
private handleApproval;
|
|
1590
|
+
private handleRateLimit;
|
|
1591
|
+
private handleSpendLimit;
|
|
1592
|
+
/**
|
|
1593
|
+
* Handle dry-run mode: compute what would have happened without forwarding
|
|
1594
|
+
* or consuming any resources. Returns an MCP-compliant synthetic response.
|
|
1595
|
+
*/
|
|
1596
|
+
private handleDryRun;
|
|
1597
|
+
/** Construct a limit bucket key based on the configured key type. */
|
|
1598
|
+
private buildLimitKey;
|
|
1599
|
+
/** Determine if the request was actually forwarded to the upstream MCP server. */
|
|
1600
|
+
private wasForwardedUpstream;
|
|
1601
|
+
private writeAuditRecord;
|
|
1602
|
+
private makeDenyResult;
|
|
1603
|
+
private makePolicyMisconfiguredResult;
|
|
1604
|
+
private makeUnsupportedResult;
|
|
1605
|
+
private makeDryRunResult;
|
|
1606
|
+
private makeEvidenceBlockResult;
|
|
1607
|
+
private makeSessionRequiredBlockResult;
|
|
1608
|
+
private makeClientDisconnectedBlockResult;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/** Options for constructing the SDK sideband Hono app. */
|
|
1612
|
+
interface SidebandAppOptions {
|
|
1613
|
+
/**
|
|
1614
|
+
* Bearer token that every request must carry in its `Authorization`
|
|
1615
|
+
* header (except `GET /healthz`). When omitted or empty, the sideband
|
|
1616
|
+
* runs open — useful for local development when the operator has
|
|
1617
|
+
* explicitly disabled the per-boot token via env.
|
|
1618
|
+
*/
|
|
1619
|
+
readonly token?: string;
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Create a Hono app for the SDK sideband API.
|
|
1623
|
+
*
|
|
1624
|
+
* The sideband API allows the Python/TypeScript SDK to report evidence
|
|
1625
|
+
* and context to the proxy's in-memory evidence store. It runs on a
|
|
1626
|
+
* separate port (default 3200) bound to 127.0.0.1 only.
|
|
1627
|
+
*
|
|
1628
|
+
* Two orthogonal defenses apply to every request:
|
|
1629
|
+
*
|
|
1630
|
+
* 1. **CORS guard.** Any request carrying an `Origin` header is rejected
|
|
1631
|
+
* with 403 (including `Origin: null`), and any `OPTIONS` preflight is
|
|
1632
|
+
* rejected with 403.
|
|
1633
|
+
* The SDK itself never sets an `Origin` header; only browsers do. This
|
|
1634
|
+
* defends against a malicious local HTML file POSTing to
|
|
1635
|
+
* 127.0.0.1:3200 through the user's browser.
|
|
1636
|
+
* 2. **Bearer auth.** When `options.token` is set, every request except
|
|
1637
|
+
* `GET /healthz` must carry `Authorization: Bearer <token>`. The proxy
|
|
1638
|
+
* generates a fresh token per boot and prints it to stderr so the SDK
|
|
1639
|
+
* can pick it up via the `HELIO_SDK_TOKEN` env var. Health is left
|
|
1640
|
+
* unauthenticated so container probes keep working.
|
|
1641
|
+
*/
|
|
1642
|
+
declare function createSidebandApp(store: EvidenceStore, options?: SidebandAppOptions): Hono;
|
|
1643
|
+
|
|
1644
|
+
/** @internal Exported for testing only. */
|
|
1645
|
+
declare class QueueChannel implements ApprovalChannel {
|
|
1646
|
+
readonly type = "dashboard";
|
|
1647
|
+
notify(): Promise<void>;
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Build a map of channel key → ApprovalChannel from the approval config.
|
|
1651
|
+
*
|
|
1652
|
+
* Each channel is keyed by its `name` field if present, otherwise by its
|
|
1653
|
+
* `type`. This allows multiple channels of the same type (e.g. two webhook
|
|
1654
|
+
* channels with different names) to coexist.
|
|
1655
|
+
*
|
|
1656
|
+
* Policy rules reference channels by this key via `approval.channel`.
|
|
1657
|
+
*/
|
|
1658
|
+
declare function createChannels(channels: ReadonlyArray<ApprovalChannel$1>): Map<string, ApprovalChannel>;
|
|
1659
|
+
|
|
1660
|
+
/** Configuration for a webhook approval channel. */
|
|
1661
|
+
interface WebhookChannelOptions {
|
|
1662
|
+
/** The URL to POST approval notifications to. */
|
|
1663
|
+
readonly url: string;
|
|
1664
|
+
/** Optional HMAC-SHA256 secret for signing outbound payloads. */
|
|
1665
|
+
readonly secret?: string;
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Webhook approval channel.
|
|
1669
|
+
*
|
|
1670
|
+
* Sends an HTTP POST to a configured URL when a new approval ticket is
|
|
1671
|
+
* created. The payload includes an `event` field for future extensibility
|
|
1672
|
+
* and the full ticket data. When a `secret` is configured, the payload is
|
|
1673
|
+
* signed with HMAC-SHA256 and the signature is sent in the
|
|
1674
|
+
* `x-helio-signature` header.
|
|
1675
|
+
*
|
|
1676
|
+
* Errors are logged but never thrown — the ticket remains resolvable via
|
|
1677
|
+
* the REST API regardless of whether the webhook notification succeeds.
|
|
1678
|
+
*/
|
|
1679
|
+
declare class WebhookChannel implements ApprovalChannel {
|
|
1680
|
+
readonly type = "webhook";
|
|
1681
|
+
private readonly url;
|
|
1682
|
+
private readonly secret;
|
|
1683
|
+
constructor(options: WebhookChannelOptions);
|
|
1684
|
+
notify(ticket: ApprovalTicket): Promise<void>;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/** Configuration for a Slack approval channel. */
|
|
1688
|
+
interface SlackChannelOptions {
|
|
1689
|
+
/** Slack bot token (xoxb-...). */
|
|
1690
|
+
readonly botToken: string;
|
|
1691
|
+
/** Slack app signing secret for verifying action callbacks. */
|
|
1692
|
+
readonly signingSecret: string;
|
|
1693
|
+
/** Slack channel ID or name to post messages to. */
|
|
1694
|
+
readonly channel: string;
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Slack approval channel.
|
|
1698
|
+
*
|
|
1699
|
+
* Posts a Block Kit interactive message to the configured Slack channel
|
|
1700
|
+
* when a new approval ticket is created. The message includes Approve
|
|
1701
|
+
* and Deny buttons. Button clicks are handled by the separate Slack
|
|
1702
|
+
* action handler ({@link createSlackActionApp} in `slack-actions.ts`).
|
|
1703
|
+
*
|
|
1704
|
+
* Errors are logged but never thrown — the ticket remains resolvable via
|
|
1705
|
+
* the REST API regardless of whether the Slack notification succeeds.
|
|
1706
|
+
*/
|
|
1707
|
+
declare class SlackChannel implements ApprovalChannel {
|
|
1708
|
+
readonly type = "slack";
|
|
1709
|
+
/** Exposed for the Slack action handler to verify request signatures. */
|
|
1710
|
+
readonly signingSecret: string;
|
|
1711
|
+
private readonly client;
|
|
1712
|
+
private readonly channel;
|
|
1713
|
+
constructor(options: SlackChannelOptions);
|
|
1714
|
+
notify(ticket: ApprovalTicket): Promise<void>;
|
|
1715
|
+
/**
|
|
1716
|
+
* Update an existing Slack message — used by the action handler to
|
|
1717
|
+
* replace the approval buttons with a resolution status.
|
|
1718
|
+
*/
|
|
1719
|
+
updateMessage(channel: string, ts: string, text: string, blocks: KnownBlock[]): Promise<void>;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/** Options for the Slack action handler. */
|
|
1723
|
+
interface SlackActionAppOptions {
|
|
1724
|
+
/** The approval router for resolving tickets. */
|
|
1725
|
+
readonly router: ApprovalRouter;
|
|
1726
|
+
/** The channel map — used to find SlackChannel instances for signature
|
|
1727
|
+
* verification and message updates. */
|
|
1728
|
+
readonly channels: Map<string, ApprovalChannel>;
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Create a Hono app for handling Slack interactive action callbacks.
|
|
1732
|
+
*
|
|
1733
|
+
* Mounted on the main proxy server at `/slack/actions`. Verifies Slack
|
|
1734
|
+
* request signatures, resolves approval tickets, and updates the original
|
|
1735
|
+
* Slack message to show the decision.
|
|
1736
|
+
*/
|
|
1737
|
+
declare function createSlackActionApp(options: SlackActionAppOptions): Hono;
|
|
1738
|
+
|
|
1739
|
+
/** Options for the approval REST API. */
|
|
1740
|
+
interface ApprovalAppOptions {
|
|
1741
|
+
/** Bearer token for authenticating POST requests. When set, POST endpoints
|
|
1742
|
+
* require an `Authorization: Bearer <token>` header. GET endpoints remain
|
|
1743
|
+
* unprotected for dashboard / monitoring read access. */
|
|
1744
|
+
readonly apiSecret?: string;
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Create a Hono app for the approval REST API.
|
|
1748
|
+
*
|
|
1749
|
+
* Mounted exclusively on the dashboard sideband under `/api/approvals`.
|
|
1750
|
+
* The dashboard sideband's own bearer middleware covers every `/api/*`
|
|
1751
|
+
* path, so callers pass `apiSecret: undefined` here to avoid double
|
|
1752
|
+
* authentication. Provides endpoints for listing, inspecting,
|
|
1753
|
+
* approving, and denying approval tickets.
|
|
1754
|
+
*
|
|
1755
|
+
* Used by:
|
|
1756
|
+
* - Webhook callbacks
|
|
1757
|
+
* - Dashboard UI
|
|
1758
|
+
* - Programmatic/CLI access (through the dashboard sideband port)
|
|
1759
|
+
*/
|
|
1760
|
+
declare function createApprovalApp(router: ApprovalRouter, queue: ApprovalQueue, options?: ApprovalAppOptions): Hono;
|
|
1761
|
+
|
|
1762
|
+
/** Payload for an action event (new tool call processed). */
|
|
1763
|
+
interface ActionEvent {
|
|
1764
|
+
readonly id: string;
|
|
1765
|
+
readonly tool_name: string;
|
|
1766
|
+
readonly policy_decision: string;
|
|
1767
|
+
readonly block_reason: string | null;
|
|
1768
|
+
readonly approval_status: string | null;
|
|
1769
|
+
readonly session_id: string | null;
|
|
1770
|
+
readonly agent_id: string | null;
|
|
1771
|
+
readonly environment: string | null;
|
|
1772
|
+
readonly timestamp: string;
|
|
1773
|
+
readonly total_duration_ms: number;
|
|
1774
|
+
readonly approval_wait_ms: number;
|
|
1775
|
+
readonly proxy_compute_ms: number;
|
|
1776
|
+
readonly flagged_destructive: boolean;
|
|
1777
|
+
readonly dry_run: boolean;
|
|
1778
|
+
readonly matched_rule: string | null;
|
|
1779
|
+
readonly matched_rule_index: number | null;
|
|
1780
|
+
}
|
|
1781
|
+
/** Payload for an approval_requested event. */
|
|
1782
|
+
interface ApprovalRequestedEvent {
|
|
1783
|
+
readonly ticket_id: string;
|
|
1784
|
+
readonly tool_name: string;
|
|
1785
|
+
readonly channel: string;
|
|
1786
|
+
readonly requested_at: string;
|
|
1787
|
+
}
|
|
1788
|
+
/** Payload for an approval_resolved event. */
|
|
1789
|
+
interface ApprovalResolvedEvent {
|
|
1790
|
+
readonly ticket_id: string;
|
|
1791
|
+
readonly status: string;
|
|
1792
|
+
readonly resolved_by?: string;
|
|
1793
|
+
readonly resolved_at: string;
|
|
1794
|
+
}
|
|
1795
|
+
/** Payload for a limit_warning event (approaching threshold). */
|
|
1796
|
+
interface LimitWarningEvent {
|
|
1797
|
+
readonly key: string;
|
|
1798
|
+
readonly type: 'rate' | 'spend';
|
|
1799
|
+
readonly current: number;
|
|
1800
|
+
readonly limit: number;
|
|
1801
|
+
readonly utilization: number;
|
|
1802
|
+
}
|
|
1803
|
+
/** Payload for approval notification delivery failures. */
|
|
1804
|
+
interface ApprovalNotificationFailedEvent {
|
|
1805
|
+
readonly ticket_id: string;
|
|
1806
|
+
readonly channel: string;
|
|
1807
|
+
readonly phase: 'initial' | 'escalation';
|
|
1808
|
+
readonly error: string;
|
|
1809
|
+
}
|
|
1810
|
+
/** Map of event type names to their payload types. */
|
|
1811
|
+
interface DashboardEvents {
|
|
1812
|
+
action: ActionEvent;
|
|
1813
|
+
approval_requested: ApprovalRequestedEvent;
|
|
1814
|
+
approval_resolved: ApprovalResolvedEvent;
|
|
1815
|
+
limit_warning: LimitWarningEvent;
|
|
1816
|
+
approval_notification_failed: ApprovalNotificationFailedEvent;
|
|
1817
|
+
}
|
|
1818
|
+
/** Union of all dashboard event type names. */
|
|
1819
|
+
type DashboardEventType = keyof DashboardEvents;
|
|
1820
|
+
/**
|
|
1821
|
+
* Typed event bus for dashboard real-time updates.
|
|
1822
|
+
*
|
|
1823
|
+
* Wraps Node's `EventEmitter` with type-safe emit/on/off methods and an
|
|
1824
|
+
* `onAny()` helper that subscribes to all event types at once (used by the
|
|
1825
|
+
* SSE endpoint).
|
|
1826
|
+
*/
|
|
1827
|
+
declare class DashboardEventBus {
|
|
1828
|
+
private readonly emitter;
|
|
1829
|
+
constructor();
|
|
1830
|
+
/** Emit a typed event to all listeners. */
|
|
1831
|
+
emit<K extends DashboardEventType>(event: K, data: DashboardEvents[K]): void;
|
|
1832
|
+
/** Subscribe to a specific event type. */
|
|
1833
|
+
on<K extends DashboardEventType>(event: K, listener: (data: DashboardEvents[K]) => void): void;
|
|
1834
|
+
/** Unsubscribe from a specific event type. */
|
|
1835
|
+
off<K extends DashboardEventType>(event: K, listener: (data: DashboardEvents[K]) => void): void;
|
|
1836
|
+
/**
|
|
1837
|
+
* Subscribe to ALL event types. The listener receives the event type name
|
|
1838
|
+
* and the payload for each event.
|
|
1839
|
+
*
|
|
1840
|
+
* @returns An unsubscribe function that removes all listeners at once.
|
|
1841
|
+
*/
|
|
1842
|
+
onAny(listener: (event: DashboardEventType, data: DashboardEvents[DashboardEventType]) => void): () => void;
|
|
1843
|
+
/** Remove all listeners and release resources. */
|
|
1844
|
+
close(): void;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
/** Dependencies for the dashboard API. */
|
|
1848
|
+
interface DashboardAppDeps {
|
|
1849
|
+
readonly auditStore: AuditStore;
|
|
1850
|
+
readonly approvalRouter: ApprovalRouter;
|
|
1851
|
+
readonly approvalQueue: ApprovalQueue;
|
|
1852
|
+
readonly rateLimiter: RateLimiter;
|
|
1853
|
+
readonly spendLimiter: SpendLimiter;
|
|
1854
|
+
readonly evidenceStore: EvidenceStore;
|
|
1855
|
+
readonly eventBus: DashboardEventBus;
|
|
1856
|
+
}
|
|
1857
|
+
/** Options for the dashboard API. */
|
|
1858
|
+
interface DashboardAppOptions {
|
|
1859
|
+
/** Bearer token for authenticating POST (mutating) endpoints. */
|
|
1860
|
+
readonly apiSecret?: string;
|
|
1861
|
+
/** Absolute path to the dashboard's built static assets directory. */
|
|
1862
|
+
readonly staticDir?: string;
|
|
1863
|
+
/** SSE heartbeat interval in milliseconds. Defaults to 30 000 (30s). */
|
|
1864
|
+
readonly sseHeartbeatMs?: number;
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Create a Hono app for the dashboard REST + SSE API.
|
|
1868
|
+
*
|
|
1869
|
+
* This wrapper preserves the original API for call sites that do not need
|
|
1870
|
+
* an explicit lifecycle close hook.
|
|
1871
|
+
*/
|
|
1872
|
+
declare function createDashboardApp(deps: DashboardAppDeps, options?: DashboardAppOptions): Hono;
|
|
1873
|
+
|
|
1874
|
+
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, UpstreamForwarder, type UpstreamForwarderOptions, VERSION, WebhookChannel, type WebhookChannelOptions, compilePolicies, createApp, createApprovalApp, createChannels, createDashboardApp, createSidebandApp, createSlackActionApp, evaluatePolicy, loadConfig, matchRule, startServer, startSidebandServer };
|