@cleocode/runtime 2026.10.4 → 2026.10.5

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.
Files changed (36) hide show
  1. package/dist/{chunk-L5BLVQDA.js → chunk-4PUSKGDO.js} +2 -2
  2. package/dist/chunk-4PUSKGDO.js.map +1 -0
  3. package/dist/{chunk-KQBVXGOL.js → chunk-GULKX44R.js} +5 -3
  4. package/dist/chunk-GULKX44R.js.map +1 -0
  5. package/dist/{chunk-7Q7BEWM7.js → chunk-HXTRXEFI.js} +7 -9
  6. package/dist/chunk-HXTRXEFI.js.map +1 -0
  7. package/dist/{chunk-U2ESNIJ5.js → chunk-NOPLC5WS.js} +4 -2
  8. package/dist/chunk-NOPLC5WS.js.map +1 -0
  9. package/dist/chunk-PDZ5T4LO.js +597 -0
  10. package/dist/chunk-PDZ5T4LO.js.map +1 -0
  11. package/dist/{chunk-XJOMQIXL.js → chunk-WO6LJMDC.js} +4 -2
  12. package/dist/chunk-WO6LJMDC.js.map +1 -0
  13. package/dist/daemon/index.js +1 -1
  14. package/dist/dispatch-BWjVeeMg.d.ts +290 -0
  15. package/dist/gateway/dispatch.d.ts +5 -0
  16. package/dist/gateway/dispatch.js +53 -0
  17. package/dist/gateway/dispatch.js.map +1 -0
  18. package/dist/gateway/http/index.d.ts +897 -11
  19. package/dist/gateway/http/index.js +2 -2
  20. package/dist/gateway/index.d.ts +549 -4
  21. package/dist/gateway/index.js +26 -581
  22. package/dist/gateway/index.js.map +1 -1
  23. package/dist/gateway/mcp/index.d.ts +232 -10
  24. package/dist/gateway/mcp/index.js +2 -2
  25. package/dist/gateway/rpc/index.d.ts +5 -2036
  26. package/dist/gateway/rpc/index.js +1 -1
  27. package/dist/gateway-handler-DCU6E0a2.d.ts +80 -0
  28. package/dist/index.d.ts +4 -3
  29. package/dist/index.js +9 -6
  30. package/dist/index.js.map +1 -1
  31. package/package.json +7 -3
  32. package/dist/chunk-7Q7BEWM7.js.map +0 -1
  33. package/dist/chunk-KQBVXGOL.js.map +0 -1
  34. package/dist/chunk-L5BLVQDA.js.map +0 -1
  35. package/dist/chunk-U2ESNIJ5.js.map +0 -1
  36. package/dist/chunk-XJOMQIXL.js.map +0 -1
@@ -1,11 +1,897 @@
1
- export { G as GateDecision, a as GatewayStreamSource, H as HttpServerHandle, b as HttpServerOptions, c as HttpUnaryRequest, d as HttpUnaryResult, N as NODE_PTY_INSTALL_HINT, S as SSE_HEADERS, e as STUDIO_PREFIX, f as SseEmitter, g as SseFrame, h as SseSource, i as StreamSourceContext, j as StreamSourceTeardown, k as StudioStaticOptions, U as UpgradableServer, W as WS_PTY_PATH, l as WsPtyEndpointHandle, m as WsPtyOptions, n as attachWsPtyEndpoint, o as authorizeUpgrade, p as computeAcceptKey, q as createSseStream, r as decodeWsFrame, s as encodeSseFrame, t as encodeStreamEvent, u as encodeWsFrame, v as isStudioPath, w as isWsPtyPath, x as loadNodePty, y as mimeType, z as parseHttpRoute, A as registerStreamSource, B as resolveStreamSource, C as resolveStudioAssetPath, D as resolveStudioStaticDir, E as routeUnary, F as scrubPtyEnv, I as serveStudioStatic, J as startHttpServer, K as statusForResponse, L as tickStreamSource, M as writeBundleAbsentError } from '../rpc/index.js';
2
- import '@cleocode/contracts/gateway';
3
- import '@cleocode/core/store/background-jobs.js';
4
- import '@cleocode/contracts';
5
- import 'node:http';
6
- import '@cleocode/contracts/operations/orchestrate';
7
- import '@cleocode/core/internal';
8
- import '@cleocode/core';
9
- import '@cleocode/contracts/gateway/rpc';
10
- import 'node:net';
11
- import 'node:stream';
1
+ import { IncomingMessage, Server, ServerResponse } from 'node:http';
2
+ import { Gateway, DispatchResponse, GatewayStreamEvent } from '@cleocode/contracts/gateway';
3
+ import { Duplex } from 'node:stream';
4
+ import { G as GatewayHandler } from '../../gateway-handler-DCU6E0a2.js';
5
+
6
+ /**
7
+ * WebSocket terminal/PTY endpoint on the `/v1` gateway HTTP listener (T11922 ·
8
+ * M5 realtime transport, WS half).
9
+ *
10
+ * The SSE half (T11921) gives the gateway a SERVER→CLIENT push stream; this
11
+ * module adds the BIDIRECTIONAL realtime transport an interactive TUI / terminal
12
+ * session needs: a WebSocket upgrade on the SAME `node:http` listener
13
+ * ({@link import('./listen.js').startHttpServer}) that bridges a PTY both
14
+ * directions over WS frames.
15
+ *
16
+ * Design constraints (all four ACs of T11922):
17
+ *
18
+ * - **AC1 — upgrade on the existing listener.** The WS endpoint is NOT a second
19
+ * server: it attaches to the daemon's existing HTTP server via its
20
+ * `'upgrade'` event ({@link attachWsPtyEndpoint}), so it inherits the loopback
21
+ * bind + the daemon's lifecycle. The endpoint path is `/v1/terminal/pty`.
22
+ * - **AC2 — bidirectional PTY bridge with backpressure.** Inbound WS frames are
23
+ * written to the PTY stdin; PTY output is framed back to the client. A control
24
+ * frame (`{type:'resize',cols,rows}`) resizes the PTY. Backpressure is honored
25
+ * on the socket write path (pause-on-`false`-write, resume on `'drain'`).
26
+ * - **AC3 — leak-free, deterministic teardown.** Every open/close/error/abort
27
+ * path routes through ONE idempotent `teardown()` that kills the PTY, removes
28
+ * its listeners, and ends the socket exactly once. A server `close()` tears
29
+ * down every live session.
30
+ * - **AC4 — auth/origin enforced at the upgrade edge; no secrets on the wire.**
31
+ * The upgrade is rejected with a `401`/`403` BEFORE any PTY is spawned unless
32
+ * it comes from loopback AND (when a token is configured) presents the bearer
33
+ * token AND (when an origin allowlist is configured) presents an allowed
34
+ * `Origin`. The spawned PTY runs under a MINIMAL, explicitly-constructed env
35
+ * ({@link scrubPtyEnv}) — the daemon's secret-bearing `process.env` is NEVER
36
+ * inherited, so a secret cannot be exfiltrated through the terminal stream.
37
+ *
38
+ * To keep the **publish surface clean** (D11142 dependency-discipline):
39
+ * - There is NO `ws` dependency. The minimal RFC 6455 server framing needed
40
+ * (accept handshake, decode masked client frames, encode unmasked server
41
+ * frames, close/ping/pong) is implemented over the raw `node:net` socket the
42
+ * `'upgrade'` event hands us. This adds ZERO weight to `@cleocode/runtime`.
43
+ * - `node-pty` is an OPTIONAL, dynamically-loaded dependency (same pattern as
44
+ * {@link import('../../../../core/src/llm/pi/gondolin-loader.js')} and
45
+ * `packages/core/src/tools/pty.ts`): the npm specifier is held in a variable,
46
+ * its shape is declared LOCALLY (no `import type`), and absence is a graceful
47
+ * close frame ("PTY backend unavailable") — NOT a crash. The endpoint always
48
+ * upgrades; it bridges a PTY only when the backend is present.
49
+ *
50
+ * @packageDocumentation
51
+ * @module @cleocode/runtime/gateway/http/ws-pty
52
+ *
53
+ * @task T11922
54
+ * @epic T11769
55
+ * @saga T10400
56
+ * @see ./sse.ts — the SSE half (server→client push) on the same listener
57
+ */
58
+
59
+ /** The canonical WS terminal/PTY endpoint path on the `/v1` facade. */
60
+ declare const WS_PTY_PATH = "/v1/terminal/pty";
61
+ /**
62
+ * The install hint surfaced to the client (as a WS close reason) when the PTY
63
+ * backend is unavailable. `node-pty` is OPTIONAL — the endpoint still upgrades
64
+ * and then closes cleanly with this reason rather than crashing the daemon.
65
+ */
66
+ declare const NODE_PTY_INSTALL_HINT: string;
67
+ /** A disposable subscription handle returned by `node-pty`'s event registrars. */
68
+ interface PtyDisposable {
69
+ dispose(): void;
70
+ }
71
+ /** The minimal structural shape of a spawned `node-pty` process the bridge uses. */
72
+ interface NodePtyProcess {
73
+ /** Subscribe to interleaved stdout/stderr output. */
74
+ onData(cb: (data: string) => void): PtyDisposable | undefined;
75
+ /** Subscribe to process exit. */
76
+ onExit(cb: (e: {
77
+ exitCode: number;
78
+ signal?: number;
79
+ }) => void): PtyDisposable | undefined;
80
+ /** Write to the PTY stdin. */
81
+ write(data: string): void;
82
+ /** Resize the PTY. */
83
+ resize(cols: number, rows: number): void;
84
+ /** Kill the underlying process (idempotent host-side; signal optional). */
85
+ kill(signal?: string): void;
86
+ }
87
+ /** The minimal structural shape of the `node-pty` module the bridge uses. */
88
+ interface NodePtyModule {
89
+ spawn(file: string, args: readonly string[], options: {
90
+ readonly name?: string;
91
+ readonly cols?: number;
92
+ readonly rows?: number;
93
+ readonly cwd?: string;
94
+ readonly env?: Record<string, string>;
95
+ }): NodePtyProcess;
96
+ }
97
+ /**
98
+ * Attempt to lazily load `node-pty`. Returns `null` when the optional dep is not
99
+ * installed, fails to load, or does not expose the `spawn` surface — so the
100
+ * caller closes the WS cleanly with {@link NODE_PTY_INSTALL_HINT} instead of
101
+ * crashing. NEVER throws.
102
+ *
103
+ * The import specifier is held in a variable (passed to the {@link importer}
104
+ * seam) so bundlers / TS do not treat the missing optional dep as a hard,
105
+ * statically-resolved dependency (same technique as the gondolin loader).
106
+ *
107
+ * @returns The shape-checked module, or `null` when unavailable.
108
+ */
109
+ declare function loadNodePty(): Promise<NodePtyModule | null>;
110
+ /**
111
+ * Build the MINIMAL environment the bridged PTY runs under (AC4).
112
+ *
113
+ * Starts EMPTY, copies only the small {@link PASSTHROUGH_KEYS} allowlist from the
114
+ * parent, pins `PATH` to {@link TRUSTED_PATH}, and forces `TERM=xterm-256color`
115
+ * for a sane terminal. The daemon's secrets are NEVER forwarded.
116
+ *
117
+ * @param parentEnv - The parent process environment (defaults to `process.env`).
118
+ * @returns The scrubbed, secret-free env for the PTY.
119
+ */
120
+ declare function scrubPtyEnv(parentEnv?: NodeJS.ProcessEnv): Record<string, string>;
121
+ /**
122
+ * Derive the `Sec-WebSocket-Accept` response header value from a client
123
+ * `Sec-WebSocket-Key` per RFC 6455 §4.2.2 (SHA-1 of key + GUID, base64).
124
+ *
125
+ * @param key - The client's `Sec-WebSocket-Key` header value.
126
+ * @returns The base64 accept hash.
127
+ */
128
+ declare function computeAcceptKey(key: string): string;
129
+ /**
130
+ * Encode a WS data frame (server→client, UNMASKED per RFC 6455 §5.1). Used for
131
+ * `text` (PTY output) and `close` control frames.
132
+ *
133
+ * @param opcode - The WS opcode (e.g. {@link OPCODE_TEXT}).
134
+ * @param payload - The frame payload bytes.
135
+ * @returns The framed bytes ready to write to the socket.
136
+ */
137
+ declare function encodeWsFrame(opcode: number, payload: Buffer): Buffer;
138
+ /** One decoded inbound WS frame. */
139
+ interface DecodedFrame {
140
+ /** The frame opcode. */
141
+ opcode: number;
142
+ /** The unmasked payload bytes. */
143
+ payload: Buffer;
144
+ /** Total bytes consumed from the input buffer (header + payload). */
145
+ consumed: number;
146
+ }
147
+ /**
148
+ * Decode a single inbound (client→server, MASKED) WS frame from a buffer.
149
+ *
150
+ * Returns `null` when the buffer does not yet hold a complete frame (the caller
151
+ * waits for more bytes). Client frames MUST be masked (RFC 6455 §5.3); the mask
152
+ * is applied to recover the payload. Fragmentation is not expected from a
153
+ * terminal client, so a non-FIN frame is decoded as-is (its opcode handled by
154
+ * the caller). NEVER throws on a partial buffer.
155
+ *
156
+ * @param buf - The accumulated inbound bytes.
157
+ * @returns The decoded frame, or `null` when more bytes are needed.
158
+ */
159
+ declare function decodeWsFrame(buf: Buffer): DecodedFrame | null;
160
+ /** Configuration for the WS terminal/PTY endpoint. */
161
+ interface WsPtyOptions {
162
+ /**
163
+ * Optional bearer token an upgrade MUST present (as `Authorization: Bearer
164
+ * <token>` or `?token=<token>`). When omitted, no token is required (loopback
165
+ * is still enforced unconditionally).
166
+ */
167
+ token?: string;
168
+ /**
169
+ * Optional `Origin` allowlist. When non-empty, an upgrade MUST present an
170
+ * `Origin` header whose value is in the set, else it is rejected `403`. When
171
+ * omitted/empty, the `Origin` header is not checked (loopback already bounds
172
+ * the surface).
173
+ */
174
+ allowedOrigins?: readonly string[];
175
+ /** The shell launched for the PTY (default `process.env.SHELL` or `/bin/sh`). */
176
+ shell?: string;
177
+ /** Working directory for the PTY (default `process.cwd()`). */
178
+ cwd?: string;
179
+ }
180
+ /** The decision of the upgrade-edge gate. */
181
+ type GateDecision = {
182
+ ok: true;
183
+ } | {
184
+ ok: false;
185
+ status: number;
186
+ reason: string;
187
+ };
188
+ /**
189
+ * Authorize a WS upgrade at the edge (AC4) — BEFORE any PTY is spawned.
190
+ *
191
+ * Enforced unconditionally:
192
+ * - the connection MUST originate from loopback (`127.0.0.1` / `::1`); a
193
+ * non-loopback peer is rejected `403` (the gateway is local-process-facing).
194
+ *
195
+ * Enforced when configured:
196
+ * - a `token` (matched against `Authorization: Bearer` or `?token=`); a missing
197
+ * or mismatched token is rejected `401`.
198
+ * - an `Origin` allowlist; a missing or disallowed `Origin` is rejected `403`.
199
+ *
200
+ * @param req - The inbound upgrade request.
201
+ * @param remoteAddress - The socket's remote address.
202
+ * @param opts - The endpoint auth configuration.
203
+ * @returns A {@link GateDecision} — `{ok:true}` to proceed, else a typed reject.
204
+ */
205
+ declare function authorizeUpgrade(req: IncomingMessage, remoteAddress: string | undefined, opts: WsPtyOptions): GateDecision;
206
+ /** Whether a request path is the WS terminal/PTY endpoint. */
207
+ declare function isWsPtyPath(url: string | undefined): boolean;
208
+ /**
209
+ * A handle returned by {@link attachWsPtyEndpoint} so the caller can detach the
210
+ * `'upgrade'` listener and tear down every live session on server close (AC3).
211
+ */
212
+ interface WsPtyEndpointHandle {
213
+ /** The number of live WS-PTY sessions (for tests / health). */
214
+ readonly sessionCount: number;
215
+ /** Detach the listener and tear down every live session. Idempotent. */
216
+ close(): void;
217
+ }
218
+ /**
219
+ * The shape of the HTTP server's `'upgrade'` event surface this module needs.
220
+ * Declared structurally so the module does not couple to a concrete `Server`.
221
+ */
222
+ interface UpgradableServer {
223
+ on(event: 'upgrade', listener: (req: IncomingMessage, socket: Duplex, head: Buffer) => void): void;
224
+ removeListener(event: 'upgrade', listener: (req: IncomingMessage, socket: Duplex, head: Buffer) => void): void;
225
+ }
226
+ /**
227
+ * Attach the WS terminal/PTY endpoint (AC1) to an existing HTTP server's
228
+ * `'upgrade'` event.
229
+ *
230
+ * Every upgrade to {@link WS_PTY_PATH} is gated ({@link authorizeUpgrade}, AC4)
231
+ * BEFORE any PTY is spawned; a rejected upgrade gets a raw `401`/`403` and the
232
+ * socket is destroyed. An accepted upgrade completes the RFC 6455 handshake and
233
+ * bridges a PTY bidirectionally ({@link bridgePty}, AC2). An upgrade to any OTHER
234
+ * path is left for other upgrade handlers (this listener only claims its own
235
+ * path and never destroys a foreign socket).
236
+ *
237
+ * Each live session is tracked so {@link WsPtyEndpointHandle.close} tears every
238
+ * one down deterministically on server close (AC3).
239
+ *
240
+ * @param server - The HTTP server whose `'upgrade'` event to attach to.
241
+ * @param opts - The endpoint auth + PTY configuration.
242
+ * @returns A handle to detach + tear down on server close.
243
+ */
244
+ declare function attachWsPtyEndpoint(server: UpgradableServer, opts?: WsPtyOptions): WsPtyEndpointHandle;
245
+
246
+ /**
247
+ * `node:http` server binding for the framework-agnostic HTTP gateway adapter.
248
+ *
249
+ * The HTTP adapter ({@link routeUnary} + the SSE primitives) is deliberately
250
+ * framework-agnostic — it owns NO port binding and NO `Request`/`Response`
251
+ * lifecycle, so an embedder (a SvelteKit route, a test harness) can drive it
252
+ * directly. The **daemon**, however, is itself the embedder: it must expose the
253
+ * gateway to external HTTP clients over a real listening socket. This module is
254
+ * that thin embedder — a minimal `node:http` server that parses the request URL
255
+ * into a `(gateway, domain, operation)` triple, reads the JSON body, and routes
256
+ * it through {@link routeUnary}. It deliberately stays in the runtime (no
257
+ * SvelteKit / no `@cleocode/cleo` / no drizzle dependency) so the daemon can
258
+ * host the gateway without reaching across a package boundary.
259
+ *
260
+ * Routing contract (intentionally minimal — the wire vocabulary is the same as
261
+ * every other transport, only the framing differs):
262
+ *
263
+ * `POST /<gateway>/<domain>/<operation>` with a JSON body → operation params
264
+ *
265
+ * where `<gateway>` is `query` | `mutate`. A non-POST method, an unparseable
266
+ * path, or a body that is not a JSON object is rejected at the wire edge with a
267
+ * LAFS-shaped error envelope — the injected {@link GatewayHandler} is never
268
+ * invoked with a malformed request.
269
+ *
270
+ * @packageDocumentation
271
+ * @module @cleocode/runtime/gateway/http
272
+ *
273
+ * @task T11451
274
+ * @epic T11254
275
+ * @saga T11243 SG-RUNTIME-UNIFICATION
276
+ */
277
+
278
+ /**
279
+ * Options for {@link startHttpServer}.
280
+ *
281
+ * Mirrors the connection-oriented {@link RpcServerOptions} shape: the caller
282
+ * resolves the bind coordinates so the runtime stays free of any
283
+ * `@cleocode/paths` dependency.
284
+ */
285
+ interface HttpServerOptions {
286
+ /**
287
+ * TCP port to bind. `0` selects an ephemeral port (the bound port is then
288
+ * readable from {@link HttpServerHandle.port}).
289
+ */
290
+ port: number;
291
+ /**
292
+ * Host/interface to bind. Defaults to `127.0.0.1` (loopback only) — the
293
+ * gateway is local-process-facing; exposing it on a public interface is an
294
+ * explicit caller decision.
295
+ */
296
+ host?: string;
297
+ /**
298
+ * WebSocket terminal/PTY endpoint configuration (T11922 · M5 WS half). When
299
+ * provided, a WS upgrade handler for {@link WS_PTY_PATH} (`/v1/terminal/pty`)
300
+ * is attached to this server's `'upgrade'` event — gated at the edge (loopback
301
+ * + optional token/origin) and bridging a PTY bidirectionally over WS frames.
302
+ * When OMITTED, no upgrade handler is attached and the server stays a pure
303
+ * unary+SSE HTTP listener (the capability is opt-in — the daemon enables it
304
+ * explicitly). The optional `node-pty` backend is loaded dynamically; if it is
305
+ * absent, the endpoint still upgrades and reports a clean "PTY backend
306
+ * unavailable" close rather than crashing.
307
+ */
308
+ wsPty?: WsPtyOptions;
309
+ /**
310
+ * Studio static asset directory for the bundled Web Studio (T11979).
311
+ *
312
+ * When provided, requests whose path starts with `/studio` are served as
313
+ * static files from this directory (the `build/client/` output from an
314
+ * `adapter-node` Studio build), with SPA fallback to `index.html` for
315
+ * sub-routes. When `undefined` or the path does not exist on disk, a clean
316
+ * JSON 503 envelope is returned instead of a stack trace — allowing the
317
+ * gateway to start without a Studio bundle present (dev checkouts).
318
+ *
319
+ * The value is resolved and injected by the caller (`cleo daemon serve`)
320
+ * using {@link resolveStudioStaticDir} from `@cleocode/cleo` so the runtime
321
+ * layer stays free of any path-resolution logic.
322
+ */
323
+ studioStaticDir?: string;
324
+ }
325
+ /**
326
+ * A live HTTP server handle returned by {@link startHttpServer}.
327
+ *
328
+ * Exposes the bound {@link Server}, the actually-bound `port` (resolved even
329
+ * when `0` was requested), and an idempotent {@link close}. The adapter never
330
+ * calls `process.exit` — lifecycle stays with the caller (R3 contract).
331
+ */
332
+ interface HttpServerHandle {
333
+ /** The bound `node:http` server. */
334
+ server: Server;
335
+ /** The actually-bound TCP port (resolved when `0` was requested). */
336
+ port: number;
337
+ /** Host/interface the server is bound to. */
338
+ host: string;
339
+ /** Close the server + all live connections; resolves once fully closed. */
340
+ close(): Promise<void>;
341
+ }
342
+ /**
343
+ * The result of parsing a request line into routing coordinates: a resolved
344
+ * unary triple, a recognized health probe, or a rejection for the wire edge.
345
+ */
346
+ type ParsedRoute = {
347
+ ok: true;
348
+ kind: 'unary';
349
+ gateway: Gateway;
350
+ domain: string;
351
+ operation: string;
352
+ } | {
353
+ ok: true;
354
+ kind: 'stream';
355
+ gateway: Gateway;
356
+ domain: string;
357
+ operation: string;
358
+ } | {
359
+ ok: true;
360
+ kind: 'health';
361
+ } | {
362
+ ok: false;
363
+ status: number;
364
+ code: string;
365
+ message: string;
366
+ };
367
+ /**
368
+ * Parse a gateway request line into a routing decision.
369
+ *
370
+ * Two POST routing vocabularies are accepted (the wire payload is identical to
371
+ * every other transport — only the framing differs):
372
+ *
373
+ * - `POST /v1/<domain>/<operation>` — the **versioned REST facade** (T11919).
374
+ * The gateway (`query` | `mutate`) is INFERRED from the registry, so the
375
+ * client need not name it. An unregistered `(domain, operation)` pair is a
376
+ * `404`.
377
+ * - `POST /<gateway>/<domain>/<operation>` — the **legacy** form, where the
378
+ * client names the gateway segment explicitly (validated against the two
379
+ * CQRS gateways so a client cannot smuggle an arbitrary value).
380
+ *
381
+ * Two GET routing vocabularies are accepted:
382
+ *
383
+ * - `GET /v1/health` — the liveness probe.
384
+ * - `GET /v1/<domain>/<operation>` — the **SSE streaming** endpoint (T11921),
385
+ * recognized only when the `(domain, operation)` pair is registered AND
386
+ * flagged `streaming: true` in {@link OPERATIONS} (resolved via
387
+ * {@link resolveStreamRoute}). The SAME pair is dispatched UNARY over POST,
388
+ * so the streaming GET co-exists with the unary POST route. A GET to a
389
+ * unary-only op is a `405` (method-not-allowed), distinguishing "route
390
+ * exists, this method is unsupported" from "no such route" (`404`).
391
+ *
392
+ * A non-POST/GET method is rejected at the edge (`405`); a path that resolves to
393
+ * neither vocabulary is a `404`. The injected {@link GatewayHandler} is never
394
+ * invoked with a malformed request.
395
+ *
396
+ * @param method - The HTTP request method.
397
+ * @param url - The HTTP request URL (path + query).
398
+ * @returns The parsed route, or a typed rejection for the wire edge.
399
+ */
400
+ declare function parseHttpRoute(method: string | undefined, url: string | undefined): ParsedRoute;
401
+ /**
402
+ * Start a `node:http` server that hosts the gateway over HTTP for external
403
+ * clients.
404
+ *
405
+ * Binds {@link HttpServerOptions.port} on {@link HttpServerOptions.host}
406
+ * (default loopback), routes every `POST /<gateway>/<domain>/<operation>`
407
+ * through the injected {@link GatewayHandler} via {@link routeUnary}, and
408
+ * resolves once listening. The returned {@link HttpServerHandle} exposes an
409
+ * idempotent `close()` for deterministic teardown (daemon shutdown, tests). The
410
+ * adapter never calls `process.exit`.
411
+ *
412
+ * The caller assembles the handler (via `createGatewayHandler` with its domain
413
+ * handlers + middleware) and injects it here — this server never builds
414
+ * handlers itself, keeping the runtime free of any `@cleocode/cleo` dependency.
415
+ *
416
+ * @param handler - The transport-neutral gateway handler to route through.
417
+ * @param opts - Bind coordinates.
418
+ * @returns A promise resolving to the live {@link HttpServerHandle}.
419
+ */
420
+ declare function startHttpServer(handler: GatewayHandler, opts: HttpServerOptions): Promise<HttpServerHandle>;
421
+
422
+ /**
423
+ * Wire + lifecycle types for the `@cleocode/runtime/gateway/http` adapter.
424
+ *
425
+ * The HTTP transport carries the SAME gateway payloads as every other transport
426
+ * — request/response shapes are owned by `@cleocode/contracts/gateway` (R3-T2 ·
427
+ * T11446) and the streaming frame by `GatewayStreamEvent`. This module holds
428
+ * only the runtime-adapter-local option/handle shapes (mirroring the MCP and RPC
429
+ * adapters' `types.ts`), so the runtime stays framework-agnostic and free of any
430
+ * `@cleocode/cleo` / SvelteKit / drizzle dependency.
431
+ *
432
+ * @task T11450
433
+ * @epic T11254
434
+ * @saga T11243
435
+ */
436
+
437
+ /**
438
+ * Where on the wire the `(gateway, domain, operation)` triple of an HTTP unary
439
+ * request is sourced from. The adapter is framework-agnostic, so the embedder
440
+ * (SvelteKit route, daemon HTTP server, test harness) supplies the resolved
441
+ * coordinates explicitly rather than the adapter parsing a URL itself.
442
+ */
443
+ interface HttpUnaryRequest {
444
+ /** CQRS gateway — `'mutate'` (write) or `'query'` (read). */
445
+ gateway: Gateway;
446
+ /** Target canonical domain (e.g. `'tasks'`). */
447
+ domain: string;
448
+ /** Domain operation name (e.g. `'show'`). */
449
+ operation: string;
450
+ /** Decoded JSON request body → operation params. */
451
+ params?: Record<string, unknown>;
452
+ /**
453
+ * Optional caller-supplied request id for tracing. When omitted the adapter
454
+ * mints a fresh UUID.
455
+ */
456
+ requestId?: string;
457
+ /** Bound session id, if any (forwarded into the dispatch request). */
458
+ sessionId?: string;
459
+ }
460
+ /**
461
+ * The result of routing one HTTP unary request through the gateway: the LAFS
462
+ * {@link DispatchResponse} JSON body plus the HTTP status the embedder should
463
+ * set (200 on success, 4xx/5xx mapped from the dispatch error).
464
+ *
465
+ * The adapter never writes the response itself (no framework coupling) — it
466
+ * returns this so the embedder serializes it onto its own `Response`.
467
+ */
468
+ interface HttpUnaryResult {
469
+ /** HTTP status code derived from the dispatch outcome. */
470
+ status: number;
471
+ /** The LAFS envelope to serialize as the JSON response body. */
472
+ body: DispatchResponse;
473
+ }
474
+
475
+ /**
476
+ * HTTP transport adapter — serves gateway unary + SSE over HTTP.
477
+ *
478
+ * The fourth and final gateway transport (after CLI, MCP, RPC). Unlike the
479
+ * connection-oriented RPC/MCP adapters, the HTTP adapter is framework-agnostic
480
+ * by design: it does NOT bind a port or own a `Request`/`Response` lifecycle.
481
+ * Instead it exposes two pure routing primitives the embedder (a SvelteKit
482
+ * route handler, the daemon's HTTP server, a test harness) calls directly:
483
+ *
484
+ * - {@link routeUnary} — one POST → `source: 'http'` {@link DispatchRequest}
485
+ * → injected {@link GatewayHandler} → `{ status, body }` (LAFS envelope).
486
+ * - the SSE primitives in `./sse.js` (`createSseStream`) for streaming ops.
487
+ *
488
+ * This keeps the runtime free of any HTTP-server / SvelteKit / `@cleocode/cleo`
489
+ * dependency: the embedder owns wire concerns (parsing the URL into a
490
+ * `(gateway, domain, operation)` triple, setting headers, `process.exit`,
491
+ * error-render at the edge), exactly mirroring how the CLI adapter wraps the
492
+ * same handler. There is NO `process.exit` and NO error-render in the handlers
493
+ * (R3 contract); the adapter never calls `process.exit` at all.
494
+ *
495
+ * The request `source` is forced to `'http'` regardless of what the embedder
496
+ * passes, so a client cannot impersonate another transport.
497
+ *
498
+ * @packageDocumentation
499
+ * @module @cleocode/runtime/gateway/http
500
+ *
501
+ * @task T11450
502
+ * @epic T11254
503
+ * @saga T11243
504
+ */
505
+
506
+ /**
507
+ * Map a LAFS dispatch error code to an HTTP status code.
508
+ *
509
+ * The canonical machine-readable error code (`E_NOT_FOUND`, `E_VALIDATION_…`,
510
+ * `E_FORBIDDEN`, …) is the source of truth; the numeric LAFS exit code is a
511
+ * secondary hint. Unknown error codes fall back to `500`. A successful envelope
512
+ * is always `200`.
513
+ *
514
+ * @param response - The dispatch response.
515
+ * @returns The HTTP status the embedder should set.
516
+ */
517
+ declare function statusForResponse(response: DispatchResponse): number;
518
+ /**
519
+ * Route a single HTTP unary request through the gateway handler.
520
+ *
521
+ * Builds a `source: 'http'` {@link DispatchRequest} from the embedder-resolved
522
+ * `(gateway, domain, operation, params)` triple and delegates to the injected
523
+ * {@link GatewayHandler}. A thrown handler error is trapped and rendered as a
524
+ * `500` LAFS error envelope — never propagated as an unhandled rejection or
525
+ * `process.exit`. The `source` is always forced to `'http'`.
526
+ *
527
+ * @param handler - The injected transport-neutral gateway handler.
528
+ * @param req - The embedder-resolved unary request coordinates + body params.
529
+ * @returns The HTTP status + LAFS response body for the embedder to serialize.
530
+ */
531
+ declare function routeUnary(handler: GatewayHandler, req: HttpUnaryRequest): Promise<HttpUnaryResult>;
532
+
533
+ /**
534
+ * Server-Sent Events (SSE) wire primitives for the `@cleocode/runtime/gateway/http`
535
+ * adapter.
536
+ *
537
+ * The HTTP transport is the only gateway transport that streams: long-running
538
+ * and subscription operations emit a sequence of {@link GatewayStreamEvent}
539
+ * frames terminated by a `done` or `error` frame. This module owns ONLY the SSE
540
+ * wire encoding + the `ReadableStream` lifecycle plumbing; the actual frame
541
+ * production (polling a DB, tailing a job, etc.) lives in the caller's async
542
+ * source. Keeping the encode/stream concern here lets BOTH the gateway-routed
543
+ * streaming ops AND the existing Studio SSE endpoints (which carry their own
544
+ * domain event shapes) share one tested, leak-free stream builder.
545
+ *
546
+ * Two encoders are provided so an existing stream can adopt this plumbing
547
+ * WITHOUT changing its observed bytes:
548
+ * - {@link encodeStreamEvent} — the canonical `data: <json>\n\n` form for
549
+ * {@link GatewayStreamEvent} frames (gateway-routed streaming ops).
550
+ * - {@link encodeSseFrame} — a generic `event:`/`id:`/`data:` writer for any
551
+ * payload, so endpoints with a bespoke wire (a named `event:` field, a
552
+ * `data:`-only frame) keep an IDENTICAL byte stream while still routing their
553
+ * lifecycle through the shared, abort-safe builder.
554
+ *
555
+ * @packageDocumentation
556
+ * @module @cleocode/runtime/gateway/http/sse
557
+ *
558
+ * @task T11450
559
+ * @epic T11254
560
+ * @saga T11243
561
+ */
562
+
563
+ /**
564
+ * One generic SSE frame: an optional named `event`, an optional `id`, and a
565
+ * `data` payload that is JSON-serialized.
566
+ *
567
+ * This is intentionally NOT {@link GatewayStreamEvent} — it is the raw wire
568
+ * shape so a bespoke endpoint can reproduce its exact existing bytes (e.g. a
569
+ * named `event: task-updated` line, or a `data:`-only frame with no `event`).
570
+ */
571
+ interface SseFrame {
572
+ /** Optional SSE `event:` field name (omitted → a `data:`-only frame). */
573
+ event?: string;
574
+ /** Optional SSE `id:` field for client-side last-event-id resumption. */
575
+ id?: string;
576
+ /** The frame payload; JSON-serialized into the `data:` field. */
577
+ data: unknown;
578
+ }
579
+ /**
580
+ * Encode a generic {@link SseFrame} to its SSE wire bytes.
581
+ *
582
+ * Field order is `event:` (if present) → `id:` (if present) → `data:`, matching
583
+ * the SSE spec record layout, then a blank line. When `event` is omitted the
584
+ * frame is a `data:`-only record — byte-identical to a handwritten
585
+ * `data: <json>\n\n`.
586
+ *
587
+ * @param frame - The frame to serialize.
588
+ * @returns The SSE record as a string (UTF-8 caller-encoded).
589
+ */
590
+ declare function encodeSseFrame(frame: SseFrame): string;
591
+ /**
592
+ * Encode a canonical {@link GatewayStreamEvent} as a `data:`-only SSE record.
593
+ *
594
+ * Gateway-routed streaming ops carry the discriminator + sequence inside the
595
+ * JSON payload (`{ kind, seq, data, error, requestId }`), so no named SSE
596
+ * `event:` field is used — the client demultiplexes on `kind`/`seq`. The
597
+ * monotonic `seq` is also surfaced as the SSE `id:` so a reconnecting client can
598
+ * send `Last-Event-ID`.
599
+ *
600
+ * @param frame - The gateway stream event to serialize.
601
+ * @returns The SSE record as a string.
602
+ */
603
+ declare function encodeStreamEvent(frame: GatewayStreamEvent): string;
604
+ /** The standard SSE response headers (no caching, no proxy buffering). */
605
+ declare const SSE_HEADERS: Readonly<Record<string, string>>;
606
+ /**
607
+ * A push-style SSE controller handed to an {@link SseSource} so it can emit
608
+ * frames and close the stream without touching the underlying
609
+ * `ReadableStreamDefaultController`.
610
+ */
611
+ interface SseEmitter {
612
+ /** Emit one generic SSE frame; a no-op once the stream is closed. */
613
+ send(frame: SseFrame): void;
614
+ /** Emit one canonical {@link GatewayStreamEvent}; a no-op once closed. */
615
+ sendStreamEvent(frame: GatewayStreamEvent): void;
616
+ /**
617
+ * Emit a pre-encoded SSE record verbatim; a no-op once closed. For endpoints
618
+ * that own their own wire encoder (e.g. a bespoke `data: <json>\n\n`) and want
619
+ * to keep emitting byte-identical records while still routing lifecycle
620
+ * through this builder.
621
+ */
622
+ sendRaw(record: string): void;
623
+ /** Whether the stream has been closed (client disconnect or `close()`). */
624
+ readonly closed: boolean;
625
+ /** Close the stream; idempotent. */
626
+ close(): void;
627
+ }
628
+ /**
629
+ * A teardown callback returned by an {@link SseSource}, invoked exactly once when
630
+ * the stream ends (client disconnect, `emitter.close()`, or producer completion).
631
+ */
632
+ type SseTeardown = () => void;
633
+ /**
634
+ * A frame source for {@link createSseStream}. Receives an {@link SseEmitter} and
635
+ * MAY return an {@link SseTeardown} callback (return nothing to skip teardown).
636
+ *
637
+ * The source is started SYNCHRONOUSLY inside `ReadableStream.start`, mirroring
638
+ * the existing Studio endpoints (which schedule timers inside `start`). A source
639
+ * may push frames immediately and/or schedule async work.
640
+ */
641
+ type SseSource = (emitter: SseEmitter) => SseTeardown | undefined;
642
+ /**
643
+ * Build a `text/event-stream` {@link ReadableStream} driven by an
644
+ * {@link SseSource}.
645
+ *
646
+ * This is the shared, abort-safe stream builder every SSE endpoint routes
647
+ * through. It guarantees:
648
+ * - frames are dropped (never throw) after the stream closes;
649
+ * - the source's teardown callback runs exactly once;
650
+ * - an optional {@link AbortSignal} (the request's) closes the stream on client
651
+ * disconnect, with its listener removed on teardown.
652
+ *
653
+ * The byte output is fully controlled by what the source emits — so an endpoint
654
+ * adopting this builder reproduces its prior wire exactly by emitting the same
655
+ * frames in the same order.
656
+ *
657
+ * @param source - The frame producer.
658
+ * @param signal - Optional request abort signal that closes the stream on
659
+ * client disconnect.
660
+ * @returns A ReadableStream of UTF-8 SSE bytes for a `Response` body.
661
+ */
662
+ declare function createSseStream(source: SseSource, signal?: AbortSignal): ReadableStream<Uint8Array>;
663
+
664
+ /**
665
+ * Streaming-operation source adapters for the gateway HTTP/SSE transport (T11921).
666
+ *
667
+ * A streaming operation (`OperationDef.streaming === true`, served as
668
+ * `GET /v1/<domain>/<operation>`) needs a *producer*: an async source that tails
669
+ * an event origin (the agent/worker/board lifecycle bus) and feeds
670
+ * {@link GatewayStreamEvent} frames into the shared, abort-safe
671
+ * {@link createSseStream} builder. This module owns ONLY that producer concern —
672
+ * the wire encoding + stream lifecycle live in `./sse.ts`, and the route parsing
673
+ * lives in `./listen.ts`.
674
+ *
675
+ * The runtime is deliberately DB-free (the runtime invariant — no drizzle, no
676
+ * `@cleocode/cleo`), so a source here MUST NOT open a store directly. Instead the
677
+ * listener resolves a source by `<domain>.<operation>` key from the
678
+ * {@link STREAM_SOURCES} registry; an embedder that owns a richer origin (the
679
+ * daemon, a SvelteKit route) can inject its own source into the listener at
680
+ * boot. The default source shipped here is a bounded tick/heartbeat producer that
681
+ * proves the pipe end-to-end without reaching across the package boundary — and
682
+ * is exactly what the in-process test exercises.
683
+ *
684
+ * Secrets never touch the wire: a source emits only the public lifecycle payload
685
+ * it is handed; credential resolution (if any) happens server-side before the
686
+ * frame is built, exactly as it does for a unary dispatch.
687
+ *
688
+ * @packageDocumentation
689
+ * @module @cleocode/runtime/gateway/http/stream-sources
690
+ *
691
+ * @task T11921
692
+ * @epic T11769
693
+ * @saga T10400
694
+ */
695
+
696
+ /**
697
+ * The bind coordinates a {@link GatewayStreamSource} receives: the resolved
698
+ * `(domain, operation)` of the streaming route, the already-validated request
699
+ * params, and the request id (so emitted frames correlate back to the origin
700
+ * request).
701
+ */
702
+ interface StreamSourceContext {
703
+ /** The canonical domain segment of the streaming route. */
704
+ domain: string;
705
+ /** The operation segment of the streaming route. */
706
+ operation: string;
707
+ /** The decoded query params for the streaming request. */
708
+ params: Record<string, unknown>;
709
+ /** The request id stamped on every emitted {@link GatewayStreamEvent}. */
710
+ requestId: string;
711
+ }
712
+ /**
713
+ * A teardown callback a {@link GatewayStreamSource} MAY return; invoked exactly
714
+ * once when the stream ends (client disconnect, terminal frame, completion).
715
+ */
716
+ type StreamSourceTeardown = () => void;
717
+ /**
718
+ * A producer for one streaming operation. Receives an {@link SseEmitter}
719
+ * (`sendStreamEvent` emits one canonical {@link GatewayStreamEvent}) plus the
720
+ * resolved {@link StreamSourceContext}, and MAY return a teardown callback.
721
+ *
722
+ * The source is started synchronously inside the stream builder, mirroring the
723
+ * Studio SSE endpoints; it may push frames immediately and/or schedule async
724
+ * work. It MUST drop frames once `emitter.closed` is true (the emitter already
725
+ * guards this) and MUST clean up timers/listeners in its teardown so a client
726
+ * disconnect closes the stream leak-free.
727
+ */
728
+ type GatewayStreamSource = (emitter: SseEmitter, context: StreamSourceContext) => StreamSourceTeardown | undefined;
729
+ /**
730
+ * The default `orchestrate.events` source: a bounded tick/heartbeat producer.
731
+ *
732
+ * Emits a monotonic sequence of `data` {@link GatewayStreamEvent} frames at
733
+ * {@link DEFAULT_TICK_INTERVAL_MS}, then a terminal `done` frame once the
734
+ * `ticks` budget is exhausted; a client disconnect tears the timer down before
735
+ * then. It carries no DB handle and no secret — each frame's payload is a public
736
+ * heartbeat (`{ tick, ts }`) standing in for an agent/worker/board lifecycle
737
+ * event until the daemon injects its real origin-tailing source.
738
+ *
739
+ * A first frame is emitted SYNCHRONOUSLY so a client (and the test) observes a
740
+ * well-formed `data:` frame immediately, without waiting a full interval.
741
+ *
742
+ * @param emitter - The SSE emitter for this stream.
743
+ * @param context - The resolved streaming-route context.
744
+ * @returns A teardown that clears the tick timer.
745
+ */
746
+ declare const tickStreamSource: GatewayStreamSource;
747
+ /**
748
+ * Register (or override) the {@link GatewayStreamSource} for a streaming op.
749
+ *
750
+ * Lets an embedder that owns a richer event origin (the daemon tailing the
751
+ * agent/worker/board lifecycle bus, a SvelteKit route reading a store) replace
752
+ * the default tick source for a given `(domain, operation)` without changing the
753
+ * listener. Idempotent — a later call wins.
754
+ *
755
+ * @param domain - The canonical domain of the streaming op.
756
+ * @param operation - The operation name of the streaming op.
757
+ * @param source - The producer to serve this streaming route with.
758
+ */
759
+ declare function registerStreamSource(domain: string, operation: string, source: GatewayStreamSource): void;
760
+ /**
761
+ * Resolve the {@link GatewayStreamSource} registered for a streaming op.
762
+ *
763
+ * @param domain - The canonical domain of the streaming op.
764
+ * @param operation - The operation name of the streaming op.
765
+ * @returns The registered source, or `undefined` when none is registered (the
766
+ * listener then closes the stream with a single terminal `error` frame).
767
+ */
768
+ declare function resolveStreamSource(domain: string, operation: string): GatewayStreamSource | undefined;
769
+
770
+ /**
771
+ * Static file serving for the bundled CLEO Web Studio assets.
772
+ *
773
+ * The gateway HTTP server calls {@link serveStudioStatic} for any request whose
774
+ * path starts with the `/studio` prefix. The handler:
775
+ *
776
+ * 1. Strips the `/studio` prefix and resolves the remainder against the
777
+ * `studioStaticDir` (the `build/client/` output of the Studio
778
+ * `adapter-node` build bundled inside `@cleocode/cleo`).
779
+ * 2. Serves the matched file with the correct MIME type (inferred from the
780
+ * extension via the built-in {@link MIME_TYPES} map).
781
+ * 3. Returns `index.html` for any path that does not resolve to a static file
782
+ * — the SPA fallback that lets the SvelteKit client-side router handle
783
+ * sub-routes.
784
+ * 4. Returns a clean JSON error envelope (not a stack trace) when
785
+ * `studioStaticDir` is absent (dev checkout without a Studio build, or an
786
+ * npm install where the postbuild copy was skipped).
787
+ *
788
+ * The handler deliberately has NO `process.exit`, NO drizzle, NO
789
+ * `@cleocode/cleo` dependency — it is a pure file-system responder that can be
790
+ * embedded in any `node:http` server (the gateway daemon, a test harness).
791
+ *
792
+ * The `build/client/` static tree is served under `/studio/` so all of its
793
+ * relative asset references (`/_app/immutable/...`) resolve correctly. The root
794
+ * `/studio` (no trailing slash) redirects to `/studio/` to keep the base URL
795
+ * consistent with the SvelteKit assets.
796
+ *
797
+ * @packageDocumentation
798
+ * @module @cleocode/runtime/gateway/http
799
+ *
800
+ * @task T11979
801
+ * @epic T11261
802
+ */
803
+
804
+ /** The URL prefix the Studio static handler claims. */
805
+ declare const STUDIO_PREFIX = "/studio";
806
+ /**
807
+ * Options for the Studio static handler, injected at server startup.
808
+ *
809
+ * The caller resolves the absolute path to the static asset directory and
810
+ * passes it here — the handler never reads environment variables or resolves
811
+ * paths itself, keeping concerns separated and the handler testable.
812
+ */
813
+ interface StudioStaticOptions {
814
+ /**
815
+ * Absolute path to the Studio static asset directory. This is the
816
+ * `build/client/` subdirectory from an `adapter-node` Studio build, bundled
817
+ * inside `@cleocode/cleo` at `studio-dist/client/`.
818
+ *
819
+ * When `undefined` or the path does not exist on disk, every request under
820
+ * `/studio` returns a clean JSON 503 "bundle absent" envelope.
821
+ */
822
+ staticDir: string | undefined;
823
+ }
824
+ /**
825
+ * Determine whether a request path falls under the Studio prefix and should be
826
+ * handled by the static asset handler rather than the API gateway.
827
+ *
828
+ * @param pathname - The URL pathname (without query string).
829
+ * @returns `true` when the path is `/studio`, `/studio/`, or any
830
+ * `/studio/<subpath>`.
831
+ */
832
+ declare function isStudioPath(pathname: string): boolean;
833
+ /**
834
+ * Resolve the file-system path for a Studio request within `staticDir`.
835
+ *
836
+ * Strips the `/studio` prefix and normalises the remainder; guards against
837
+ * path-traversal (a normalised path that escapes `staticDir` is rejected).
838
+ *
839
+ * @param staticDir - Absolute path to the Studio static asset directory.
840
+ * @param pathname - The request path (already verified to start with `/studio`).
841
+ * @returns The absolute candidate path, or `undefined` when path traversal is
842
+ * detected.
843
+ */
844
+ declare function resolveStudioAssetPath(staticDir: string, pathname: string): string | undefined;
845
+ /**
846
+ * Infer the MIME type for a file from its extension.
847
+ *
848
+ * @param filePath - The file path (only the extension is used).
849
+ * @returns The appropriate `Content-Type` header value.
850
+ */
851
+ declare function mimeType(filePath: string): string;
852
+ /**
853
+ * Write the "bundle absent" JSON error envelope and close the response.
854
+ *
855
+ * Used when `staticDir` is `undefined` or does not exist on disk — the Studio
856
+ * build was not found. The response is a `503 Service Unavailable` with a clean
857
+ * LAFS-flavoured JSON body so clients can detect the condition programmatically.
858
+ *
859
+ * @param res - The server response to write to.
860
+ * @param hint - Optional developer hint (e.g. the expected bundle path).
861
+ */
862
+ declare function writeBundleAbsentError(res: ServerResponse, hint?: string): void;
863
+ /**
864
+ * Handle a single HTTP request whose path falls under `/studio`.
865
+ *
866
+ * Routing logic:
867
+ * - `GET /studio` → 302 redirect to `/studio/` (keep asset refs consistent)
868
+ * - `GET /studio/<static-asset>` → serve the file from `staticDir`
869
+ * - `GET /studio/*` (no matching file) → serve `index.html` (SPA fallback)
870
+ * - Any request when `staticDir` is absent → 503 JSON envelope
871
+ * - Non-GET method → 405 JSON envelope (Studio is read-only from the gateway)
872
+ *
873
+ * @param req - The inbound HTTP request.
874
+ * @param res - The outbound HTTP response.
875
+ * @param opts - Static handler options (resolved at server startup).
876
+ * @returns A promise that resolves once the response has been written.
877
+ */
878
+ declare function serveStudioStatic(req: IncomingMessage, res: ServerResponse, opts: StudioStaticOptions): Promise<void>;
879
+ /**
880
+ * Resolve the bundled Studio static directory from the `@cleocode/cleo` package
881
+ * location.
882
+ *
883
+ * When running from an npm install, the Studio build is bundled at
884
+ * `<cleo-package-root>/studio-dist/client/`. When running from a monorepo dev
885
+ * checkout, the directory may not exist (developers must run
886
+ * `pnpm --filter @cleocode/studio run build` manually) — `undefined` is
887
+ * returned in that case, and the gateway handler will serve the bundle-absent
888
+ * error instead of crashing.
889
+ *
890
+ * @param cleoPackageDir - Absolute path to the `@cleocode/cleo` package root.
891
+ * Typically derived from `import.meta.url` at the call site.
892
+ * @returns The absolute path to `studio-dist/client/`, or `undefined` if the
893
+ * directory does not exist.
894
+ */
895
+ declare function resolveStudioStaticDir(cleoPackageDir: string): string | undefined;
896
+
897
+ export { type GateDecision, type GatewayStreamSource, type HttpServerHandle, type HttpServerOptions, type HttpUnaryRequest, type HttpUnaryResult, NODE_PTY_INSTALL_HINT, SSE_HEADERS, STUDIO_PREFIX, type SseEmitter, type SseFrame, type SseSource, type StreamSourceContext, type StreamSourceTeardown, type StudioStaticOptions, type UpgradableServer, WS_PTY_PATH, type WsPtyEndpointHandle, type WsPtyOptions, attachWsPtyEndpoint, authorizeUpgrade, computeAcceptKey, createSseStream, decodeWsFrame, encodeSseFrame, encodeStreamEvent, encodeWsFrame, isStudioPath, isWsPtyPath, loadNodePty, mimeType, parseHttpRoute, registerStreamSource, resolveStreamSource, resolveStudioAssetPath, resolveStudioStaticDir, routeUnary, scrubPtyEnv, serveStudioStatic, startHttpServer, statusForResponse, tickStreamSource, writeBundleAbsentError };