From b5359ca7e381ae15b0d08a030d3ea465da2ac043 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 4 Jul 2026 02:54:20 -0700 Subject: [PATCH] Seed WSL Codex runtime config through the fresh-mirror preparation (#7343) * Seed WSL Codex runtime config through the fresh-mirror preparation The WSL runtime home seed copied config.toml verbatim, so relative path-valued settings (model_instructions_file etc.) resolved against the runtime home inside the distro and aborted Codex config load with 'os error 2' -- the same failure class #7157 fixed for host runtime and managed account homes. Seed now applies the shared fresh-mirror preparation (deprecated codex_hooks normalization + relative-path rewrite anchored to the Linux-side source home + system hook-trust strip). Also fixes CRLF configs skipping deprecated codex_hooks normalization: the feature-section header regexes did not tolerate the trailing \r. Co-authored-by: Orca * Extract prepareWslRuntimeSeedConfig and pin UNC->Linux anchor in tests The e2e seed test's mocked WSL home is a plain local path, so the parseWslUncPath linuxPath branch was never exercised. Extract the seed preparation as a pure exported function and cover both UNC spellings (wsl.localhost and wsl$) with hardcoded literals. Co-authored-by: Orca --------- Co-authored-by: Orca --- .../runtime-home-service.test.ts | 80 +++++++++++++++++++ .../codex-accounts/runtime-home-service.ts | 23 +++++- src/main/codex/codex-config-mirror.test.ts | 34 +++++++- src/main/codex/codex-config-mirror.ts | 33 +++++--- 4 files changed, 158 insertions(+), 12 deletions(-) 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 } }