import { E2eAead, FrameSink, SpawnFrame, HelloFrame, acceptSinkE2E as acceptSinkE2E$1, TunnelE2EOptions, AcceptSinkE2EResult, E2eKeys, HandshakeOverSinkOptions, E2eFrameSink, daemonHandshakeOverSink as daemonHandshakeOverSink$1, WrapE2EOptions } from './browser.js'; export { DEFAULT_E2E_MAX_FRAMES, DEFAULT_TUNNEL_E2E_TIMEOUT_MS, DaemonToHostFrame, E2eError, E2eErrorCode, E2eFrame, E2eHandshakeFrame, ErrorFrame, ExitFrame, HostToDaemonFrame, KillFrame, MAX_FRAME_PAYLOAD_BYTES, PingFrame, PongFrame, ReconnectSoonFrame, ResizeFrame, SpawnedFrame, StderrFrame, StdinFrame, StdoutFrame, TUNNEL_VERSION, TunnelFrame, WebSocketLike, WsCloseFrame, WsMessageFrame, WsOpenAckFrame, WsOpenFrame, createWebCryptoAead, encodeData, encodeFrame, holdFrames, parseFrame, splitPayload, webCryptoAead, wrapWebSocket } from './browser.js'; import { ChildProcess } from 'node:child_process'; import { Writable, Readable } from 'node:stream'; /** * Node-side `decodeData`: the frame codec's portable `decodeData` returns a * `Uint8Array` (so it runs in a browser); the Node tunnel client/server and the * `@agentproto/acp/tunnel` entry keep the `Buffer`-typed signature they always * had. Under Node the underlying value already is a `Buffer`, so this is free. */ declare function decodeData(data: string): Buffer; /** * `E2eAead` over `node:crypto` — the default through the `@agentproto/acp/tunnel` * Node entry. The exact cipher calls `wrapE2E` made before the AEAD seam. */ declare const nodeAead: E2eAead; /** * Tunnel server (daemon side). * * Sits on the local machine that actually owns the binaries (e.g. the * user's laptop running `agentproto serve`). Accepts spawn frames from * a remote host, spawns the requested child via node:child_process, * pipes stdio back as `stdout`/`stderr` frames, and reports lifecycle * via `spawned`/`exit`/`error`. * * Transport-agnostic: the caller passes a `FrameSink`. For WebSocket * callers, see `wrapWebSocket()` in ./ws-adapter.ts. * * Security: this process MUST refuse spawn requests by default unless * the caller installed an `authorize(spawn)` hook. We don't expose a * "trust everything" mode — every host integration is responsible for * deciding what the remote may exec. The hook gets the full SpawnFrame * and can mutate it (e.g. force `cwd` into a workspace) or reject. */ /** * Default ceiling for an upstream WS dial. An upstream that accepts the TCP * connection but never completes (or rejects) the WebSocket upgrade would * otherwise leave the `ws_open` pending forever — the host never gets an ack or * a reject. Bounded so a hung upstream surfaces as a 504 instead. */ declare const DEFAULT_WS_DIAL_TIMEOUT_MS = 10000; /** * Default ceiling for an `http_request` forward when the frame carries no * `timeoutMs`. Covers connect + buffered body (or connect + headers for a * stream, after which the bound is handed to `httpStreamIdleTimeoutMs`). */ declare const DEFAULT_HTTP_FORWARD_TIMEOUT_MS = 30000; interface TunnelServerOptions { sink: FrameSink; /** * Authorization hook. Called for each `spawn` frame BEFORE the * subprocess is created. Return the (possibly mutated) frame to * proceed; throw or return null to reject. Rejection causes an * `error` frame with code "spawn_unauthorized". * * No default — callers MUST decide. Pass `(req) => req` to allow * everything (useful for tests; never for production). */ authorize: (req: SpawnFrame) => SpawnFrame | null | Promise; /** * Upstream HTTP base URL for `http_request` frames. The daemon * appends the frame's `path` and forwards. Typically the local * gateway address — `http://127.0.0.1:18790`. When omitted, the * daemon replies to any `http_request` with * `error: { code: "http_upstream_not_configured" }`. */ httpUpstream?: string; /** * Headers merged into EVERY forwarded `http_request` before it hits the * upstream, overriding any same-named header from the frame. Used by the E2E * pairing serve path to inject the daemon's own bearer token so a * peer-authenticated pairing passes the gateway's mutating-route token gate * (`checkSessionsToken` has no loopback bypass) — see DESIGN §5. Omitted for * the trusted `serve --connect` host, which needs no injection. The pairing * layer never learns or transports the user's daemon bearer beyond this hop. */ httpInjectHeaders?: Readonly>; /** * Default ceiling for an `http_request` forward (connect + buffered body, or * connect + headers for a stream) when the frame carries no `timeoutMs`. * Defaults to DEFAULT_HTTP_FORWARD_TIMEOUT_MS. Disarmed once a streaming * response is detected — see `httpStreamIdleTimeoutMs` for the stream phase. */ httpForwardTimeoutMs?: number; /** * Idle ceiling between chunks of a STREAMING response (SSE / ndjson). A * stalled upstream that sends headers then hangs forever between chunks would * otherwise block indefinitely (the regular timeout is disarmed for streams, * since they're long-lived by design). Counts time since the last byte and * resets on every chunk, so a heartbeating stream is never cut. Omitted ⇒ no * idle bound (current behaviour); set it to guard against silent stalls. */ httpStreamIdleTimeoutMs?: number; /** User-friendly label sent in the hello frame. */ label?: string; /** * Capability identifiers advertised in the hello frame — the agent * adapters / tool surfaces this daemon serves (e.g. the gateway's * registered adapter ids). Lets a multi-daemon host enumerate and * route by capability without a round-trip. Omit to advertise none. */ tools?: ReadonlyArray; /** * Set when the `sink` is an E2E-encrypted channel (a `wrapE2E` box). Purely * informational — surfaced to the host as `hello.capabilities.e2e` so it can * confirm/display that the tunnel is end-to-end encrypted. The encryption * itself is established by the handshake BEFORE this server is constructed * (see ./tunnel-e2e.ts); this flag does not turn anything on. Default false. */ e2e?: boolean; /** * Whether this daemon advertises PTY support. Set to true only when * `spawnPty` is also provided. */ pty?: boolean; /** * Factory for PTY-backed processes. When provided (and `pty: true`), * spawn frames with `pty: true` use this instead of node:child_process. * Injected from the CLI layer so the ACP package stays native-free. */ spawnPty?: (opts: { command: string; args: string[]; cwd?: string; env?: Record; cols: number; rows: number; }) => PtyProcess; /** * Optional hook fired once per successful spawn — after authorize * passed and the ChildProcess has emitted its `spawn` event. * Lets the daemon adopt this child into a higher-level registry * (e.g. @agentproto/runtime's sessions registry, AIP-46) so the * spawn shows up in /sessions, the LocalDaemonSessionsCard, and * the CLI TUI alongside spawns originated locally. * * The hook is fire-and-forget — the tunnel doesn't await its * return. Errors from the hook are swallowed so an observer * registry hiccup never breaks the tunnel exchange. */ onChildSpawned?: (info: { execId: string; child: ChildProcess; request: SpawnFrame; }) => void; /** * Optional hook fired when the host sends a `reconnect_soon` frame — * graceful drain signal during host rollover. The daemon SHOULD * close + reconnect immediately (skipping any backoff) so the new * host replica picks it up. Default: ignore (the host follows up * with `close(1012)` and the daemon's own reconnect supervisor * handles it via normal backoff, ~30 s gap). * * Implementations typically call into their connection supervisor * to mark the next reconnect as "no-backoff" then close the sink. * `reasonMs` is the host's hint for how long it will wait before * forcing close — daemons MAY use it to drain in-flight RPCs first. */ onReconnectSoon?: (info: { reasonMs?: number; }) => void; /** * Factory that dials a WS connection to the local upstream. When * provided (and `httpUpstream` is set), the daemon advertises * `wsForward` capability and handles `ws_open` frames by piping the * upstream WS through the tunnel. Injected so `@agentproto/acp` * stays free of a hard `ws` dependency — the CLI provides this * factory using its already-pulled-in `ws` package. */ dialUpstreamWs?: (params: { url: string; protocols?: readonly string[]; headers?: Readonly>; /** * Aborted when the dial exceeds `wsDialTimeoutMs` (or the tunnel tears down * mid-dial). A cooperating implementation SHOULD abort the in-flight upgrade * and reject so the socket isn't left half-open; even if it doesn't, the * server bounds the wait independently and rejects the `ws_open`. */ signal?: AbortSignal; }) => Promise; /** * Ceiling for `dialUpstreamWs` to resolve before the `ws_open` is rejected * with 504 `upstream_ws_timeout`. Defaults to DEFAULT_WS_DIAL_TIMEOUT_MS. */ wsDialTimeoutMs?: number; /** * Largest raw payload (bytes) one outgoing frame carries; bigger response * bodies and (negotiated) WS messages are split. Defaults to * `MAX_FRAME_PAYLOAD_BYTES`, sized for the rendezvous' 1 MiB message cap * after E2E wrapping. Also bounds an assembled chunked request body at * `maxRequestBodyBytes`. */ maxFramePayloadBytes?: number; /** * Ceiling for a chunked (`bodyChunked`) request body the daemon assembles * before forwarding. Default 64 MiB; over it the request is answered 413. */ maxRequestBodyBytes?: number; /** * Resolve a named upstream (a `ws_open` frame's `upstream` field) to an * HTTP base URL the daemon should dial instead of `httpUpstream`. Lets a * host reach an imported local service by alias (e.g. a browser capability * server) without ever naming a raw origin — the daemon owns the name→URL * mapping. Return undefined for an unknown alias (the open is rejected). * Omitted ⇒ every `ws_open` dials the default `httpUpstream`. */ resolveWsUpstream?: (upstream: string) => string | undefined | Promise; } /** * Minimal upstream WS surface — satisfied by the `ws` library's * client socket. Daemons supplying `dialUpstreamWs` resolve into * one of these as soon as the WS is OPEN. `protocol` is the * subprotocol negotiated by the upstream (empty string when none). */ interface UpstreamWebSocket { readonly protocol: string; /** Send a frame to the upstream. */ send(data: Buffer | string, opts: { binary: boolean; }): void; /** Close the upstream connection. */ close(code?: number, reason?: string): void; /** Subscribe to inbound frames. */ onMessage(handler: (data: Buffer, isBinary: boolean) => void): void; /** Subscribe to upstream close. */ onClose(handler: (code: number, reason: string) => void): void; /** Subscribe to transport errors. */ onError(handler: (err: Error) => void): void; } /** Minimal PTY process surface — satisfied by node-pty's IPty. */ interface PtyProcess { readonly pid: number; write(data: string): void; resize(cols: number, rows: number): void; kill(signal?: string): void; onData(handler: (data: string) => void): void; onExit(handler: (event: { exitCode: number; signal?: number; }) => void): void; } interface TunnelServer { /** Currently-live execId → ChildProcess. Read-only diagnostic. */ readonly children: ReadonlyMap; /** Kill all live children and close the sink. */ close(): Promise; } declare function createTunnelServer(opts: TunnelServerOptions): TunnelServer; /** * Tunnel client (host side). * * Sits on the orchestrator (e.g. Guilde API). Speaks to a remote daemon * over a `FrameSink`. Exposes `spawn(command, args, opts)` that returns * an object shaped like Node's `ChildProcess` — same `.stdin` / * `.stdout` / `.stderr` / `.on('exit')` / `.kill()` surface. This lets * the existing ACP protocol arm drive remote subprocesses with zero * code changes; the arm thinks it's talking to a local child. * * The duck is intentionally minimal — only the methods the ACP arm * actually calls. If you need the full ChildProcess interface, wrap * this with a node:stream.Readable shim. */ /** Resolved response from `forwardHttp`. Mirrors `HttpResponseFrame` * but with the body decoded back to a Buffer for the caller. */ interface TunnelHttpResponse { status: number; headers: Readonly>; body: Buffer; /** Set when the daemon failed to complete the upstream call. */ error?: Readonly<{ code: string; message: string; }>; } /** Resolved response from `forwardHttpStream`. Body is a Web * `ReadableStream` — consume it to receive chunks as * they arrive from the daemon's upstream (SSE, long-poll, NDJSON). * The stream closes when the daemon's upstream closes OR an upstream * error occurs (surfaced as a stream error on the reader). */ interface TunnelHttpStreamResponse { status: number; headers: Readonly>; body: ReadableStream; } interface TunnelHttpRequest { method: string; path: string; headers?: Readonly>; body?: Buffer | Uint8Array | string; /** Per-request timeout in ms. Default 30_000. */ timeoutMs?: number; } interface TunnelWebSocketOpenRequest { /** Path + querystring on the daemon-local upstream. Daemon prepends * its `ws://` base; absolute URLs are rejected. */ path: string; /** Optional registered upstream to dial instead of the daemon's default * gateway — a NAME the daemon resolves to a base URL (e.g. an imported * service alias). The host never names a raw origin. */ upstream?: string; /** Headers forwarded to the upstream upgrade. Hop-by-hop / Sec-* are * stripped by the daemon. Cookies are NOT forwarded unless the * daemon was started with `--upstream-cookies`. */ headers?: Readonly>; /** Subprotocols requested. Echoed in `protocol` on success. */ protocols?: readonly string[]; /** How long to wait for the upstream upgrade before erroring. The * message stream itself has no timeout (WS lives as long as either * end keeps it open). Default 30_000. */ openTimeoutMs?: number; } /** * Bridged WS connection. Mirrors the surface of a browser WebSocket * minus URL/protocol bookkeeping — the host already chose those when * calling `forwardWebSocket`. Messages are typed as text (UTF-8 string) * or binary (Uint8Array); the bridge preserves the upstream's framing. */ interface TunnelWebSocket { /** True until `close` runs in either direction. */ readonly readyState: "open" | "closing" | "closed"; /** Selected subprotocol from the upstream's upgrade response, if any. */ readonly protocol: string | null; /** Send a frame to the daemon's upstream. No-op when not open. */ send(data: string | Uint8Array): void; /** Initiate a clean close. Idempotent. */ close(code?: number, reason?: string): void; /** Subscribe to inbound frames. Returns an unsubscribe fn. */ onMessage(listener: (data: Buffer, isBinary: boolean) => void): () => void; /** Subscribe to close. Fires exactly once (after either side closes * OR the tunnel itself drops). Returns an unsubscribe fn. */ onClose(listener: (code: number, reason: string) => void): () => void; /** Subscribe to transport-level errors. Closes follow shortly. */ onError(listener: (err: Error) => void): () => void; } interface TunnelClientOptions { sink: FrameSink; /** * Reject the `spawn` call if the daemon hasn't sent its `hello` * within this window. Defaults to 10s — generous for high-latency * tunnels but fast enough that misconfigured daemons fail loudly. */ helloTimeoutMs?: number; } interface TunnelSpawnOptions { cwd?: string; env?: Readonly>; /** Allocate a PTY on the daemon. Requires `hello.capabilities.pty`. */ pty?: boolean; /** Initial PTY column width (ignored when `pty` is false). Default 80. */ cols?: number; /** Initial PTY row height (ignored when `pty` is false). Default 24. */ rows?: number; } /** * Minimal ChildProcess-shaped duck. Covers the surface our ACP arm * calls; expand only when a real consumer needs more. */ interface TunnelChildProcess { readonly execId: string; readonly pid: number | null; readonly stdin: Writable; readonly stdout: Readable; readonly stderr: Readable; /** "exit" → (code, signal). "error" → (Error). */ on(event: "exit", listener: (code: number | null, signal: string | null) => void): this; on(event: "error", listener: (err: Error) => void): this; kill(signal?: NodeJS.Signals | number): boolean; /** Send a terminal resize to the daemon. No-op if the exec isn't PTY-backed. */ resize(cols: number, rows: number): void; } interface TunnelClient { /** * Resolves once the daemon has sent its `hello` frame, OR rejects * with a tunnel-level error. Subsequent calls return the cached * promise. */ ready(): Promise; spawn(command: string, args: readonly string[], opts?: TunnelSpawnOptions): Promise; /** * Forward an HTTP request through the tunnel to the daemon's * configured upstream (typically `http://127.0.0.1:`). The * daemon completes the upstream call and replies with the response, * which this promise resolves to. Rejects on tunnel-transport * failure; the daemon-reported HTTP status (incl. 5xx) is returned * in the resolved value. * * Streaming responses (text/event-stream, NDJSON) are buffered into * the returned Buffer. For real streaming, use `forwardHttpStream`. */ forwardHttp(req: TunnelHttpRequest): Promise; /** * Stream an HTTP response through the tunnel. The promise resolves * as soon as the response head (status + headers) arrives — the * body chunks then flow through `body` (a ReadableStream) until the * daemon's upstream closes. Required for SSE / NDJSON / long-poll * upstreams where buffering until the end would defeat the point. * For one-shot HTTP, prefer `forwardHttp`. */ forwardHttpStream(req: TunnelHttpRequest): Promise; /** * Open a bridged WebSocket through the tunnel. The promise resolves * once the daemon has completed the upstream upgrade; from that * point messages flow via the returned `TunnelWebSocket`. * * Rejects on: * - missing `hello.capabilities.wsForward` — older daemon, no bridge * - upstream upgrade failure (4xx/5xx) — `cause` carries the status * - timeout waiting for `ws_open_ack` */ forwardWebSocket(req: TunnelWebSocketOpenRequest): Promise; close(): Promise; } declare function createTunnelClient(opts: TunnelClientOptions): TunnelClient; /** * @agentproto/acp/tunnel — frame protocol + client/server for relaying * subprocess I/O across a duplex transport (typically WebSocket). Node entry. * * Wire spec: agentproto/tunnel/v1, defined in ./frames.ts. * * The frame codec, `wrapE2E` and the handshake-over-sink helpers are shared * with the browser-safe entry (`@agentproto/acp/tunnel/browser`, ./browser.ts). * The only difference here: the E2E helpers default their AES-GCM to * `node:crypto` (`nodeAead`) instead of WebCrypto — same bytes either way — * and `decodeData` keeps its `Buffer` return type. */ /** `wrapE2E` (see ./e2e.ts), AES-GCM defaulting to `node:crypto`. */ declare function wrapE2E(inner: FrameSink, keys: E2eKeys, opts?: WrapE2EOptions): E2eFrameSink; /** `clientHandshakeOverSink` (see ./e2e.ts), AES-GCM defaulting to `node:crypto`. */ declare function clientHandshakeOverSink(sink: FrameSink, hello: Uint8Array, deriveKeys: (reply: Uint8Array) => E2eKeys | Promise, opts?: HandshakeOverSinkOptions): Promise; /** `daemonHandshakeOverSink` (see ./e2e.ts), AES-GCM defaulting to `node:crypto`. */ declare function daemonHandshakeOverSink(sink: FrameSink, respond: Parameters[1], opts?: HandshakeOverSinkOptions): Promise; /** `connectSinkE2E` (see ./tunnel-e2e.ts), AES-GCM defaulting to `node:crypto`. */ declare function connectSinkE2E(sink: FrameSink, offer: Uint8Array, deriveKeys: (reply: Uint8Array) => E2eKeys | Promise, opts?: TunnelE2EOptions): Promise; /** `acceptSinkE2E` (see ./tunnel-e2e.ts), AES-GCM defaulting to `node:crypto`. */ declare function acceptSinkE2E(sink: FrameSink, respond: Parameters[1], opts?: TunnelE2EOptions): Promise; export { AcceptSinkE2EResult, DEFAULT_HTTP_FORWARD_TIMEOUT_MS, DEFAULT_WS_DIAL_TIMEOUT_MS, E2eAead, E2eFrameSink, E2eKeys, FrameSink, HandshakeOverSinkOptions, HelloFrame, type PtyProcess, SpawnFrame, type TunnelChildProcess, type TunnelClient, type TunnelClientOptions, TunnelE2EOptions, type TunnelHttpRequest, type TunnelHttpResponse, type TunnelHttpStreamResponse, type TunnelServer, type TunnelServerOptions, type TunnelSpawnOptions, type TunnelWebSocket, type TunnelWebSocketOpenRequest, type UpstreamWebSocket, WrapE2EOptions, acceptSinkE2E, clientHandshakeOverSink, connectSinkE2E, createTunnelClient, createTunnelServer, daemonHandshakeOverSink, decodeData, nodeAead, wrapE2E };