@cleocode/runtime 2026.10.4 → 2026.10.5-rc.ci.672

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,5 +1,5 @@
1
1
  // src/gateway/registry.ts
2
- import { OPERATIONS as _OPERATIONS } from "@cleocode/contracts";
2
+ import { OPERATIONS as _OPERATIONS } from "@cleocode/contracts/dispatch/operations-registry.js";
3
3
  var OPERATIONS = _OPERATIONS;
4
4
  function deriveGatewayMatrix(gateway) {
5
5
  const matrix = {};
@@ -88,4 +88,4 @@ export {
88
88
  getActiveDomains,
89
89
  getCounts
90
90
  };
91
- //# sourceMappingURL=chunk-L5BLVQDA.js.map
91
+ //# sourceMappingURL=chunk-4PUSKGDO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/gateway/registry.ts"],"sourcesContent":["/**\n * Unified CQRS Dispatch Layer -- Operation Registry (shim)\n *\n * The `OPERATIONS` data array was relocated to\n * `@cleocode/contracts/dispatch/operations-registry` in T10061 / T9833b\n * (E-CLI-BOUNDARY · Saga T9831 SG-ARCH-SOLID). This file is now a thin\n * re-export shim that:\n *\n * 1. Re-exports `OPERATIONS` from contracts so every downstream consumer\n * that already imports `{ OPERATIONS }` from this path continues to\n * compile unchanged.\n * 2. Keeps the pure utility / derivation functions (`resolve`,\n * `validateRequiredParams`, `getByDomain`, etc.) that operate on\n * the data and are rightly colocated with the CLI package.\n *\n * @epic T4820\n * @task T4814, T5241, T5615, T10061\n */\n\n// OperationDef + Resolution live in @cleocode/contracts (SSoT — promoted in\n// T9954 / Phase 0b of SG-ARCH-SOLID Saga T9831 · E-CONTRACTS-FOUNDATION\n// T9832). Imported below for internal use AND re-exported so that every\n// downstream consumer that already imports from './registry.js' continues\n// to compile unchanged.\nimport type { OperationDef, Resolution } from '@cleocode/contracts';\nimport { OPERATIONS as _OPERATIONS } from '@cleocode/contracts/dispatch/operations-registry.js';\nimport type { CanonicalDomain, Gateway, Tier } from '@cleocode/contracts/gateway';\n\nexport type { OperationDef, Resolution };\n\n/** The single source of truth for all operations in CLEO (data lives in @cleocode/contracts). */\nexport const OPERATIONS: OperationDef[] = _OPERATIONS;\n\n// ---------------------------------------------------------------------------\n// Gateway Matrix Derivation\n// ---------------------------------------------------------------------------\n\n/**\n * Derive a gateway operation matrix from the registry.\n *\n * Returns `Record<string, string[]>` containing:\n * - All canonical domains with their operations\n *\n * This is the SINGLE derivation point — gateways use this instead of\n * maintaining independent operation lists.\n */\nexport function deriveGatewayMatrix(gateway: Gateway): Record<string, string[]> {\n const matrix: Record<string, string[]> = {};\n\n for (const op of OPERATIONS) {\n if (op.gateway !== gateway) continue;\n if (!matrix[op.domain]) matrix[op.domain] = [];\n matrix[op.domain].push(op.operation);\n }\n\n return matrix;\n}\n\n/**\n * Get all accepted domain names for a gateway (canonical only).\n */\nexport function getGatewayDomains(gateway: Gateway): string[] {\n return Object.keys(deriveGatewayMatrix(gateway));\n}\n\n// ---------------------------------------------------------------------------\n// Lookup & Validation\n// ---------------------------------------------------------------------------\n\n/**\n * Resolves a domain + operation to its registered definition.\n */\nexport function resolve(\n gateway: Gateway,\n domain: string,\n operation: string,\n): Resolution | undefined {\n const def = OPERATIONS.find(\n (o) => o.gateway === gateway && o.domain === domain && o.operation === operation,\n );\n\n if (!def) return undefined;\n\n return { domain: def.domain, operation: def.operation, def };\n}\n\n/**\n * Infer the CQRS {@link Gateway} for a `(domain, operation)` pair from the\n * registry, without the caller naming `query`/`mutate` up front.\n *\n * This backs the versioned `/v1/<domain>/<operation>` REST facade (T11919),\n * where — unlike the legacy `/<gateway>/<domain>/<operation>` form — the client\n * does not spell the gateway segment: the registry is the single source of truth\n * for whether an operation is a read (`query`) or a write (`mutate`). Each\n * `(domain, operation)` pair maps to exactly one gateway in {@link OPERATIONS},\n * so the inference is unambiguous.\n *\n * @param domain - The canonical domain segment (e.g. `'tasks'`).\n * @param operation - The operation segment (e.g. `'show'`).\n * @returns The owning gateway, or `undefined` when no registered operation\n * matches the pair (the wire edge then renders a `404`).\n */\nexport function inferGateway(domain: string, operation: string): Gateway | undefined {\n const def = OPERATIONS.find((o) => o.domain === domain && o.operation === operation);\n return def?.gateway;\n}\n\n/**\n * The result of resolving a streaming route: the owning gateway plus the matched\n * {@link OperationDef} (so the caller has the op's tier/params on hand without a\n * second lookup).\n */\nexport interface StreamResolution {\n /** The CQRS gateway the streaming op is registered under. */\n gateway: Gateway;\n /** The matched streaming operation definition. */\n def: OperationDef;\n}\n\n/**\n * Resolve a `(domain, operation)` pair to a registered **streaming** operation.\n *\n * Backs the `GET /v1/<domain>/<operation>` SSE route (T11921): the listener only\n * opens a `text/event-stream` for a `(domain, operation)` pair that is BOTH\n * registered AND explicitly flagged `streaming: true` in {@link OPERATIONS}. A\n * unary-only op (or an unknown pair) resolves to `undefined`, so the listener\n * never streams an operation that was not declared streamable — a GET to a\n * unary op stays a `405` (method-not-allowed) rather than silently opening a\n * never-ending stream.\n *\n * @param domain - The canonical domain segment (e.g. `'orchestrate'`).\n * @param operation - The operation segment (e.g. `'events'`).\n * @returns The streaming resolution, or `undefined` when no registered op\n * matches the pair OR the matched op is not flagged `streaming: true`.\n */\nexport function resolveStreamRoute(\n domain: string,\n operation: string,\n): StreamResolution | undefined {\n const def = OPERATIONS.find(\n (o) => o.domain === domain && o.operation === operation && o.streaming === true,\n );\n if (def === undefined) return undefined;\n return { gateway: def.gateway, def };\n}\n\n/**\n * Validates that all required parameters are present in the request.\n * Returns an array of missing parameter keys.\n */\nexport function validateRequiredParams(\n def: OperationDef,\n params?: Record<string, unknown>,\n): string[] {\n if (!def.requiredParams || def.requiredParams.length === 0) return [];\n const provided = params || {};\n // Build a quick lookup so we can honor per-param `allowEmpty` declarations.\n // When a ParamDef sets `allowEmpty: true`, the empty string `\"\"` is a\n // valid value rather than \"missing\" — e.g. `provenance.backfill --since \"\"`\n // walks every reachable tag from the beginning of history.\n const allowEmptyKeys = new Set<string>();\n if (def.params) {\n for (const p of def.params) {\n if (p.allowEmpty === true) allowEmptyKeys.add(p.name);\n }\n }\n return def.requiredParams.filter((key) => {\n const v = provided[key];\n if (v === undefined || v === null) return true;\n if (v === '' && !allowEmptyKeys.has(key)) return true;\n return false;\n });\n}\n\n/** Get all operations for a specific canonical domain. */\nexport function getByDomain(domain: CanonicalDomain): OperationDef[] {\n return OPERATIONS.filter((o) => o.domain === domain);\n}\n\n/** Get all operations for a specific gateway. */\nexport function getByGateway(gateway: Gateway): OperationDef[] {\n return OPERATIONS.filter((o) => o.gateway === gateway);\n}\n\n/** Get all operations available at or below a specific tier. */\nexport function getByTier(tier: Tier): OperationDef[] {\n return OPERATIONS.filter((o) => o.tier <= tier);\n}\n\n/** Get a list of canonical domains that actually have operations registered. */\nexport function getActiveDomains(): CanonicalDomain[] {\n const active = new Set(OPERATIONS.map((o) => o.domain));\n return Array.from(active);\n}\n\n/**\n * Returns summary counts of operations for module validation.\n */\nexport function getCounts(): { query: number; mutate: number; total: number } {\n return {\n query: OPERATIONS.filter((o) => o.gateway === 'query').length,\n mutate: OPERATIONS.filter((o) => o.gateway === 'mutate').length,\n total: OPERATIONS.length,\n };\n}\n\n// Module load validation (dynamic, no hardcoded operation totals)\nconst counts = getCounts();\nif (counts.total !== OPERATIONS.length) {\n console.warn(\n `[Registry] Operation count mismatch: total=${counts.total}, registry=${OPERATIONS.length}`,\n );\n}\n"],"mappings":";AAyBA,SAAS,cAAc,mBAAmB;AAMnC,IAAM,aAA6B;AAenC,SAAS,oBAAoB,SAA4C;AAC9E,QAAM,SAAmC,CAAC;AAE1C,aAAW,MAAM,YAAY;AAC3B,QAAI,GAAG,YAAY,QAAS;AAC5B,QAAI,CAAC,OAAO,GAAG,MAAM,EAAG,QAAO,GAAG,MAAM,IAAI,CAAC;AAC7C,WAAO,GAAG,MAAM,EAAE,KAAK,GAAG,SAAS;AAAA,EACrC;AAEA,SAAO;AACT;AAKO,SAAS,kBAAkB,SAA4B;AAC5D,SAAO,OAAO,KAAK,oBAAoB,OAAO,CAAC;AACjD;AASO,SAAS,QACd,SACA,QACA,WACwB;AACxB,QAAM,MAAM,WAAW;AAAA,IACrB,CAAC,MAAM,EAAE,YAAY,WAAW,EAAE,WAAW,UAAU,EAAE,cAAc;AAAA,EACzE;AAEA,MAAI,CAAC,IAAK,QAAO;AAEjB,SAAO,EAAE,QAAQ,IAAI,QAAQ,WAAW,IAAI,WAAW,IAAI;AAC7D;AAkBO,SAAS,aAAa,QAAgB,WAAwC;AACnF,QAAM,MAAM,WAAW,KAAK,CAAC,MAAM,EAAE,WAAW,UAAU,EAAE,cAAc,SAAS;AACnF,SAAO,KAAK;AACd;AA8BO,SAAS,mBACd,QACA,WAC8B;AAC9B,QAAM,MAAM,WAAW;AAAA,IACrB,CAAC,MAAM,EAAE,WAAW,UAAU,EAAE,cAAc,aAAa,EAAE,cAAc;AAAA,EAC7E;AACA,MAAI,QAAQ,OAAW,QAAO;AAC9B,SAAO,EAAE,SAAS,IAAI,SAAS,IAAI;AACrC;AAMO,SAAS,uBACd,KACA,QACU;AACV,MAAI,CAAC,IAAI,kBAAkB,IAAI,eAAe,WAAW,EAAG,QAAO,CAAC;AACpE,QAAM,WAAW,UAAU,CAAC;AAK5B,QAAM,iBAAiB,oBAAI,IAAY;AACvC,MAAI,IAAI,QAAQ;AACd,eAAW,KAAK,IAAI,QAAQ;AAC1B,UAAI,EAAE,eAAe,KAAM,gBAAe,IAAI,EAAE,IAAI;AAAA,IACtD;AAAA,EACF;AACA,SAAO,IAAI,eAAe,OAAO,CAAC,QAAQ;AACxC,UAAM,IAAI,SAAS,GAAG;AACtB,QAAI,MAAM,UAAa,MAAM,KAAM,QAAO;AAC1C,QAAI,MAAM,MAAM,CAAC,eAAe,IAAI,GAAG,EAAG,QAAO;AACjD,WAAO;AAAA,EACT,CAAC;AACH;AAGO,SAAS,YAAY,QAAyC;AACnE,SAAO,WAAW,OAAO,CAAC,MAAM,EAAE,WAAW,MAAM;AACrD;AAGO,SAAS,aAAa,SAAkC;AAC7D,SAAO,WAAW,OAAO,CAAC,MAAM,EAAE,YAAY,OAAO;AACvD;AAGO,SAAS,UAAU,MAA4B;AACpD,SAAO,WAAW,OAAO,CAAC,MAAM,EAAE,QAAQ,IAAI;AAChD;AAGO,SAAS,mBAAsC;AACpD,QAAM,SAAS,IAAI,IAAI,WAAW,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;AACtD,SAAO,MAAM,KAAK,MAAM;AAC1B;AAKO,SAAS,YAA8D;AAC5E,SAAO;AAAA,IACL,OAAO,WAAW,OAAO,CAAC,MAAM,EAAE,YAAY,OAAO,EAAE;AAAA,IACvD,QAAQ,WAAW,OAAO,CAAC,MAAM,EAAE,YAAY,QAAQ,EAAE;AAAA,IACzD,OAAO,WAAW;AAAA,EACpB;AACF;AAGA,IAAM,SAAS,UAAU;AACzB,IAAI,OAAO,UAAU,WAAW,QAAQ;AACtC,UAAQ;AAAA,IACN,8CAA8C,OAAO,KAAK,cAAc,WAAW,MAAM;AAAA,EAC3F;AACF;","names":[]}
@@ -90,12 +90,13 @@ import { randomUUID } from "crypto";
90
90
  import { existsSync, unlinkSync } from "fs";
91
91
  import { createServer } from "net";
92
92
  import { GATEWAY_RPC_PROTOCOL_VERSION as GATEWAY_RPC_PROTOCOL_VERSION2 } from "@cleocode/contracts/gateway/rpc";
93
- import { getLogger } from "@cleocode/core";
93
+ import { getLogger } from "@cleocode/core/logger";
94
+ import { useBrainWriterThread } from "@cleocode/core/memory/brain-writer-thread";
94
95
  import {
95
96
  bindConnectionSession,
96
97
  runWithConnectionHandle,
97
98
  unbindConnectionSession
98
- } from "@cleocode/core/internal";
99
+ } from "@cleocode/core/sessions/connection-session-handle";
99
100
  async function routeFrame(handler, frame, connId = randomUUID()) {
100
101
  const request = {
101
102
  ...frame.request,
@@ -153,6 +154,7 @@ function handleConnection(handler, socket, log) {
153
154
  });
154
155
  }
155
156
  function startRpcServer(handler, opts) {
157
+ useBrainWriterThread();
156
158
  const log = getLogger("gateway-rpc");
157
159
  const removeStale = opts.removeStaleSocket ?? true;
158
160
  if (removeStale && existsSync(opts.socketPath)) {
@@ -204,4 +206,4 @@ export {
204
206
  routeFrame,
205
207
  startRpcServer
206
208
  };
207
- //# sourceMappingURL=chunk-KQBVXGOL.js.map
209
+ //# sourceMappingURL=chunk-GULKX44R.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/gateway/rpc/codec.ts","../src/gateway/rpc/server.ts"],"sourcesContent":["/**\n * NDJSON codec for the CLI-RPC transport.\n *\n * Implements the SAME framing discipline as the FROZEN `supervisor-ipc` v1.0\n * contract — one zod-validated JSON envelope per newline-delimited line, with\n * an explicit `protocol_version` negotiation step — but for the gateway-RPC\n * frame set (`@cleocode/contracts/gateway/rpc`). The supervisor-ipc module is\n * frozen and is NOT edited; this codec re-applies its pattern.\n *\n * The codec is intentionally socket-agnostic: it owns line buffering, JSON\n * parse, zod decode, and version negotiation, returning typed results the\n * server maps onto `node:net` reads/writes. This keeps the parse/validate logic\n * unit-testable without binding a real socket.\n *\n * @task T11449\n * @epic T11254\n * @saga T11243\n */\n\nimport {\n GATEWAY_RPC_PROTOCOL_VERSION,\n type GatewayRpcErrorCode,\n type GatewayRpcErrorFrame,\n type GatewayRpcFrame,\n GatewayRpcFrameSchema,\n type GatewayRpcRequestFrame,\n type GatewayRpcResponseFrame,\n isFrozenGatewayRpcVersion,\n} from '@cleocode/contracts/gateway/rpc';\n\n/** Newline delimiter for the NDJSON wire (mirrors supervisor-ipc). */\nconst NEWLINE = '\\n';\n\n/**\n * Encode any RPC frame to a single NDJSON line (JSON + trailing `\\n`).\n *\n * @param frame - The response or error frame to serialize.\n * @returns A newline-terminated JSON string ready to write to the socket.\n */\nexport function encodeFrame(frame: GatewayRpcResponseFrame | GatewayRpcErrorFrame): string {\n return `${JSON.stringify(frame)}${NEWLINE}`;\n}\n\n/**\n * Build a protocol-level error frame.\n *\n * @param id - Correlation id (`'0'` when the originating frame's id is unknown).\n * @param code - One of the frozen {@link GatewayRpcErrorCode} values.\n * @param message - Human-readable detail.\n * @returns A fully-formed {@link GatewayRpcErrorFrame}.\n */\nexport function buildErrorFrame(\n id: string,\n code: GatewayRpcErrorCode,\n message: string,\n): GatewayRpcErrorFrame {\n return {\n protocol_version: GATEWAY_RPC_PROTOCOL_VERSION,\n id,\n direction: 'error',\n error: { code, message },\n };\n}\n\n/**\n * The discriminated outcome of decoding a single inbound NDJSON line.\n *\n * - `request` — a valid, version-matched request frame ready to route.\n * - `error` — a protocol-level error frame the server should write straight\n * back (parse failure, version mismatch, or malformed frame). The\n * server never routes these to the gateway.\n */\nexport type DecodeResult =\n | { kind: 'request'; frame: GatewayRpcRequestFrame }\n | { kind: 'error'; frame: GatewayRpcErrorFrame };\n\n/**\n * Decode + validate a single inbound NDJSON line into a routable request or a\n * protocol-level error frame.\n *\n * Steps (the supervisor-ipc pattern, applied to gateway frames):\n * 1. JSON parse — failure → `E_RPC_PARSE`.\n * 2. zod decode against {@link GatewayRpcFrameSchema} — failure → `E_RPC_BAD_FRAME`.\n * 3. version negotiation via {@link isFrozenGatewayRpcVersion} —\n * mismatch → `E_RPC_BAD_VERSION`.\n * 4. direction must be `request` (a server never receives response/error\n * frames) — otherwise → `E_RPC_BAD_FRAME`.\n *\n * The best-effort correlation `id` is recovered from the raw JSON when possible\n * so the client can match the error to its request even on a decode failure.\n *\n * @param line - One trimmed NDJSON line (no trailing newline).\n * @returns A {@link DecodeResult}.\n */\nexport function decodeLine(line: string): DecodeResult {\n let raw: unknown;\n try {\n raw = JSON.parse(line);\n } catch {\n return { kind: 'error', frame: buildErrorFrame('0', 'E_RPC_PARSE', 'invalid JSON line') };\n }\n\n // Best-effort id recovery for error correlation before full validation.\n const recoveredId = recoverId(raw);\n\n const parsed = GatewayRpcFrameSchema.safeParse(raw);\n if (!parsed.success) {\n return {\n kind: 'error',\n frame: buildErrorFrame(recoveredId, 'E_RPC_BAD_FRAME', 'frame failed schema validation'),\n };\n }\n\n const frame: GatewayRpcFrame = parsed.data;\n\n if (!isFrozenGatewayRpcVersion(frame.protocol_version)) {\n return {\n kind: 'error',\n frame: buildErrorFrame(\n frame.id,\n 'E_RPC_BAD_VERSION',\n `unsupported protocol version ${frame.protocol_version}; server speaks ${GATEWAY_RPC_PROTOCOL_VERSION}`,\n ),\n };\n }\n\n if (frame.direction !== 'request') {\n return {\n kind: 'error',\n frame: buildErrorFrame(\n frame.id,\n 'E_RPC_BAD_FRAME',\n `server cannot accept a ${frame.direction} frame`,\n ),\n };\n }\n\n return { kind: 'request', frame };\n}\n\n/**\n * Recover a best-effort correlation id from raw decoded JSON, used so a\n * validation failure can still be correlated to the client's request.\n *\n * @param raw - The result of `JSON.parse` on the inbound line.\n * @returns The string `id` if present and non-empty, else `'0'`.\n */\nfunction recoverId(raw: unknown): string {\n if (raw && typeof raw === 'object' && 'id' in raw) {\n const id = (raw as { id: unknown }).id;\n if (typeof id === 'string' && id.length > 0) return id;\n }\n return '0';\n}\n\n/**\n * A streaming line splitter for the NDJSON wire.\n *\n * `node:net` sockets deliver arbitrary chunk boundaries; a single `data` event\n * may carry a partial line or several lines. This buffer accumulates bytes and\n * yields only complete lines (split on `\\n`), matching the supervisor-ipc\n * client's `readline`-style framing without depending on `readline` (so it can\n * be driven directly off socket chunks).\n */\nexport class LineBuffer {\n /** Pending bytes that have not yet completed a line. */\n private buffer = '';\n\n /**\n * Push a decoded UTF-8 chunk; return every complete line it completes.\n *\n * @param chunk - A UTF-8 string chunk from the socket.\n * @returns Zero or more complete lines (trailing newline stripped, blanks\n * skipped).\n */\n push(chunk: string): string[] {\n this.buffer += chunk;\n const lines: string[] = [];\n let idx = this.buffer.indexOf(NEWLINE);\n while (idx !== -1) {\n const line = this.buffer.slice(0, idx).trim();\n this.buffer = this.buffer.slice(idx + 1);\n if (line.length > 0) lines.push(line);\n idx = this.buffer.indexOf(NEWLINE);\n }\n return lines;\n }\n}\n","/**\n * CLI-RPC unix-socket transport adapter — routes NDJSON frames through the gateway.\n *\n * Serves the gateway over a long-lived unix-domain socket using newline-delimited\n * JSON (NDJSON): one zod-validated {@link GatewayRpcFrame} per line. Any local\n * client (the `cleo` CLI talking to a warm daemon, Studio's server process, a\n * test harness) can open the socket, write `request` frames, and read back\n * `response`/`error` frames — without paying the per-invocation cold-start of\n * spawning a fresh `cleo` process.\n *\n * This is the THIRD transport adapter, mirroring the just-merged MCP stdio\n * adapter (`packages/runtime/src/gateway/mcp/`):\n *\n * RPC request frame { direction:'request', request:{…} }\n * → decode + validate (`@cleocode/contracts/gateway/rpc`)\n * → DispatchRequest { source: 'rpc', … } (re-validated at the boundary)\n * → injected {@link GatewayHandler}.handle()\n * → DispatchResponse (LAFS envelope) → RPC response frame\n *\n * The adapter owns ONLY wire concerns: socket lifecycle, line framing, version\n * negotiation, error rendering, and per-connection cleanup. All domain logic\n * (resolution, validation, middleware, handler execution) lives behind the\n * {@link GatewayHandler} — exactly mirroring the CLI and MCP adapters. There is\n * NO `process.exit` and NO error-render inside the handlers (R3 contract); the\n * adapter never calls `process.exit` at all (lifecycle is the caller's).\n *\n * The request `source` is forced to `'rpc'` regardless of what the client sent,\n * so a client cannot impersonate another transport.\n *\n * @task T11449\n * @epic T11254\n * @saga T11243\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { existsSync, unlinkSync } from 'node:fs';\nimport { createServer, type Socket } from 'node:net';\nimport type { DispatchRequest } from '@cleocode/contracts/gateway';\nimport type {\n GatewayRpcRequestFrame,\n GatewayRpcResponseFrame,\n} from '@cleocode/contracts/gateway/rpc';\nimport { GATEWAY_RPC_PROTOCOL_VERSION } from '@cleocode/contracts/gateway/rpc';\nimport { getLogger } from '@cleocode/core/logger';\nimport { useBrainWriterThread } from '@cleocode/core/memory/brain-writer-thread';\nimport {\n bindConnectionSession,\n runWithConnectionHandle,\n unbindConnectionSession,\n} from '@cleocode/core/sessions/connection-session-handle';\nimport type { GatewayHandler } from '../index.js';\nimport { buildErrorFrame, decodeLine, encodeFrame, LineBuffer } from './codec.js';\nimport type { RpcServerHandle, RpcServerOptions } from './types.js';\n\n/**\n * Route a single decoded request frame through the gateway handler and build\n * the correlated response frame.\n *\n * The request's `source` is forced to `'rpc'`; the frame `id` is reused as the\n * response correlation id (and, when the client omitted a `requestId` shaped\n * for tracing, the frame id flows through as the dispatch `requestId`). Thrown\n * handler errors are caught and rendered as a protocol-level `E_RPC_INTERNAL`\n * error frame — never propagated as an unhandled rejection or `process.exit`.\n *\n * ## Connection-scoped session binding (T11640 · Epic T11638)\n *\n * The daemon serves many connections over one process, so it cannot rely on the\n * single-process session-context singleton for identity. Instead, a frame on a\n * connection that declares a `sessionId` binds it into the `{connId → sessionId}`\n * registry, and the whole dispatch runs inside {@link runWithConnectionHandle}\n * so `core`'s `resolveCurrentSession` resolves THIS connection's session (its\n * highest-precedence tier) rather than \"whoever wrote the DB last\". When a frame\n * carries no `sessionId`, no binding is performed and resolution transparently\n * falls through to the env / active tiers — preserving existing behavior for\n * anonymous frames.\n *\n * ### Intra-connection bleed safety\n *\n * Frames on one connection are dispatched CONCURRENTLY (each `routeFrame` is\n * fired without awaiting the prior one), and registry binding is last-write-wins.\n * To stop a still-in-flight Frame A from resolving a sibling Frame B's session,\n * the frame's OWN declared `sessionId` is snapshotted into the per-frame\n * connection handle ({@link runWithConnectionHandle}'s third arg) — that snapshot\n * is authoritative for the dispatch, so a later re-bind of the same `connId`\n * cannot retroactively change who Frame A is. The registry binding remains only\n * for the late-binding path used by frames that declare no session of their own.\n *\n * @param handler - The injected transport-neutral gateway handler.\n * @param frame - A validated, version-matched request frame.\n * @param connId - The opaque per-connection id (bound at accept-time). Omitted\n * only by direct/test callers that route a single frame outside a socket; a\n * fresh ephemeral id is minted so the call still runs in an isolated\n * connection-handle scope.\n * @returns The response or error frame to write back on the same connection.\n */\nexport async function routeFrame(\n handler: GatewayHandler,\n frame: GatewayRpcRequestFrame,\n connId: string = randomUUID(),\n): Promise<GatewayRpcResponseFrame | ReturnType<typeof buildErrorFrame>> {\n const request: DispatchRequest = {\n ...frame.request,\n // Force the transport of origin — a client cannot impersonate cli/mcp/http.\n source: 'rpc',\n requestId: frame.request.requestId,\n };\n\n // Bind this connection to the session the frame declared (empty/absent ids are\n // ignored by the registry). This feeds the late-binding path for any sibling\n // frame that declares no session of its own. The per-frame snapshot below — not\n // this mutable registry — is what THIS dispatch resolves against, so a\n // concurrent sibling frame's re-bind cannot bleed into it.\n if (request.sessionId) {\n bindConnectionSession(connId, request.sessionId);\n }\n\n try {\n // Snapshot the frame's own session into the per-frame handle so this dispatch\n // is immune to concurrent last-write-wins re-binds of the same connId.\n const response = await runWithConnectionHandle(\n connId,\n () => handler.handle(request),\n request.sessionId,\n );\n return {\n protocol_version: GATEWAY_RPC_PROTOCOL_VERSION,\n id: frame.id,\n direction: 'response',\n response,\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return buildErrorFrame(frame.id, 'E_RPC_INTERNAL', message);\n }\n}\n\n/**\n * Handle a single client connection: buffer NDJSON lines, decode each, route\n * request frames through the gateway, and write back response/error frames.\n *\n * Each connection owns its own {@link LineBuffer}, so partial-line chunk\n * boundaries are reassembled per socket. Frames are processed concurrently;\n * each correlated response carries the originating frame `id`, so the client\n * can demultiplex out-of-order completions.\n *\n * ## Accept-time session binding (T11640)\n *\n * A fresh opaque `connId` is minted per accepted socket and threaded into every\n * {@link routeFrame} call so the connection's declared session is bound into the\n * `{connId → sessionId}` registry and made the authoritative identity for that\n * dispatch (see {@link routeFrame}). On socket `close` the binding is removed so\n * the registry never grows unbounded across a long-lived daemon process.\n *\n * @param handler - The injected gateway handler.\n * @param socket - The accepted `node:net` connection.\n * @param log - The adapter's pino logger.\n */\nfunction handleConnection(\n handler: GatewayHandler,\n socket: Socket,\n log: ReturnType<typeof getLogger>,\n): void {\n socket.setEncoding('utf8');\n const lines = new LineBuffer();\n const connId = randomUUID();\n\n socket.on('data', (chunk: string) => {\n for (const line of lines.push(chunk)) {\n const decoded = decodeLine(line);\n if (decoded.kind === 'error') {\n socket.write(encodeFrame(decoded.frame));\n continue;\n }\n routeFrame(handler, decoded.frame, connId)\n .then((out) => {\n if (!socket.destroyed) socket.write(encodeFrame(out));\n })\n .catch((err: unknown) => {\n // routeFrame already traps handler errors; this guards the framing path.\n const message = err instanceof Error ? err.message : String(err);\n log.warn({ err }, 'rpc frame routing failed');\n if (!socket.destroyed) {\n socket.write(encodeFrame(buildErrorFrame(decoded.frame.id, 'E_RPC_INTERNAL', message)));\n }\n });\n }\n });\n\n socket.on('error', (err) => {\n log.warn({ err }, 'rpc connection error');\n });\n\n // Release the connection→session binding when the socket goes away so the\n // in-memory registry stays bounded across the daemon's lifetime (T11640).\n socket.on('close', () => {\n unbindConnectionSession(connId);\n });\n}\n\n/**\n * Start the CLI-RPC unix-socket server over the gateway.\n *\n * Binds {@link RpcServerOptions.socketPath}, accepts connections, and routes\n * each inbound NDJSON `request` frame through the injected {@link GatewayHandler}.\n * The caller assembles the handler (via `createGatewayHandler` with its domain\n * handlers + middleware) and injects it here — this adapter never builds\n * handlers itself, keeping the runtime free of any `@cleocode/cleo` dependency.\n *\n * Returns once the socket is listening. The returned {@link RpcServerHandle}\n * exposes an idempotent `close()` for deterministic teardown (daemon shutdown,\n * tests). The adapter never calls `process.exit`.\n *\n * @param handler - The transport-neutral gateway handler to route through.\n * @param opts - Socket path + lifecycle options.\n * @returns A promise resolving to the live {@link RpcServerHandle}.\n */\nexport function startRpcServer(\n handler: GatewayHandler,\n opts: RpcServerOptions,\n): Promise<RpcServerHandle> {\n // A long-lived host: brain writes run on the worker thread (T13126).\n useBrainWriterThread();\n const log = getLogger('gateway-rpc');\n const removeStale = opts.removeStaleSocket ?? true;\n\n if (removeStale && existsSync(opts.socketPath)) {\n try {\n unlinkSync(opts.socketPath);\n } catch (err) {\n log.warn({ err, socketPath: opts.socketPath }, 'failed to unlink stale rpc socket');\n }\n }\n\n const server = createServer((socket) => handleConnection(handler, socket, log));\n\n return new Promise<RpcServerHandle>((resolve, reject) => {\n server.once('error', reject);\n server.listen(opts.socketPath, () => {\n server.removeListener('error', reject);\n log.info(\n { socketPath: opts.socketPath, version: GATEWAY_RPC_PROTOCOL_VERSION },\n 'rpc server listening',\n );\n\n let closed = false;\n const handle: RpcServerHandle = {\n server,\n socketPath: opts.socketPath,\n close(): Promise<void> {\n if (closed) return Promise.resolve();\n closed = true;\n return new Promise<void>((res) => {\n server.close(() => {\n if (existsSync(opts.socketPath)) {\n try {\n unlinkSync(opts.socketPath);\n } catch {\n // best-effort cleanup; the OS reclaims the node on exit anyway.\n }\n }\n res();\n });\n });\n },\n };\n resolve(handle);\n });\n });\n}\n"],"mappings":"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","names":["GATEWAY_RPC_PROTOCOL_VERSION","GATEWAY_RPC_PROTOCOL_VERSION"]}
@@ -22,10 +22,10 @@ function defineSubsystem(definition) {
22
22
 
23
23
  // src/daemon/lease-ipc-client.ts
24
24
  import {
25
- LEASE_IPC_PROTOCOL_VERSION,
26
25
  LeaseIpcRequestEnvelopeSchema,
27
26
  LeaseIpcResponseEnvelopeSchema
28
- } from "@cleocode/contracts";
27
+ } from "@cleocode/contracts/lease-ipc/messages.js";
28
+ import { LEASE_IPC_PROTOCOL_VERSION } from "@cleocode/contracts/lease-ipc/version.js";
29
29
  var MalformedLeaseIpcFrameError = class extends Error {
30
30
  /** The raw NDJSON line that failed to decode. */
31
31
  line;
@@ -98,7 +98,7 @@ function createHeartbeatSender(writeLine, client = createLeaseIpcClient()) {
98
98
  }
99
99
 
100
100
  // src/daemon/logger.ts
101
- import { getLogger } from "@cleocode/core";
101
+ import { getLogger } from "@cleocode/core/logger";
102
102
  function createSubsystemLogger(name) {
103
103
  const log = getLogger("daemon").child({ daemonSubsystem: name });
104
104
  return {
@@ -110,9 +110,7 @@ function createSubsystemLogger(name) {
110
110
  }
111
111
 
112
112
  // src/daemon/registry.ts
113
- import {
114
- summarizeHealth
115
- } from "@cleocode/contracts";
113
+ import { summarizeHealth } from "@cleocode/contracts/daemon/health.js";
116
114
  function toError(value) {
117
115
  return value instanceof Error ? value : new Error(String(value));
118
116
  }
@@ -229,10 +227,10 @@ var SubsystemRegistry = class {
229
227
 
230
228
  // src/daemon/supervisor-client.ts
231
229
  import {
232
- SUPERVISOR_IPC_PROTOCOL_VERSION,
233
230
  SupervisorIpcRequestEnvelopeSchema,
234
231
  SupervisorIpcResponseEnvelopeSchema
235
- } from "@cleocode/contracts";
232
+ } from "@cleocode/contracts/supervisor-ipc/messages.js";
233
+ import { SUPERVISOR_IPC_PROTOCOL_VERSION } from "@cleocode/contracts/supervisor-ipc/version.js";
236
234
  var MalformedIpcFrameError = class extends Error {
237
235
  /** The raw NDJSON line that failed to decode. */
238
236
  line;
@@ -292,4 +290,4 @@ export {
292
290
  MalformedIpcFrameError,
293
291
  createSupervisorIpcClient
294
292
  };
295
- //# sourceMappingURL=chunk-7Q7BEWM7.js.map
293
+ //# sourceMappingURL=chunk-HXTRXEFI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/daemon/define-subsystem.ts","../src/daemon/lease-ipc-client.ts","../src/daemon/heartbeat-sender.ts","../src/daemon/logger.ts","../src/daemon/registry.ts","../src/daemon/supervisor-client.ts"],"sourcesContent":["/**\n * `defineSubsystem()` — declare a supervised daemon subsystem.\n *\n * The single factory R4-R7 adapters call to express a long-running concern\n * (Studio supervision, GC cron, web server, docs-viewer, runtime poller) as a\n * uniform {@link Subsystem}. The returned descriptor is frozen so the registry\n * can drive `start → healthProbe → shutdown` without the definition mutating\n * underfoot.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/daemon\n *\n * @epic T11253 R2 — `@cleocode/runtime/daemon` submodule\n * @task T11367 — defineSubsystem + lifecycle/health registry + IPC client\n * @saga T11243 SG-RUNTIME-UNIFICATION\n */\n\nimport type { Subsystem, SubsystemDefinition } from '@cleocode/contracts';\n\n/**\n * Declare a daemon subsystem.\n *\n * Validates that the four required surfaces (`name`, `start`, `healthProbe`,\n * `shutdown`) are present and that `name` is a non-empty string, then returns a\n * frozen {@link Subsystem} ready to register with a `SubsystemRegistry`.\n *\n * @typeParam TContext - Opaque context threaded from `start` into `shutdown`\n * (e.g. a child handle or resolved config). Defaults to `void`.\n * @param definition - The subsystem descriptor.\n * @returns A frozen, registrable {@link Subsystem}.\n * @throws {TypeError} When `name` is empty or a required method is missing.\n *\n * @example\n * ```ts\n * const studio = defineSubsystem({\n * name: 'studio',\n * start: () => supervisor.start(),\n * healthProbe: () => ({ child_id: 'studio', pid: supervisor.pid ?? 0,\n * state: supervisor.status === 'running' ? 'running' : 'stopped',\n * restart_count: 0 }),\n * shutdown: () => supervisor.stop(),\n * });\n * ```\n */\nexport function defineSubsystem<TContext = void>(\n definition: SubsystemDefinition<TContext>,\n): Subsystem<TContext> {\n if (typeof definition.name !== 'string' || definition.name.length === 0) {\n throw new TypeError('defineSubsystem: `name` must be a non-empty string');\n }\n if (typeof definition.start !== 'function') {\n throw new TypeError(`defineSubsystem(${definition.name}): \\`start\\` must be a function`);\n }\n if (typeof definition.healthProbe !== 'function') {\n throw new TypeError(`defineSubsystem(${definition.name}): \\`healthProbe\\` must be a function`);\n }\n if (typeof definition.shutdown !== 'function') {\n throw new TypeError(`defineSubsystem(${definition.name}): \\`shutdown\\` must be a function`);\n }\n\n return Object.freeze({\n name: definition.name,\n start: definition.start,\n healthProbe: definition.healthProbe,\n shutdown: definition.shutdown,\n });\n}\n","/**\n * Typed NDJSON client for the PARALLEL `lease-ipc` v1.1 contract (T11627 ST-5).\n *\n * Mirrors {@link ./supervisor-client.ts | supervisor-client.ts} conventions\n * exactly — a transport-agnostic codec that **owns no socket**. It encodes\n * outbound {@link LeaseIpcRequestEnvelope}s to newline-delimited JSON (NDJSON)\n * lines and decodes inbound lines through the v1.1 Zod schemas. **Every inbound\n * line is parsed** — a malformed or schema-violating frame is rejected with a\n * typed {@link MalformedLeaseIpcFrameError} and never silently dropped.\n *\n * The wire format is byte-identical framing to the frozen `supervisor-ipc` v1.0\n * envelope; only the version string ({@link LEASE_IPC_PROTOCOL_VERSION} =\n * `1.2.0`) and the inner union differ. A single supervisor accept loop routes\n * `1.0.0` frames to the v1.0 union and `1.2.0` frames to this lease union.\n *\n * The Rust `cleo-supervisor` is the arbiter (server); a CLEO process running in\n * `CLEO_WRITER_LEASE_MODE=supervisor` is a client that sends lease requests and\n * receives lease responses + unsolicited events (`lease_revoked`,\n * `child_killed_unresponsive`), one NDJSON envelope per line.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/daemon\n *\n * @epic T11625\n * @task T11894 — supervisor fast path: TS lease-ipc-client (ST-5)\n * @saga T11243 SG-RUNTIME-UNIFICATION\n * @see ./supervisor-client.ts — the v1.0 codec this mirrors\n */\n\nimport type {\n LeaseIpcRequest,\n LeaseIpcRequestEnvelope,\n LeaseIpcResponseEnvelope,\n} from '@cleocode/contracts';\nimport {\n LeaseIpcRequestEnvelopeSchema,\n LeaseIpcResponseEnvelopeSchema,\n} from '@cleocode/contracts/lease-ipc/messages.js';\nimport { LEASE_IPC_PROTOCOL_VERSION } from '@cleocode/contracts/lease-ipc/version.js';\n\n/**\n * A typed error raised when an inbound NDJSON line cannot be decoded into a\n * valid {@link LeaseIpcResponseEnvelope}.\n *\n * Carries the offending raw `line` and the underlying `cause` (a JSON\n * `SyntaxError` or a Zod validation error) so callers can log + correlate.\n * Invalid frames are surfaced as this error — never silently dropped. This is\n * the v1.1 sibling of {@link MalformedIpcFrameError}.\n *\n * @public\n */\nexport class MalformedLeaseIpcFrameError extends Error {\n /** The raw NDJSON line that failed to decode. */\n readonly line: string;\n\n /**\n * @param line - The offending raw NDJSON line.\n * @param cause - The underlying JSON or Zod error.\n */\n constructor(line: string, cause: unknown) {\n super(`Malformed lease-ipc frame: ${cause instanceof Error ? cause.message : cause}`);\n this.name = 'MalformedLeaseIpcFrameError';\n this.line = line;\n this.cause = cause;\n }\n}\n\n/**\n * A transport-agnostic codec for the PARALLEL `lease-ipc` v1.1 wire format.\n *\n * Construct via {@link createLeaseIpcClient}. The client is stateful only in its\n * monotonic correlation-id counter; it owns no socket. Callers feed it raw lines\n * from whatever transport (Unix socket, named pipe, test pair) they own.\n */\nexport interface LeaseIpcClient {\n /**\n * Encode a request body into a complete, newline-terminated NDJSON envelope\n * line ready to write to the transport.\n *\n * Stamps the parallel protocol version ({@link LEASE_IPC_PROTOCOL_VERSION}) and\n * a fresh correlation `id`, validates the result against\n * {@link LeaseIpcRequestEnvelopeSchema}, and returns the serialized line\n * (including the trailing `\\n`).\n *\n * @param request - The request body (validated by the envelope schema).\n * @returns `{ id, line }` — the correlation id and the NDJSON line to send.\n */\n encodeRequest: (request: LeaseIpcRequest) => { id: string; line: string };\n\n /**\n * Decode a single inbound NDJSON line into a typed response envelope.\n *\n * Parses the line as JSON, then validates it against\n * {@link LeaseIpcResponseEnvelopeSchema}. A malformed or schema-violating line\n * throws {@link MalformedLeaseIpcFrameError} — it is never silently dropped.\n *\n * @param line - A single NDJSON line (without the trailing newline).\n * @returns The decoded {@link LeaseIpcResponseEnvelope}.\n * @throws {MalformedLeaseIpcFrameError} When the line is not a valid response frame.\n */\n decodeResponseLine: (line: string) => LeaseIpcResponseEnvelope;\n}\n\n/**\n * Generate a process-unique correlation id for a lease IPC request envelope.\n *\n * @param seq - The client's monotonic sequence number.\n * @returns A correlation id of the form `l<seq>-<random>`.\n */\nfunction nextCorrelationId(seq: number): string {\n return `l${seq}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Create a typed NDJSON codec for the PARALLEL `lease-ipc` v1.1 contract.\n *\n * @returns A {@link LeaseIpcClient}.\n *\n * @example\n * ```ts\n * const client = createLeaseIpcClient();\n * const { id, line } = client.encodeRequest({\n * kind: 'lease_acquire', scope: 'project', lane: 'tasks',\n * holder_id: 'pid-42:tasks', priority: 0, ttl_ms: 30_000, reentrant: true,\n * });\n * socket.write(line);\n * socket.on('line', (raw) => {\n * const env = client.decodeResponseLine(raw); // throws on a bad frame\n * if (env.response.kind === 'lease_granted') { /* ... *\\/ }\n * });\n * ```\n */\nexport function createLeaseIpcClient(): LeaseIpcClient {\n let seq = 0;\n\n return {\n encodeRequest(request: LeaseIpcRequest): { id: string; line: string } {\n seq += 1;\n const id = nextCorrelationId(seq);\n const envelope: LeaseIpcRequestEnvelope = {\n protocol_version: LEASE_IPC_PROTOCOL_VERSION,\n id,\n direction: 'request',\n request,\n };\n // Validate before serialization so we never emit an off-contract frame.\n const parsed = LeaseIpcRequestEnvelopeSchema.parse(envelope);\n return { id, line: `${JSON.stringify(parsed)}\\n` };\n },\n\n decodeResponseLine(line: string): LeaseIpcResponseEnvelope {\n let json: unknown;\n try {\n json = JSON.parse(line);\n } catch (cause) {\n throw new MalformedLeaseIpcFrameError(line, cause);\n }\n const result = LeaseIpcResponseEnvelopeSchema.safeParse(json);\n if (!result.success) {\n throw new MalformedLeaseIpcFrameError(line, result.error);\n }\n return result.data;\n },\n };\n}\n","/**\n * Worker-side heartbeat sender for the watchdog (T11628).\n *\n * A thin wrapper over {@link createLeaseIpcClient} that encodes a\n * `worker_heartbeat` frame and writes it to the supervisor over whatever\n * transport the caller owns. It is **degrade-by-default**: when no transport is\n * wired (no supervisor up / the daemon-off posture) {@link sendHeartbeat} is a\n * no-op, exactly mirroring how the LLM-queue admit path degrades when the\n * supervisor arbiter is absent. A managed worker calls {@link sendHeartbeat}\n * periodically (well inside the watchdog's NORMAL deadline) so the supervisor\n * knows it is alive; on a miss the watchdog SIGTERM→SIGKILLs it.\n *\n * The sender owns no socket: the caller supplies a `writeLine` sink (the\n * connected Unix-socket write half, a test double, …) — keeping this module free\n * of platform IO and trivially testable, consistent with the v1.0/v1.1 codecs.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/daemon\n *\n * @epic T11625\n * @task T11628 — supervisor watchdog: worker heartbeat sender\n * @saga T11243 SG-RUNTIME-UNIFICATION\n * @see ./lease-ipc-client.ts — the codec this wraps\n */\n\nimport { createLeaseIpcClient, type LeaseIpcClient } from './lease-ipc-client.js';\n\n/**\n * A transport sink that writes one already-framed NDJSON line (including the\n * trailing `\\n`) to the supervisor. Returning a promise lets the caller await a\n * flush; the sender does not require it.\n */\nexport type WriteLine = (line: string) => void | Promise<void>;\n\n/**\n * A degrade-by-default worker heartbeat sender.\n *\n * Construct via {@link createHeartbeatSender}. When no transport is wired,\n * {@link HeartbeatSender.sendHeartbeat} is a no-op and {@link HeartbeatSender.isActive}\n * is `false`.\n */\nexport interface HeartbeatSender {\n /**\n * Send one `worker_heartbeat` for `childId`. A no-op (resolves immediately)\n * when no transport is wired — the worker degrades gracefully when no\n * supervisor is up.\n *\n * @param childId - The heartbeating child's logical id (registry key).\n * @param inFlightLlm - The worker's own view of whether it is currently\n * inside an LLM call. `true` lets the watchdog grant the\n * EXTENDED deadline so a slow-but-healthy long call is\n * never false-killed (AC2 · RISK-7).\n * @returns The correlation `id` of the sent frame, or `null` when no transport\n * is wired (the no-op path).\n */\n sendHeartbeat: (childId: string, inFlightLlm: boolean) => Promise<string | null>;\n\n /**\n * Whether a transport is wired. `false` means {@link sendHeartbeat} is a\n * no-op (no supervisor up).\n */\n isActive: () => boolean;\n}\n\n/**\n * Create a worker heartbeat sender.\n *\n * @param writeLine - The transport sink that writes one NDJSON line to the\n * supervisor, or `undefined`/`null` when no supervisor is up\n * (the no-op / degrade path).\n * @param client - An optional pre-built {@link LeaseIpcClient} codec (defaults\n * to a fresh one); injectable for tests.\n * @returns A {@link HeartbeatSender}.\n *\n * @example\n * ```ts\n * // With a connected supervisor socket:\n * const sender = createHeartbeatSender((line) => socket.write(line));\n * setInterval(() => sender.sendHeartbeat('worker-7', isInLlmCall()), 10_000);\n *\n * // No supervisor up — every sendHeartbeat is a silent no-op:\n * const offline = createHeartbeatSender(undefined);\n * await offline.sendHeartbeat('worker-7', false); // resolves to null\n * ```\n */\nexport function createHeartbeatSender(\n writeLine: WriteLine | undefined | null,\n client: LeaseIpcClient = createLeaseIpcClient(),\n): HeartbeatSender {\n const active = typeof writeLine === 'function';\n\n return {\n isActive(): boolean {\n return active;\n },\n\n async sendHeartbeat(childId: string, inFlightLlm: boolean): Promise<string | null> {\n // Degrade: no transport wired → no-op (mirrors the queue's degrade).\n if (!active || writeLine == null) {\n return null;\n }\n const { id, line } = client.encodeRequest({\n kind: 'worker_heartbeat',\n child_id: childId,\n in_flight_llm: inFlightLlm,\n });\n await writeLine(line);\n return id;\n },\n };\n}\n","/**\n * Daemon subsystem log-routing primitive.\n *\n * `createSubsystemLogger(name)` is the **single** logging surface the\n * `@cleocode/runtime/daemon` submodule exposes. It routes through the canonical\n * pino factory `getLogger` (`packages/core/src/logger.ts`) — NOT raw\n * `createWriteStream` and NOT `console.*` — so every daemon log line lands in\n * the rotating, retention-managed CLEO log files with a consistent structured\n * shape and a per-subsystem `subsystem` field.\n *\n * It supersedes the **5 ad-hoc logging conventions** the legacy daemons grew,\n * which R4-R7 will migrate onto this primitive:\n *\n * 1. `sentient/daemon.ts` — `createWriteStream(sentient.log)` /\n * `createWriteStream(sentient.err)` + `process.stderr.write` lines.\n * 2. `gc/daemon.ts` — `createWriteStream(gc.log)` / `createWriteStream(gc.err)`.\n * 3. `web.ts` — `console.*`.\n * 4. `docs-viewer.ts` — ad-hoc stdout/stderr writes.\n * 5. the `runtime` services — bespoke per-service logging.\n *\n * ## Migration guidance for R4-R7 adapters\n *\n * Each adapter (Studio, GC, web, docs-viewer) replaces its bespoke sink with:\n *\n * ```ts\n * import { createSubsystemLogger } from '@cleocode/runtime/daemon';\n * const log = createSubsystemLogger('studio');\n * log.info({ pid }, 'Studio started'); // not process.stderr.write(...)\n * log.error({ err }, 'Studio crashed'); // not createWriteStream('sentient.err')\n * ```\n *\n * Do not import pino directly and do not open file streams — the canonical\n * factory owns rotation, retention, level, and the `projectHash` correlation\n * field. Routing through it is what makes the daemon's logs observable via the\n * same `cleo` log surface as the rest of the system.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/daemon\n *\n * @epic T11253 R2 — `@cleocode/runtime/daemon` submodule\n * @task T11368 — log routing primitive (pino getLogger canonical sink)\n * @saga T11243 SG-RUNTIME-UNIFICATION\n */\n\nimport { getLogger } from '@cleocode/core/logger';\n\n/**\n * A structured log payload — a plain object of serializable fields merged into\n * the emitted log record (e.g. `{ pid, err }`).\n */\nexport type SubsystemLogFields = Record<string, unknown>;\n\n/**\n * The narrow logging surface a daemon subsystem uses.\n *\n * Each method accepts an optional structured `fields` object followed by the\n * message, mirroring pino's `(obj, msg)` call shape. This is intentionally\n * smaller than the full pino `Logger` so the daemon submodule does not leak the\n * pino type across its public boundary while still routing through the\n * canonical pino sink.\n */\nexport interface SubsystemLogger {\n /** Log at `debug` level. */\n debug: (fields: SubsystemLogFields, msg: string) => void;\n /** Log at `info` level. */\n info: (fields: SubsystemLogFields, msg: string) => void;\n /** Log at `warn` level. */\n warn: (fields: SubsystemLogFields, msg: string) => void;\n /** Log at `error` level. */\n error: (fields: SubsystemLogFields, msg: string) => void;\n}\n\n/**\n * Create the single per-subsystem logger for the daemon submodule.\n *\n * Routes through the canonical pino factory `getLogger('daemon')` and namespaces\n * the result with a per-subsystem child binding (`{ subsystem: name }`), so all\n * daemon log lines share the `daemon` subsystem root while remaining\n * individually attributable. Never opens a file stream and never writes to\n * `console`.\n *\n * @param name - The logical subsystem name (e.g. `'studio'`, `'gc'`, `'web'`).\n * @returns A {@link SubsystemLogger} bound to `name`.\n *\n * @example\n * ```ts\n * const log = createSubsystemLogger('studio');\n * log.info({ pid: 4242, port: 3456 }, 'Studio started');\n * ```\n */\nexport function createSubsystemLogger(name: string): SubsystemLogger {\n // Bind under the canonical 'daemon' root, then a per-subsystem child so each\n // subsystem's lines carry `{ subsystem: 'daemon', daemonSubsystem: name }`.\n const log = getLogger('daemon').child({ daemonSubsystem: name });\n return {\n debug: (fields, msg) => log.debug(fields, msg),\n info: (fields, msg) => log.info(fields, msg),\n warn: (fields, msg) => log.warn(fields, msg),\n error: (fields, msg) => log.error(fields, msg),\n };\n}\n","/**\n * `SubsystemRegistry` — drives the uniform daemon subsystem lifecycle.\n *\n * Aggregates {@link Subsystem}s declared via `defineSubsystem` and runs them\n * through a uniform `start → healthProbe → shutdown` lifecycle, firing the\n * typed {@link DaemonLifecycleHooks} in order. Health probes are aggregated\n * into a {@link HealthStatus} that projects onto the FROZEN `supervisor-ipc`\n * `MonitorResponse` the Rust supervisor consumes (AC4).\n *\n * Shutdown runs in reverse registration order (LIFO) so dependants stop before\n * their dependencies — the conventional supervised-process teardown ordering.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/daemon\n *\n * @epic T11253 R2 — `@cleocode/runtime/daemon` submodule\n * @task T11367 — defineSubsystem + lifecycle/health registry + IPC client\n * @saga T11243 SG-RUNTIME-UNIFICATION\n */\n\nimport type {\n DaemonLifecycleHooks,\n HealthStatus,\n Subsystem,\n SubsystemHealth,\n SubsystemLifecyclePhase,\n} from '@cleocode/contracts';\nimport { summarizeHealth } from '@cleocode/contracts/daemon/health.js';\n\n/**\n * A registered subsystem plus its runtime bookkeeping.\n *\n * The `context` is the value `start()` resolved to, threaded back into\n * `shutdown()`. It is type-erased to `unknown` inside the registry because the\n * registry holds subsystems of heterogeneous context types; each entry's\n * `subsystem.shutdown` is the only consumer and re-narrows it at the call site\n * via the subsystem's own typing — no `any`, and the erased value never escapes\n * the registry surface.\n */\ninterface RegisteredSubsystem {\n readonly subsystem: Subsystem<unknown>;\n started: boolean;\n context: unknown;\n}\n\n/** Normalize an unknown thrown value into an `Error`. */\nfunction toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value));\n}\n\n/**\n * The registry that owns and drives the daemon's subsystems.\n *\n * Construct with optional {@link DaemonLifecycleHooks}; register subsystems,\n * then `startAll()` / `aggregateHealth()` / `shutdownAll()`.\n */\nexport class SubsystemRegistry {\n readonly #entries: RegisteredSubsystem[] = [];\n readonly #hooks: DaemonLifecycleHooks;\n\n /**\n * @param hooks - Optional lifecycle hooks fired around start/shutdown and on\n * any lifecycle error.\n */\n constructor(hooks: DaemonLifecycleHooks = {}) {\n this.#hooks = hooks;\n }\n\n /**\n * Register a subsystem. Registration order defines start order; shutdown runs\n * in reverse.\n *\n * @typeParam TContext - The subsystem's start→shutdown context type.\n * @param subsystem - A subsystem produced by `defineSubsystem`.\n * @throws {Error} When a subsystem with the same `name` is already registered.\n */\n register<TContext>(subsystem: Subsystem<TContext>): void {\n if (this.#entries.some((entry) => entry.subsystem.name === subsystem.name)) {\n throw new Error(`SubsystemRegistry: duplicate subsystem name '${subsystem.name}'`);\n }\n // Widen the context type to `unknown` for heterogeneous storage. The\n // subsystem's own `start`/`shutdown` remain individually well-typed; only\n // the registry's internal handle is erased, and it never leaks out.\n this.#entries.push({\n subsystem: subsystem as Subsystem<unknown>,\n started: false,\n context: undefined,\n });\n }\n\n /** The names of all registered subsystems, in registration order. */\n get names(): readonly string[] {\n return this.#entries.map((entry) => entry.subsystem.name);\n }\n\n /**\n * Start every registered subsystem in registration order.\n *\n * For each subsystem: awaits `start()`, stores the resolved context, then\n * fires `onStart`. If `start()` throws, fires `onError` with phase `start`\n * and re-throws — a failed start is surfaced, never swallowed.\n *\n * @throws The first subsystem `start()` error (after firing `onError`).\n */\n async startAll(): Promise<void> {\n for (const entry of this.#entries) {\n try {\n entry.context = await entry.subsystem.start();\n entry.started = true;\n await this.#hooks.onStart?.(entry.subsystem.name);\n } catch (cause) {\n await this.#fireError(entry.subsystem.name, cause, 'start');\n throw toError(cause);\n }\n }\n }\n\n /**\n * Probe every registered subsystem and aggregate into a {@link HealthStatus}.\n *\n * A subsystem whose `healthProbe()` rejects does not abort the aggregate:\n * `onError` fires with phase `healthProbe` and the subsystem is reported as a\n * `stopped` row carrying the error in its `detail`, so the supervisor still\n * receives a complete snapshot.\n *\n * @returns The aggregate health snapshot (projects onto `MonitorResponse`).\n */\n async aggregateHealth(): Promise<HealthStatus> {\n const rows: SubsystemHealth[] = [];\n for (const entry of this.#entries) {\n try {\n rows.push(await entry.subsystem.healthProbe());\n } catch (cause) {\n await this.#fireError(entry.subsystem.name, cause, 'healthProbe');\n rows.push({\n child_id: entry.subsystem.name,\n pid: 0,\n state: 'stopped',\n restart_count: 0,\n detail: `healthProbe failed: ${toError(cause).message}`,\n });\n }\n }\n return summarizeHealth(rows);\n }\n\n /**\n * Shut down every started subsystem in reverse registration order (LIFO).\n *\n * Each subsystem's `shutdown(context)` is awaited and `onShutdown` fires. A\n * throwing `shutdown` fires `onError` with phase `shutdown` but does NOT\n * abort the teardown of the remaining subsystems — best-effort graceful\n * shutdown of the whole daemon.\n */\n async shutdownAll(): Promise<void> {\n for (let i = this.#entries.length - 1; i >= 0; i -= 1) {\n const entry = this.#entries[i];\n if (entry === undefined || !entry.started) continue;\n try {\n await entry.subsystem.shutdown(entry.context);\n entry.started = false;\n await this.#hooks.onShutdown?.(entry.subsystem.name);\n } catch (cause) {\n await this.#fireError(entry.subsystem.name, cause, 'shutdown');\n // Continue tearing down the rest — do not abort on one failure.\n }\n }\n }\n\n /** Fire the `onError` hook, swallowing any error the hook itself throws. */\n async #fireError(name: string, cause: unknown, phase: SubsystemLifecyclePhase): Promise<void> {\n try {\n await this.#hooks.onError?.(name, toError(cause), phase);\n } catch {\n // A throwing error-hook must not mask the original failure.\n }\n }\n}\n","/**\n * Typed NDJSON client for the FROZEN `supervisor-ipc` v1.0 contract.\n *\n * The Rust `cleo-supervisor` is the server; a CLEO daemon process is a client\n * that sends {@link SupervisorIpcRequestEnvelope}s and receives\n * {@link SupervisorIpcResponseEnvelope}s, one newline-delimited JSON\n * (NDJSON) envelope per line. This module is a transport-agnostic codec: it\n * encodes outbound envelopes to NDJSON lines and decodes inbound lines through\n * the frozen Zod schemas. **Every inbound line is parsed** — a malformed or\n * schema-violating frame is rejected with a typed {@link MalformedIpcFrameError}\n * and never silently dropped (T11367 AC2).\n *\n * The codec does not own a socket — callers feed it raw lines from whatever\n * transport (Unix socket, named pipe, test pair) they own, keeping the daemon\n * submodule free of platform IO and trivially testable.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/daemon\n *\n * @epic T11253 R2 — `@cleocode/runtime/daemon` submodule\n * @task T11367 — defineSubsystem + lifecycle/health registry + IPC client\n * @saga T11243 SG-RUNTIME-UNIFICATION\n */\n\nimport type {\n SupervisorIpcRequest,\n SupervisorIpcRequestEnvelope,\n SupervisorIpcResponseEnvelope,\n} from '@cleocode/contracts';\nimport {\n SupervisorIpcRequestEnvelopeSchema,\n SupervisorIpcResponseEnvelopeSchema,\n} from '@cleocode/contracts/supervisor-ipc/messages.js';\nimport { SUPERVISOR_IPC_PROTOCOL_VERSION } from '@cleocode/contracts/supervisor-ipc/version.js';\n\n/**\n * A typed error raised when an inbound NDJSON line cannot be decoded into a\n * valid {@link SupervisorIpcResponseEnvelope}.\n *\n * Carries the offending raw `line` and the underlying `cause` (a JSON\n * `SyntaxError` or a Zod validation error) so callers can log + correlate.\n * Invalid frames are surfaced as this error — never silently dropped.\n */\nexport class MalformedIpcFrameError extends Error {\n /** The raw NDJSON line that failed to decode. */\n readonly line: string;\n\n /**\n * @param line - The offending raw NDJSON line.\n * @param cause - The underlying JSON or Zod error.\n */\n constructor(line: string, cause: unknown) {\n super(`Malformed supervisor-ipc frame: ${cause instanceof Error ? cause.message : cause}`);\n this.name = 'MalformedIpcFrameError';\n this.line = line;\n this.cause = cause;\n }\n}\n\n/**\n * A transport-agnostic codec for the FROZEN `supervisor-ipc` v1.0 wire format.\n *\n * Construct via {@link createSupervisorIpcClient}. The client is stateful only\n * in its monotonic correlation-id counter; it owns no socket.\n */\nexport interface SupervisorIpcClient {\n /**\n * Encode a request body into a complete, newline-terminated NDJSON envelope\n * line ready to write to the transport.\n *\n * Stamps the frozen protocol version and a fresh correlation `id`, validates\n * the result against {@link SupervisorIpcRequestEnvelopeSchema}, and returns\n * the serialized line (including the trailing `\\n`).\n *\n * @param request - The request body (validated by the envelope schema).\n * @returns `{ id, line }` — the correlation id and the NDJSON line to send.\n */\n encodeRequest: (request: SupervisorIpcRequest) => { id: string; line: string };\n\n /**\n * Decode a single inbound NDJSON line into a typed response envelope.\n *\n * Parses the line as JSON, then validates it against\n * {@link SupervisorIpcResponseEnvelopeSchema}. A malformed or\n * schema-violating line throws {@link MalformedIpcFrameError} — it is never\n * silently dropped.\n *\n * @param line - A single NDJSON line (without the trailing newline).\n * @returns The decoded {@link SupervisorIpcResponseEnvelope}.\n * @throws {MalformedIpcFrameError} When the line is not a valid response frame.\n */\n decodeResponseLine: (line: string) => SupervisorIpcResponseEnvelope;\n}\n\n/**\n * Generate a process-unique correlation id for an IPC request envelope.\n *\n * @param seq - The client's monotonic sequence number.\n * @returns A correlation id of the form `c<seq>-<random>`.\n */\nfunction nextCorrelationId(seq: number): string {\n return `c${seq}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Create a typed NDJSON codec for the FROZEN `supervisor-ipc` v1.0 contract.\n *\n * @returns A {@link SupervisorIpcClient}.\n *\n * @example\n * ```ts\n * const client = createSupervisorIpcClient();\n * const { id, line } = client.encodeRequest({ kind: 'health' });\n * socket.write(line);\n * socket.on('line', (raw) => {\n * const env = client.decodeResponseLine(raw); // throws MalformedIpcFrameError on bad frame\n * if (env.response.kind === 'health') { /* ... *\\/ }\n * });\n * ```\n */\nexport function createSupervisorIpcClient(): SupervisorIpcClient {\n let seq = 0;\n\n return {\n encodeRequest(request: SupervisorIpcRequest): { id: string; line: string } {\n seq += 1;\n const id = nextCorrelationId(seq);\n const envelope: SupervisorIpcRequestEnvelope = {\n protocol_version: SUPERVISOR_IPC_PROTOCOL_VERSION,\n id,\n direction: 'request',\n request,\n };\n // Validate before serialization so we never emit an off-contract frame.\n const parsed = SupervisorIpcRequestEnvelopeSchema.parse(envelope);\n return { id, line: `${JSON.stringify(parsed)}\\n` };\n },\n\n decodeResponseLine(line: string): SupervisorIpcResponseEnvelope {\n let json: unknown;\n try {\n json = JSON.parse(line);\n } catch (cause) {\n throw new MalformedIpcFrameError(line, cause);\n }\n const result = SupervisorIpcResponseEnvelopeSchema.safeParse(json);\n if (!result.success) {\n throw new MalformedIpcFrameError(line, result.error);\n }\n return result.data;\n },\n };\n}\n"],"mappings":"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","names":["nextCorrelationId"]}
@@ -1,12 +1,13 @@
1
1
  import {
2
2
  inferGateway,
3
3
  resolveStreamRoute
4
- } from "./chunk-L5BLVQDA.js";
4
+ } from "./chunk-4PUSKGDO.js";
5
5
 
6
6
  // src/gateway/http/listen.ts
7
7
  import { randomUUID as randomUUID2 } from "crypto";
8
8
  import { createServer } from "http";
9
9
  import { getLogger as getLogger2 } from "@cleocode/core";
10
+ import { useBrainWriterThread } from "@cleocode/core/memory/brain-writer-thread";
10
11
 
11
12
  // src/gateway/http/server.ts
12
13
  import { randomUUID } from "crypto";
@@ -1026,6 +1027,7 @@ async function handleHttpRequest(handler, req, res, log2, studioOpts) {
1026
1027
  );
1027
1028
  }
1028
1029
  function startHttpServer(handler, opts) {
1030
+ useBrainWriterThread();
1029
1031
  const log2 = getLogger2("gateway-http");
1030
1032
  const host = opts.host ?? "127.0.0.1";
1031
1033
  const studioOpts = opts.studioStaticDir !== void 0 ? { staticDir: opts.studioStaticDir } : void 0;
@@ -1104,4 +1106,4 @@ export {
1104
1106
  parseHttpRoute,
1105
1107
  startHttpServer
1106
1108
  };
1107
- //# sourceMappingURL=chunk-U2ESNIJ5.js.map
1109
+ //# sourceMappingURL=chunk-NOPLC5WS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/gateway/http/listen.ts","../src/gateway/http/server.ts","../src/gateway/http/sse.ts","../src/gateway/http/stream-sources.ts","../src/gateway/http/studio-static.ts","../src/gateway/http/ws-pty.ts"],"sourcesContent":["/**\n * `node:http` server binding for the framework-agnostic HTTP gateway adapter.\n *\n * The HTTP adapter ({@link routeUnary} + the SSE primitives) is deliberately\n * framework-agnostic — it owns NO port binding and NO `Request`/`Response`\n * lifecycle, so an embedder (a SvelteKit route, a test harness) can drive it\n * directly. The **daemon**, however, is itself the embedder: it must expose the\n * gateway to external HTTP clients over a real listening socket. This module is\n * that thin embedder — a minimal `node:http` server that parses the request URL\n * into a `(gateway, domain, operation)` triple, reads the JSON body, and routes\n * it through {@link routeUnary}. It deliberately stays in the runtime (no\n * SvelteKit / no `@cleocode/cleo` / no drizzle dependency) so the daemon can\n * host the gateway without reaching across a package boundary.\n *\n * Routing contract (intentionally minimal — the wire vocabulary is the same as\n * every other transport, only the framing differs):\n *\n * `POST /<gateway>/<domain>/<operation>` with a JSON body → operation params\n *\n * where `<gateway>` is `query` | `mutate`. A non-POST method, an unparseable\n * path, or a body that is not a JSON object is rejected at the wire edge with a\n * LAFS-shaped error envelope — the injected {@link GatewayHandler} is never\n * invoked with a malformed request.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/gateway/http\n *\n * @task T11451\n * @epic T11254\n * @saga T11243 SG-RUNTIME-UNIFICATION\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport type { DispatchResponse, Gateway, GatewayStreamEvent } from '@cleocode/contracts/gateway';\nimport { getLogger } from '@cleocode/core';\nimport { useBrainWriterThread } from '@cleocode/core/memory/brain-writer-thread';\nimport type { GatewayHandler } from '../index.js';\nimport { inferGateway, resolveStreamRoute } from '../registry.js';\nimport { routeUnary } from './server.js';\nimport { createSseStream, encodeStreamEvent, SSE_HEADERS } from './sse.js';\nimport { resolveStreamSource, type StreamSourceContext } from './stream-sources.js';\nimport { isStudioPath, type StudioStaticOptions, serveStudioStatic } from './studio-static.js';\nimport type { HttpUnaryRequest } from './types.js';\nimport { attachWsPtyEndpoint, type WsPtyOptions } from './ws-pty.js';\n\n/** The two CQRS gateways a path segment may name. */\nconst GATEWAYS: ReadonlySet<string> = new Set<Gateway>(['query', 'mutate']);\n\n/** Maximum accepted request-body size (1 MiB) — a crude DoS guard at the edge. */\nconst MAX_BODY_BYTES = 1_048_576;\n\n/**\n * The versioned REST facade prefix (T11919). `/v1/<domain>/<operation>` infers\n * the gateway from the registry; `/v1/health` is the liveness probe. The legacy\n * `/<gateway>/<domain>/<operation>` form is retained for backward compatibility.\n */\nconst V1_PREFIX = 'v1';\n\n/**\n * Options for {@link startHttpServer}.\n *\n * Mirrors the connection-oriented {@link RpcServerOptions} shape: the caller\n * resolves the bind coordinates so the runtime stays free of any\n * `@cleocode/paths` dependency.\n */\nexport interface HttpServerOptions {\n /**\n * TCP port to bind. `0` selects an ephemeral port (the bound port is then\n * readable from {@link HttpServerHandle.port}).\n */\n port: number;\n /**\n * Host/interface to bind. Defaults to `127.0.0.1` (loopback only) — the\n * gateway is local-process-facing; exposing it on a public interface is an\n * explicit caller decision.\n */\n host?: string;\n /**\n * WebSocket terminal/PTY endpoint configuration (T11922 · M5 WS half). When\n * provided, a WS upgrade handler for {@link WS_PTY_PATH} (`/v1/terminal/pty`)\n * is attached to this server's `'upgrade'` event — gated at the edge (loopback\n * + optional token/origin) and bridging a PTY bidirectionally over WS frames.\n * When OMITTED, no upgrade handler is attached and the server stays a pure\n * unary+SSE HTTP listener (the capability is opt-in — the daemon enables it\n * explicitly). The optional `node-pty` backend is loaded dynamically; if it is\n * absent, the endpoint still upgrades and reports a clean \"PTY backend\n * unavailable\" close rather than crashing.\n */\n wsPty?: WsPtyOptions;\n /**\n * Studio static asset directory for the bundled Web Studio (T11979).\n *\n * When provided, requests whose path starts with `/studio` are served as\n * static files from this directory (the `build/client/` output from an\n * `adapter-node` Studio build), with SPA fallback to `index.html` for\n * sub-routes. When `undefined` or the path does not exist on disk, a clean\n * JSON 503 envelope is returned instead of a stack trace — allowing the\n * gateway to start without a Studio bundle present (dev checkouts).\n *\n * The value is resolved and injected by the caller (`cleo daemon serve`)\n * using {@link resolveStudioStaticDir} from `@cleocode/cleo` so the runtime\n * layer stays free of any path-resolution logic.\n */\n studioStaticDir?: string;\n}\n\n/**\n * A live HTTP server handle returned by {@link startHttpServer}.\n *\n * Exposes the bound {@link Server}, the actually-bound `port` (resolved even\n * when `0` was requested), and an idempotent {@link close}. The adapter never\n * calls `process.exit` — lifecycle stays with the caller (R3 contract).\n */\nexport interface HttpServerHandle {\n /** The bound `node:http` server. */\n server: Server;\n /** The actually-bound TCP port (resolved when `0` was requested). */\n port: number;\n /** Host/interface the server is bound to. */\n host: string;\n /** Close the server + all live connections; resolves once fully closed. */\n close(): Promise<void>;\n}\n\n/**\n * The result of parsing a request line into routing coordinates: a resolved\n * unary triple, a recognized health probe, or a rejection for the wire edge.\n */\ntype ParsedRoute =\n | { ok: true; kind: 'unary'; gateway: Gateway; domain: string; operation: string }\n | { ok: true; kind: 'stream'; gateway: Gateway; domain: string; operation: string }\n | { ok: true; kind: 'health' }\n | { ok: false; status: number; code: string; message: string };\n\n/**\n * Parse a gateway request line into a routing decision.\n *\n * Two POST routing vocabularies are accepted (the wire payload is identical to\n * every other transport — only the framing differs):\n *\n * - `POST /v1/<domain>/<operation>` — the **versioned REST facade** (T11919).\n * The gateway (`query` | `mutate`) is INFERRED from the registry, so the\n * client need not name it. An unregistered `(domain, operation)` pair is a\n * `404`.\n * - `POST /<gateway>/<domain>/<operation>` — the **legacy** form, where the\n * client names the gateway segment explicitly (validated against the two\n * CQRS gateways so a client cannot smuggle an arbitrary value).\n *\n * Two GET routing vocabularies are accepted:\n *\n * - `GET /v1/health` — the liveness probe.\n * - `GET /v1/<domain>/<operation>` — the **SSE streaming** endpoint (T11921),\n * recognized only when the `(domain, operation)` pair is registered AND\n * flagged `streaming: true` in {@link OPERATIONS} (resolved via\n * {@link resolveStreamRoute}). The SAME pair is dispatched UNARY over POST,\n * so the streaming GET co-exists with the unary POST route. A GET to a\n * unary-only op is a `405` (method-not-allowed), distinguishing \"route\n * exists, this method is unsupported\" from \"no such route\" (`404`).\n *\n * A non-POST/GET method is rejected at the edge (`405`); a path that resolves to\n * neither vocabulary is a `404`. The injected {@link GatewayHandler} is never\n * invoked with a malformed request.\n *\n * @param method - The HTTP request method.\n * @param url - The HTTP request URL (path + query).\n * @returns The parsed route, or a typed rejection for the wire edge.\n */\nexport function parseHttpRoute(method: string | undefined, url: string | undefined): ParsedRoute {\n // Parse against a dummy origin so only the path is consumed (query ignored).\n const pathname = new URL(url ?? '/', 'http://localhost').pathname;\n const segments = pathname.split('/').filter((s) => s.length > 0);\n\n // GET routes: the liveness probe and the SSE streaming endpoint.\n if (method === 'GET') {\n if (segments.length === 2 && segments[0] === V1_PREFIX && segments[1] === 'health') {\n return { ok: true, kind: 'health' };\n }\n // GET /v1/<domain>/<operation> — SSE streaming endpoint (T11921). Only a\n // `(domain, operation)` pair registered AND flagged `streaming: true` opens\n // a `text/event-stream`; this co-exists with the unary POST routes (the\n // SAME pair is dispatched unary over POST). A GET to a unary-only op falls\n // through to a 405 so the caller can tell \"route exists, method unsupported\"\n // from \"no such route\" (404).\n if (segments.length === 3 && segments[0] === V1_PREFIX) {\n const [, domain, operation] = segments;\n const stream = resolveStreamRoute(domain, operation);\n if (stream !== undefined) {\n return { ok: true, kind: 'stream', gateway: stream.gateway, domain, operation };\n }\n }\n return {\n ok: false,\n status: 405,\n code: 'E_HTTP_METHOD_NOT_ALLOWED',\n message: `method GET not allowed for '${pathname}'; only GET /v1/health and registered streaming ops are served`,\n };\n }\n\n if (method !== 'POST') {\n return {\n ok: false,\n status: 405,\n code: 'E_HTTP_METHOD_NOT_ALLOWED',\n message: `method ${method ?? '<none>'} not allowed; gateway accepts POST`,\n };\n }\n\n // POST /v1/<domain>/<operation> — versioned facade; gateway inferred.\n if (segments.length === 3 && segments[0] === V1_PREFIX) {\n const [, domain, operation] = segments;\n const gateway = inferGateway(domain, operation);\n if (gateway === undefined) {\n return {\n ok: false,\n status: 404,\n code: 'E_HTTP_NOT_FOUND',\n message: `unknown operation '${domain}/${operation}'; no registered query/mutate matches`,\n };\n }\n return { ok: true, kind: 'unary', gateway, domain, operation };\n }\n\n // POST /<gateway>/<domain>/<operation> — legacy explicit-gateway form.\n if (segments.length !== 3) {\n return {\n ok: false,\n status: 404,\n code: 'E_HTTP_NOT_FOUND',\n message: `expected POST /v1/<domain>/<operation> or /<gateway>/<domain>/<operation>, got '${pathname}'`,\n };\n }\n const [gateway, domain, operation] = segments;\n if (!GATEWAYS.has(gateway)) {\n return {\n ok: false,\n status: 404,\n code: 'E_HTTP_NOT_FOUND',\n message: `unknown gateway '${gateway}'; expected one of query, mutate`,\n };\n }\n return { ok: true, kind: 'unary', gateway: gateway as Gateway, domain, operation };\n}\n\n/**\n * Read the full request body as a string, enforcing {@link MAX_BODY_BYTES}.\n *\n * @param req - The inbound request stream.\n * @returns The decoded UTF-8 body.\n * @throws {Error} When the body exceeds the size cap (the connection is then\n * destroyed by the caller).\n */\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise<string>((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n req.on('data', (chunk: Buffer) => {\n size += chunk.length;\n if (size > MAX_BODY_BYTES) {\n reject(new Error(`request body exceeds ${MAX_BODY_BYTES} bytes`));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));\n req.on('error', reject);\n });\n}\n\n/**\n * Write a LAFS-shaped error envelope to the response at the wire edge.\n *\n * Used for rejections that never reach the {@link GatewayHandler} (bad method,\n * unroutable path, malformed body), so a client always sees a well-formed LAFS\n * envelope rather than a bare HTTP error.\n *\n * @param res - The server response.\n * @param status - The HTTP status to set.\n * @param code - The machine-readable LAFS error code.\n * @param message - A human-readable message.\n */\nfunction writeEdgeError(res: ServerResponse, status: number, code: string, message: string): void {\n const body: DispatchResponse = {\n meta: {\n gateway: 'query',\n domain: '',\n operation: '',\n timestamp: new Date().toISOString(),\n duration_ms: 0,\n source: 'http',\n requestId: '',\n },\n success: false,\n error: { code, message },\n };\n res.writeHead(status, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(body));\n}\n\n/**\n * Write a LAFS-shaped success envelope for the `GET /v1/health` liveness probe.\n *\n * The probe never touches the {@link GatewayHandler} (it is a pure wire-edge\n * liveness signal), so it returns a self-contained `200` envelope reporting the\n * server is up and the wire version it serves.\n *\n * @param res - The server response.\n */\nfunction writeHealth(res: ServerResponse): void {\n const body: DispatchResponse = {\n meta: {\n gateway: 'query',\n domain: 'gateway',\n operation: 'health',\n timestamp: new Date().toISOString(),\n duration_ms: 0,\n source: 'http',\n requestId: '',\n },\n success: true,\n data: { status: 'ok', version: V1_PREFIX },\n };\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(body));\n}\n\n/**\n * Decode the query string of a streaming `GET` into a params object.\n *\n * Numeric-looking values (e.g. `?ticks=3`) are coerced to numbers so a declared\n * `number` param (`ticks`) arrives typed; everything else stays a string. This\n * mirrors how the unary path receives an already-decoded JSON body, but for the\n * GET-with-query-string streaming shape.\n *\n * @param url - The raw request URL (path + query).\n * @returns The decoded params object (empty when there is no query string).\n */\nfunction parseStreamParams(url: string | undefined): Record<string, unknown> {\n const parsed = new URL(url ?? '/', 'http://localhost');\n const params: Record<string, unknown> = {};\n for (const [key, value] of parsed.searchParams.entries()) {\n const asNumber = Number(value);\n params[key] = value !== '' && Number.isFinite(asNumber) ? asNumber : value;\n }\n return params;\n}\n\n/**\n * Write a single terminal SSE `error` frame and close the response.\n *\n * Used when a streaming route is recognized but has no registered source — the\n * client gets one well-formed `data: {kind:'error',…}` frame on the\n * `text/event-stream` body (not a JSON error envelope, which a streaming client\n * is not parsing for), then a clean close.\n *\n * @param res - The server response.\n * @param domain - The streaming route domain (for the error message).\n * @param operation - The streaming route operation (for the error message).\n * @param requestId - The request id stamped on the frame.\n */\nfunction writeStreamError(\n res: ServerResponse,\n domain: string,\n operation: string,\n requestId: string,\n): void {\n const frame: GatewayStreamEvent = {\n kind: 'error',\n seq: 0,\n error: {\n code: 'E_HTTP_NO_STREAM_SOURCE',\n message: `no streaming source registered for '${domain}/${operation}'`,\n },\n requestId,\n };\n res.writeHead(200, { ...SSE_HEADERS });\n res.end(encodeStreamEvent(frame));\n}\n\n/**\n * Handle a `GET /v1/<domain>/<operation>` streaming request: resolve the\n * registered {@link GatewayStreamSource}, open a `text/event-stream`, and pipe\n * its {@link GatewayStreamEvent} frames through the abort-safe\n * {@link createSseStream} builder until the source completes or the client\n * disconnects.\n *\n * The request's `close` is bridged to an {@link AbortController} so a client\n * disconnect closes the stream leak-free (the source's teardown runs exactly\n * once). The web {@link ReadableStream} the builder produces is adapted onto the\n * `node:http` response via {@link Readable.fromWeb}. Secrets never touch the\n * wire — the source resolves any credential server-side before emitting.\n *\n * @param req - The inbound request.\n * @param res - The outbound response.\n * @param domain - The resolved streaming-route domain.\n * @param operation - The resolved streaming-route operation.\n * @param log - The adapter's pino logger.\n */\nasync function handleStreamRequest(\n req: IncomingMessage,\n res: ServerResponse,\n domain: string,\n operation: string,\n log: ReturnType<typeof getLogger>,\n): Promise<void> {\n const requestId = randomUUID();\n const source = resolveStreamSource(domain, operation);\n if (source === undefined) {\n writeStreamError(res, domain, operation, requestId);\n log.warn({ domain, operation }, 'sse stream has no registered source');\n return;\n }\n\n // Bridge client disconnect → abort so createSseStream tears the source down.\n const controller = new AbortController();\n const onClose = (): void => controller.abort();\n req.once('close', onClose);\n\n const context: StreamSourceContext = {\n domain,\n operation,\n params: parseStreamParams(req.url),\n requestId,\n };\n\n res.writeHead(200, { ...SSE_HEADERS });\n\n const webStream = createSseStream((emitter) => source(emitter, context), controller.signal);\n\n // Drain the web ReadableStream's reader directly onto the node response. We do\n // NOT use `Readable.fromWeb` here: its DOM-typed `ReadableStream<any>` clashes\n // with the global web-streams `ReadableStream<Uint8Array>` over ArrayBuffer\n // variance under `tsup --dts`. Reading the reader manually keeps the types\n // exact and gives us a single place to honor backpressure + abort.\n const reader = webStream.getReader();\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (res.writableEnded) break;\n // Honor backpressure: if the kernel buffer is full, wait for `drain`.\n if (!res.write(value)) {\n await new Promise<void>((resolve) => res.once('drain', resolve));\n }\n }\n } catch (err) {\n log.warn({ err, domain, operation }, 'sse stream pipe failed');\n } finally {\n reader.releaseLock();\n req.removeListener('close', onClose);\n if (!controller.signal.aborted) controller.abort();\n if (!res.writableEnded) res.end();\n log.debug({ domain, operation, requestId }, 'sse stream closed');\n }\n}\n\n/**\n * Handle a single inbound HTTP request: parse the route, read + JSON-parse the\n * body, route it through the gateway handler, and serialize the LAFS response.\n *\n * Requests whose path starts with `/studio` are delegated to the Studio static\n * file handler ({@link serveStudioStatic}) before the normal gateway routing is\n * consulted — they never reach the gateway handler.\n *\n * All failure modes are rendered as LAFS envelopes — a bad route or malformed\n * body is rejected at the edge (handler never invoked); a thrown handler error\n * is already trapped inside {@link routeUnary} as a `500`. The function never\n * throws and never calls `process.exit`.\n *\n * @param handler - The injected transport-neutral gateway handler.\n * @param req - The inbound request.\n * @param res - The outbound response.\n * @param log - The adapter's pino logger.\n * @param studioOpts - Studio static serving options (T11979). When provided,\n * requests under `/studio` are served from the bundled static assets.\n */\nasync function handleHttpRequest(\n handler: GatewayHandler,\n req: IncomingMessage,\n res: ServerResponse,\n log: ReturnType<typeof getLogger>,\n studioOpts?: StudioStaticOptions,\n): Promise<void> {\n // Studio static serving (T11979) — intercept /studio/* before the API gateway.\n const pathname = new URL(req.url ?? '/', 'http://localhost').pathname;\n if (studioOpts !== undefined && isStudioPath(pathname)) {\n await serveStudioStatic(req, res, studioOpts);\n return;\n }\n\n const route = parseHttpRoute(req.method, req.url);\n if (!route.ok) {\n writeEdgeError(res, route.status, route.code, route.message);\n return;\n }\n if (route.kind === 'health') {\n writeHealth(res);\n log.debug({ route: 'health' }, 'http health probe served');\n return;\n }\n if (route.kind === 'stream') {\n await handleStreamRequest(req, res, route.domain, route.operation, log);\n return;\n }\n\n let params: Record<string, unknown> | undefined;\n try {\n const raw = await readBody(req);\n if (raw.trim().length > 0) {\n const parsed: unknown = JSON.parse(raw);\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n writeEdgeError(res, 400, 'E_HTTP_BAD_REQUEST', 'request body must be a JSON object');\n return;\n }\n params = parsed as Record<string, unknown>;\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n writeEdgeError(res, 400, 'E_HTTP_BAD_REQUEST', `invalid request body: ${message}`);\n return;\n }\n\n const unary: HttpUnaryRequest = {\n gateway: route.gateway,\n domain: route.domain,\n operation: route.operation,\n params,\n };\n const result = await routeUnary(handler, unary);\n res.writeHead(result.status, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(result.body));\n log.debug(\n {\n gateway: route.gateway,\n domain: route.domain,\n operation: route.operation,\n status: result.status,\n },\n 'http request routed',\n );\n}\n\n/**\n * Start a `node:http` server that hosts the gateway over HTTP for external\n * clients.\n *\n * Binds {@link HttpServerOptions.port} on {@link HttpServerOptions.host}\n * (default loopback), routes every `POST /<gateway>/<domain>/<operation>`\n * through the injected {@link GatewayHandler} via {@link routeUnary}, and\n * resolves once listening. The returned {@link HttpServerHandle} exposes an\n * idempotent `close()` for deterministic teardown (daemon shutdown, tests). The\n * adapter never calls `process.exit`.\n *\n * The caller assembles the handler (via `createGatewayHandler` with its domain\n * handlers + middleware) and injects it here — this server never builds\n * handlers itself, keeping the runtime free of any `@cleocode/cleo` dependency.\n *\n * @param handler - The transport-neutral gateway handler to route through.\n * @param opts - Bind coordinates.\n * @returns A promise resolving to the live {@link HttpServerHandle}.\n */\nexport function startHttpServer(\n handler: GatewayHandler,\n opts: HttpServerOptions,\n): Promise<HttpServerHandle> {\n // A long-lived host: brain writes run on the worker thread (T13126).\n useBrainWriterThread();\n const log = getLogger('gateway-http');\n const host = opts.host ?? '127.0.0.1';\n // Build StudioStaticOptions from the caller-injected dir (T11979).\n // Passing `undefined` when no dir is configured keeps the gateway purely\n // as an API server (dev checkouts where no Studio build exists).\n const studioOpts: StudioStaticOptions | undefined =\n opts.studioStaticDir !== undefined ? { staticDir: opts.studioStaticDir } : undefined;\n\n if (studioOpts !== undefined) {\n log.info({ studioStaticDir: opts.studioStaticDir }, 'studio static serving enabled at /studio');\n }\n\n const server = createServer((req, res) => {\n handleHttpRequest(handler, req, res, log, studioOpts).catch((err: unknown) => {\n // handleHttpRequest traps its own failures; this guards the wire path.\n const message = err instanceof Error ? err.message : String(err);\n log.warn({ err }, 'http request handling failed');\n if (!res.headersSent) {\n writeEdgeError(res, 500, 'E_HTTP_INTERNAL', message);\n } else if (!res.writableEnded) {\n res.end();\n }\n });\n });\n\n // Attach the WS terminal/PTY endpoint (T11922) to the SAME server's `'upgrade'`\n // event when configured — the WS capability lives on the existing listener\n // (AC1), gated at the edge (AC4) and torn down with the server (AC3).\n const wsPty = opts.wsPty !== undefined ? attachWsPtyEndpoint(server, opts.wsPty) : undefined;\n\n return new Promise<HttpServerHandle>((resolve, reject) => {\n server.once('error', reject);\n server.listen(opts.port, host, () => {\n server.removeListener('error', reject);\n const address = server.address();\n const boundPort = typeof address === 'object' && address !== null ? address.port : opts.port;\n log.info(\n { host, port: boundPort, wsPty: wsPty !== undefined },\n 'http gateway server listening',\n );\n\n let closed = false;\n const handle: HttpServerHandle = {\n server,\n port: boundPort,\n host,\n close(): Promise<void> {\n if (closed) return Promise.resolve();\n closed = true;\n // Tear down every live WS-PTY session + detach the upgrade listener\n // BEFORE closing the HTTP server, so a hung terminal cannot block\n // shutdown (deterministic teardown — AC3).\n wsPty?.close();\n return new Promise<void>((res) => {\n server.close(() => res());\n // Drop keep-alive connections so close() resolves promptly.\n server.closeIdleConnections();\n });\n },\n };\n resolve(handle);\n });\n });\n}\n","/**\n * HTTP transport adapter — serves gateway unary + SSE over HTTP.\n *\n * The fourth and final gateway transport (after CLI, MCP, RPC). Unlike the\n * connection-oriented RPC/MCP adapters, the HTTP adapter is framework-agnostic\n * by design: it does NOT bind a port or own a `Request`/`Response` lifecycle.\n * Instead it exposes two pure routing primitives the embedder (a SvelteKit\n * route handler, the daemon's HTTP server, a test harness) calls directly:\n *\n * - {@link routeUnary} — one POST → `source: 'http'` {@link DispatchRequest}\n * → injected {@link GatewayHandler} → `{ status, body }` (LAFS envelope).\n * - the SSE primitives in `./sse.js` (`createSseStream`) for streaming ops.\n *\n * This keeps the runtime free of any HTTP-server / SvelteKit / `@cleocode/cleo`\n * dependency: the embedder owns wire concerns (parsing the URL into a\n * `(gateway, domain, operation)` triple, setting headers, `process.exit`,\n * error-render at the edge), exactly mirroring how the CLI adapter wraps the\n * same handler. There is NO `process.exit` and NO error-render in the handlers\n * (R3 contract); the adapter never calls `process.exit` at all.\n *\n * The request `source` is forced to `'http'` regardless of what the embedder\n * passes, so a client cannot impersonate another transport.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/gateway/http\n *\n * @task T11450\n * @epic T11254\n * @saga T11243\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { DispatchRequest, DispatchResponse } from '@cleocode/contracts/gateway';\nimport type { GatewayHandler } from '../index.js';\nimport type { HttpUnaryRequest, HttpUnaryResult } from './types.js';\n\n/**\n * Map a LAFS dispatch error code to an HTTP status code.\n *\n * The canonical machine-readable error code (`E_NOT_FOUND`, `E_VALIDATION_…`,\n * `E_FORBIDDEN`, …) is the source of truth; the numeric LAFS exit code is a\n * secondary hint. Unknown error codes fall back to `500`. A successful envelope\n * is always `200`.\n *\n * @param response - The dispatch response.\n * @returns The HTTP status the embedder should set.\n */\nexport function statusForResponse(response: DispatchResponse): number {\n if (response.success) return 200;\n const code = response.error?.code ?? '';\n if (code === 'E_NOT_FOUND' || code.endsWith('_NOT_FOUND')) return 404;\n if (code === 'E_UNAUTHORIZED' || code === 'E_AUTH_REQUIRED') return 401;\n if (code === 'E_FORBIDDEN') return 403;\n if (code === 'E_RATE_LIMITED' || code === 'E_RATE_LIMIT_EXCEEDED') return 429;\n if (\n code === 'E_VALIDATION' ||\n code.startsWith('E_VALIDATION') ||\n code === 'E_INVALID_INPUT' ||\n code === 'E_BAD_REQUEST'\n ) {\n return 400;\n }\n if (code === 'E_NOT_IMPLEMENTED' || code === 'E_UNSUPPORTED') return 501;\n return 500;\n}\n\n/**\n * Route a single HTTP unary request through the gateway handler.\n *\n * Builds a `source: 'http'` {@link DispatchRequest} from the embedder-resolved\n * `(gateway, domain, operation, params)` triple and delegates to the injected\n * {@link GatewayHandler}. A thrown handler error is trapped and rendered as a\n * `500` LAFS error envelope — never propagated as an unhandled rejection or\n * `process.exit`. The `source` is always forced to `'http'`.\n *\n * @param handler - The injected transport-neutral gateway handler.\n * @param req - The embedder-resolved unary request coordinates + body params.\n * @returns The HTTP status + LAFS response body for the embedder to serialize.\n */\nexport async function routeUnary(\n handler: GatewayHandler,\n req: HttpUnaryRequest,\n): Promise<HttpUnaryResult> {\n const requestId = req.requestId ?? randomUUID();\n const request: DispatchRequest = {\n gateway: req.gateway,\n domain: req.domain,\n operation: req.operation,\n params: req.params,\n // Force the transport of origin — a client cannot impersonate cli/mcp/rpc.\n source: 'http',\n requestId,\n sessionId: req.sessionId,\n };\n\n try {\n const body = await handler.handle(request);\n return { status: statusForResponse(body), body };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const body: DispatchResponse = {\n meta: {\n gateway: req.gateway,\n domain: req.domain,\n operation: req.operation,\n timestamp: new Date().toISOString(),\n duration_ms: 0,\n source: 'http',\n requestId,\n },\n success: false,\n error: { code: 'E_HTTP_INTERNAL', message },\n };\n return { status: 500, body };\n }\n}\n","/**\n * Server-Sent Events (SSE) wire primitives for the `@cleocode/runtime/gateway/http`\n * adapter.\n *\n * The HTTP transport is the only gateway transport that streams: long-running\n * and subscription operations emit a sequence of {@link GatewayStreamEvent}\n * frames terminated by a `done` or `error` frame. This module owns ONLY the SSE\n * wire encoding + the `ReadableStream` lifecycle plumbing; the actual frame\n * production (polling a DB, tailing a job, etc.) lives in the caller's async\n * source. Keeping the encode/stream concern here lets BOTH the gateway-routed\n * streaming ops AND the existing Studio SSE endpoints (which carry their own\n * domain event shapes) share one tested, leak-free stream builder.\n *\n * Two encoders are provided so an existing stream can adopt this plumbing\n * WITHOUT changing its observed bytes:\n * - {@link encodeStreamEvent} — the canonical `data: <json>\\n\\n` form for\n * {@link GatewayStreamEvent} frames (gateway-routed streaming ops).\n * - {@link encodeSseFrame} — a generic `event:`/`id:`/`data:` writer for any\n * payload, so endpoints with a bespoke wire (a named `event:` field, a\n * `data:`-only frame) keep an IDENTICAL byte stream while still routing their\n * lifecycle through the shared, abort-safe builder.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/gateway/http/sse\n *\n * @task T11450\n * @epic T11254\n * @saga T11243\n */\n\nimport type { GatewayStreamEvent } from '@cleocode/contracts/gateway';\n\n/** SSE field/record delimiter constants (the SSE wire is line-oriented). */\nconst LF = '\\n';\n/** A blank line terminates an SSE record (`\\n\\n`). */\nconst RECORD_END = '\\n\\n';\n\n/**\n * One generic SSE frame: an optional named `event`, an optional `id`, and a\n * `data` payload that is JSON-serialized.\n *\n * This is intentionally NOT {@link GatewayStreamEvent} — it is the raw wire\n * shape so a bespoke endpoint can reproduce its exact existing bytes (e.g. a\n * named `event: task-updated` line, or a `data:`-only frame with no `event`).\n */\nexport interface SseFrame {\n /** Optional SSE `event:` field name (omitted → a `data:`-only frame). */\n event?: string;\n /** Optional SSE `id:` field for client-side last-event-id resumption. */\n id?: string;\n /** The frame payload; JSON-serialized into the `data:` field. */\n data: unknown;\n}\n\n/**\n * Encode a generic {@link SseFrame} to its SSE wire bytes.\n *\n * Field order is `event:` (if present) → `id:` (if present) → `data:`, matching\n * the SSE spec record layout, then a blank line. When `event` is omitted the\n * frame is a `data:`-only record — byte-identical to a handwritten\n * `data: <json>\\n\\n`.\n *\n * @param frame - The frame to serialize.\n * @returns The SSE record as a string (UTF-8 caller-encoded).\n */\nexport function encodeSseFrame(frame: SseFrame): string {\n let out = '';\n if (frame.event !== undefined) out += `event: ${frame.event}${LF}`;\n if (frame.id !== undefined) out += `id: ${frame.id}${LF}`;\n out += `data: ${JSON.stringify(frame.data)}${RECORD_END}`;\n return out;\n}\n\n/**\n * Encode a canonical {@link GatewayStreamEvent} as a `data:`-only SSE record.\n *\n * Gateway-routed streaming ops carry the discriminator + sequence inside the\n * JSON payload (`{ kind, seq, data, error, requestId }`), so no named SSE\n * `event:` field is used — the client demultiplexes on `kind`/`seq`. The\n * monotonic `seq` is also surfaced as the SSE `id:` so a reconnecting client can\n * send `Last-Event-ID`.\n *\n * @param frame - The gateway stream event to serialize.\n * @returns The SSE record as a string.\n */\nexport function encodeStreamEvent(frame: GatewayStreamEvent): string {\n return encodeSseFrame({ id: String(frame.seq), data: frame });\n}\n\n/** The standard SSE response headers (no caching, no proxy buffering). */\nexport const SSE_HEADERS: Readonly<Record<string, string>> = Object.freeze({\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n});\n\n/**\n * A push-style SSE controller handed to an {@link SseSource} so it can emit\n * frames and close the stream without touching the underlying\n * `ReadableStreamDefaultController`.\n */\nexport interface SseEmitter {\n /** Emit one generic SSE frame; a no-op once the stream is closed. */\n send(frame: SseFrame): void;\n /** Emit one canonical {@link GatewayStreamEvent}; a no-op once closed. */\n sendStreamEvent(frame: GatewayStreamEvent): void;\n /**\n * Emit a pre-encoded SSE record verbatim; a no-op once closed. For endpoints\n * that own their own wire encoder (e.g. a bespoke `data: <json>\\n\\n`) and want\n * to keep emitting byte-identical records while still routing lifecycle\n * through this builder.\n */\n sendRaw(record: string): void;\n /** Whether the stream has been closed (client disconnect or `close()`). */\n readonly closed: boolean;\n /** Close the stream; idempotent. */\n close(): void;\n}\n\n/**\n * A teardown callback returned by an {@link SseSource}, invoked exactly once when\n * the stream ends (client disconnect, `emitter.close()`, or producer completion).\n */\nexport type SseTeardown = () => void;\n\n/**\n * A frame source for {@link createSseStream}. Receives an {@link SseEmitter} and\n * MAY return an {@link SseTeardown} callback (return nothing to skip teardown).\n *\n * The source is started SYNCHRONOUSLY inside `ReadableStream.start`, mirroring\n * the existing Studio endpoints (which schedule timers inside `start`). A source\n * may push frames immediately and/or schedule async work.\n */\nexport type SseSource = (emitter: SseEmitter) => SseTeardown | undefined;\n\n/**\n * Build a `text/event-stream` {@link ReadableStream} driven by an\n * {@link SseSource}.\n *\n * This is the shared, abort-safe stream builder every SSE endpoint routes\n * through. It guarantees:\n * - frames are dropped (never throw) after the stream closes;\n * - the source's teardown callback runs exactly once;\n * - an optional {@link AbortSignal} (the request's) closes the stream on client\n * disconnect, with its listener removed on teardown.\n *\n * The byte output is fully controlled by what the source emits — so an endpoint\n * adopting this builder reproduces its prior wire exactly by emitting the same\n * frames in the same order.\n *\n * @param source - The frame producer.\n * @param signal - Optional request abort signal that closes the stream on\n * client disconnect.\n * @returns A ReadableStream of UTF-8 SSE bytes for a `Response` body.\n */\nexport function createSseStream(\n source: SseSource,\n signal?: AbortSignal,\n): ReadableStream<Uint8Array> {\n const encoder = new TextEncoder();\n\n return new ReadableStream<Uint8Array>({\n start(controller): void {\n let closed = false;\n /** Set once the source returns; invoked exactly once when the stream ends. */\n let teardown: (() => void) | undefined;\n /** Guards against running the source teardown more than once. */\n let teardownDone = false;\n /** True while `source(emitter)` is still executing synchronously. */\n let sourceRunning = false;\n\n const onAbort = (): void => emitter.close();\n\n /** Invoke the source teardown at most once (no-op until the source returns). */\n const runTeardown = (): void => {\n if (teardownDone) return;\n teardownDone = true;\n if (typeof teardown === 'function') teardown();\n };\n\n const emitter: SseEmitter = {\n get closed(): boolean {\n return closed;\n },\n send(frame: SseFrame): void {\n if (closed) return;\n try {\n controller.enqueue(encoder.encode(encodeSseFrame(frame)));\n } catch {\n emitter.close();\n }\n },\n sendStreamEvent(frame: GatewayStreamEvent): void {\n if (closed) return;\n try {\n controller.enqueue(encoder.encode(encodeStreamEvent(frame)));\n } catch {\n emitter.close();\n }\n },\n sendRaw(record: string): void {\n if (closed) return;\n try {\n controller.enqueue(encoder.encode(record));\n } catch {\n emitter.close();\n }\n },\n close(): void {\n if (closed) return;\n closed = true;\n if (signal) signal.removeEventListener('abort', onAbort);\n // If the source closed synchronously mid-body, it has not yet returned\n // its teardown — defer it to the post-return reconciliation below.\n if (!sourceRunning) runTeardown();\n try {\n controller.close();\n } catch {\n // already closed by the runtime — nothing to do.\n }\n },\n };\n\n if (signal) {\n if (signal.aborted) {\n // Client already gone before we started — close immediately. The\n // source is never invoked, so there is no teardown to run.\n closed = true;\n teardownDone = true;\n try {\n controller.close();\n } catch {\n // already closed.\n }\n return;\n }\n signal.addEventListener('abort', onAbort);\n }\n\n sourceRunning = true;\n const returned = source(emitter);\n teardown = typeof returned === 'function' ? returned : undefined;\n sourceRunning = false;\n\n // The source may have called `emitter.close()` synchronously before\n // returning its teardown; run it now that the callback is in hand.\n if (closed) runTeardown();\n },\n });\n}\n","/**\n * Streaming-operation source adapters for the gateway HTTP/SSE transport (T11921).\n *\n * A streaming operation (`OperationDef.streaming === true`, served as\n * `GET /v1/<domain>/<operation>`) needs a *producer*: an async source that tails\n * an event origin (the agent/worker/board lifecycle bus) and feeds\n * {@link GatewayStreamEvent} frames into the shared, abort-safe\n * {@link createSseStream} builder. This module owns ONLY that producer concern —\n * the wire encoding + stream lifecycle live in `./sse.ts`, and the route parsing\n * lives in `./listen.ts`.\n *\n * The runtime is deliberately DB-free (the runtime invariant — no drizzle, no\n * `@cleocode/cleo`), so a source here MUST NOT open a store directly. Instead the\n * listener resolves a source by `<domain>.<operation>` key from the\n * {@link STREAM_SOURCES} registry; an embedder that owns a richer origin (the\n * daemon, a SvelteKit route) can inject its own source into the listener at\n * boot. The default source shipped here is a bounded tick/heartbeat producer that\n * proves the pipe end-to-end without reaching across the package boundary — and\n * is exactly what the in-process test exercises.\n *\n * Secrets never touch the wire: a source emits only the public lifecycle payload\n * it is handed; credential resolution (if any) happens server-side before the\n * frame is built, exactly as it does for a unary dispatch.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/gateway/http/stream-sources\n *\n * @task T11921\n * @epic T11769\n * @saga T10400\n */\n\nimport type { GatewayStreamEvent } from '@cleocode/contracts/gateway';\nimport type { SseEmitter } from './sse.js';\n\n/** Default interval (ms) between emitted stream frames — the keepalive cadence. */\nconst DEFAULT_TICK_INTERVAL_MS = 1000;\n\n/** Hard upper bound on emitted frames so an unbounded `ticks` cannot run forever. */\nconst MAX_TICKS = 86_400;\n\n/**\n * The bind coordinates a {@link GatewayStreamSource} receives: the resolved\n * `(domain, operation)` of the streaming route, the already-validated request\n * params, and the request id (so emitted frames correlate back to the origin\n * request).\n */\nexport interface StreamSourceContext {\n /** The canonical domain segment of the streaming route. */\n domain: string;\n /** The operation segment of the streaming route. */\n operation: string;\n /** The decoded query params for the streaming request. */\n params: Record<string, unknown>;\n /** The request id stamped on every emitted {@link GatewayStreamEvent}. */\n requestId: string;\n}\n\n/**\n * A teardown callback a {@link GatewayStreamSource} MAY return; invoked exactly\n * once when the stream ends (client disconnect, terminal frame, completion).\n */\nexport type StreamSourceTeardown = () => void;\n\n/**\n * A producer for one streaming operation. Receives an {@link SseEmitter}\n * (`sendStreamEvent` emits one canonical {@link GatewayStreamEvent}) plus the\n * resolved {@link StreamSourceContext}, and MAY return a teardown callback.\n *\n * The source is started synchronously inside the stream builder, mirroring the\n * Studio SSE endpoints; it may push frames immediately and/or schedule async\n * work. It MUST drop frames once `emitter.closed` is true (the emitter already\n * guards this) and MUST clean up timers/listeners in its teardown so a client\n * disconnect closes the stream leak-free.\n */\nexport type GatewayStreamSource = (\n emitter: SseEmitter,\n context: StreamSourceContext,\n) => StreamSourceTeardown | undefined;\n\n/**\n * Coerce a `ticks` param into a bounded, non-negative integer frame budget.\n *\n * `undefined` / non-finite → open-ended (`Infinity`, capped at {@link MAX_TICKS}\n * worth of timer fires); a finite value is floored to `[0, MAX_TICKS]`.\n *\n * @param raw - The raw `ticks` param value.\n * @returns The resolved frame budget.\n */\nfunction resolveTickBudget(raw: unknown): number {\n if (typeof raw !== 'number' || !Number.isFinite(raw)) return MAX_TICKS;\n if (raw <= 0) return 0;\n return Math.min(Math.floor(raw), MAX_TICKS);\n}\n\n/**\n * The default `orchestrate.events` source: a bounded tick/heartbeat producer.\n *\n * Emits a monotonic sequence of `data` {@link GatewayStreamEvent} frames at\n * {@link DEFAULT_TICK_INTERVAL_MS}, then a terminal `done` frame once the\n * `ticks` budget is exhausted; a client disconnect tears the timer down before\n * then. It carries no DB handle and no secret — each frame's payload is a public\n * heartbeat (`{ tick, ts }`) standing in for an agent/worker/board lifecycle\n * event until the daemon injects its real origin-tailing source.\n *\n * A first frame is emitted SYNCHRONOUSLY so a client (and the test) observes a\n * well-formed `data:` frame immediately, without waiting a full interval.\n *\n * @param emitter - The SSE emitter for this stream.\n * @param context - The resolved streaming-route context.\n * @returns A teardown that clears the tick timer.\n */\nexport const tickStreamSource: GatewayStreamSource = (emitter, context) => {\n const budget = resolveTickBudget(context.params.ticks);\n let seq = 0;\n\n /** Emit one `data` frame; the emitter drops it after close (never throws). */\n const emitData = (): void => {\n const frame: GatewayStreamEvent = {\n kind: 'data',\n seq,\n data: { tick: seq, ts: new Date().toISOString() },\n requestId: context.requestId,\n };\n emitter.sendStreamEvent(frame);\n seq += 1;\n };\n\n /** Emit the terminal `done` frame and close the stream. */\n const emitDone = (): void => {\n const frame: GatewayStreamEvent = {\n kind: 'done',\n seq,\n data: { ticks: seq, ts: new Date().toISOString() },\n requestId: context.requestId,\n };\n emitter.sendStreamEvent(frame);\n emitter.close();\n };\n\n // Emit the first frame synchronously so a consumer sees a frame immediately.\n if (budget <= 0) {\n emitDone();\n return undefined;\n }\n emitData();\n\n const interval = setInterval(() => {\n if (emitter.closed) {\n clearInterval(interval);\n return;\n }\n if (seq >= budget) {\n clearInterval(interval);\n emitDone();\n return;\n }\n emitData();\n }, DEFAULT_TICK_INTERVAL_MS);\n\n // Teardown: stop the timer when the stream ends (disconnect, done, close).\n return () => clearInterval(interval);\n};\n\n/**\n * The default `tasks.subscribe` source: a bounded board-event heartbeat producer\n * (T11785 · epic T11556).\n *\n * Emits `data` {@link GatewayStreamEvent} frames standing in for task/board\n * lifecycle events (`created`/`updated`/`deleted`) until the `ticks` budget is\n * exhausted, then a terminal `done` frame; a client disconnect tears the timer\n * down before then. Each frame carries the optional `root` scope (a saga/parent\n * task ID) so a consumer can correlate the stream to the board slice it\n * subscribed to. Like {@link tickStreamSource} it carries NO DB handle and NO\n * secret — it proves the SSE pipe end-to-end without reaching across the runtime\n * package boundary, and the daemon injects its real store-tailing source via\n * {@link registerStreamSource} at boot.\n *\n * @param emitter - The SSE emitter for this stream.\n * @param context - The resolved streaming-route context (carries `params.root`).\n * @returns A teardown that clears the heartbeat timer.\n */\nexport const taskBoardStreamSource: GatewayStreamSource = (emitter, context) => {\n const budget = resolveTickBudget(context.params.ticks);\n const root = typeof context.params.root === 'string' ? context.params.root : null;\n let seq = 0;\n\n /** Emit one board-event `data` frame; dropped after close (never throws). */\n const emitData = (): void => {\n const frame: GatewayStreamEvent = {\n kind: 'data',\n seq,\n data: { scope: 'tasks.board', root, event: 'heartbeat', ts: new Date().toISOString() },\n requestId: context.requestId,\n };\n emitter.sendStreamEvent(frame);\n seq += 1;\n };\n\n /** Emit the terminal `done` frame and close the stream. */\n const emitDone = (): void => {\n const frame: GatewayStreamEvent = {\n kind: 'done',\n seq,\n data: { scope: 'tasks.board', root, frames: seq, ts: new Date().toISOString() },\n requestId: context.requestId,\n };\n emitter.sendStreamEvent(frame);\n emitter.close();\n };\n\n if (budget <= 0) {\n emitDone();\n return undefined;\n }\n // Emit the first frame synchronously so a consumer sees a frame immediately.\n emitData();\n\n const interval = setInterval(() => {\n if (emitter.closed) {\n clearInterval(interval);\n return;\n }\n if (seq >= budget) {\n clearInterval(interval);\n emitDone();\n return;\n }\n emitData();\n }, DEFAULT_TICK_INTERVAL_MS);\n\n return () => clearInterval(interval);\n};\n\n/**\n * The registry of streaming-operation sources, keyed by `<domain>.<operation>`.\n *\n * The listener resolves a source from this map when a `GET /v1/<domain>/<operation>`\n * matches a registered streaming op. An embedder MAY override an entry (or add a\n * domain-specific origin-tailing source) via {@link registerStreamSource} before\n * starting the server; until then `orchestrate.events` (and `tasks.subscribe`)\n * are served by their default tick sources so the pipe is exercisable with zero\n * injection.\n */\nconst STREAM_SOURCES = new Map<string, GatewayStreamSource>([\n ['orchestrate.events', tickStreamSource],\n // T11785 (epic T11556) — the FIRST streaming task op; default board heartbeat.\n ['tasks.subscribe', taskBoardStreamSource],\n]);\n\n/**\n * Build the `<domain>.<operation>` registry key for a streaming source.\n *\n * @param domain - The canonical domain.\n * @param operation - The operation name.\n * @returns The composite registry key.\n */\nfunction streamSourceKey(domain: string, operation: string): string {\n return `${domain}.${operation}`;\n}\n\n/**\n * Register (or override) the {@link GatewayStreamSource} for a streaming op.\n *\n * Lets an embedder that owns a richer event origin (the daemon tailing the\n * agent/worker/board lifecycle bus, a SvelteKit route reading a store) replace\n * the default tick source for a given `(domain, operation)` without changing the\n * listener. Idempotent — a later call wins.\n *\n * @param domain - The canonical domain of the streaming op.\n * @param operation - The operation name of the streaming op.\n * @param source - The producer to serve this streaming route with.\n */\nexport function registerStreamSource(\n domain: string,\n operation: string,\n source: GatewayStreamSource,\n): void {\n STREAM_SOURCES.set(streamSourceKey(domain, operation), source);\n}\n\n/**\n * Resolve the {@link GatewayStreamSource} registered for a streaming op.\n *\n * @param domain - The canonical domain of the streaming op.\n * @param operation - The operation name of the streaming op.\n * @returns The registered source, or `undefined` when none is registered (the\n * listener then closes the stream with a single terminal `error` frame).\n */\nexport function resolveStreamSource(\n domain: string,\n operation: string,\n): GatewayStreamSource | undefined {\n return STREAM_SOURCES.get(streamSourceKey(domain, operation));\n}\n","/**\n * Static file serving for the bundled CLEO Web Studio assets.\n *\n * The gateway HTTP server calls {@link serveStudioStatic} for any request whose\n * path starts with the `/studio` prefix. The handler:\n *\n * 1. Strips the `/studio` prefix and resolves the remainder against the\n * `studioStaticDir` (the `build/client/` output of the Studio\n * `adapter-node` build bundled inside `@cleocode/cleo`).\n * 2. Serves the matched file with the correct MIME type (inferred from the\n * extension via the built-in {@link MIME_TYPES} map).\n * 3. Returns `index.html` for any path that does not resolve to a static file\n * — the SPA fallback that lets the SvelteKit client-side router handle\n * sub-routes.\n * 4. Returns a clean JSON error envelope (not a stack trace) when\n * `studioStaticDir` is absent (dev checkout without a Studio build, or an\n * npm install where the postbuild copy was skipped).\n *\n * The handler deliberately has NO `process.exit`, NO drizzle, NO\n * `@cleocode/cleo` dependency — it is a pure file-system responder that can be\n * embedded in any `node:http` server (the gateway daemon, a test harness).\n *\n * The `build/client/` static tree is served under `/studio/` so all of its\n * relative asset references (`/_app/immutable/...`) resolve correctly. The root\n * `/studio` (no trailing slash) redirects to `/studio/` to keep the base URL\n * consistent with the SvelteKit assets.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/gateway/http\n *\n * @task T11979\n * @epic T11261\n */\n\nimport { createReadStream, existsSync } from 'node:fs';\nimport { stat } from 'node:fs/promises';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { extname, join, resolve } from 'node:path';\n\n// Minimal logger shim — avoids importing @cleocode/core so this module stays\n// side-effect-free and testable without a core build. The gateway listen.ts\n// already owns a full pino logger; we only need debug/warn here.\nconst log = {\n info: (obj: Record<string, unknown>, msg: string) => {\n // Intentionally quiet in production; structured logs route through gateway logger.\n if (process.env['CLEO_DEBUG_STUDIO']) console.info('[studio-static]', msg, obj);\n },\n warn: (obj: Record<string, unknown>, msg: string) => {\n if (process.env['NODE_ENV'] !== 'test') {\n console.warn('[studio-static]', msg, obj);\n }\n },\n debug: (_obj: Record<string, unknown>, _msg: string) => {\n // debug suppressed unless explicit env var\n if (process.env['CLEO_DEBUG_STUDIO']) console.debug('[studio-static]', _msg, _obj);\n },\n};\n\n// ---------------------------------------------------------------------------\n// MIME type map\n// ---------------------------------------------------------------------------\n\n/** MIME types for static file extensions served by the Studio bundle. */\nconst MIME_TYPES: ReadonlyMap<string, string> = new Map([\n ['.html', 'text/html; charset=utf-8'],\n ['.js', 'application/javascript; charset=utf-8'],\n ['.mjs', 'application/javascript; charset=utf-8'],\n ['.css', 'text/css; charset=utf-8'],\n ['.json', 'application/json; charset=utf-8'],\n ['.png', 'image/png'],\n ['.jpg', 'image/jpeg'],\n ['.jpeg', 'image/jpeg'],\n ['.svg', 'image/svg+xml'],\n ['.ico', 'image/x-icon'],\n ['.woff', 'font/woff'],\n ['.woff2', 'font/woff2'],\n ['.ttf', 'font/ttf'],\n ['.map', 'application/json; charset=utf-8'],\n ['.txt', 'text/plain; charset=utf-8'],\n ['.webmanifest', 'application/manifest+json; charset=utf-8'],\n]);\n\n/** Default MIME type for unknown extensions. */\nconst MIME_FALLBACK = 'application/octet-stream';\n\n/** The URL prefix the Studio static handler claims. */\nexport const STUDIO_PREFIX = '/studio';\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Options for the Studio static handler, injected at server startup.\n *\n * The caller resolves the absolute path to the static asset directory and\n * passes it here — the handler never reads environment variables or resolves\n * paths itself, keeping concerns separated and the handler testable.\n */\nexport interface StudioStaticOptions {\n /**\n * Absolute path to the Studio static asset directory. This is the\n * `build/client/` subdirectory from an `adapter-node` Studio build, bundled\n * inside `@cleocode/cleo` at `studio-dist/client/`.\n *\n * When `undefined` or the path does not exist on disk, every request under\n * `/studio` returns a clean JSON 503 \"bundle absent\" envelope.\n */\n staticDir: string | undefined;\n}\n\n/**\n * Determine whether a request path falls under the Studio prefix and should be\n * handled by the static asset handler rather than the API gateway.\n *\n * @param pathname - The URL pathname (without query string).\n * @returns `true` when the path is `/studio`, `/studio/`, or any\n * `/studio/<subpath>`.\n */\nexport function isStudioPath(pathname: string): boolean {\n return pathname === STUDIO_PREFIX || pathname.startsWith(`${STUDIO_PREFIX}/`);\n}\n\n/**\n * Resolve the file-system path for a Studio request within `staticDir`.\n *\n * Strips the `/studio` prefix and normalises the remainder; guards against\n * path-traversal (a normalised path that escapes `staticDir` is rejected).\n *\n * @param staticDir - Absolute path to the Studio static asset directory.\n * @param pathname - The request path (already verified to start with `/studio`).\n * @returns The absolute candidate path, or `undefined` when path traversal is\n * detected.\n */\nexport function resolveStudioAssetPath(staticDir: string, pathname: string): string | undefined {\n const base = resolve(staticDir);\n\n // Strip the /studio prefix; the remainder is the asset subpath.\n const stripped = pathname.slice(STUDIO_PREFIX.length) || '/';\n\n // Join the base with the stripped path. resolve() normalises `..` sequences\n // and always returns an absolute path rooted at base (since we join two\n // absolute paths, the second one wins when it is also absolute — but because\n // `stripped` is NOT an absolute path from the filesystem root, we must\n // remove the leading slash before joining to make it relative).\n const relative = stripped.replace(/^[\\\\/]+/, '') || '.';\n const candidate = resolve(base, relative);\n\n // Guard: the resolved candidate must remain under staticDir to prevent\n // path traversal. Adding a trailing separator to the base ensures that\n // /srv/studio-dist/client-extra is NOT mistaken for a child of\n // /srv/studio-dist/client.\n const safeBase = base.endsWith('/') ? base : `${base}/`;\n if (candidate !== base && !candidate.startsWith(safeBase)) {\n return undefined; // path traversal — reject\n }\n return candidate;\n}\n\n/**\n * Infer the MIME type for a file from its extension.\n *\n * @param filePath - The file path (only the extension is used).\n * @returns The appropriate `Content-Type` header value.\n */\nexport function mimeType(filePath: string): string {\n return MIME_TYPES.get(extname(filePath).toLowerCase()) ?? MIME_FALLBACK;\n}\n\n/**\n * Write the \"bundle absent\" JSON error envelope and close the response.\n *\n * Used when `staticDir` is `undefined` or does not exist on disk — the Studio\n * build was not found. The response is a `503 Service Unavailable` with a clean\n * LAFS-flavoured JSON body so clients can detect the condition programmatically.\n *\n * @param res - The server response to write to.\n * @param hint - Optional developer hint (e.g. the expected bundle path).\n */\nexport function writeBundleAbsentError(res: ServerResponse, hint?: string): void {\n const body = JSON.stringify({\n success: false,\n error: {\n code: 'E_STUDIO_BUNDLE_ABSENT',\n message:\n 'CLEO Studio assets are not available. ' +\n 'Run `pnpm --filter @cleocode/studio run build` to build them, ' +\n 'or install from npm (npm i -g @cleocode/cleo) for a batteries-included release.',\n hint: hint ?? null,\n },\n });\n res.writeHead(503, { 'Content-Type': 'application/json; charset=utf-8' });\n res.end(body);\n}\n\n/**\n * Handle a single HTTP request whose path falls under `/studio`.\n *\n * Routing logic:\n * - `GET /studio` → 302 redirect to `/studio/` (keep asset refs consistent)\n * - `GET /studio/<static-asset>` → serve the file from `staticDir`\n * - `GET /studio/*` (no matching file) → serve `index.html` (SPA fallback)\n * - Any request when `staticDir` is absent → 503 JSON envelope\n * - Non-GET method → 405 JSON envelope (Studio is read-only from the gateway)\n *\n * @param req - The inbound HTTP request.\n * @param res - The outbound HTTP response.\n * @param opts - Static handler options (resolved at server startup).\n * @returns A promise that resolves once the response has been written.\n */\nexport async function serveStudioStatic(\n req: IncomingMessage,\n res: ServerResponse,\n opts: StudioStaticOptions,\n): Promise<void> {\n const pathname = new URL(req.url ?? '/', 'http://localhost').pathname;\n\n // Only GET and HEAD are sensible for static assets.\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n res.writeHead(405, {\n 'Content-Type': 'application/json; charset=utf-8',\n Allow: 'GET, HEAD',\n });\n res.end(\n JSON.stringify({\n success: false,\n error: {\n code: 'E_METHOD_NOT_ALLOWED',\n message: `Method ${req.method ?? '<none>'} not allowed for /studio routes`,\n },\n }),\n );\n return;\n }\n\n // Redirect /studio → /studio/ so relative asset imports resolve correctly.\n if (pathname === STUDIO_PREFIX) {\n res.writeHead(302, { Location: `${STUDIO_PREFIX}/` });\n res.end();\n return;\n }\n\n // Bundle-absent guard — return a helpful error instead of a stack trace.\n const staticDir = opts.staticDir;\n if (staticDir === undefined || !existsSync(staticDir)) {\n log.warn({ staticDir }, 'studio static dir absent');\n writeBundleAbsentError(res, staticDir);\n return;\n }\n\n // Resolve the asset path, guarding against path traversal.\n const assetPath = resolveStudioAssetPath(staticDir, pathname);\n if (assetPath === undefined) {\n res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });\n res.end(\n JSON.stringify({\n success: false,\n error: { code: 'E_BAD_REQUEST', message: 'Invalid path' },\n }),\n );\n return;\n }\n\n // Try to serve the file; fall back to index.html for SPA routing.\n let targetPath = assetPath;\n let isSpaFallback = false;\n try {\n const st = await stat(targetPath);\n if (st.isDirectory()) {\n // Treat directory requests as index.html within the dir.\n const dirIndex = join(targetPath, 'index.html');\n if (existsSync(dirIndex)) {\n targetPath = dirIndex;\n } else {\n // SPA fallback — root index.html.\n targetPath = join(staticDir, 'index.html');\n isSpaFallback = true;\n }\n }\n } catch {\n // File not found — SPA fallback.\n targetPath = join(staticDir, 'index.html');\n isSpaFallback = true;\n }\n\n if (!existsSync(targetPath)) {\n // Even the fallback index.html is missing — the bundle is corrupted.\n res.writeHead(503, { 'Content-Type': 'application/json; charset=utf-8' });\n res.end(\n JSON.stringify({\n success: false,\n error: {\n code: 'E_STUDIO_BUNDLE_CORRUPT',\n message: 'Studio index.html not found in the static bundle.',\n },\n }),\n );\n return;\n }\n\n const ct = mimeType(targetPath);\n\n // For HEAD requests, report headers without the body.\n if (req.method === 'HEAD') {\n try {\n const st = await stat(targetPath);\n res.writeHead(200, {\n 'Content-Type': ct,\n 'Content-Length': String(st.size),\n ...(isSpaFallback ? {} : { 'Cache-Control': 'public, max-age=31536000, immutable' }),\n });\n res.end();\n } catch {\n res.writeHead(404);\n res.end();\n }\n return;\n }\n\n // Stream the file.\n try {\n const st = await stat(targetPath);\n res.writeHead(200, {\n 'Content-Type': ct,\n 'Content-Length': String(st.size),\n // Immutable cache for fingerprinted assets; no cache for SPA fallback.\n ...(isSpaFallback\n ? { 'Cache-Control': 'no-cache' }\n : {\n 'Cache-Control': 'public, max-age=31536000, immutable',\n }),\n });\n const stream = createReadStream(targetPath);\n stream.on('error', (err: Error) => {\n log.warn({ err, targetPath }, 'studio static read error');\n if (!res.writableEnded) res.end();\n });\n stream.pipe(res);\n log.debug({ pathname, targetPath, isSpaFallback, ct }, 'studio static served');\n } catch (err) {\n log.warn({ err, targetPath }, 'studio static stat error');\n if (!res.headersSent) {\n res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });\n res.end(\n JSON.stringify({\n success: false,\n error: { code: 'E_INTERNAL', message: 'Failed to read Studio asset' },\n }),\n );\n } else if (!res.writableEnded) {\n res.end();\n }\n }\n}\n\n/**\n * Resolve the bundled Studio static directory from the `@cleocode/cleo` package\n * location.\n *\n * When running from an npm install, the Studio build is bundled at\n * `<cleo-package-root>/studio-dist/client/`. When running from a monorepo dev\n * checkout, the directory may not exist (developers must run\n * `pnpm --filter @cleocode/studio run build` manually) — `undefined` is\n * returned in that case, and the gateway handler will serve the bundle-absent\n * error instead of crashing.\n *\n * @param cleoPackageDir - Absolute path to the `@cleocode/cleo` package root.\n * Typically derived from `import.meta.url` at the call site.\n * @returns The absolute path to `studio-dist/client/`, or `undefined` if the\n * directory does not exist.\n */\nexport function resolveStudioStaticDir(cleoPackageDir: string): string | undefined {\n const candidate = join(cleoPackageDir, 'studio-dist', 'client');\n return existsSync(candidate) ? candidate : undefined;\n}\n","/**\n * WebSocket terminal/PTY endpoint on the `/v1` gateway HTTP listener (T11922 ·\n * M5 realtime transport, WS half).\n *\n * The SSE half (T11921) gives the gateway a SERVER→CLIENT push stream; this\n * module adds the BIDIRECTIONAL realtime transport an interactive TUI / terminal\n * session needs: a WebSocket upgrade on the SAME `node:http` listener\n * ({@link import('./listen.js').startHttpServer}) that bridges a PTY both\n * directions over WS frames.\n *\n * Design constraints (all four ACs of T11922):\n *\n * - **AC1 — upgrade on the existing listener.** The WS endpoint is NOT a second\n * server: it attaches to the daemon's existing HTTP server via its\n * `'upgrade'` event ({@link attachWsPtyEndpoint}), so it inherits the loopback\n * bind + the daemon's lifecycle. The endpoint path is `/v1/terminal/pty`.\n * - **AC2 — bidirectional PTY bridge with backpressure.** Inbound WS frames are\n * written to the PTY stdin; PTY output is framed back to the client. A control\n * frame (`{type:'resize',cols,rows}`) resizes the PTY. Backpressure is honored\n * on the socket write path (pause-on-`false`-write, resume on `'drain'`).\n * - **AC3 — leak-free, deterministic teardown.** Every open/close/error/abort\n * path routes through ONE idempotent `teardown()` that kills the PTY, removes\n * its listeners, and ends the socket exactly once. A server `close()` tears\n * down every live session.\n * - **AC4 — auth/origin enforced at the upgrade edge; no secrets on the wire.**\n * The upgrade is rejected with a `401`/`403` BEFORE any PTY is spawned unless\n * it comes from loopback AND (when a token is configured) presents the bearer\n * token AND (when an origin allowlist is configured) presents an allowed\n * `Origin`. The spawned PTY runs under a MINIMAL, explicitly-constructed env\n * ({@link scrubPtyEnv}) — the daemon's secret-bearing `process.env` is NEVER\n * inherited, so a secret cannot be exfiltrated through the terminal stream.\n *\n * To keep the **publish surface clean** (D11142 dependency-discipline):\n * - There is NO `ws` dependency. The minimal RFC 6455 server framing needed\n * (accept handshake, decode masked client frames, encode unmasked server\n * frames, close/ping/pong) is implemented over the raw `node:net` socket the\n * `'upgrade'` event hands us. This adds ZERO weight to `@cleocode/runtime`.\n * - `node-pty` is an OPTIONAL, dynamically-loaded dependency (same pattern as\n * {@link import('../../../../core/src/llm/pi/gondolin-loader.js')} and\n * `packages/core/src/tools/pty.ts`): the npm specifier is held in a variable,\n * its shape is declared LOCALLY (no `import type`), and absence is a graceful\n * close frame (\"PTY backend unavailable\") — NOT a crash. The endpoint always\n * upgrades; it bridges a PTY only when the backend is present.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/gateway/http/ws-pty\n *\n * @task T11922\n * @epic T11769\n * @saga T10400\n * @see ./sse.ts — the SSE half (server→client push) on the same listener\n */\n\nimport { createHash } from 'node:crypto';\nimport type { IncomingMessage } from 'node:http';\nimport { delimiter } from 'node:path';\nimport type { Duplex } from 'node:stream';\nimport { getLogger } from '@cleocode/core';\n\n/** The canonical WS terminal/PTY endpoint path on the `/v1` facade. */\nexport const WS_PTY_PATH = '/v1/terminal/pty';\n\n/** The RFC 6455 GUID concatenated with `Sec-WebSocket-Key` to derive the accept hash. */\nconst WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';\n\n/** Default PTY geometry when the client does not request one. */\nconst DEFAULT_COLS = 80;\nconst DEFAULT_ROWS = 24;\n\n/** WS opcodes (RFC 6455 §5.2) the bridge handles. */\nconst OPCODE_TEXT = 0x1;\nconst OPCODE_BINARY = 0x2;\nconst OPCODE_CLOSE = 0x8;\nconst OPCODE_PING = 0x9;\nconst OPCODE_PONG = 0xa;\n\n/** Loopback hosts an upgrade may originate from (AC4). */\nconst LOOPBACK_HOSTS: ReadonlySet<string> = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);\n\n// ---------------------------------------------------------------------------\n// node-pty optional-dep loader — mirrors gondolin-loader.ts / tools/pty.ts.\n// The specifier is held in a variable; the consumed shape is declared LOCALLY\n// (no `import type` from the optional package) so the type-check + bundle pass\n// with node-pty uninstalled, and absence resolves `null` instead of throwing.\n// ---------------------------------------------------------------------------\n\n/**\n * The install hint surfaced to the client (as a WS close reason) when the PTY\n * backend is unavailable. `node-pty` is OPTIONAL — the endpoint still upgrades\n * and then closes cleanly with this reason rather than crashing the daemon.\n */\nexport const NODE_PTY_INSTALL_HINT =\n 'PTY backend unavailable: the optional \"node-pty\" package is not installed. ' +\n 'Install it with `pnpm add node-pty` (a native build) to enable interactive terminal sessions.';\n\n/** A disposable subscription handle returned by `node-pty`'s event registrars. */\ninterface PtyDisposable {\n dispose(): void;\n}\n\n/** The minimal structural shape of a spawned `node-pty` process the bridge uses. */\ninterface NodePtyProcess {\n /** Subscribe to interleaved stdout/stderr output. */\n onData(cb: (data: string) => void): PtyDisposable | undefined;\n /** Subscribe to process exit. */\n onExit(cb: (e: { exitCode: number; signal?: number }) => void): PtyDisposable | undefined;\n /** Write to the PTY stdin. */\n write(data: string): void;\n /** Resize the PTY. */\n resize(cols: number, rows: number): void;\n /** Kill the underlying process (idempotent host-side; signal optional). */\n kill(signal?: string): void;\n}\n\n/** The minimal structural shape of the `node-pty` module the bridge uses. */\ninterface NodePtyModule {\n spawn(\n file: string,\n args: readonly string[],\n options: {\n readonly name?: string;\n readonly cols?: number;\n readonly rows?: number;\n readonly cwd?: string;\n readonly env?: Record<string, string>;\n },\n ): NodePtyProcess;\n}\n\n/** A pluggable dynamic-importer (test seam). Defaults to the real `import()`. */\ntype DynamicImporter = (specifier: string) => Promise<unknown>;\n\n/** The npm specifier — held in a VARIABLE so it is never statically resolved. */\nconst NODE_PTY_SPECIFIER = 'node-pty';\n\n/** The real dynamic importer (specifier passed as an argument, never inlined). */\nconst realImporter: DynamicImporter = (specifier) => import(specifier);\n\n/** The active importer (swapped only by {@link __setWsPtyTestHooks}). */\nlet importer: DynamicImporter = realImporter;\n\n/**\n * Structurally validate a dynamically-imported candidate against the `spawn`\n * export the bridge consumes. Returns the typed module on a match, else `null`.\n */\nfunction shapeCheckPty(candidate: unknown): NodePtyModule | null {\n if (\n candidate !== null &&\n typeof candidate === 'object' &&\n typeof (candidate as { spawn?: unknown }).spawn === 'function'\n ) {\n return candidate as NodePtyModule;\n }\n return null;\n}\n\n/**\n * Attempt to lazily load `node-pty`. Returns `null` when the optional dep is not\n * installed, fails to load, or does not expose the `spawn` surface — so the\n * caller closes the WS cleanly with {@link NODE_PTY_INSTALL_HINT} instead of\n * crashing. NEVER throws.\n *\n * The import specifier is held in a variable (passed to the {@link importer}\n * seam) so bundlers / TS do not treat the missing optional dep as a hard,\n * statically-resolved dependency (same technique as the gondolin loader).\n *\n * @returns The shape-checked module, or `null` when unavailable.\n */\nexport async function loadNodePty(): Promise<NodePtyModule | null> {\n try {\n const mod: unknown = await importer(NODE_PTY_SPECIFIER);\n const candidate = (mod as { default?: unknown }).default ?? mod;\n return shapeCheckPty(candidate);\n } catch {\n return null;\n }\n}\n\n/**\n * Override the dynamic-importer (TESTS ONLY) so a unit test can deterministically\n * simulate \"node-pty absent\" (importer rejects) and \"node-pty present\" (importer\n * resolves a mock module with a fake `spawn`) WITHOUT touching the module graph\n * or building the native package. Call with no arguments to restore the real\n * importer.\n *\n * @param hooks - Partial override for the importer.\n * @internal\n */\nexport function __setWsPtyTestHooks(hooks?: { importer?: DynamicImporter }): void {\n importer = hooks?.importer ?? realImporter;\n}\n\n// ---------------------------------------------------------------------------\n// PTY environment scrub (AC4 — secrets never traverse the socket).\n//\n// The spawned PTY must NOT inherit the daemon's secret-bearing `process.env`\n// (`*_API_KEY`, `*_TOKEN`, `CLEO_VAULT_*`, OAuth headers, …) — a single `env`\n// line in an interactive terminal would otherwise exfiltrate them over the WS.\n// We build a MINIMAL, allowlist-only env with a pinned trusted PATH, mirroring\n// the doctrine of `@cleocode/core`'s `scrubSubprocessEnv` (kept inline so the\n// framework-agnostic runtime does not reach into core's tools subpath).\n// ---------------------------------------------------------------------------\n\n/** A fixed, trusted absolute `PATH` — never the (possibly poisoned) caller PATH. */\nconst TRUSTED_PATH = [\n '/usr/local/sbin',\n '/usr/local/bin',\n '/usr/sbin',\n '/usr/bin',\n '/sbin',\n '/bin',\n].join(delimiter);\n\n/**\n * Benign environment variables copied through from the parent when present.\n * These carry NO secrets and NO code-execution surface — locale, terminal type,\n * timezone, home/user identity, and the tmp dir. `PATH` is deliberately ABSENT\n * (pinned to {@link TRUSTED_PATH}); every secret / loader hook is ABSENT.\n */\nconst PASSTHROUGH_KEYS: readonly string[] = [\n 'HOME',\n 'USER',\n 'LOGNAME',\n 'LANG',\n 'LC_ALL',\n 'LC_CTYPE',\n 'TZ',\n 'TMPDIR',\n];\n\n/**\n * Build the MINIMAL environment the bridged PTY runs under (AC4).\n *\n * Starts EMPTY, copies only the small {@link PASSTHROUGH_KEYS} allowlist from the\n * parent, pins `PATH` to {@link TRUSTED_PATH}, and forces `TERM=xterm-256color`\n * for a sane terminal. The daemon's secrets are NEVER forwarded.\n *\n * @param parentEnv - The parent process environment (defaults to `process.env`).\n * @returns The scrubbed, secret-free env for the PTY.\n */\nexport function scrubPtyEnv(parentEnv: NodeJS.ProcessEnv = process.env): Record<string, string> {\n const env: Record<string, string> = { PATH: TRUSTED_PATH, TERM: 'xterm-256color' };\n for (const key of PASSTHROUGH_KEYS) {\n const value = parentEnv[key];\n if (typeof value === 'string' && value.length > 0) env[key] = value;\n }\n return env;\n}\n\n// ---------------------------------------------------------------------------\n// Minimal RFC 6455 server framing — no `ws` dependency.\n// ---------------------------------------------------------------------------\n\n/**\n * Derive the `Sec-WebSocket-Accept` response header value from a client\n * `Sec-WebSocket-Key` per RFC 6455 §4.2.2 (SHA-1 of key + GUID, base64).\n *\n * @param key - The client's `Sec-WebSocket-Key` header value.\n * @returns The base64 accept hash.\n */\nexport function computeAcceptKey(key: string): string {\n return createHash('sha1')\n .update(key + WS_GUID)\n .digest('base64');\n}\n\n/**\n * Encode a WS data frame (server→client, UNMASKED per RFC 6455 §5.1). Used for\n * `text` (PTY output) and `close` control frames.\n *\n * @param opcode - The WS opcode (e.g. {@link OPCODE_TEXT}).\n * @param payload - The frame payload bytes.\n * @returns The framed bytes ready to write to the socket.\n */\nexport function encodeWsFrame(opcode: number, payload: Buffer): Buffer {\n const len = payload.length;\n let header: Buffer;\n if (len < 126) {\n header = Buffer.alloc(2);\n header[1] = len;\n } else if (len < 65536) {\n header = Buffer.alloc(4);\n header[1] = 126;\n header.writeUInt16BE(len, 2);\n } else {\n header = Buffer.alloc(10);\n header[1] = 127;\n // High 32 bits are 0 for any realistic terminal frame.\n header.writeUInt32BE(Math.floor(len / 0x1_0000_0000), 2);\n header.writeUInt32BE(len >>> 0, 6);\n }\n header[0] = 0x80 | (opcode & 0x0f); // FIN + opcode\n return Buffer.concat([header, payload]);\n}\n\n/** Build an unmasked `close` control frame carrying a status code + UTF-8 reason. */\nfunction encodeCloseFrame(code: number, reason: string): Buffer {\n const reasonBytes = Buffer.from(reason, 'utf8').subarray(0, 123);\n const payload = Buffer.alloc(2 + reasonBytes.length);\n payload.writeUInt16BE(code, 0);\n reasonBytes.copy(payload, 2);\n return encodeWsFrame(OPCODE_CLOSE, payload);\n}\n\n/** One decoded inbound WS frame. */\ninterface DecodedFrame {\n /** The frame opcode. */\n opcode: number;\n /** The unmasked payload bytes. */\n payload: Buffer;\n /** Total bytes consumed from the input buffer (header + payload). */\n consumed: number;\n}\n\n/**\n * Decode a single inbound (client→server, MASKED) WS frame from a buffer.\n *\n * Returns `null` when the buffer does not yet hold a complete frame (the caller\n * waits for more bytes). Client frames MUST be masked (RFC 6455 §5.3); the mask\n * is applied to recover the payload. Fragmentation is not expected from a\n * terminal client, so a non-FIN frame is decoded as-is (its opcode handled by\n * the caller). NEVER throws on a partial buffer.\n *\n * @param buf - The accumulated inbound bytes.\n * @returns The decoded frame, or `null` when more bytes are needed.\n */\nexport function decodeWsFrame(buf: Buffer): DecodedFrame | null {\n if (buf.length < 2) return null;\n const opcode = buf[0] & 0x0f;\n const masked = (buf[1] & 0x80) !== 0;\n let len = buf[1] & 0x7f;\n let offset = 2;\n if (len === 126) {\n if (buf.length < offset + 2) return null;\n len = buf.readUInt16BE(offset);\n offset += 2;\n } else if (len === 127) {\n if (buf.length < offset + 8) return null;\n // Low 32 bits suffice for any realistic terminal frame.\n len = buf.readUInt32BE(offset + 4);\n offset += 8;\n }\n let mask: Buffer | undefined;\n if (masked) {\n if (buf.length < offset + 4) return null;\n mask = buf.subarray(offset, offset + 4);\n offset += 4;\n }\n if (buf.length < offset + len) return null;\n const raw = buf.subarray(offset, offset + len);\n const payload = Buffer.allocUnsafe(len);\n if (mask) {\n for (let i = 0; i < len; i++) payload[i] = raw[i] ^ mask[i & 3];\n } else {\n raw.copy(payload);\n }\n return { opcode, payload, consumed: offset + len };\n}\n\n// ---------------------------------------------------------------------------\n// Auth/origin gate (AC4) — enforced at the upgrade edge, before any PTY spawn.\n// ---------------------------------------------------------------------------\n\n/** Configuration for the WS terminal/PTY endpoint. */\nexport interface WsPtyOptions {\n /**\n * Optional bearer token an upgrade MUST present (as `Authorization: Bearer\n * <token>` or `?token=<token>`). When omitted, no token is required (loopback\n * is still enforced unconditionally).\n */\n token?: string;\n /**\n * Optional `Origin` allowlist. When non-empty, an upgrade MUST present an\n * `Origin` header whose value is in the set, else it is rejected `403`. When\n * omitted/empty, the `Origin` header is not checked (loopback already bounds\n * the surface).\n */\n allowedOrigins?: readonly string[];\n /** The shell launched for the PTY (default `process.env.SHELL` or `/bin/sh`). */\n shell?: string;\n /** Working directory for the PTY (default `process.cwd()`). */\n cwd?: string;\n}\n\n/** The decision of the upgrade-edge gate. */\nexport type GateDecision = { ok: true } | { ok: false; status: number; reason: string };\n\n/**\n * Authorize a WS upgrade at the edge (AC4) — BEFORE any PTY is spawned.\n *\n * Enforced unconditionally:\n * - the connection MUST originate from loopback (`127.0.0.1` / `::1`); a\n * non-loopback peer is rejected `403` (the gateway is local-process-facing).\n *\n * Enforced when configured:\n * - a `token` (matched against `Authorization: Bearer` or `?token=`); a missing\n * or mismatched token is rejected `401`.\n * - an `Origin` allowlist; a missing or disallowed `Origin` is rejected `403`.\n *\n * @param req - The inbound upgrade request.\n * @param remoteAddress - The socket's remote address.\n * @param opts - The endpoint auth configuration.\n * @returns A {@link GateDecision} — `{ok:true}` to proceed, else a typed reject.\n */\nexport function authorizeUpgrade(\n req: IncomingMessage,\n remoteAddress: string | undefined,\n opts: WsPtyOptions,\n): GateDecision {\n // 1. Loopback-only (unconditional).\n if (remoteAddress === undefined || !LOOPBACK_HOSTS.has(remoteAddress)) {\n return { ok: false, status: 403, reason: 'non-loopback connections are not permitted' };\n }\n\n // 2. Origin allowlist (when configured).\n if (opts.allowedOrigins !== undefined && opts.allowedOrigins.length > 0) {\n const origin = req.headers.origin;\n if (typeof origin !== 'string' || !opts.allowedOrigins.includes(origin)) {\n return { ok: false, status: 403, reason: 'origin not allowed' };\n }\n }\n\n // 3. Bearer token (when configured).\n if (opts.token !== undefined && opts.token.length > 0) {\n const presented = extractToken(req);\n if (presented === undefined || !timingSafeEqualStr(presented, opts.token)) {\n return { ok: false, status: 401, reason: 'missing or invalid token' };\n }\n }\n\n return { ok: true };\n}\n\n/** Extract a bearer token from `Authorization: Bearer <t>` or the `?token=<t>` query. */\nfunction extractToken(req: IncomingMessage): string | undefined {\n const auth = req.headers.authorization;\n if (typeof auth === 'string' && auth.startsWith('Bearer ')) {\n return auth.slice('Bearer '.length).trim();\n }\n const url = new URL(req.url ?? '/', 'http://localhost');\n const q = url.searchParams.get('token');\n return q !== null && q.length > 0 ? q : undefined;\n}\n\n/** Constant-time string compare (avoids leaking token length/contents via timing). */\nfunction timingSafeEqualStr(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n return diff === 0;\n}\n\n// ---------------------------------------------------------------------------\n// The upgrade handler + PTY bridge.\n// ---------------------------------------------------------------------------\n\n/** Whether a request path is the WS terminal/PTY endpoint. */\nexport function isWsPtyPath(url: string | undefined): boolean {\n const pathname = new URL(url ?? '/', 'http://localhost').pathname;\n return pathname === WS_PTY_PATH;\n}\n\n/** A live WS-PTY session, tracked so a server close tears every one down. */\ninterface WsPtySession {\n /** Tear down this session (idempotent) — kills the PTY + ends the socket. */\n teardown(code?: number, reason?: string): void;\n}\n\n/**\n * Write a raw HTTP rejection to the upgrade socket and destroy it. Used by the\n * auth/origin gate (AC4) so a rejected upgrade never reaches the PTY spawn.\n */\nfunction rejectUpgrade(socket: Duplex, status: number, reason: string): void {\n const statusText = status === 401 ? 'Unauthorized' : status === 403 ? 'Forbidden' : 'Bad Request';\n socket.write(\n `HTTP/1.1 ${status} ${statusText}\\r\\n` +\n 'Connection: close\\r\\n' +\n 'Content-Type: text/plain\\r\\n' +\n `Content-Length: ${Buffer.byteLength(reason)}\\r\\n` +\n '\\r\\n' +\n reason,\n );\n socket.destroy();\n}\n\n/**\n * Terminal `'error'` sink installed by a session's teardown.\n *\n * Teardown removes the live `onError` listener (it routes back into teardown)\n * but the socket still has a close frame + FIN in flight; an abrupt peer RST\n * surfaces that write failure asynchronously. Without a listener Node turns\n * the `ECONNRESET`/`EPIPE` into an uncaught exception — fatal for the daemon.\n * The session is already dead at this point, so the error is intentionally\n * dropped.\n *\n * @task T11961\n */\nfunction swallowLateSocketError(): void {\n // intentionally empty — late socket errors on a torn-down session are noise\n}\n\n/**\n * Complete the RFC 6455 handshake on a gated upgrade socket, then spawn + bridge\n * a PTY bidirectionally. Returns a {@link WsPtySession} whose `teardown()` is the\n * single idempotent close path (AC3). When `node-pty` is absent, the socket is\n * upgraded and then closed with {@link NODE_PTY_INSTALL_HINT} — never crashed.\n *\n * @param req - The inbound upgrade request.\n * @param socket - The raw duplex socket handed to us by the `'upgrade'` event.\n * @param opts - The endpoint configuration (shell, cwd).\n * @param log - The adapter logger.\n * @param onTeardown - Invoked exactly once when this session tears down (any\n * path: client close, error, abort, PTY exit, server close) — the registry\n * uses it to de-register the session leak-free (AC3).\n * @returns The tracked session.\n */\nasync function bridgePty(\n req: IncomingMessage,\n socket: Duplex,\n opts: WsPtyOptions,\n log: ReturnType<typeof getLogger>,\n onTeardown: () => void,\n): Promise<WsPtySession> {\n const key = req.headers['sec-websocket-key'];\n socket.write(\n 'HTTP/1.1 101 Switching Protocols\\r\\n' +\n 'Upgrade: websocket\\r\\n' +\n 'Connection: Upgrade\\r\\n' +\n `Sec-WebSocket-Accept: ${computeAcceptKey(typeof key === 'string' ? key : '')}\\r\\n` +\n '\\r\\n',\n );\n\n let tornDown = false;\n let pty: NodePtyProcess | undefined;\n const disposables: PtyDisposable[] = [];\n // Widened to `ArrayBufferLike` so concatenations of socket chunks (which are\n // `Buffer<ArrayBufferLike>`) assign cleanly under `@types/node`'s generic Buffer.\n let inbound: Buffer<ArrayBufferLike> = Buffer.alloc(0);\n\n /** Send one server→client text frame, honoring socket backpressure. */\n const sendText = (data: string): void => {\n if (tornDown || socket.writableEnded) return;\n const frame = encodeWsFrame(OPCODE_TEXT, Buffer.from(data, 'utf8'));\n if (!socket.write(frame)) {\n // Kernel buffer full — pause PTY-driven writes until the socket drains so\n // a fast producer cannot blow the buffer (AC2 backpressure).\n socket.once('drain', () => undefined);\n }\n };\n\n /** The single idempotent teardown (AC3) — every close path routes here. */\n const teardown = (code = 1000, reason = ''): void => {\n if (tornDown) return;\n tornDown = true;\n for (const d of disposables.splice(0)) {\n try {\n d.dispose();\n } catch {\n // best-effort listener disposal\n }\n }\n if (pty !== undefined) {\n try {\n pty.kill();\n } catch {\n // best-effort PTY kill\n }\n pty = undefined;\n }\n socket.removeListener('data', onData);\n socket.removeListener('error', onError);\n socket.removeListener('close', onSocketClose);\n // The socket outlives teardown: the close frame + FIN below are still in\n // flight, and a peer that destroyed abruptly (RST) surfaces the write\n // failure ASYNCHRONOUSLY as a socket 'error' AFTER the listeners above are\n // removed. The try/catch only covers synchronous throws — without a sink\n // the late ECONNRESET/EPIPE becomes an uncaught exception that kills the\n // daemon (T11961; also the macOS CI shard killer in ws-pty.test.ts).\n socket.on('error', swallowLateSocketError);\n if (!socket.writableEnded) {\n try {\n socket.write(encodeCloseFrame(code, reason.slice(0, 120)));\n } catch {\n // socket may already be gone\n }\n socket.end();\n }\n try {\n onTeardown();\n } catch {\n // registry de-registration is best-effort\n }\n log.debug({ code, reason }, 'ws-pty session torn down');\n };\n\n /** Handle one fully-decoded inbound frame. */\n const onFrame = (frame: DecodedFrame): void => {\n switch (frame.opcode) {\n case OPCODE_TEXT:\n case OPCODE_BINARY: {\n const text = frame.payload.toString('utf8');\n // A JSON control frame resizes the PTY; everything else is stdin bytes.\n const control = tryParseControl(text);\n if (control !== undefined && control.type === 'resize') {\n pty?.resize(control.cols, control.rows);\n } else {\n pty?.write(text);\n }\n break;\n }\n case OPCODE_PING:\n if (!tornDown && !socket.writableEnded)\n socket.write(encodeWsFrame(OPCODE_PONG, frame.payload));\n break;\n case OPCODE_CLOSE:\n teardown(1000, 'client closed');\n break;\n // PONG and continuation frames are ignored.\n default:\n break;\n }\n };\n\n const onData = (chunk: Buffer): void => {\n inbound = inbound.length === 0 ? chunk : Buffer.concat([inbound, chunk]);\n for (;;) {\n const frame = decodeWsFrame(inbound);\n if (frame === null) break;\n inbound = inbound.subarray(frame.consumed);\n onFrame(frame);\n if (tornDown) break;\n }\n };\n const onError = (): void => teardown(1011, 'socket error');\n const onSocketClose = (): void => teardown();\n\n socket.on('data', onData);\n socket.on('error', onError);\n socket.on('close', onSocketClose);\n\n // Load the OPTIONAL PTY backend. Absent → clean close with the install hint\n // (the upgrade already succeeded; we never crash the daemon).\n const ptyModule = await loadNodePty();\n if (tornDown) return { teardown };\n if (ptyModule === null) {\n teardown(1011, NODE_PTY_INSTALL_HINT);\n return { teardown };\n }\n\n const shell = opts.shell ?? process.env.SHELL ?? '/bin/sh';\n try {\n pty = ptyModule.spawn(shell, [], {\n name: 'xterm-256color',\n cols: DEFAULT_COLS,\n rows: DEFAULT_ROWS,\n cwd: opts.cwd ?? process.cwd(),\n env: scrubPtyEnv(),\n });\n } catch (err) {\n log.warn({ err }, 'ws-pty spawn failed');\n teardown(1011, 'PTY spawn failed');\n return { teardown };\n }\n\n const dataSub = pty.onData((d) => sendText(d));\n if (dataSub && typeof dataSub.dispose === 'function') disposables.push(dataSub);\n const exitSub = pty.onExit(({ exitCode }) => teardown(1000, `pty exited (${exitCode})`));\n if (exitSub && typeof exitSub.dispose === 'function') disposables.push(exitSub);\n\n log.debug({ shell }, 'ws-pty session established');\n return { teardown };\n}\n\n/** A `{type:'resize',cols,rows}` control frame parsed from an inbound text frame. */\ninterface ResizeControl {\n type: 'resize';\n cols: number;\n rows: number;\n}\n\n/** Parse an inbound text frame as a JSON control message; `undefined` when it is plain stdin. */\nfunction tryParseControl(text: string): ResizeControl | undefined {\n if (text.length === 0 || text[0] !== '{') return undefined;\n try {\n const parsed: unknown = JSON.parse(text);\n if (\n parsed !== null &&\n typeof parsed === 'object' &&\n (parsed as { type?: unknown }).type === 'resize' &&\n typeof (parsed as { cols?: unknown }).cols === 'number' &&\n typeof (parsed as { rows?: unknown }).rows === 'number'\n ) {\n const p = parsed as { cols: number; rows: number };\n return { type: 'resize', cols: p.cols, rows: p.rows };\n }\n } catch {\n // Not JSON — treat as plain stdin bytes.\n }\n return undefined;\n}\n\n/**\n * A handle returned by {@link attachWsPtyEndpoint} so the caller can detach the\n * `'upgrade'` listener and tear down every live session on server close (AC3).\n */\nexport interface WsPtyEndpointHandle {\n /** The number of live WS-PTY sessions (for tests / health). */\n readonly sessionCount: number;\n /** Detach the listener and tear down every live session. Idempotent. */\n close(): void;\n}\n\n/**\n * The shape of the HTTP server's `'upgrade'` event surface this module needs.\n * Declared structurally so the module does not couple to a concrete `Server`.\n */\nexport interface UpgradableServer {\n on(\n event: 'upgrade',\n listener: (req: IncomingMessage, socket: Duplex, head: Buffer) => void,\n ): void;\n removeListener(\n event: 'upgrade',\n listener: (req: IncomingMessage, socket: Duplex, head: Buffer) => void,\n ): void;\n}\n\n/**\n * Attach the WS terminal/PTY endpoint (AC1) to an existing HTTP server's\n * `'upgrade'` event.\n *\n * Every upgrade to {@link WS_PTY_PATH} is gated ({@link authorizeUpgrade}, AC4)\n * BEFORE any PTY is spawned; a rejected upgrade gets a raw `401`/`403` and the\n * socket is destroyed. An accepted upgrade completes the RFC 6455 handshake and\n * bridges a PTY bidirectionally ({@link bridgePty}, AC2). An upgrade to any OTHER\n * path is left for other upgrade handlers (this listener only claims its own\n * path and never destroys a foreign socket).\n *\n * Each live session is tracked so {@link WsPtyEndpointHandle.close} tears every\n * one down deterministically on server close (AC3).\n *\n * @param server - The HTTP server whose `'upgrade'` event to attach to.\n * @param opts - The endpoint auth + PTY configuration.\n * @returns A handle to detach + tear down on server close.\n */\nexport function attachWsPtyEndpoint(\n server: UpgradableServer,\n opts: WsPtyOptions = {},\n): WsPtyEndpointHandle {\n const log = getLogger('gateway-ws-pty');\n const sessions = new Set<WsPtySession>();\n\n const onUpgrade = (req: IncomingMessage, socket: Duplex, _head: Buffer): void => {\n // Only claim our own path — leave every other upgrade for other handlers.\n if (!isWsPtyPath(req.url)) return;\n\n const decision = authorizeUpgrade(req, req.socket.remoteAddress, opts);\n if (!decision.ok) {\n log.debug({ status: decision.status, reason: decision.reason }, 'ws-pty upgrade rejected');\n rejectUpgrade(socket, decision.status, decision.reason);\n return;\n }\n\n // A box the bridge's teardown callback closes over to de-register itself —\n // the session is added to the registry below, but teardown may fire (PTY\n // absent → immediate close) before the promise resolves, so the callback\n // removes whatever was registered for this socket once it is known.\n let session: WsPtySession | undefined;\n let deregistered = false;\n const deregister = (): void => {\n if (deregistered) return;\n deregistered = true;\n if (session !== undefined) sessions.delete(session);\n };\n\n bridgePty(req, socket, opts, log, deregister)\n .then((s) => {\n session = s;\n // If teardown already ran (e.g. PTY absent), do not re-register a dead\n // session; otherwise track it for the server-close sweep.\n if (!deregistered) sessions.add(s);\n })\n .catch((err: unknown) => {\n log.warn({ err }, 'ws-pty bridge failed');\n deregister();\n if (!socket.destroyed) socket.destroy();\n });\n };\n\n server.on('upgrade', onUpgrade);\n\n let closed = false;\n return {\n get sessionCount(): number {\n return sessions.size;\n },\n close(): void {\n if (closed) return;\n closed = true;\n server.removeListener('upgrade', onUpgrade);\n for (const session of [...sessions]) session.teardown(1001, 'server closing');\n sessions.clear();\n },\n };\n}\n"],"mappings":"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","names":["randomUUID","getLogger","log","domain","operation","gateway","resolve","log","randomUUID","getLogger"]}