Skip to content

feat(agent-core-v2): add experimental remote control device tunnel - #2557

Open
sailist wants to merge 1 commit into
MoonshotAI:mainfrom
sailist:feat/RC
Open

feat(agent-core-v2): add experimental remote control device tunnel#2557
sailist wants to merge 1 commit into
MoonshotAI:mainfrom
sailist:feat/RC

Conversation

@sailist

@sailist sailist commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

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 remoteControl domain 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:

  • Added the remoteControl domain under packages/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).
  • Gated the whole feature behind the experimental remote_control flag (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:

  • Relay connections authenticate with the standard Authorization header; the bearer-subprotocol convention is kept only for the localhost hop.
  • device_id is passed as a query parameter when connecting the HTTP tunnel so the relay can bind the socket to the registered device.
  • On open_ws, the relay tunnel stream is opened before the local WebSocket so early local frames (e.g. server_hello) are not lost.
  • Relay paths are spliced under the relay base-URL mount prefix (e.g. /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:

  • startServer accepts an optional remoteControl: { relayBaseUrl, alias? } option; after the listener is bound it starts the tunnel against the loopback address (with 0.0.0.0 / :: normalization) using the server auth token, and stops it on server close (local_server_stopped).
  • Startup failures of the tunnel abort the server start with best-effort cleanup.

4. Tests

  • Added a 499-line suite at packages/agent-core-v2/test/app/remoteControl/remoteControl.test.ts covering registration, HTTP tunneling, WS bridging, and auth.
  • Extended packages/kap-server/test/boot.test.ts for the new remoteControl start option.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

- 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
@changeset-bot

changeset-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 3d9aa76

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Aug 3, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@3d9aa76
npx https://pkg.pr.new/@moonshot-ai/kimi-code@3d9aa76

commit: 3d9aa76

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +108 to +110
local.close(code, reason);
tunnel.close(code, reason);
subscriptions?.dispose();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +108 to +112
if (this.management !== undefined) {
this.management.send(encodeManagementMessage({ type: 'disconnect', payload: { reason } }));
await Promise.resolve();
}
this.resetConnections();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant