feat(agent-core-v2): add experimental remote control device tunnel - #2557
feat(agent-core-v2): add experimental remote control device tunnel#2557sailist wants to merge 1 commit into
Conversation
- add the remoteControl domain (App scope): management socket registration, HTTP tunnel forwarding, and WebSocket stream bridging to the local server, gated by the experimental remote_control flag - authenticate relay connections with the standard Authorization header; the bearer subprotocol convention is only for the localhost hop - pass device_id as a query parameter when connecting the HTTP tunnel so the relay can bind the socket to the registered device - open the relay tunnel stream before the local WebSocket when handling open_ws, so early local frames (e.g. server_hello) are not lost - splice relay paths under the relay base URL mount prefix (e.g. /coding-relay) instead of discarding it via origin-absolute URL resolution - wire the tunnel into kap-server startServer via the remoteControl option
|
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3d9aa761c3
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| local.close(code, reason); | ||
| tunnel.close(code, reason); | ||
| subscriptions?.dispose(); |
There was a problem hiding this comment.
Dispose bridged sockets when streams close
When a remote WebSocket stream closes normally, closeBoth only calls close() on the local and tunnel sockets. The production NodeRemoteSocket removes itself from RemoteControlTransportService.sockets only in dispose(), not on close, so each browser/local stream that ends leaves two closed WebSocket objects referenced until the whole App-scope service is disposed. In long-running remote-control sessions this leaks sockets/listeners per /api/v1/ws connection; dispose the sockets here or remove them from the set on their close event.
Useful? React with 👍 / 👎.
| if (this.management !== undefined) { | ||
| this.management.send(encodeManagementMessage({ type: 'disconnect', payload: { reason } })); | ||
| await Promise.resolve(); | ||
| } | ||
| this.resetConnections(); |
There was a problem hiding this comment.
Wait for disconnect to flush before terminating
When the server shuts down while the management socket is open, stop() queues the disconnect frame and then immediately calls resetConnections(), which disposes the socket via terminate(). The await Promise.resolve() does not wait for the WebSocket send to reach the relay, so local_server_stopped/user_requested can be dropped and the relay never gets the graceful unregister reason. Use a send callback/ack or a close handshake before terminating the management connection.
Useful? React with 👍 / 👎.
Related Issue
No linked issue — the problem is explained below.
Problem
A running Kimi Code server (kap-server) is only reachable on the machine it runs on. There is no supported path for a remote client to drive a local Kimi Code instance through an authenticated relay, which blocks remote-control style scenarios (e.g. operating a local CLI session from another device).
What changed
1. New
remoteControldomain in agent-core-v2 (App scope)Problem: The engine had no tunnel/proxy capability for exposing the local server through a relay.
What was done:
remoteControldomain underpackages/agent-core-v2/src/app/remoteControl/: management-socket registration with the relay (remoteControlService), HTTP tunnel forwarding, and WebSocket stream bridging to the local server (remoteControlTransportService), plus the wire protocol definitions (protocol.ts).remote_controlflag (KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL, default off), registered via the App-scope flag registry.2. Relay authentication and tunnel protocol details
Problem: The relay hop and the localhost hop have different trust levels, and naive forwarding loses frames or path prefixes.
What was done:
Authorizationheader; the bearer-subprotocol convention is kept only for the localhost hop.device_idis passed as a query parameter when connecting the HTTP tunnel so the relay can bind the socket to the registered device.open_ws, the relay tunnel stream is opened before the local WebSocket so early local frames (e.g.server_hello) are not lost./coding-relay) instead of being discarded by origin-absolute URL resolution.3. kap-server wiring
Problem: The tunnel had to be started and stopped with the server's lifecycle.
What was done:
startServeraccepts an optionalremoteControl: { relayBaseUrl, alias? }option; after the listener is bound it starts the tunnel against the loopback address (with0.0.0.0/::normalization) using the server auth token, and stops it on server close (local_server_stopped).4. Tests
packages/agent-core-v2/test/app/remoteControl/remoteControl.test.tscovering registration, HTTP tunneling, WS bridging, and auth.packages/kap-server/test/boot.test.tsfor the newremoteControlstart option.Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.