fix(rate-limits): bound and offload the Kimi credentials read for WSL homes

Adopted from @cengiz-io's #12372: read credentials through
createAuthFilesystemOperation (async, per-path dedup, AbortSignal bound) so a
stopped distro degrades to an error instead of parking Electron main on a UNC
read. ENOTDIR joins ENOENT as "not signed in" to keep existsSync parity, and the
WSL runtime target is now probed with the async wsl.exe helpers.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-08-07 18:35:39 -07:00
committed by OrcaWin
co-authored by Orca
parent 2b1fb9121e
commit cfde30da44
7 changed files with 202 additions and 64 deletions
+24 -24
View File
@@ -1,8 +1,8 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const wslMocks = vi.hoisted(() => ({
getDefaultWslDistro: vi.fn<() => string | null>(),
getWslHome: vi.fn<(distro: string) => string | null>()
listWslDistrosAsync: vi.fn<() => Promise<string[]>>(),
getWslHomeAsync: vi.fn<(distro: string) => Promise<string | null>>()
}))
vi.mock('../wsl', () => wslMocks)
@@ -52,8 +52,8 @@ describe('resolveKimiHome', () => {
beforeEach(() => {
delete process.env.KIMI_CODE_HOME
wslMocks.getDefaultWslDistro.mockReset().mockReturnValue('Ubuntu')
wslMocks.getWslHome.mockReset().mockReturnValue('\\\\wsl.localhost\\Ubuntu\\home\\neil')
wslMocks.listWslDistrosAsync.mockReset().mockResolvedValue(['Ubuntu'])
wslMocks.getWslHomeAsync.mockReset().mockResolvedValue('\\\\wsl.localhost\\Ubuntu\\home\\neil')
})
afterEach(() => {
@@ -64,62 +64,62 @@ describe('resolveKimiHome', () => {
}
})
it('resolves the host home for a host target', () => {
expect(resolveKimiHome({ runtime: 'host', wslDistro: null }, 'win32')).toEqual({
it('resolves the host home for a host target', async () => {
expect(await resolveKimiHome({ runtime: 'host', wslDistro: null }, 'win32')).toEqual({
runtime: 'host',
wslDistro: null,
path: getHostKimiHome()
})
expect(wslMocks.getWslHome).not.toHaveBeenCalled()
expect(wslMocks.getWslHomeAsync).not.toHaveBeenCalled()
})
it('resolves the WSL distro home for a WSL target', () => {
expect(resolveKimiHome({ runtime: 'wsl', wslDistro: 'Ubuntu' }, 'win32')).toEqual({
it('resolves the WSL distro home for a WSL target', async () => {
expect(await resolveKimiHome({ runtime: 'wsl', wslDistro: 'Ubuntu' }, 'win32')).toEqual({
runtime: 'wsl',
wslDistro: 'Ubuntu',
path: '\\\\wsl.localhost\\Ubuntu\\home\\neil\\.kimi-code'
})
})
it('ignores the host KIMI_CODE_HOME when reading a WSL home', () => {
it('ignores the host KIMI_CODE_HOME when reading a WSL home', async () => {
process.env.KIMI_CODE_HOME = 'D:\\kimi-home'
expect(resolveKimiHome({ runtime: 'wsl', wslDistro: 'Ubuntu' }, 'win32').path).toBe(
expect((await resolveKimiHome({ runtime: 'wsl', wslDistro: 'Ubuntu' }, 'win32')).path).toBe(
'\\\\wsl.localhost\\Ubuntu\\home\\neil\\.kimi-code'
)
})
it('falls back to the default distro when none is configured', () => {
expect(resolveKimiHome({ runtime: 'wsl', wslDistro: null }, 'win32')).toMatchObject({
it('falls back to the default distro when none is configured', async () => {
expect(await resolveKimiHome({ runtime: 'wsl', wslDistro: null }, 'win32')).toMatchObject({
wslDistro: 'Ubuntu'
})
expect(wslMocks.getWslHome).toHaveBeenCalledWith('Ubuntu')
expect(wslMocks.getWslHomeAsync).toHaveBeenCalledWith('Ubuntu')
})
it('reports no path when the distro home cannot be probed', () => {
wslMocks.getWslHome.mockReturnValue(null)
expect(resolveKimiHome({ runtime: 'wsl', wslDistro: 'Ubuntu' }, 'win32')).toEqual({
it('reports no path when the distro home cannot be probed', async () => {
wslMocks.getWslHomeAsync.mockResolvedValue(null)
expect(await resolveKimiHome({ runtime: 'wsl', wslDistro: 'Ubuntu' }, 'win32')).toEqual({
runtime: 'wsl',
wslDistro: 'Ubuntu',
path: null
})
})
it('reports no path when no distro exists at all', () => {
wslMocks.getDefaultWslDistro.mockReturnValue(null)
expect(resolveKimiHome({ runtime: 'wsl', wslDistro: null }, 'win32')).toEqual({
it('reports no path when no distro exists at all', async () => {
wslMocks.listWslDistrosAsync.mockResolvedValue([])
expect(await resolveKimiHome({ runtime: 'wsl', wslDistro: null }, 'win32')).toEqual({
runtime: 'wsl',
wslDistro: null,
path: null
})
})
it('never probes WSL off Windows', () => {
expect(resolveKimiHome({ runtime: 'wsl', wslDistro: 'Ubuntu' }, 'darwin')).toEqual({
it('never probes WSL off Windows', async () => {
expect(await resolveKimiHome({ runtime: 'wsl', wslDistro: 'Ubuntu' }, 'darwin')).toEqual({
runtime: 'host',
wslDistro: null,
path: getHostKimiHome()
})
expect(wslMocks.getDefaultWslDistro).not.toHaveBeenCalled()
expect(wslMocks.getWslHome).not.toHaveBeenCalled()
expect(wslMocks.listWslDistrosAsync).not.toHaveBeenCalled()
expect(wslMocks.getWslHomeAsync).not.toHaveBeenCalled()
})
})
+15 -6
View File
@@ -6,7 +6,7 @@ import {
type LocalAccountRuntimeTarget
} from '../../shared/local-account-runtime'
import type { GlobalSettings } from '../../shared/types'
import { getDefaultWslDistro, getWslHome } from '../wsl'
import { getWslHomeAsync, listWslDistrosAsync } from '../wsl'
export type KimiHomeResolution = LocalAccountRuntimeTarget & {
/** null when the WSL distro's home could not be probed (distro missing/stopped). */
@@ -34,23 +34,32 @@ export function getKimiRuntimeTarget(
return resolveLocalAccountRuntimeTarget(settings, platform)
}
/** Resolve a runtime target to the Kimi home Orca should read (UNC path for WSL). */
export function resolveKimiHome(
/**
* Resolve a runtime target to the Kimi home Orca should read (UNC path for WSL).
* Async because it runs on every quota poll: the sync `wsl.exe` probes park
* Electron's main process for up to 5s each cycle while a distro is stopped.
*/
export async function resolveKimiHome(
target: LocalAccountRuntimeTarget,
platform: NodeJS.Platform = process.platform
): KimiHomeResolution {
): Promise<KimiHomeResolution> {
if (target.runtime !== 'wsl' || platform !== 'win32') {
return { runtime: 'host', wslDistro: null, path: getHostKimiHome() }
}
const distro = target.wslDistro?.trim() || getDefaultWslDistro()
const distro = target.wslDistro?.trim() || (await defaultWslDistro())
if (!distro) {
return { runtime: 'wsl', wslDistro: null, path: null }
}
const home = getWslHome(distro)
const home = await getWslHomeAsync(distro)
// KIMI_CODE_HOME describes the Windows host process, never the distro's home.
return { runtime: 'wsl', wslDistro: distro, path: home ? joinKimiHome(home) : null }
}
/** Async twin of `getDefaultWslDistro`, which shells out to wsl.exe synchronously. */
async function defaultWslDistro(): Promise<string | null> {
return (await listWslDistrosAsync())[0] ?? null
}
function joinKimiHome(home: string): string {
return parseWslUncPath(home) ? pathWin32.join(home, '.kimi-code') : join(home, '.kimi-code')
}
@@ -2,14 +2,20 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
const netFetchMock = vi.hoisted(() => vi.fn())
const files = vi.hoisted(() => new Map<string, string>())
const STALLED = vi.hoisted(() => '__stalled_unc_read__')
vi.mock('electron', () => ({ net: { fetch: netFetchMock } }))
vi.mock('node:fs', () => ({
existsSync: (path: string) => files.has(path),
readFileSync: (path: string) => {
vi.mock('node:fs/promises', () => ({
readFile: async (path: string) => {
const contents = files.get(path)
if (contents === undefined) {
throw new Error(`ENOENT: ${path}`)
const error = new Error(`ENOENT: ${path}`) as NodeJS.ErrnoException
error.code = 'ENOENT'
throw error
}
if (contents === STALLED) {
// A distro that is down parks the UNC read instead of failing.
return await new Promise<string>(() => {})
}
return contents
}
@@ -108,4 +114,27 @@ describe('fetchKimiRateLimits with a WSL credentials home', () => {
expect(result.status).toBe('unavailable')
})
// Last: an unsettled UNC read stays shared for its path by design, so the stalled
// distro gets its own home to avoid poisoning the other cases.
it('bounds a stalled UNC read instead of parking the poll cycle', async () => {
const stalledHome = '\\\\wsl.localhost\\Stopped\\home\\neil\\.kimi-code'
files.set(`${stalledHome}\\credentials\\kimi-code.json`, STALLED)
const timeoutController = new AbortController()
const timeout = vi.spyOn(AbortSignal, 'timeout').mockReturnValue(timeoutController.signal)
const pending = fetchKimiRateLimits({
home: { runtime: 'wsl', wslDistro: 'Stopped', path: stalledHome }
})
expect(timeout).toHaveBeenCalledWith(5_000)
await Promise.resolve()
await Promise.resolve()
timeoutController.abort()
const result = await pending
expect(result.status).toBe('error')
expect(result.error).toContain('(WSL Stopped)')
expect(netFetchMock).not.toHaveBeenCalled()
timeout.mockRestore()
})
})
+46 -9
View File
@@ -1,28 +1,33 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const netFetchMock = vi.hoisted(() => vi.fn())
const fsState = vi.hoisted<{ credentials: string | null; readError: Error | null }>(() => ({
const fsState = vi.hoisted<{
credentials: string | null
readError: Error | null
readPaths: string[]
}>(() => ({
credentials: null,
readError: null
readError: null,
readPaths: []
}))
vi.mock('electron', () => ({
net: { fetch: netFetchMock }
}))
vi.mock('node:fs', () => ({
existsSync: () => fsState.credentials !== null,
readFileSync: () => {
vi.mock('node:fs/promises', () => ({
readFile: async (path: string) => {
fsState.readPaths.push(String(path))
if (fsState.readError) {
throw fsState.readError
}
if (fsState.credentials === null) {
throw new Error('ENOENT')
const error = new Error('ENOENT: no such file or directory') as NodeJS.ErrnoException
error.code = 'ENOENT'
throw error
}
return fsState.credentials
},
writeFileSync: () => {},
renameSync: () => {}
}
}))
vi.mock('node:os', () => ({ homedir: () => '/home/test' }))
@@ -60,6 +65,7 @@ describe('fetchKimiRateLimits', () => {
netFetchMock.mockReset()
fsState.credentials = null
fsState.readError = null
fsState.readPaths = []
})
afterEach(() => {
@@ -147,4 +153,35 @@ describe('fetchKimiRateLimits', () => {
})
expect(netFetchMock).not.toHaveBeenCalled()
})
it('reads the host ~/.kimi-code credentials by default', async () => {
fsState.credentials = freshCredentials()
netFetchMock.mockResolvedValueOnce(jsonResponse(USAGE_RESPONSE))
const result = await fetchKimiRateLimits()
expect(result.status).toBe('ok')
expect(fsState.readPaths).toEqual(['/home/test/.kimi-code/credentials/kimi-code.json'])
})
it('honors KIMI_CODE_HOME for the host home', async () => {
vi.stubEnv('KIMI_CODE_HOME', '/custom/kimi-home')
fsState.credentials = freshCredentials()
netFetchMock.mockResolvedValueOnce(jsonResponse(USAGE_RESPONSE))
const result = await fetchKimiRateLimits()
expect(result.status).toBe('ok')
expect(fsState.readPaths).toEqual(['/custom/kimi-home/credentials/kimi-code.json'])
})
it('ignores a blank KIMI_CODE_HOME instead of reading from the process cwd', async () => {
vi.stubEnv('KIMI_CODE_HOME', ' ')
fsState.credentials = freshCredentials()
netFetchMock.mockResolvedValueOnce(jsonResponse(USAGE_RESPONSE))
await fetchKimiRateLimits()
expect(fsState.readPaths).toEqual(['/home/test/.kimi-code/credentials/kimi-code.json'])
})
})
+70 -17
View File
@@ -1,4 +1,4 @@
import { existsSync, readFileSync } from 'node:fs'
import { readFile } from 'node:fs/promises'
import { join, win32 as pathWin32 } from 'node:path'
import { net } from 'electron'
import type {
@@ -8,6 +8,10 @@ import type {
} from '../../shared/rate-limit-types'
import { parseWslUncPath } from '../../shared/wsl-paths'
import { getHostKimiHome, type KimiHomeResolution } from '../kimi/kimi-runtime-home'
import {
createAuthFilesystemOperation,
type SharedAuthFilesystemOperation
} from './auth-filesystem-operation'
// Why: Kimi Code's managed coding plan exposes subscription usage at
// `${base}/usages` (see packages/oauth/src/managed-usage.ts in the CLI bundle).
@@ -15,6 +19,7 @@ import { getHostKimiHome, type KimiHomeResolution } from '../kimi/kimi-runtime-h
// stays aligned with a user's self-hosted/staging config.
const KIMI_BASE_URL = process.env.KIMI_CODE_BASE_URL ?? 'https://api.kimi.com/coding/v1'
const API_TIMEOUT_MS = 10_000
const CREDENTIALS_READ_TIMEOUT_MS = 5_000
const SESSION_WINDOW_MINUTES = 300 // 5h
const WEEKLY_WINDOW_MINUTES = 10080 // 7d
@@ -50,22 +55,65 @@ function parseCredentials(value: unknown): KimiCredentials | null {
return credentials
}
function readCredentials(kimiHome: string): CredentialsReadResult {
const path = getCredentialsPath(kimiHome)
if (!existsSync(path)) {
return { status: 'missing' }
const credentialsReadByPath = new Map<
string,
SharedAuthFilesystemOperation<CredentialsReadResult>
>()
function isMissingPathError(error: unknown): boolean {
const code = (error as NodeJS.ErrnoException | null)?.code
return code === 'ENOENT' || code === 'ENOTDIR'
}
function readErrorMessage(err: unknown): string {
// Why: AbortSignal timeouts reject with a DOMException, not an Error.
const message = (err as { message?: unknown } | null)?.message
return typeof message === 'string' ? message : 'Unable to read Kimi credentials'
}
function getCredentialsRead(path: string): SharedAuthFilesystemOperation<CredentialsReadResult> {
const existing = credentialsReadByPath.get(path)
if (existing) {
return existing
}
try {
const parsed: unknown = JSON.parse(readFileSync(path, 'utf-8'))
const credentials = parseCredentials(parsed)
return credentials
? { status: 'ok', credentials }
: { status: 'error', error: 'Kimi credentials file is invalid' }
} catch (err) {
return {
status: 'error',
error: err instanceof Error ? err.message : 'Unable to read Kimi credentials'
// Why: aborting an fs promise does not cancel an already issued UNC request, so
// share one raw read per path until it settles (mirrors codex-fetcher's auth read).
const read = createAuthFilesystemOperation(path, async (): Promise<CredentialsReadResult> => {
let raw: string
try {
raw = await readFile(path, 'utf-8')
} catch (err) {
return isMissingPathError(err)
? { status: 'missing' }
: { status: 'error', error: readErrorMessage(err) }
}
try {
const credentials = parseCredentials(JSON.parse(raw))
return credentials
? { status: 'ok', credentials }
: { status: 'error', error: 'Kimi credentials file is invalid' }
} catch (err) {
return { status: 'error', error: readErrorMessage(err) }
}
})
credentialsReadByPath.set(path, read)
const clearRead = (): void => {
if (credentialsReadByPath.get(path) === read) {
credentialsReadByPath.delete(path)
}
}
void read.result.then(clearRead, clearRead)
return read
}
async function readCredentials(kimiHome: string): Promise<CredentialsReadResult> {
const path = getCredentialsPath(kimiHome)
try {
// Why: a stopped distro parks a UNC read for minutes; bound it so a WSL home
// degrades to an error instead of stalling the poll cycle.
return await getCredentialsRead(path).wait(AbortSignal.timeout(CREDENTIALS_READ_TIMEOUT_MS))
} catch (err) {
return { status: 'error', error: readErrorMessage(err) }
}
}
@@ -210,6 +258,11 @@ function mapUsageResponse(data: KimiUsageResponse): ProviderRateLimits {
}
}
// A bare "operation was aborted due to timeout" hides which machine stalled.
function readErrorContext(home: KimiHomeResolution, error: string): string {
return home.runtime === 'wsl' ? `${error} (WSL ${home.wslDistro ?? 'default distro'})` : error
}
function expiredSessionMessage(home: KimiHomeResolution): string {
const where =
home.runtime === 'wsl'
@@ -260,12 +313,12 @@ export async function fetchKimiRateLimits(options?: {
if (home.path === null) {
return result('error', `WSL Kimi home unavailable for ${home.wslDistro ?? 'default distro'}`)
}
const readResult = readCredentials(home.path)
const readResult = await readCredentials(home.path)
if (readResult.status === 'missing') {
return result('unavailable', 'Not signed in to Kimi Code')
}
if (readResult.status === 'error') {
return result('error', readResult.error)
return result('error', readErrorContext(home, readResult.error))
}
const creds = readResult.credentials
if (typeof creds.access_token !== 'string' || creds.access_token.length === 0) {
+1 -1
View File
@@ -1439,7 +1439,7 @@ describe('RateLimitService', () => {
wslDistro: 'Ubuntu',
path: '\\\\wsl.localhost\\Ubuntu\\home\\neil\\.kimi-code'
}
const resolver = vi.fn(() => home)
const resolver = vi.fn(async () => home)
service.setKimiHomeResolver(resolver)
mockFreshBackgroundProviderFetches()
vi.mocked(fetchClaudeRateLimits).mockResolvedValue(okProvider('claude', 10))
+13 -3
View File
@@ -39,6 +39,7 @@ export type InactiveCodexAccountInfo = {
}
type CodexHomePathResolver = (target?: CodexAccountSelectionTarget) => string | null
type KimiHomeResolver = () => Promise<KimiHomeResolution>
type ClaudeAuthPreparationResolver = (
target?: ClaudeAccountSelectionTarget
) => Promise<ClaudeRuntimeAuthPreparation>
@@ -224,7 +225,7 @@ export class RateLimitService {
wslDistro: null
}
// Why: resolved per cycle — the local-account runtime policy can flip between fetches.
private kimiHomeResolver: (() => KimiHomeResolution) | null = null
private kimiHomeResolver: KimiHomeResolver | null = null
private claudeAuthPreparationResolver: ClaudeAuthPreparationResolver | null = null
private claudeFetchTarget: NormalizedClaudeAccountSelectionTarget = {
runtime: 'host',
@@ -263,10 +264,19 @@ export class RateLimitService {
this.codexFetchTarget = normalizeCodexAccountSelectionTarget(target)
}
setKimiHomeResolver(resolver: () => KimiHomeResolution): void {
setKimiHomeResolver(resolver: KimiHomeResolver): void {
this.kimiHomeResolver = resolver
}
// Why: resolving a WSL home probes wsl.exe, so it must not run before the other
// providers' fetches are started; chaining keeps the no-resolver path immediate.
private fetchKimiWithResolvedHome(): Promise<ProviderRateLimits> {
const pendingHome = this.kimiHomeResolver?.()
return pendingHome
? pendingHome.then((home) => fetchKimiRateLimits({ home }))
: fetchKimiRateLimits({ home: undefined })
}
setClaudeAuthPreparationResolver(resolver: ClaudeAuthPreparationResolver): void {
this.claudeAuthPreparationResolver = resolver
}
@@ -1668,7 +1678,7 @@ export class RateLimitService {
workspaceIdOverride || undefined,
this.networkProxySettingsResolver?.()
),
fetchKimiRateLimits({ home: this.kimiHomeResolver?.() }),
this.fetchKimiWithResolvedHome(),
miniMaxConfigResult.error
? Promise.resolve(this.getMiniMaxCredentialError(miniMaxConfigResult.error))
: fetchMiniMaxRateLimits({