diff --git a/src/main/codex-accounts/runtime-home-service.test.ts b/src/main/codex-accounts/runtime-home-service.test.ts index 87e3a0ea90d..51908be3253 100644 --- a/src/main/codex-accounts/runtime-home-service.test.ts +++ b/src/main/codex-accounts/runtime-home-service.test.ts @@ -1324,6 +1324,86 @@ describe('CodexRuntimeHomeService', () => { } }) + it('seeds the WSL runtime config with rewritten paths and no system hook trust', async () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform') + Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) + const wslHome = join(testState.userDataDir, 'wsl-home') + vi.doMock('../wsl', () => ({ + getDefaultWslDistro: () => 'Ubuntu', + getWslHome: () => wslHome + })) + const systemCodexHomePath = join(wslHome, '.codex') + mkdirSync(systemCodexHomePath, { recursive: true }) + writeFileSync( + join(systemCodexHomePath, 'config.toml'), + [ + 'model_instructions_file = "instructions.md"', + '', + '[hooks.state."system-hooks:stop:0:0"]', + 'enabled = true', + '', + '[projects."/home/alice/repo"]', + 'trust_level = "trusted"', + '' + ].join('\n'), + 'utf-8' + ) + const store = createStore(createSettings()) + + try { + const { CodexRuntimeHomeService } = await import('./runtime-home-service') + const service = new CodexRuntimeHomeService(store as never) + const wslRuntimeHomePath = join( + wslHome, + '.local', + 'share', + 'orca', + 'codex-runtime-home', + 'home' + ) + + expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe( + wslRuntimeHomePath + ) + const runtimeConfigPath = join(wslRuntimeHomePath, 'config.toml') + const runtimeConfig = readFileSync(runtimeConfigPath, 'utf-8') + expect(runtimeConfig).toContain( + `model_instructions_file = '${join(systemCodexHomePath, 'instructions.md')}'` + ) + expect(runtimeConfig).toContain('[projects."/home/alice/repo"]') + expect(runtimeConfig).not.toContain('[hooks.state.') + + // Why: WSL runtime configs are seeded once; Codex writes trust into them + // afterwards, so a relaunch must not clobber the seeded file. + writeFileSync(runtimeConfigPath, `${runtimeConfig}\n[projects."/tmp/x"]\n`, 'utf-8') + service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' }) + expect(readFileSync(runtimeConfigPath, 'utf-8')).toContain('[projects."/tmp/x"]') + } finally { + if (originalPlatform) { + Object.defineProperty(process, 'platform', originalPlatform) + } + } + }) + + it('anchors WSL seed rewrites to the Linux-side home parsed from the UNC source', async () => { + const { prepareWslRuntimeSeedConfig } = await import('./runtime-home-service') + + // Why: real UNC sources cannot back live fs operations in tests, so pin + // the UNC -> Linux-side anchor translation on the extracted seed function. + expect( + prepareWslRuntimeSeedConfig( + 'model_instructions_file = "instructions.md"\n', + '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex' + ) + ).toContain("model_instructions_file = '/home/alice/.codex/instructions.md'") + expect( + prepareWslRuntimeSeedConfig( + 'model_instructions_file = "instructions.md"\n', + '\\\\wsl$\\Ubuntu\\home\\alice\\.codex' + ) + ).toContain("model_instructions_file = '/home/alice/.codex/instructions.md'") + }) + it('switches WSL accounts by rewriting one stable WSL runtime home', async () => { const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform') Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) diff --git a/src/main/codex-accounts/runtime-home-service.ts b/src/main/codex-accounts/runtime-home-service.ts index 12d119a8510..4d5d8e4c645 100644 --- a/src/main/codex-accounts/runtime-home-service.ts +++ b/src/main/codex-accounts/runtime-home-service.ts @@ -40,7 +40,10 @@ import { syncSystemCodexResourcesIntoManagedHome } from '../codex/codex-home-paths' import { startSystemCodexSessionBridgeInBackground } from '../codex/codex-session-bridge' -import { syncSystemConfigIntoManagedCodexHome } from '../codex/codex-config-mirror' +import { + prepareSystemConfigForFreshRuntimeMirror, + syncSystemConfigIntoManagedCodexHome +} from '../codex/codex-config-mirror' import { parseWslUncPath } from '../../shared/wsl-paths' import { getWslSelectionKey, @@ -705,7 +708,10 @@ export class CodexRuntimeHomeService { for (const homePath of candidateHomes) { const configPath = join(homePath, 'config.toml') if (existsSync(configPath)) { - copyFileSync(configPath, runtimeConfigPath) + writeFileAtomically( + runtimeConfigPath, + prepareWslRuntimeSeedConfig(readFileSync(configPath, 'utf-8'), homePath) + ) return } } @@ -1562,3 +1568,16 @@ export class CodexRuntimeHomeService { rmSync(this.getSystemDefaultSnapshotPath(), { force: true }) } } + +// Why: the seed config is read over UNC but consumed by Codex inside WSL, so +// relative path-valued settings must anchor to the Linux-side source home; a +// verbatim copy breaks Codex config load (os error 2). +export function prepareWslRuntimeSeedConfig( + configContents: string, + sourceHomePath: string +): string { + return prepareSystemConfigForFreshRuntimeMirror( + configContents, + parseWslUncPath(sourceHomePath)?.linuxPath ?? sourceHomePath + ) +} diff --git a/src/main/codex/codex-config-mirror.test.ts b/src/main/codex/codex-config-mirror.test.ts index ff3ad9ba3bd..416ce9af78b 100644 --- a/src/main/codex/codex-config-mirror.test.ts +++ b/src/main/codex/codex-config-mirror.test.ts @@ -23,7 +23,10 @@ vi.mock('node:os', async () => { } }) -import { syncSystemConfigIntoManagedCodexHome } from './codex-config-mirror' +import { + prepareSystemConfigForFreshRuntimeMirror, + syncSystemConfigIntoManagedCodexHome +} from './codex-config-mirror' let fakeHomeDir: string let userDataDir: string @@ -397,3 +400,32 @@ describe('syncSystemConfigIntoManagedCodexHome', () => { expect(existsSync(getRuntimeConfigPath())).toBe(false) }) }) + +describe('prepareSystemConfigForFreshRuntimeMirror', () => { + it('rewrites relative paths against a Linux-side home and strips hook trust', () => { + const prepared = prepareSystemConfigForFreshRuntimeMirror( + [ + 'model_instructions_file = "instructions.md"', + '', + '[features]', + 'codex_hooks = true', + '', + '[hooks.state."system-hooks:stop:0:0"]', + 'enabled = true', + '', + '[projects."/home/alice/repo"]', + 'trust_level = "trusted"', + '' + ].join('\r\n'), + '/home/alice/.codex' + ) + + // Why: WSL configs are consumed inside the distro, so rewrites must use + // posix join semantics regardless of the host platform. + expect(prepared).toContain("model_instructions_file = '/home/alice/.codex/instructions.md'") + expect(prepared).toContain('hooks = true') + expect(prepared).not.toContain('codex_hooks') + expect(prepared).toContain('[projects."/home/alice/repo"]') + expect(prepared).not.toContain('[hooks.state."system-hooks:stop:0:0"]') + }) +}) diff --git a/src/main/codex/codex-config-mirror.ts b/src/main/codex/codex-config-mirror.ts index ff9f72c55fe..ce9208e9b05 100644 --- a/src/main/codex/codex-config-mirror.ts +++ b/src/main/codex/codex-config-mirror.ts @@ -35,17 +35,19 @@ function syncSystemConfigIntoManagedCodexHomeUnsafe(): void { return } - const systemConfig = prepareSystemConfigForRuntimeMirror( - systemConfigExists ? readFileSync(systemConfigPath, 'utf-8') : '', - dirname(systemConfigPath) - ) + const rawSystemConfig = systemConfigExists ? readFileSync(systemConfigPath, 'utf-8') : '' if (!runtimeConfigExists) { - // Why: trust blocks reference a hooks.json path, so system-home hook trust - // entries are not valid in Orca's runtime CODEX_HOME until install remaps them. - writeFileAtomically(runtimeConfigPath, stripRuntimeOwnedTomlSections(systemConfig)) + writeFileAtomically( + runtimeConfigPath, + prepareSystemConfigForFreshRuntimeMirror(rawSystemConfig, dirname(systemConfigPath)) + ) return } + const systemConfig = prepareSystemConfigForRuntimeMirror( + rawSystemConfig, + dirname(systemConfigPath) + ) const runtimeConfig = readFileSync(runtimeConfigPath, 'utf-8') const mergedConfig = mergeSystemCodexConfigIntoRuntime(runtimeConfig, systemConfig) if (mergedConfig !== runtimeConfig) { @@ -60,6 +62,17 @@ function prepareSystemConfigForRuntimeMirror(config: string, systemConfigDir: st ) } +// Why: trust blocks reference a hooks.json path, so system-home hook trust +// entries are not valid in a fresh runtime CODEX_HOME until install remaps +// them. Also seeds WSL runtime homes, where systemConfigDir must be the +// Linux-side ~/.codex the config resolves against inside the distro. +export function prepareSystemConfigForFreshRuntimeMirror( + config: string, + systemConfigDir: string +): string { + return stripRuntimeOwnedTomlSections(prepareSystemConfigForRuntimeMirror(config, systemConfigDir)) +} + function normalizeDeprecatedCodexHookFeatureFlag(config: string): string { if (!config.includes('codex_hooks')) { return config @@ -71,7 +84,9 @@ function normalizeDeprecatedCodexHookFeatureFlag(config: string): string { for (let index = 0; index <= lines.length; index += 1) { const line = lines[index] - const isHeader = line === undefined || /^[ \t]*\[[^\]]+\][ \t]*(?:#.*)?$/.test(line) + // Why: CRLF configs keep a trailing \r after the split, so header anchors + // must tolerate it or Windows-shaped configs skip normalization entirely. + const isHeader = line === undefined || /^[ \t]*\[[^\]]+\][ \t]*(?:#.*)?\r?$/.test(line) if (!isHeader) { continue } @@ -80,7 +95,7 @@ function normalizeDeprecatedCodexHookFeatureFlag(config: string): string { featureSections.push({ start: featureStart, end: index }) featureStart = null } - if (line !== undefined && /^[ \t]*\[features\][ \t]*(?:#.*)?$/.test(line)) { + if (line !== undefined && /^[ \t]*\[features\][ \t]*(?:#.*)?\r?$/.test(line)) { featureStart = index } }