Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b704a3a
feat(kap-server): add remote control device management
liruifengv Oct 10, 2026
0fca826
feat(remote-control): tag share URLs with the caller's platform source
liruifengv Oct 12, 2026
cb840e6
fix(kap-server): stop the local tunnel when the current device is dea…
liruifengv Oct 12, 2026
e77399b
feat(kap-server)!: drop the remote-control device delete route
liruifengv Oct 12, 2026
fcacfda
feat(kap-server): carry the lock holder in the already-running error
liruifengv Oct 12, 2026
b5f0577
feat(kap-server): expose the lock conflict in the remote control status
liruifengv Oct 12, 2026
94ce023
feat(kap-server): resolve the remote control lock conflict live in st…
liruifengv Oct 12, 2026
5498fc5
feat(kap-server): wait for the lock holder to release on current-devi…
liruifengv Oct 12, 2026
90168d7
style(kap-server): silence promise-executor-return in lock release wait
liruifengv Oct 12, 2026
e29ad69
Merge remote-tracking branch 'origin/main' into feat/remote-control-d…
liruifengv Oct 12, 2026
32fd047
test(remote-control): pass urlSource in the close-reason cases
liruifengv Oct 12, 2026
6614c60
feat(kap-server)!: register remote control devices with the host prod…
liruifengv Oct 12, 2026
3deb1ef
refactor(cli): source the remote control identity from app constants
liruifengv Oct 12, 2026
ed43353
Merge branch 'main' into feat/remote-control-device-management
liruifengv Oct 12, 2026
8e34e74
fix(remote-control): make starts cancellable and harden logout cleanup
liruifengv Oct 12, 2026
a638c64
Merge remote-tracking branch 'origin/feat/remote-control-device-manag…
liruifengv Oct 12, 2026
cf9a27d
fix(remote-control): close the remaining cancellation and invalidatio…
liruifengv Oct 12, 2026
a2ab229
fix(kap-server): sweep remote control cleanup once more after logout
liruifengv Oct 12, 2026
a094289
docs: note the lock-release failure of remote control deactivation
liruifengv Oct 12, 2026
5e695f5
fix(remote-control): key cloud sessions by credential and settle canc…
liruifengv Oct 12, 2026
4f63cca
fix(kap-server): block remote control starts during logout and serial…
liruifengv Oct 13, 2026
9f62801
fix(kap-server): gate local starts during current-device deactivation
liruifengv Oct 13, 2026
1955b5d
fix(kap-server): match active tunnel ids on deactivation and re-clear…
liruifengv Oct 13, 2026
de8830e
fix(kap-server): serialize Kimi logouts, harden lock cleanup, and whi…
liruifengv Oct 13, 2026
34f23dd
fix(remote-control): abort pending handshakes on close and answer log…
liruifengv Oct 13, 2026
442aecf
fix(remote-control): cancel enables that are still queued behind a pe…
liruifengv Oct 13, 2026
ab76e38
fix(kap-server): recognize foreign logout gates and retry post-logout…
liruifengv Oct 13, 2026
d35a762
fix(remote-control): verify the machine lock after disable and rechec…
liruifengv Oct 13, 2026
732b45a
feat(kap-server): export loadOrCreateServerToken for host token provi…
liruifengv Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/kimi-code/src/cli/sub/web/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { shutdownTelemetry, track } from '@moonshot-ai/kimi-telemetry';
import chalk from 'chalk';
import { type Command, Option } from 'commander';

import { CLI_SHUTDOWN_TIMEOUT_MS, WEB_USER_AGENT_SUFFIX } from '#/constant/app';
import { CLI_MSH_PLATFORM, CLI_SHUTDOWN_TIMEOUT_MS, CLI_USER_AGENT_PRODUCT, WEB_USER_AGENT_SUFFIX } from '#/constant/app';
import { getNativeWebAssetsDir } from '#/native/web-assets';
import { darkColors } from '#/tui/theme/colors';
import { openUrl as defaultOpenUrl } from '#/utils/open-url';
Expand Down Expand Up @@ -231,7 +231,8 @@ export async function handleWebCommand(
homeDir: dataDir,
localOrigin: origin,
localServerToken: token,
clientVersion: `kimi-code/${getVersion()}`,
clientVersion: `${CLI_USER_AGENT_PRODUCT}/${getVersion()}`,
urlSource: CLI_MSH_PLATFORM,
configuredOAuthKey: persisted?.key,
configuredOAuthHost: persisted?.oauthHost,
stderr: deps.stderr,
Expand Down
6 changes: 3 additions & 3 deletions apps/kimi-code/src/cli/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import { existsSync, readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';

import { createKimiUserAgent, KIMI_CODE_PLATFORM, type KimiHostIdentity } from '@moonshot-ai/kimi-code-oauth';
import { createKimiUserAgent, type KimiHostIdentity } from '@moonshot-ai/kimi-code-oauth';

import { CLI_USER_AGENT_PRODUCT } from '#/constant/app';
import { CLI_MSH_PLATFORM, CLI_USER_AGENT_PRODUCT } from '#/constant/app';

import { KIMI_BUILD_INFO } from './build-info';

Expand Down Expand Up @@ -51,7 +51,7 @@ export function createKimiCodeHostIdentity(version = getVersion()): KimiHostIden
return {
productName: CLI_USER_AGENT_PRODUCT,
version,
platform: KIMI_CODE_PLATFORM,
platform: CLI_MSH_PLATFORM,
};
}

Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/constant/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const PROCESS_NAME = 'kimi-code';

// Used in telemetry app names and HTTP User-Agent headers.
export const CLI_USER_AGENT_PRODUCT = 'kimi-code-cli';
export const CLI_MSH_PLATFORM = 'kimi_code_cli';
export const CLI_UI_MODE = 'shell';
// UI surfaces the TUI renders; declared to the engine at bootstrap so features that need a
// host-side surface (the NotifyUser update panel) are offered to this process only.
Expand Down
5 changes: 4 additions & 1 deletion apps/kimi-code/src/tui/commands/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import chalk from 'chalk';

import { splitTokenFragment } from '#/cli/sub/web/access-urls';
import { getVersion } from '#/cli/version';
import { CLI_MSH_PLATFORM, CLI_USER_AGENT_PRODUCT } from '#/constant/app';
import {
buildRemoteControlUrl,
formatRemoteControlAlreadyRunning,
Expand Down Expand Up @@ -73,7 +74,8 @@ export async function handleRemoteControlCommand(host: SlashCommandHost): Promis
homeDir: dataDir,
localOrigin: origin,
localServerToken: token,
clientVersion: `kimi-code/${getVersion()}`,
clientVersion: `${CLI_USER_AGENT_PRODUCT}/${getVersion()}`,
urlSource: CLI_MSH_PLATFORM,
configuredOAuthKey: persisted?.key,
configuredOAuthHost: persisted?.oauthHost,
onStatus,
Expand All @@ -95,6 +97,7 @@ export async function handleRemoteControlCommand(host: SlashCommandHost): Promis
remoteControl.deviceId,
session?.id,
remoteControl.relayOrigin,
CLI_MSH_PLATFORM,
);
const qrCode = await generateRemoteControlQr(url, dataDir);
process.stdout.write(
Expand Down
49 changes: 49 additions & 0 deletions docs/en/reference/server-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ On success, `data` is `{ cancelled, status }`: `cancelled` is `true` only when a

Logs out the managed provider: discards the stored OAuth credential, aborts any pending login flow, and removes the managed provider from the configuration. OAuth-managed providers reject manual edit and delete (see `PUT` / `DELETE /api/v1/providers/{provider_id}` below), so log out first to remove one.

For the default Kimi provider (`managed:kimi-code`), logout also tears down this machine's Remote Control and clears cached Remote Control cloud sessions. When no tunnel is running or another process hosts it, logout first deactivates this machine's device and holds the machine lock so no process on this machine can start Remote Control again until the credential is discarded; a cleanup failure aborts the logout. When this server hosts the tunnel, the credential is discarded first and the tunnel is torn down right after the response is flushed, so a logout sent through the Remote Control link still receives its response. Logging out any other provider leaves Remote Control untouched.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `provider` | body | string | Managed provider name. Default `managed:kimi-code` |
Expand Down Expand Up @@ -269,6 +271,53 @@ Resolves which Kimi region this client belongs to. The answer is derived locally

On success, `data` is `{ region }` with `region` one of `mainland-cn` / `global`.

### Remote Control

These endpoints manage the machine-wide Remote Control tunnel (see the [Remote Control guide](../guides/remote-control.md)) and the account's device registry. The tunnel state is per machine; the device registry is per account and lives in the cloud, so device endpoints require a Kimi login and proxy the relay with a server-side session — OAuth credentials never reach the caller.

#### `GET /api/v1/remote-control`

Returns the current tunnel status. When the tunnel is off but another live process on this machine holds the single-instance lock, `data.conflict` carries that holder; the lock is re-inspected on every read, so a dead holder clears both `conflict` and the stored `error`.

On success, `data` is `{ enabled, state, url?, device_id?, device_name?, error?, conflict? }` with `state` one of `off` / `starting` / `on` / `stopping` and `conflict` as `{ pid, local_origin, device_id, url, started_at }`.

#### `POST /api/v1/remote-control`

Starts or stops the tunnel.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `enabled` | body | boolean | `true` to start the tunnel, `false` to stop it |

On success, `data` is the same status object as `GET /api/v1/remote-control`. Starting while another live process on this machine holds the single-instance lock fails with `REMOTE_CONTROL_ALREADY_RUNNING` (40928), with the lock holder in `details` in the same shape as `conflict`.

#### `GET /api/v1/remote-control/devices`

Lists the account's Remote Control devices from the cloud registry.

On success, `data` is `{ devices, max_devices? }`. Each device is `{ device_id, alias, status, platform, client_version, local_base_url, created_at, updated_at, last_remote_access_at }` with `status` one of `online` / `offline`.

#### `PATCH /api/v1/remote-control/devices/{id}`

Renames a device.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | string | Device identifier |
| `alias` | body | string | New display name, trimmed; 1–50 characters |

Blank or over-long aliases fail with `VALIDATION_FAILED` before reaching the cloud. On success, `data` is `{ ok: true }`.

#### `POST /api/v1/remote-control/devices/{id}/deactivate`

Takes a device offline in the cloud registry. Offline devices stay listed and do not consume the plan's device quota; a device comes back the next time it turns on Remote Control. When the target is this machine, the local tunnel is stopped first (an in-flight start is cancelled); the request then holds the machine lock while deactivating, so a concurrent enable cannot re-register the device behind a successful response. A holder that does not stop in time fails the request with `INTERNAL_ERROR` instead of reporting success.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | string | Device identifier |

On success, `data` is `{ ok: true }`.

### Config

| Method and path | Description |
Expand Down
49 changes: 49 additions & 0 deletions docs/zh/reference/server-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ curl -s -H "Authorization: Bearer $TOKEN" \

登出托管供应商:丢弃已存储的 OAuth 凭据、中止进行中的登录流程,并把托管供应商从配置中移除。OAuth 托管的供应商拒绝手动编辑与删除(见下文 `PUT` / `DELETE /api/v1/providers/{provider_id}`),因此要移除它需先登出。

对默认 Kimi 供应商(`managed:kimi-code`),登出还会拆除本机的远程控制并清除缓存的远程控制云端会话。当没有隧道运行或隧道由本机其他进程持有时,登出会先把本机设备下线并持有本机锁,确保凭据被丢弃之前本机任何进程都无法再次开启远程控制;清理失败会中止登出。当隧道由本服务持有时,先丢弃凭据,隧道在响应发出后随即拆除,因此通过远程控制链接发起的登出仍能收到响应。登出其他供应商不影响远程控制。

| 参数 | 位置 | 类型 | 说明 |
| --- | --- | --- | --- |
| `provider` | body | string | 托管供应商名称。默认 `managed:kimi-code` |
Expand Down Expand Up @@ -269,6 +271,53 @@ curl -s -H "Authorization: Bearer $TOKEN" \

成功时 `data` 为 `{ region }`,`region` 为 `mainland-cn` / `global` 之一。

### 远程控制

这组端点管理机器级的远程控制隧道(见 [远程控制指南](../guides/remote-control.md))和账号的设备注册表。隧道状态按机器区分;设备注册表按账号区分、保存在云端,因此设备端点需要 Kimi 登录,并由服务端持会话代理到 relay——OAuth 凭据不会暴露给调用方。

#### `GET /api/v1/remote-control`

返回当前隧道状态。当隧道处于关闭状态、但本机另一个存活进程持有单实例锁时,`data.conflict` 携带该持有者信息;每次读取都会重新检查锁,持有者进程退出后 `conflict` 与已存的 `error` 会一并清除。

成功时 `data` 为 `{ enabled, state, url?, device_id?, device_name?, error?, conflict? }`,其中 `state` 为 `off` / `starting` / `on` / `stopping` 之一,`conflict` 为 `{ pid, local_origin, device_id, url, started_at }`。

#### `POST /api/v1/remote-control`

开启或关闭隧道。

| 参数 | 位置 | 类型 | 说明 |
| --- | --- | --- | --- |
| `enabled` | body | boolean | `true` 开启隧道,`false` 关闭隧道 |

成功时 `data` 与 `GET /api/v1/remote-control` 的状态对象相同。当本机另一个存活进程持有单实例锁时,开启会失败并返回 `REMOTE_CONTROL_ALREADY_RUNNING`(40928),`details` 中携带与 `conflict` 相同结构的锁持有者信息。

#### `GET /api/v1/remote-control/devices`

从云端注册表列出账号的远程控制设备。

成功时 `data` 为 `{ devices, max_devices? }`。每个设备为 `{ device_id, alias, status, platform, client_version, local_base_url, created_at, updated_at, last_remote_access_at }`,其中 `status` 为 `online` / `offline` 之一。

#### `PATCH /api/v1/remote-control/devices/{id}`

重命名设备。

| 参数 | 位置 | 类型 | 说明 |
| --- | --- | --- | --- |
| `id` | path | string | 设备标识 |
| `alias` | body | string | 新的显示名称,去除首尾空白;1–50 个字符 |

名称为空或超长时,在到达云端之前即返回 `VALIDATION_FAILED`。成功时 `data` 为 `{ ok: true }`。

#### `POST /api/v1/remote-control/devices/{id}/deactivate`

在云端注册表把设备置为离线。离线设备保留在列表中,且不占用套餐的设备额度;该设备下次开启远程控制时恢复在线。当目标设备是本机时,先停止本地隧道(启动中的开启会被取消);随后请求持有本机锁完成下线,确保并发的开启无法在成功响应背后重新注册设备。持有者未在限定时间内停止时,请求以 `INTERNAL_ERROR` 失败,而不是报告成功。

| 参数 | 位置 | 类型 | 说明 |
| --- | --- | --- | --- |
| `id` | path | string | 设备标识 |

成功时 `data` 为 `{ ok: true }`。

### 配置

| 方法与路径 | 说明 |
Expand Down
6 changes: 5 additions & 1 deletion packages/kap-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ export { okEnvelope, errEnvelope } from './envelope';
export type { Envelope } from './envelope';
export { classify } from './security/bindClassify';
export type { BindClass } from './security/bindClassify';
export { rotateServerToken, serverTokenPath } from './services/auth/persistentToken';
export {
loadOrCreateServerToken,
rotateServerToken,
serverTokenPath,
} from './services/auth/persistentToken';
export { createServerLogger } from './services/pinoLoggerService';
export type {
CreateLoggerOptions,
Expand Down
3 changes: 2 additions & 1 deletion packages/kap-server/src/protocol/envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function errEnvelope(
msg: string,
requestId: string,
stack?: string,
details?: unknown,
): Envelope<null> {
return { code, msg, data: null, request_id: requestId, stack };
return { code, msg, data: null, request_id: requestId, stack, details };
}
58 changes: 58 additions & 0 deletions packages/kap-server/src/protocol/rest-remote-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,68 @@ export const remoteControlStatusSchema = z.object({
device_id: z.string().optional(),
device_name: z.string().optional(),
error: z.string().optional(),
conflict: z
.object({
pid: z.number().int(),
local_origin: z.string(),
device_id: z.string(),
url: z.string(),
started_at: z.number(),
})
.optional(),
});
export type RemoteControlStatusResponse = z.infer<typeof remoteControlStatusSchema>;

export const setRemoteControlRequestSchema = z.object({
enabled: z.boolean(),
});
export type SetRemoteControlRequest = z.infer<typeof setRemoteControlRequestSchema>;

export const remoteControlDeviceSchema = z.object({
device_id: z.string(),
alias: z.string(),
status: z.enum(['online', 'offline']),
platform: z.string(),
client_version: z.string(),
local_base_url: z.string(),
created_at: z.string(),
updated_at: z.string(),
last_remote_access_at: z.string(),
});
export type RemoteControlDeviceResponse = z.infer<typeof remoteControlDeviceSchema>;

export const remoteControlDeviceListSchema = z.object({
devices: z.array(remoteControlDeviceSchema),
max_devices: z.number().optional(),
});
export type RemoteControlDeviceListResponse = z.infer<typeof remoteControlDeviceListSchema>;

export const remoteControlDeviceParamsSchema = z.object({
id: z.string().min(1),
});
export type RemoteControlDeviceParams = z.infer<typeof remoteControlDeviceParamsSchema>;

export const renameRemoteControlDeviceRequestSchema = z.object({
alias: z.string().trim().min(1).max(50),
});
export type RenameRemoteControlDeviceRequest = z.infer<
typeof renameRemoteControlDeviceRequestSchema
>;

export const remoteControlDeviceMutationResponseSchema = z.object({
ok: z.literal(true),
});
export type RemoteControlDeviceMutationResponse = z.infer<
typeof remoteControlDeviceMutationResponseSchema
>;

export const remoteControlAlreadyRunningDetailsSchema = z.object({
pid: z.number().int(),
local_origin: z.string(),
device_id: z.string(),
url: z.string(),
started_at: z.number(),
});
export type RemoteControlAlreadyRunningDetails = z.infer<
typeof remoteControlAlreadyRunningDetailsSchema
>;
Loading
Loading