@cleocode/runtime 2026.10.4 → 2026.10.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-L5BLVQDA.js → chunk-4PUSKGDO.js} +2 -2
- package/dist/chunk-4PUSKGDO.js.map +1 -0
- package/dist/{chunk-KQBVXGOL.js → chunk-GULKX44R.js} +5 -3
- package/dist/chunk-GULKX44R.js.map +1 -0
- package/dist/{chunk-7Q7BEWM7.js → chunk-HXTRXEFI.js} +7 -9
- package/dist/chunk-HXTRXEFI.js.map +1 -0
- package/dist/{chunk-U2ESNIJ5.js → chunk-NOPLC5WS.js} +4 -2
- package/dist/chunk-NOPLC5WS.js.map +1 -0
- package/dist/chunk-PDZ5T4LO.js +597 -0
- package/dist/chunk-PDZ5T4LO.js.map +1 -0
- package/dist/{chunk-XJOMQIXL.js → chunk-WO6LJMDC.js} +4 -2
- package/dist/chunk-WO6LJMDC.js.map +1 -0
- package/dist/daemon/index.js +1 -1
- package/dist/dispatch-BWjVeeMg.d.ts +290 -0
- package/dist/gateway/dispatch.d.ts +5 -0
- package/dist/gateway/dispatch.js +53 -0
- package/dist/gateway/dispatch.js.map +1 -0
- package/dist/gateway/http/index.d.ts +897 -11
- package/dist/gateway/http/index.js +2 -2
- package/dist/gateway/index.d.ts +549 -4
- package/dist/gateway/index.js +26 -581
- package/dist/gateway/index.js.map +1 -1
- package/dist/gateway/mcp/index.d.ts +232 -10
- package/dist/gateway/mcp/index.js +2 -2
- package/dist/gateway/rpc/index.d.ts +5 -2036
- package/dist/gateway/rpc/index.js +1 -1
- package/dist/gateway-handler-DCU6E0a2.d.ts +80 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +9 -6
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
- package/dist/chunk-7Q7BEWM7.js.map +0 -1
- package/dist/chunk-KQBVXGOL.js.map +0 -1
- package/dist/chunk-L5BLVQDA.js.map +0 -1
- package/dist/chunk-U2ESNIJ5.js.map +0 -1
- package/dist/chunk-XJOMQIXL.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
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';\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":[]}
|
|
@@ -1 +0,0 @@
|
|
|
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 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 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"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/gateway/mcp/server.ts","../src/gateway/mcp/tool-naming.ts","../src/gateway/mcp/tools-list.ts"],"sourcesContent":["/**\n * MCP stdio transport adapter — routes `tools/call` through the gateway.\n *\n * Serves the Model Context Protocol over stdio (newline-delimited JSON-RPC 2.0)\n * so any MCP-compatible client (Claude Code, LLM-agnostic tools) can discover\n * and invoke CLEO operations. Unlike the legacy standalone `@cleocode/mcp-adapter`\n * — which called the `@cleocode/core` SDK directly — this adapter is a thin\n * TRANSPORT over the unified gateway:\n *\n * MCP `tools/call` { name, arguments }\n * → {@link GatewayOperationKey} (via the `mcpExposed` registry subset)\n * → DispatchRequest { source: 'mcp', gateway, domain, operation, params }\n * → injected {@link GatewayHandler}.handle()\n * → DispatchResponse (LAFS envelope) → MCP tool result (JSON text content)\n *\n * The adapter owns ONLY wire concerns: JSON-RPC framing, the stdio read loop,\n * `tools/list` generation, error rendering, and `process.exit` on stream close.\n * All domain logic (resolution, validation, middleware, handler execution) lives\n * behind the {@link GatewayHandler} — exactly mirroring how the CLI adapter\n * wraps the same handler. `process.exit` and error-render stay HERE, never in\n * the handlers (R3-T4 contract).\n *\n * `tools/list` is generated from the OPERATIONS registry behind the default-deny\n * `mcpExposed` flag, so the external tool surface is identical to the historical\n * standalone adapter (no behavior change).\n *\n * @task T11448\n * @epic T11254\n * @saga T11243\n */\n\nimport { randomUUID } from 'node:crypto';\nimport * as readline from 'node:readline';\nimport type { DispatchRequest, DispatchResponse } from '@cleocode/contracts/gateway';\nimport { getLogger } from '@cleocode/core';\nimport type { GatewayHandler } from '../index.js';\nimport { toolNameToOperationKey } from './tool-naming.js';\nimport { buildToolsList, exposedOperations } from './tools-list.js';\nimport type { McpTool, McpToolResult } from './types.js';\n\n// ---------------------------------------------------------------------------\n// JSON-RPC 2.0 envelope helpers\n// ---------------------------------------------------------------------------\n\n/** Inbound JSON-RPC request frame. */\ninterface JsonRpcRequest {\n jsonrpc: '2.0';\n id: number | string | null;\n method: string;\n params?: unknown;\n}\n\n/** Outbound JSON-RPC response frame. */\ninterface JsonRpcResponse {\n jsonrpc: '2.0';\n id: number | string | null;\n result?: unknown;\n error?: { code: number; message: string; data?: unknown };\n}\n\n/** Build a JSON-RPC success response. */\nfunction ok(id: number | string | null, result: unknown): JsonRpcResponse {\n return { jsonrpc: '2.0', id, result };\n}\n\n/** Build a JSON-RPC error response. */\nfunction rpcError(id: number | string | null, code: number, message: string): JsonRpcResponse {\n return { jsonrpc: '2.0', id, error: { code, message } };\n}\n\n// ---------------------------------------------------------------------------\n// Server identity\n// ---------------------------------------------------------------------------\n\n/** Advertised MCP server name. */\nconst SERVER_NAME = 'cleo-gateway-mcp';\n/** MCP protocol revision the adapter speaks. */\nconst PROTOCOL_VERSION = '2024-11-05';\n/** Version string advertised in the `initialize` handshake. */\nconst GATEWAY_MCP_VERSION = '1.0.0';\n\n// ---------------------------------------------------------------------------\n// tools/call → gateway routing\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a {@link DispatchResponse} envelope into an MCP tool result.\n *\n * The full LAFS envelope is serialized as the text content so MCP clients see\n * the same `{ success, data, meta, error }` payload every other transport\n * returns. `isError` is set from the envelope's `success` flag.\n *\n * @param response - The gateway dispatch response.\n * @returns An MCP {@link McpToolResult}.\n */\nfunction responseToToolResult(response: DispatchResponse): McpToolResult {\n return {\n content: [{ type: 'text', text: JSON.stringify(response) }],\n isError: response.success ? undefined : true,\n };\n}\n\n/**\n * Route a single MCP `tools/call` through the gateway handler.\n *\n * Resolves the tool name to a `(domain, operation)` key against the exposed\n * (`mcpExposed`) registry subset, builds a `source: 'mcp'` {@link DispatchRequest},\n * and delegates to the injected {@link GatewayHandler}. Unknown tool names and\n * thrown handler errors are rendered as MCP error results — never propagated as\n * `process.exit` from this layer.\n *\n * @param handler - The injected transport-neutral gateway handler.\n * @param toolName - The MCP `tools/call` tool name.\n * @param args - The parsed tool arguments (already JSON-decoded by the client).\n * @returns An MCP {@link McpToolResult}.\n */\nexport async function callTool(\n handler: GatewayHandler,\n toolName: string,\n args: Record<string, unknown>,\n): Promise<McpToolResult> {\n const exposed = exposedOperations();\n const key = toolNameToOperationKey(toolName, exposed);\n if (!key) {\n return {\n content: [{ type: 'text', text: `Unknown tool: ${toolName}` }],\n isError: true,\n };\n }\n\n // The exposed def carries the CQRS gateway — no second lookup needed.\n const def = exposed.find((op) => op.domain === key.domain && op.operation === key.operation);\n if (!def) {\n return {\n content: [{ type: 'text', text: `Unknown tool: ${toolName}` }],\n isError: true,\n };\n }\n\n const request: DispatchRequest = {\n gateway: def.gateway,\n domain: key.domain,\n operation: key.operation,\n params: args,\n source: 'mcp',\n requestId: randomUUID(),\n };\n\n try {\n const response = await handler.handle(request);\n return responseToToolResult(response);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return {\n content: [{ type: 'text', text: `Tool execution failed: ${message}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// JSON-RPC method handlers\n// ---------------------------------------------------------------------------\n\n/**\n * Handle a single decoded JSON-RPC request frame.\n *\n * @param handler - The injected gateway handler.\n * @param req - The decoded JSON-RPC request.\n * @returns The JSON-RPC response frame to write back.\n */\nasync function handleRequest(\n handler: GatewayHandler,\n req: JsonRpcRequest,\n): Promise<JsonRpcResponse> {\n const { id, method, params } = req;\n\n switch (method) {\n case 'initialize':\n return ok(id, {\n protocolVersion: PROTOCOL_VERSION,\n capabilities: { tools: {} },\n serverInfo: { name: SERVER_NAME, version: GATEWAY_MCP_VERSION },\n });\n\n case 'tools/list':\n return ok(id, { tools: buildToolsList() satisfies McpTool[] });\n\n case 'tools/call': {\n const p = params as { name?: string; arguments?: Record<string, unknown> } | undefined;\n const toolName = p?.name;\n if (!toolName) {\n return rpcError(id, -32602, 'Missing tool name in params.name');\n }\n const result = await callTool(handler, toolName, p?.arguments ?? {});\n return ok(id, result);\n }\n\n case 'notifications/initialized':\n case 'ping':\n return ok(id, {});\n\n default:\n return rpcError(id, -32601, `Method not found: ${method}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// stdio server entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link startMcpServer}.\n */\nexport interface McpServerOptions {\n /** Inbound stream (defaults to `process.stdin`). */\n input?: NodeJS.ReadableStream;\n /** Outbound stream (defaults to `process.stdout`). */\n output?: NodeJS.WritableStream;\n /**\n * Whether to call `process.exit(0)` when the input stream closes.\n * Defaults to `true` (production stdio). Tests pass `false`.\n */\n exitOnClose?: boolean;\n}\n\n/**\n * Start the MCP stdio server over the gateway.\n *\n * Reads newline-delimited JSON-RPC 2.0 requests from the input stream, routes\n * `tools/call` through the injected {@link GatewayHandler}, and writes responses\n * to the output stream. Each inbound line must be one complete JSON-RPC request.\n *\n * The caller assembles the {@link GatewayHandler} (via `createGatewayHandler`\n * with its domain handlers + middleware) and injects it here — this adapter\n * never builds handlers itself, keeping the runtime free of any\n * `@cleocode/cleo` dependency.\n *\n * @param handler - The transport-neutral gateway handler to route through.\n * @param opts - Optional stream + lifecycle overrides (defaults to stdio).\n */\nexport function startMcpServer(handler: GatewayHandler, opts?: McpServerOptions): void {\n const input = opts?.input ?? process.stdin;\n const output = opts?.output ?? process.stdout;\n const exitOnClose = opts?.exitOnClose ?? true;\n const log = getLogger('gateway-mcp');\n\n const rl = readline.createInterface({ input, terminal: false });\n\n rl.on('line', (line) => {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n let req: JsonRpcRequest;\n try {\n req = JSON.parse(trimmed) as JsonRpcRequest;\n } catch {\n output.write(`${JSON.stringify(rpcError(null, -32700, 'Parse error: invalid JSON'))}\\n`);\n return;\n }\n\n handleRequest(handler, req)\n .then((response) => {\n output.write(`${JSON.stringify(response)}\\n`);\n })\n .catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n log.warn({ err }, 'mcp request handling failed');\n output.write(\n `${JSON.stringify(rpcError(req.id ?? null, -32603, `Internal error: ${message}`))}\\n`,\n );\n });\n });\n\n rl.on('close', () => {\n if (exitOnClose) process.exit(0);\n });\n}\n","/**\n * MCP tool-name <-> gateway-operation mapping.\n *\n * The MCP wire protocol identifies a tool by a single flat `name` string\n * (snake_case, e.g. `cleo_sentient_propose_list`), whereas the gateway routes\n * on a `(gateway, domain, operation)` triple (e.g.\n * `query / sentient / propose.list`). This module is the bijective bridge\n * between the two surfaces:\n *\n * gateway op → MCP tool name {@link operationToToolName}\n * MCP tool name → domain+operation {@link toolNameToOperationKey}\n *\n * ## Naming convention (frozen — matches the legacy `@cleocode/mcp-adapter`)\n *\n * `cleo_<domain>_<operation>` where every `.` and `-` in the dotted operation\n * name becomes `_`. The three historically-exposed tools therefore map as:\n *\n * | gateway op | MCP tool name |\n * |-----------------------------|--------------------------------|\n * | `sentient` · `status` | `cleo_sentient_status` |\n * | `sentient` · `propose.list` | `cleo_sentient_propose_list` |\n * | `sentient` · `propose.enable` | `cleo_sentient_propose_enable` |\n *\n * Preserving this exact mapping is what guarantees R3-T4's \"no behavior change\n * to the MCP tool surface\" acceptance criterion.\n *\n * @task T11448\n * @epic T11254\n * @saga T11243\n */\n\nimport type { OperationDef } from '@cleocode/contracts';\n\n/** Prefix every CLEO MCP tool name carries. */\nexport const MCP_TOOL_PREFIX = 'cleo_';\n\n/**\n * Identifies a gateway operation by its `(domain, operation)` pair. The CQRS\n * gateway resolves which `Gateway` ('query' | 'mutate') the pair belongs to via\n * the operation registry, so the MCP adapter does not need to carry it.\n */\nexport interface GatewayOperationKey {\n /** Canonical domain (e.g. `'sentient'`). */\n readonly domain: string;\n /** Dotted operation name (e.g. `'propose.list'`). */\n readonly operation: string;\n}\n\n/**\n * Convert a dotted operation segment into its MCP-safe snake_case form.\n *\n * @param segment - A domain or operation string (may contain `.`/`-`).\n * @returns The segment with every `.`/`-` replaced by `_`.\n */\nfunction toSnake(segment: string): string {\n return segment.replace(/[.-]/g, '_');\n}\n\n/**\n * Derive the flat MCP tool name for a gateway operation.\n *\n * @param op - An {@link OperationDef} (or any object carrying `domain` +\n * `operation`) from the registry.\n * @returns The MCP tool name, e.g. `'cleo_sentient_propose_list'`.\n */\nexport function operationToToolName(op: Pick<OperationDef, 'domain' | 'operation'>): string {\n return `${MCP_TOOL_PREFIX}${toSnake(op.domain)}_${toSnake(op.operation)}`;\n}\n\n/**\n * Resolve an MCP tool name back to its `(domain, operation)` gateway key.\n *\n * The reverse of {@link operationToToolName} cannot recover dots vs dashes from\n * the snake_case wire form, so resolution is performed against the provided\n * `exposed` set (the same `mcpExposed: true` subset used to build `tools/list`).\n * This keeps the mapping exact and rejects unknown tool names.\n *\n * @param toolName - The MCP `tools/call` tool name.\n * @param exposed - The exposed operation defs (the `mcpExposed` subset).\n * @returns The matching {@link GatewayOperationKey}, or `undefined` when the\n * tool name does not correspond to any exposed operation.\n */\nexport function toolNameToOperationKey(\n toolName: string,\n exposed: readonly OperationDef[],\n): GatewayOperationKey | undefined {\n const match = exposed.find((op) => operationToToolName(op) === toolName);\n return match ? { domain: match.domain, operation: match.operation } : undefined;\n}\n","/**\n * MCP `tools/list` generation from the OPERATIONS registry.\n *\n * The MCP tool surface is **default-deny**: only {@link OperationDef} entries\n * that explicitly set `mcpExposed: true` are surfaced. This replaces the legacy\n * `@cleocode/mcp-adapter`'s hand-maintained `ALL_TOOLS` array with a single\n * generation point driven by the canonical registry — promoting a new tool is\n * now one `mcpExposed: true` edit in `@cleocode/contracts`, not a fork-and-copy\n * into a standalone adapter.\n *\n * @task T11448\n * @epic T11254\n * @saga T11243\n */\n\nimport type { OperationDef, ParamDef } from '@cleocode/contracts';\nimport { OPERATIONS } from '../registry.js';\nimport { operationToToolName } from './tool-naming.js';\nimport type { McpInputProperty, McpTool } from './types.js';\n\n/**\n * The subset of OPERATIONS surfaced over MCP (default-deny `mcpExposed` filter).\n *\n * @returns Every {@link OperationDef} with `mcpExposed === true`, in registry\n * order. Empty when no operation opts in.\n */\nexport function exposedOperations(): OperationDef[] {\n return OPERATIONS.filter((op) => op.mcpExposed === true);\n}\n\n/**\n * Map a JSON-ish {@link ParamDef} type to its JSON Schema primitive.\n *\n * MCP clients consume standard JSON Schema, so `'array'` widens to `'string'`\n * the same way the CLI's citty bridge does (callers split/parse) and every\n * other type passes through unchanged.\n *\n * @param type - The {@link ParamDef} runtime type.\n * @returns The JSON Schema `type` keyword.\n */\nfunction paramTypeToJsonSchema(type: ParamDef['type']): string {\n return type === 'array' ? 'string' : type;\n}\n\n/**\n * Build the MCP `inputSchema` for a single operation from its `params`.\n *\n * Hidden params (`hidden: true`) are excluded from the public tool surface,\n * matching the JSON Schema generation policy declared on {@link ParamDef}.\n *\n * @param op - The exposed operation definition.\n * @returns A JSON Schema object describing the tool's input.\n */\nfunction buildInputSchema(op: OperationDef): McpTool['inputSchema'] {\n const properties: Record<string, McpInputProperty> = {};\n const required: string[] = [];\n\n for (const param of op.params ?? []) {\n if (param.hidden === true) continue;\n const prop: McpInputProperty = {\n type: paramTypeToJsonSchema(param.type),\n description: param.description,\n };\n if (param.enum !== undefined && param.enum.length > 0) {\n prop.enum = param.enum;\n }\n properties[param.name] = prop;\n if (param.required) required.push(param.name);\n }\n\n const schema: McpTool['inputSchema'] = { type: 'object', properties };\n if (required.length > 0) schema.required = required;\n return schema;\n}\n\n/**\n * Convert a single exposed {@link OperationDef} into its MCP tool definition.\n *\n * @param op - An operation with `mcpExposed === true`.\n * @returns The MCP {@link McpTool} (name + description + inputSchema).\n */\nexport function operationToMcpTool(op: OperationDef): McpTool {\n return {\n name: operationToToolName(op),\n description: op.description,\n inputSchema: buildInputSchema(op),\n };\n}\n\n/**\n * Generate the full MCP `tools/list` payload from the registry.\n *\n * @returns Every exposed operation rendered as an {@link McpTool}, in registry\n * order (stable across calls).\n */\nexport function buildToolsList(): McpTool[] {\n return exposedOperations().map(operationToMcpTool);\n}\n"],"mappings":"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","names":[]}
|