Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 14 additions & 1 deletion apps/kimi-code/src/cli/sub/web/remote-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
createKimiDeviceId,
FileTokenStorage,
KIMI_CODE_PROVIDER_NAME,
kimiCodeEnvBaseUrl,
kimiCodeEnvOAuthHost,
resolveKimiCodeOAuthKey,
resolveKimiTokenStorageName,
} from '@moonshot-ai/kimi-code-oauth';
import { WebSocket, type RawData } from 'ws';
Expand Down Expand Up @@ -294,8 +297,18 @@ export async function startRemoteControl(
throw new Error('Remote Control requires local server authentication.');
}
const storage = new FileTokenStorage(join(options.homeDir, 'credentials'));
// Resolve the credential slot the same way login and the runtime provider
// do: with KIMI_CODE_OAUTH_HOST / KIMI_CODE_BASE_URL overrides the token
// lives in an env-scoped slot (kimi-code-env-<hash>), and reading only the
// default slot would pick up a credential for the wrong environment.
const token = await storage.load(
resolveKimiTokenStorageName({ providerName: KIMI_CODE_PROVIDER_NAME }),
resolveKimiTokenStorageName({
providerName: KIMI_CODE_PROVIDER_NAME,
oauthKey: resolveKimiCodeOAuthKey({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add a changeset for the user-visible credential fix

This changes shipped CLI behavior by making Remote Control select a different credential slot, but the commit contains no .changeset entry. Without the required CLI patch changeset, this user-visible bug fix will be omitted from the generated release changelog; add a short @moonshot-ai/kimi-code patch changeset.

AGENTS.md reference: AGENTS.md:L85-L85

Useful? React with 👍 / 👎.

oauthHost: kimiCodeEnvOAuthHost(),
baseUrl: kimiCodeEnvBaseUrl(),
}),
Comment on lines +307 to +310

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resolve the persisted OAuth ref before loading the token

When a user selects the global OAuth login (or another endpoint persisted in config.toml) without endpoint environment variables, both values passed here are undefined, so this derives the default mainland credential key. Login persists a scoped key from the configured baseUrl and OAuth ref, and normal runtime auth reads that configuration, so /remote-control will either claim the user is not logged in or use a stale credential from the wrong region. Load the persisted provider configuration through the SDK and apply the same runtime-auth resolution used by login/runtime before selecting the token slot.

AGENTS.md reference: apps/kimi-code/AGENTS.md:L45-L45

Useful? React with 👍 / 👎.

}),
);
if (token?.refreshToken === undefined || token.refreshToken.length === 0) {
throw new Error('Remote Control requires a Kimi login. Run `kimi login` first.');
Expand Down
56 changes: 56 additions & 0 deletions apps/kimi-code/test/cli/web/remote-control.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { join } from 'node:path';
import {
FileTokenStorage,
KIMI_CODE_PROVIDER_NAME,
resolveKimiCodeOAuthKey,
resolveKimiTokenStorageName,
type TokenInfo,
} from '@moonshot-ai/kimi-code-oauth';
Expand Down Expand Up @@ -243,6 +244,61 @@ describe('Remote Control tunnel', () => {
).rejects.toThrow(/DEVICE_LIMIT_EXCEEDED.*membership allows 3 devices/);
});

it('loads the env-scoped credential when OAuth env overrides are set', async () => {
const oauthHost = 'https://auth.dev.example.test';
const baseUrl = 'https://api.dev.example.test/coding/v1';
vi.stubEnv('KIMI_CODE_OAUTH_HOST', oauthHost);
vi.stubEnv('KIMI_CODE_BASE_URL', baseUrl);
const homeDir = mkdtempSync(join(tmpdir(), 'kimi-rc-env-'));
cleanups.push(() => rmSync(homeDir, { recursive: true, force: true }));
const storage = new FileTokenStorage(join(homeDir, 'credentials'));
// The default (production) slot holds a credential for a different
// environment; the dev login wrote to the env-scoped slot instead.
await storage.save(resolveKimiTokenStorageName({ providerName: KIMI_CODE_PROVIDER_NAME }), {
...TOKEN,
refreshToken: 'prod-refresh-token',
});
await storage.save(
resolveKimiTokenStorageName({
providerName: KIMI_CODE_PROVIDER_NAME,
oauthKey: resolveKimiCodeOAuthKey({ oauthHost, baseUrl }),
}),
{ ...TOKEN, refreshToken: 'dev-refresh-token' },
);
const relay = await startAuthRelay();
let handle: RemoteControlHandle | undefined;
cleanups.push(async () => handle?.close());

handle = await startRemoteControl({
homeDir,
localOrigin: 'http://127.0.0.1:1',
localServerToken: 'local-server-token',
relayOrigin: `http://127.0.0.1:${relay.port}/coding-relay`,
stderr: { write: () => true },
});

const bearerTokens = relay.requests.map(
(request) => request.authorization ?? request.protocol?.replace('kimi-code.bearer.', ''),
);
expect(bearerTokens).toContain('dev-refresh-token');
expect(bearerTokens).not.toContain('prod-refresh-token');
});

it('ignores the default credential slot when OAuth env overrides are set', async () => {
vi.stubEnv('KIMI_CODE_OAUTH_HOST', 'https://auth.dev.example.test');
const homeDir = await createRemoteControlHome(TOKEN.refreshToken);

await expect(
startRemoteControl({
homeDir,
localOrigin: 'http://127.0.0.1:1',
localServerToken: 'local-server-token',
relayOrigin: 'http://127.0.0.1:1',
stderr: { write: () => true },
}),
).rejects.toThrow('Remote Control requires a Kimi login');
});

it('uses only Authorization when the refresh token is not a valid subprotocol token', async () => {
const homeDir = await createRemoteControlHome('invalid/token=');
const relay = await startAuthRelay();
Expand Down