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 <help@stably.ai>

* 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 <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-07-04 02:54:20 -07:00
committed by GitHub
co-authored by Orca
parent 533cafdfa9
commit b5359ca7e3
4 changed files with 158 additions and 12 deletions
@@ -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' })
@@ -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
)
}
+33 -1
View File
@@ -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"]')
})
})
+24 -9
View File
@@ -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
}
}