diff --git a/src/main/codex/codex-config-mirror.test.ts b/src/main/codex/codex-config-mirror.test.ts index 325faf70e15..1ef1f61dd0b 100644 --- a/src/main/codex/codex-config-mirror.test.ts +++ b/src/main/codex/codex-config-mirror.test.ts @@ -334,6 +334,72 @@ describe('syncSystemConfigIntoManagedCodexHome', () => { expect(runtimeConfig).toContain("[projects.'c:\\gemini_etl']") }) + it('deduplicates a CRLF system project header against an LF runtime header', () => { + mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true }) + const projectHeader = '[projects."C:/Users/jinwo/orca/workspaces/orca/repo"]' + writeFileSync( + getRuntimeConfigPath(), + [projectHeader, 'trust_level = "trusted"', ''].join('\n'), + 'utf-8' + ) + writeFileSync( + getSystemConfigPath(), + [projectHeader, 'trust_level = "trusted"', ''].join('\r\n'), + 'utf-8' + ) + + syncSystemConfigIntoManagedCodexHome() + + const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8') + expect(runtimeConfig.match(/\[projects\./g)).toHaveLength(1) + expect(runtimeConfig).toContain(`${projectHeader}\ntrust_level = "trusted"`) + }) + + it('self-heals duplicate project tables in a CRLF runtime config', () => { + mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true }) + const projectHeader = '[projects."C:/Users/jinwo/orca/workspaces/orca/repo"]' + writeFileSync( + getRuntimeConfigPath(), + [ + projectHeader, + 'trust_level = "trusted"', + '', + projectHeader, + 'trust_level = "trusted"', + '' + ].join('\r\n'), + 'utf-8' + ) + writeFileSync(getSystemConfigPath(), 'model = "gpt-5"\n', 'utf-8') + + syncSystemConfigIntoManagedCodexHome() + + const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8') + expect(runtimeConfig.match(/\[projects\./g)).toHaveLength(1) + expect(runtimeConfig).toContain('trust_level = "trusted"') + }) + + it('applies a CRLF system revocation to an LF runtime project', () => { + mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true }) + writeFileSync( + getRuntimeConfigPath(), + ["[projects.'c:\\repo']", 'trust_level = "trusted"', ''].join('\n'), + 'utf-8' + ) + writeFileSync( + getSystemConfigPath(), + ['[projects."C:/repo"]', 'trust_level = "untrusted"', ''].join('\r\n'), + 'utf-8' + ) + + syncSystemConfigIntoManagedCodexHome() + + const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8') + expect(runtimeConfig.match(/\[projects\./g)).toHaveLength(1) + expect(runtimeConfig).toContain('trust_level = "untrusted"') + expect(runtimeConfig).not.toContain('trust_level = "trusted"') + }) + it('lets a semantically matching system revocation replace runtime trust', () => { mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true }) writeFileSync( diff --git a/src/main/codex/config-toml-trust.ts b/src/main/codex/config-toml-trust.ts index d3b827bd26b..02cd0372837 100644 --- a/src/main/codex/config-toml-trust.ts +++ b/src/main/codex/config-toml-trust.ts @@ -591,7 +591,9 @@ function parseHookStateHeaderKey(line: string): string | null { } export function parseCodexProjectHeaderPath(line: string): string | null { - const trimmed = line.trimStart() + // Why: mirror section headers come from split CRLF files and retain the + // terminal carriage return, while direct upserts scan CR-stripped lines. + const trimmed = line.replace(/\r$/, '').trimStart() const prefixMatch = /^\[[ \t]*projects[ \t]*\.[ \t]*/.exec(trimmed) if (!prefixMatch) { return null