Preserve Codex runtime config preferences (#3944)

This commit is contained in:
Jinwoo Hong
2026-05-31 16:40:39 -04:00
committed by GitHub
parent 4b904a4e4d
commit ec01ffb986
12 changed files with 2806 additions and 80 deletions
+818 -4
View File
@@ -1,5 +1,16 @@
/* eslint-disable max-lines -- Why: these cases exercise one stateful Codex
config sync contract across first-run, upgrade, corrupt-state, and trust
preservation paths. */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import {
existsSync,
mkdtempSync,
mkdirSync,
readFileSync,
rmSync,
utimesSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import type * as NodeOs from 'node:os'
import { join } from 'node:path'
@@ -41,6 +52,15 @@ function getRuntimeConfigPath(): string {
return join(userDataDir, 'codex-runtime-home', 'home', 'config.toml')
}
function getConfigSyncStatePath(): string {
return join(userDataDir, 'codex-runtime-home', 'config-sync-state.json')
}
function establishSystemConfigBaseline(config: string): void {
writeFileSync(getSystemConfigPath(), config, 'utf-8')
syncSystemConfigIntoManagedCodexHome()
}
beforeEach(() => {
fakeHomeDir = mkdtempSync(join(tmpdir(), 'orca-codex-config-home-'))
userDataDir = mkdtempSync(join(tmpdir(), 'orca-codex-config-user-data-'))
@@ -93,6 +113,45 @@ describe('syncSystemConfigIntoManagedCodexHome', () => {
expect(runtimeConfig).not.toContain('[hooks.state."system-hooks:stop:0:0"]')
})
it('treats whitespace-formatted hook trust headers as runtime-owned', () => {
writeFileSync(
getSystemConfigPath(),
[
'model = "system-model"',
'',
'["hooks" . "state" . "system-hooks:stop:0:0"]',
'enabled = true',
'trusted_hash = "sha256:system"',
''
].join('\n'),
'utf-8'
)
syncSystemConfigIntoManagedCodexHome()
writeFileSync(
getRuntimeConfigPath(),
[
'model = "runtime-model"',
'',
'["hooks" . "state" . "runtime-hooks:stop:0:0"]',
'enabled = false',
'trusted_hash = "sha256:runtime"',
''
].join('\n'),
'utf-8'
)
writeFileSync(getSystemConfigPath(), 'model = "next-system-model"\n', 'utf-8')
syncSystemConfigIntoManagedCodexHome()
const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8')
expect(runtimeConfig).toContain('model = "next-system-model"')
expect(runtimeConfig).toContain('["hooks" . "state" . "runtime-hooks:stop:0:0"]')
expect(runtimeConfig).toContain('trusted_hash = "sha256:runtime"')
expect(runtimeConfig).not.toContain('["hooks" . "state" . "system-hooks:stop:0:0"]')
expect(runtimeConfig).not.toContain('trusted_hash = "sha256:system"')
})
it('normalizes deprecated codex_hooks feature flag only in runtime config', () => {
writeFileSync(
getSystemConfigPath(),
@@ -123,7 +182,7 @@ describe('syncSystemConfigIntoManagedCodexHome', () => {
})
it('mirrors system config updates while preserving runtime-owned trust sections', () => {
mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true })
establishSystemConfigBaseline('model = "initial-system-model"\n')
writeFileSync(
getRuntimeConfigPath(),
[
@@ -175,8 +234,763 @@ describe('syncSystemConfigIntoManagedCodexHome', () => {
expect(runtimeConfig.match(/\[projects\."\/repo"\]/g)?.length).toBe(1)
})
it('does not treat TOML table headers inside multiline strings as sections', () => {
it('keeps runtime Codex preference changes when the system config has not changed', () => {
mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true })
writeFileSync(getSystemConfigPath(), 'model = "system-model"\n', 'utf-8')
syncSystemConfigIntoManagedCodexHome()
writeFileSync(getRuntimeConfigPath(), 'model = "runtime-model"\n', 'utf-8')
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "runtime-model"\n')
})
it('keeps runtime Codex preference changes when the system config is missing', () => {
mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true })
writeFileSync(getRuntimeConfigPath(), 'model = "runtime-model"\n', 'utf-8')
syncSystemConfigIntoManagedCodexHome()
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "runtime-model"\n')
})
it('keeps runtime preferences on first baseline while honoring system project trust', () => {
mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true })
writeFileSync(
getSystemConfigPath(),
[
'model = "system-model"',
'',
'[projects."/repo"] # explicit revocation',
'trust_level = "untrusted"',
'',
'[projects."/system-only"]',
'trust_level = "trusted"',
'',
'[hooks.state."system-hooks:stop:0:0"]',
'enabled = true',
'trusted_hash = "sha256:system"',
''
].join('\n'),
'utf-8'
)
writeFileSync(
getRuntimeConfigPath(),
[
'model = "runtime-model"',
'',
'[projects."/repo"]',
'trust_level = "trusted"',
'metadata = "runtime-owned"',
'',
'[projects."/runtime-only"]',
'trust_level = "trusted"',
'',
'[hooks.state."runtime-hooks:stop:0:0"]',
'enabled = true',
'trusted_hash = "sha256:runtime"',
''
].join('\n'),
'utf-8'
)
utimesSync(
getSystemConfigPath(),
new Date('2024-01-01T00:00:00Z'),
new Date('2024-01-01T00:00:00Z')
)
utimesSync(
getRuntimeConfigPath(),
new Date('2024-01-01T00:01:00Z'),
new Date('2024-01-01T00:01:00Z')
)
syncSystemConfigIntoManagedCodexHome()
const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8')
expect(runtimeConfig).toContain('model = "runtime-model"')
expect(runtimeConfig).not.toContain('model = "system-model"')
expect(runtimeConfig).toContain('[projects."/repo"]')
expect(runtimeConfig).toContain('trust_level = "untrusted"')
expect(runtimeConfig).toContain('metadata = "runtime-owned"')
expect(runtimeConfig).toContain('[projects."/runtime-only"]')
expect(runtimeConfig).toContain('[projects."/system-only"]')
expect(runtimeConfig).toContain('[hooks.state."runtime-hooks:stop:0:0"]')
expect(runtimeConfig).not.toContain('[hooks.state."system-hooks:stop:0:0"]')
expect(runtimeConfig.match(/\[projects\."\/repo"\]/g)?.length).toBe(1)
writeFileSync(getSystemConfigPath(), 'model = "next-system-model"\n', 'utf-8')
syncSystemConfigIntoManagedCodexHome()
const updatedRuntimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8')
expect(updatedRuntimeConfig).toContain('model = "next-system-model"')
expect(updatedRuntimeConfig).not.toContain('model = "runtime-model"')
expect(updatedRuntimeConfig).toContain('[projects."/runtime-only"]')
expect(updatedRuntimeConfig).toContain('[hooks.state."runtime-hooks:stop:0:0"]')
})
it('baselines skipped no-baseline system settings until their contents change', () => {
mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true })
writeFileSync(getSystemConfigPath(), 'model = "system-model"\n', 'utf-8')
writeFileSync(getRuntimeConfigPath(), 'model = "runtime-model"\n', 'utf-8')
utimesSync(
getSystemConfigPath(),
new Date('2024-01-01T00:00:00Z'),
new Date('2024-01-01T00:00:00Z')
)
utimesSync(
getRuntimeConfigPath(),
new Date('2024-01-01T00:01:00Z'),
new Date('2024-01-01T00:01:00Z')
)
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "runtime-model"\n')
expect(readFileSync(getConfigSyncStatePath(), 'utf-8')).toMatch(
/"lastMirrorableSystemConfigDigest": "sha256:[a-f0-9]{64}"/
)
utimesSync(
getSystemConfigPath(),
new Date('2024-01-01T00:02:00Z'),
new Date('2024-01-01T00:02:00Z')
)
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "runtime-model"\n')
writeFileSync(getSystemConfigPath(), 'model = "next-system-model"\n', 'utf-8')
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "next-system-model"\n')
})
it('keeps runtime preferences when only system project trust changes', () => {
establishSystemConfigBaseline('model = "system-model"\n')
writeFileSync(getRuntimeConfigPath(), 'model = "runtime-model"\n', 'utf-8')
writeFileSync(
getSystemConfigPath(),
[
'model = "system-model"',
'',
'[projects."/new-system-project"]',
'trust_level = "trusted"',
'',
'[hooks.state."system-hooks:stop:0:0"]',
'enabled = true',
'trusted_hash = "sha256:system"',
''
].join('\n'),
'utf-8'
)
syncSystemConfigIntoManagedCodexHome()
const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8')
expect(runtimeConfig).toContain('model = "runtime-model"')
expect(runtimeConfig).toContain('[projects."/new-system-project"]')
expect(runtimeConfig).not.toContain('[hooks.state."system-hooks:stop:0:0"]')
})
it('keeps runtime preferences when an unrelated ordinary system section changes', () => {
establishSystemConfigBaseline('model = "system-model"\n')
writeFileSync(getRuntimeConfigPath(), 'model = "runtime-model"\n', 'utf-8')
writeFileSync(
getSystemConfigPath(),
[
'model = "system-model"',
'',
'[mcp_servers.files]',
'command = "node"',
'args = ["server.js"]',
''
].join('\n'),
'utf-8'
)
syncSystemConfigIntoManagedCodexHome()
const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8')
expect(runtimeConfig).toContain('model = "runtime-model"')
expect(runtimeConfig).toContain('[mcp_servers.files]')
expect(runtimeConfig).toContain('command = "node"')
expect(runtimeConfig).not.toContain('model = "system-model"')
})
it('matches equivalent ordinary table headers before merging system sections', () => {
establishSystemConfigBaseline(
['model = "system-model"', '', '[mcp_servers.files]', 'command = "node"', ''].join('\n')
)
writeFileSync(
getRuntimeConfigPath(),
[
'model = "runtime-model"',
'',
'[mcp_servers.files]',
'command = "node"',
'args = ["runtime.js"]',
''
].join('\n'),
'utf-8'
)
writeFileSync(
getSystemConfigPath(),
['model = "system-model"', '', '[mcp_servers . files]', 'command = "node"', ''].join('\n'),
'utf-8'
)
syncSystemConfigIntoManagedCodexHome()
const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8')
expect(runtimeConfig).toContain('model = "runtime-model"')
expect(runtimeConfig).toContain('[mcp_servers.files]')
expect(runtimeConfig).toContain('args = ["runtime.js"]')
expect(runtimeConfig).not.toContain('[mcp_servers . files]')
expect(runtimeConfig.match(/mcp_servers/g)?.length).toBe(1)
})
it('keeps changed top-level settings before TOML table sections', () => {
establishSystemConfigBaseline(
['model = "system-model"', '', '[mcp_servers.files]', 'command = "node"', ''].join('\n')
)
writeFileSync(
getRuntimeConfigPath(),
[
'model = "runtime-model"',
'',
'model_reasoning_effort = "low"',
'',
'[mcp_servers.files]',
'command = "node"',
''
].join('\n'),
'utf-8'
)
writeFileSync(
getSystemConfigPath(),
['model = "next-system-model"', '', '[mcp_servers.files]', 'command = "node"', ''].join('\n'),
'utf-8'
)
syncSystemConfigIntoManagedCodexHome()
const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8')
expect(runtimeConfig).toContain('model = "next-system-model"')
expect(runtimeConfig.indexOf('model = "next-system-model"')).toBeLessThan(
runtimeConfig.indexOf('[mcp_servers.files]')
)
})
it('keeps runtime preferences when the system config is deleted after a baseline', () => {
establishSystemConfigBaseline('model = "system-model"\n')
writeFileSync(getRuntimeConfigPath(), 'model = "runtime-model"\n', 'utf-8')
rmSync(getSystemConfigPath(), { force: true })
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "runtime-model"\n')
})
it('keeps runtime-edited settings when a deleted system config reappears unchanged', () => {
establishSystemConfigBaseline('model = "system-model"\n')
writeFileSync(getRuntimeConfigPath(), 'model = "runtime-model"\n', 'utf-8')
rmSync(getSystemConfigPath(), { force: true })
syncSystemConfigIntoManagedCodexHome()
writeFileSync(getSystemConfigPath(), 'model = "system-model"\n', 'utf-8')
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "runtime-model"\n')
})
it('removes unchanged mirrored settings when the system config is deleted', () => {
establishSystemConfigBaseline(
['model = "system-model"', 'model_reasoning_effort = "high"', ''].join('\n')
)
rmSync(getSystemConfigPath(), { force: true })
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('')
})
it('keeps a migrated legacy baseline when system config is temporarily missing', () => {
const systemConfig = 'model = "system-model"\n'
mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true })
writeFileSync(getRuntimeConfigPath(), 'model = "runtime-model"\n', 'utf-8')
writeFileSync(
getConfigSyncStatePath(),
`${JSON.stringify({ lastSystemConfig: systemConfig }, null, 2)}\n`,
'utf-8'
)
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "runtime-model"\n')
expect(readFileSync(getConfigSyncStatePath(), 'utf-8')).toContain('lastSystemConfigUnitDigests')
writeFileSync(getSystemConfigPath(), systemConfig, 'utf-8')
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "runtime-model"\n')
writeFileSync(getSystemConfigPath(), 'model = "next-system-model"\n', 'utf-8')
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "next-system-model"\n')
})
it('defers digest-only state migration while system config is temporarily missing', () => {
const systemConfig = 'model = "system-model"\n'
establishSystemConfigBaseline(systemConfig)
const state = JSON.parse(readFileSync(getConfigSyncStatePath(), 'utf-8')) as {
lastMirrorableSystemConfigDigest: string
}
writeFileSync(
getConfigSyncStatePath(),
`${JSON.stringify(
{ lastMirrorableSystemConfigDigest: state.lastMirrorableSystemConfigDigest },
null,
2
)}\n`,
'utf-8'
)
writeFileSync(getRuntimeConfigPath(), 'model = "runtime-model"\n', 'utf-8')
rmSync(getSystemConfigPath(), { force: true })
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "runtime-model"\n')
writeFileSync(getSystemConfigPath(), systemConfig, 'utf-8')
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "runtime-model"\n')
expect(readFileSync(getConfigSyncStatePath(), 'utf-8')).toContain('lastSystemConfigUnitDigests')
})
it('applies system changes when digest-only state proves runtime still matches baseline', () => {
const systemConfig = 'model = "system-model"\n'
establishSystemConfigBaseline(systemConfig)
const state = JSON.parse(readFileSync(getConfigSyncStatePath(), 'utf-8')) as {
lastMirrorableSystemConfigDigest: string
}
writeFileSync(
getConfigSyncStatePath(),
`${JSON.stringify(
{ lastMirrorableSystemConfigDigest: state.lastMirrorableSystemConfigDigest },
null,
2
)}\n`,
'utf-8'
)
writeFileSync(getSystemConfigPath(), 'model = "next-system-model"\n', 'utf-8')
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "next-system-model"\n')
expect(readFileSync(getConfigSyncStatePath(), 'utf-8')).toContain('lastSystemConfigUnitDigests')
})
it('baselines digest-only recovery after preserving ambiguous runtime edits', () => {
const systemConfig = 'model = "system-model"\n'
establishSystemConfigBaseline(systemConfig)
const state = JSON.parse(readFileSync(getConfigSyncStatePath(), 'utf-8')) as {
lastMirrorableSystemConfigDigest: string
}
writeFileSync(
getConfigSyncStatePath(),
`${JSON.stringify(
{ lastMirrorableSystemConfigDigest: state.lastMirrorableSystemConfigDigest },
null,
2
)}\n`,
'utf-8'
)
writeFileSync(getRuntimeConfigPath(), 'model = "runtime-model"\n', 'utf-8')
writeFileSync(getSystemConfigPath(), 'model = "current-system-model"\n', 'utf-8')
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "runtime-model"\n')
writeFileSync(getSystemConfigPath(), 'model = "next-system-model"\n', 'utf-8')
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "next-system-model"\n')
})
it('rewrites hybrid legacy sync state without keeping sensitive system config contents', () => {
const systemConfig = 'model = "system-model"\napi_key = "sk-sensitive-value"\n'
mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true })
writeFileSync(getRuntimeConfigPath(), systemConfig, 'utf-8')
writeFileSync(getSystemConfigPath(), systemConfig, 'utf-8')
syncSystemConfigIntoManagedCodexHome()
const stateWithDigest = JSON.parse(readFileSync(getConfigSyncStatePath(), 'utf-8')) as {
lastMirrorableSystemConfigDigest: string
}
writeFileSync(
getConfigSyncStatePath(),
`${JSON.stringify({ ...stateWithDigest, lastSystemConfig: systemConfig }, null, 2)}\n`,
'utf-8'
)
syncSystemConfigIntoManagedCodexHome()
const state = readFileSync(getConfigSyncStatePath(), 'utf-8')
expect(state).toContain('lastMirrorableSystemConfigDigest')
expect(state).toMatch(/sha256:[a-f0-9]{64}/)
expect(state).not.toContain('sk-sensitive-value')
expect(state).not.toContain('api_key')
expect(Object.hasOwn(JSON.parse(state) as Record<string, unknown>, 'lastSystemConfig')).toBe(
false
)
})
it('rewrites hybrid sync state with a non-string legacy key', () => {
const systemConfig = 'model = "system-model"\n'
mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true })
writeFileSync(getRuntimeConfigPath(), systemConfig, 'utf-8')
writeFileSync(getSystemConfigPath(), systemConfig, 'utf-8')
syncSystemConfigIntoManagedCodexHome()
const stateWithDigest = JSON.parse(readFileSync(getConfigSyncStatePath(), 'utf-8')) as {
lastMirrorableSystemConfigDigest: string
}
writeFileSync(
getConfigSyncStatePath(),
`${JSON.stringify({ ...stateWithDigest, lastSystemConfig: { token: 'sk-sensitive-value' } }, null, 2)}\n`,
'utf-8'
)
syncSystemConfigIntoManagedCodexHome()
const parsed = JSON.parse(readFileSync(getConfigSyncStatePath(), 'utf-8')) as Record<
string,
unknown
>
expect(parsed.lastMirrorableSystemConfigDigest).toMatch(/^sha256:[a-f0-9]{64}$/)
expect(Object.hasOwn(parsed, 'lastSystemConfig')).toBe(false)
})
it('keeps runtime preferences when the first sync baseline is missing', () => {
mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true })
writeFileSync(
getRuntimeConfigPath(),
[
'model = "stale-runtime-model"',
'',
'[hooks.state."runtime-hooks:stop:0:0"]',
'enabled = true',
'trusted_hash = "sha256:runtime"',
'',
'[projects."/system-project"]',
'trust_level = "trusted"',
''
].join('\n'),
'utf-8'
)
writeFileSync(
getSystemConfigPath(),
[
'model = "new-system-model"',
'',
'[hooks.state."system-hooks:stop:0:0"]',
'enabled = true',
'trusted_hash = "sha256:system"',
'',
'[projects."/system-project"]',
'trust_level = "untrusted"',
''
].join('\n'),
'utf-8'
)
utimesSync(
getRuntimeConfigPath(),
new Date('2024-01-01T00:00:00Z'),
new Date('2024-01-01T00:00:00Z')
)
utimesSync(
getSystemConfigPath(),
new Date('2024-01-01T00:01:00Z'),
new Date('2024-01-01T00:01:00Z')
)
syncSystemConfigIntoManagedCodexHome()
const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8')
expect(runtimeConfig).toContain('model = "stale-runtime-model"')
expect(runtimeConfig).not.toContain('model = "new-system-model"')
expect(runtimeConfig).toContain('[hooks.state."runtime-hooks:stop:0:0"]')
expect(runtimeConfig).not.toContain('[hooks.state."system-hooks:stop:0:0"]')
expect(runtimeConfig).toContain('[projects."/system-project"]')
expect(runtimeConfig).toContain('trust_level = "untrusted"')
})
it('updates project trust when the system project trust changes from untrusted to trusted', () => {
establishSystemConfigBaseline(
['model = "system-model"', '', '[projects."/repo"]', 'trust_level = "untrusted"', ''].join(
'\n'
)
)
writeFileSync(getSystemConfigPath(), '[projects."/repo"]\ntrust_level = "trusted"\n', 'utf-8')
syncSystemConfigIntoManagedCodexHome()
const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8')
expect(runtimeConfig).toContain('[projects."/repo"]')
expect(runtimeConfig).toContain('trust_level = "trusted"')
expect(runtimeConfig).not.toContain('trust_level = "untrusted"')
})
it('updates project trust without removing runtime-owned project settings', () => {
establishSystemConfigBaseline(['[projects."/repo"]', 'trust_level = "trusted"', ''].join('\n'))
writeFileSync(
getRuntimeConfigPath(),
['[projects."/repo"]', 'trust_level = "trusted"', 'metadata = "runtime-owned"', ''].join(
'\n'
),
'utf-8'
)
writeFileSync(getSystemConfigPath(), '[projects."/repo"]\ntrust_level = "untrusted"\n')
syncSystemConfigIntoManagedCodexHome()
const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8')
expect(runtimeConfig).toContain('[projects."/repo"]')
expect(runtimeConfig).toContain('trust_level = "untrusted"')
expect(runtimeConfig).toContain('metadata = "runtime-owned"')
expect(runtimeConfig).not.toContain('trust_level = "trusted"')
})
it('applies system trusted project state during missing-baseline recovery', () => {
mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true })
writeFileSync(getRuntimeConfigPath(), '[projects."/repo"]\ntrust_level = "untrusted"\n')
writeFileSync(getSystemConfigPath(), '[projects."/repo"]\ntrust_level = "trusted"\n')
syncSystemConfigIntoManagedCodexHome()
const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8')
expect(runtimeConfig).toContain('[projects."/repo"]')
expect(runtimeConfig).toContain('trust_level = "trusted"')
expect(runtimeConfig).not.toContain('trust_level = "untrusted"')
})
it('matches equivalent quoted project headers before applying system untrust', () => {
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('\n'),
'utf-8'
)
syncSystemConfigIntoManagedCodexHome()
const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8')
expect(runtimeConfig).toContain("[projects . 'C:\\Repo']")
expect(runtimeConfig).toContain('trust_level = "untrusted"')
expect(runtimeConfig).not.toContain('[projects."c:/repo"]')
expect(runtimeConfig.match(/trust_level/g)?.length).toBe(1)
})
it('tracks duplicate array-table sections by occurrence', () => {
establishSystemConfigBaseline(
[
'[[hooks.PermissionRequest]]',
'command = "first"',
'',
'[[hooks.PermissionRequest]]',
'command = "second"',
''
].join('\n')
)
writeFileSync(
getSystemConfigPath(),
[
'[[hooks.PermissionRequest]]',
'command = "first"',
'',
'[[hooks.PermissionRequest]]',
'command = "updated-second"',
''
].join('\n'),
'utf-8'
)
syncSystemConfigIntoManagedCodexHome()
const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8')
expect(runtimeConfig).toContain('command = "first"')
expect(runtimeConfig).toContain('command = "updated-second"')
expect(runtimeConfig).not.toContain('command = "second"')
expect(runtimeConfig.match(/\[\[hooks\.PermissionRequest\]\]/g)?.length).toBe(2)
})
it('preserves duplicate array-table order when an earlier occurrence changes', () => {
establishSystemConfigBaseline(
[
'[[hooks.PermissionRequest]]',
'command = "first"',
'',
'[[hooks.PermissionRequest]]',
'command = "second"',
''
].join('\n')
)
writeFileSync(
getSystemConfigPath(),
[
'[[hooks.PermissionRequest]]',
'command = "updated-first"',
'',
'[[hooks.PermissionRequest]]',
'command = "second"',
''
].join('\n'),
'utf-8'
)
syncSystemConfigIntoManagedCodexHome()
const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8')
expect(runtimeConfig).toContain('command = "updated-first"')
expect(runtimeConfig).toContain('command = "second"')
expect(runtimeConfig).not.toContain('command = "first"')
expect(runtimeConfig.indexOf('command = "updated-first"')).toBeLessThan(
runtimeConfig.indexOf('command = "second"')
)
expect(runtimeConfig.match(/\[\[hooks\.PermissionRequest\]\]/g)?.length).toBe(2)
})
it('recovers a corrupt sync state with newer system mtime without clobbering runtime preferences', () => {
writeFileSync(getSystemConfigPath(), 'model = "system-model"\n', 'utf-8')
syncSystemConfigIntoManagedCodexHome()
writeFileSync(getRuntimeConfigPath(), 'model = "runtime-model"\n', 'utf-8')
writeFileSync(getConfigSyncStatePath(), '{not-json', 'utf-8')
utimesSync(
getRuntimeConfigPath(),
new Date('2024-01-01T00:00:00Z'),
new Date('2024-01-01T00:00:00Z')
)
utimesSync(
getSystemConfigPath(),
new Date('2024-01-01T00:01:00Z'),
new Date('2024-01-01T00:01:00Z')
)
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "runtime-model"\n')
expect(readFileSync(getConfigSyncStatePath(), 'utf-8')).toMatch(
/"lastMirrorableSystemConfigDigest": "sha256:[a-f0-9]{64}"/
)
writeFileSync(getSystemConfigPath(), 'model = "next-system-model"\n', 'utf-8')
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "next-system-model"\n')
})
it('recovers an invalid sync-state digest with newer system mtime without clobbering runtime preferences', () => {
writeFileSync(getSystemConfigPath(), 'model = "system-model"\n', 'utf-8')
syncSystemConfigIntoManagedCodexHome()
writeFileSync(getRuntimeConfigPath(), 'model = "runtime-model"\n', 'utf-8')
writeFileSync(
getConfigSyncStatePath(),
`${JSON.stringify({ lastMirrorableSystemConfigDigest: 'not-a-digest' }, null, 2)}\n`,
'utf-8'
)
utimesSync(
getRuntimeConfigPath(),
new Date('2024-01-01T00:00:00Z'),
new Date('2024-01-01T00:00:00Z')
)
utimesSync(
getSystemConfigPath(),
new Date('2024-01-01T00:01:00Z'),
new Date('2024-01-01T00:01:00Z')
)
syncSystemConfigIntoManagedCodexHome()
expect(readFileSync(getRuntimeConfigPath(), 'utf-8')).toBe('model = "runtime-model"\n')
expect(readFileSync(getConfigSyncStatePath(), 'utf-8')).toMatch(
/"lastMirrorableSystemConfigDigest": "sha256:[a-f0-9]{64}"/
)
})
it('does not duplicate sensitive system config contents in sync state', () => {
writeFileSync(
getSystemConfigPath(),
'model = "system-model"\napi_key = "sk-sensitive-value"\n',
'utf-8'
)
syncSystemConfigIntoManagedCodexHome()
const state = readFileSync(getConfigSyncStatePath(), 'utf-8')
expect(state).toContain('lastMirrorableSystemConfigDigest')
expect(state).toMatch(/sha256:[a-f0-9]{64}/)
expect(state).not.toContain('sk-sensitive-value')
expect(state).not.toContain('api_key')
})
it('migrates legacy sync state without keeping sensitive system config contents', () => {
const systemConfig = 'model = "system-model"\napi_key = "sk-sensitive-value"\n'
mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true })
writeFileSync(getRuntimeConfigPath(), systemConfig, 'utf-8')
writeFileSync(getSystemConfigPath(), systemConfig, 'utf-8')
writeFileSync(
getConfigSyncStatePath(),
`${JSON.stringify({ lastSystemConfig: systemConfig }, null, 2)}\n`,
'utf-8'
)
syncSystemConfigIntoManagedCodexHome()
const state = readFileSync(getConfigSyncStatePath(), 'utf-8')
expect(state).toContain('lastMirrorableSystemConfigDigest')
expect(state).toMatch(/sha256:[a-f0-9]{64}/)
expect(state).not.toContain('sk-sensitive-value')
expect(state).not.toContain('api_key')
})
it('normalizes legacy codex_hooks sync state before comparing digests', () => {
const systemConfig = [
'model = "system-model"',
'',
'[features]',
'codex_hooks = true',
''
].join('\n')
mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true })
writeFileSync(
getRuntimeConfigPath(),
['model = "runtime-model"', '', '[features]', 'hooks = true', ''].join('\n'),
'utf-8'
)
writeFileSync(getSystemConfigPath(), systemConfig, 'utf-8')
writeFileSync(
getConfigSyncStatePath(),
`${JSON.stringify({ lastSystemConfig: systemConfig }, null, 2)}\n`,
'utf-8'
)
syncSystemConfigIntoManagedCodexHome()
const runtimeConfig = readFileSync(getRuntimeConfigPath(), 'utf-8')
expect(runtimeConfig).toContain('model = "runtime-model"')
expect(runtimeConfig).not.toContain('model = "system-model"')
expect(runtimeConfig).toContain('[features]\nhooks = true')
})
it('does not treat TOML table headers inside multiline strings as sections', () => {
establishSystemConfigBaseline('model = "initial-system-model"\n')
writeFileSync(
getRuntimeConfigPath(),
[
@@ -216,7 +1030,7 @@ describe('syncSystemConfigIntoManagedCodexHome', () => {
})
it('does not let triple quotes in comments affect runtime-owned section mirroring', () => {
mkdirSync(join(userDataDir, 'codex-runtime-home', 'home'), { recursive: true })
establishSystemConfigBaseline('model = "initial-system-model"\n')
writeFileSync(
getRuntimeConfigPath(),
[
+622 -30
View File
@@ -1,7 +1,18 @@
/* eslint-disable max-lines -- Why: keeping Codex config merge policy beside
the TOML section scanner makes precedence between system config, runtime
preferences, and trust state auditable in one place. */
import { existsSync, readFileSync } from 'fs'
import { join } from 'path'
import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path'
import { writeFileAtomically } from '../codex-accounts/fs-utils'
import { getOrcaManagedCodexHomePath, getSystemCodexHomePath } from './codex-home-paths'
import {
getSystemCodexConfigDigest,
readLastSyncedSystemCodexConfigState,
writeLastSyncedMirrorableSystemCodexConfigDigest,
writeLastSyncedMirrorableSystemCodexConfigDigestOnly,
writeLastSyncedMirrorableSystemCodexConfigDigestValue
} from './codex-config-sync-state'
function getRuntimeCodexConfigTomlPath(): string {
return join(getOrcaManagedCodexHomePath(), 'config.toml')
@@ -31,18 +42,163 @@ function syncSystemConfigIntoManagedCodexHomeUnsafe(): void {
const systemConfig = normalizeDeprecatedCodexHookFeatureFlag(
systemConfigExists ? readFileSync(systemConfigPath, 'utf-8') : ''
)
const systemConfigUnits = getSystemConfigUnits(systemConfig)
const systemConfigUnitDigests = getSystemConfigUnitDigestRecord(systemConfigUnits)
const mirrorableSystemConfig = getMirrorableSystemCodexConfig(systemConfig)
const lastSyncedSystemConfig = readLastSyncedSystemCodexConfigState()
const lastSyncedMirrorableSystemConfig =
lastSyncedSystemConfig.status === 'legacy'
? {
status: 'valid' as const,
digest: getSystemCodexConfigDigest(
getMirrorableSystemCodexConfig(
normalizeDeprecatedCodexHookFeatureFlag(lastSyncedSystemConfig.systemConfig)
)
),
unitDigests: getSystemConfigUnitDigestRecord(
getSystemConfigUnits(
normalizeDeprecatedCodexHookFeatureFlag(lastSyncedSystemConfig.systemConfig)
)
),
needsRewrite: true
}
: lastSyncedSystemConfig
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))
writeLastSyncedMirrorableSystemCodexConfigDigest(
mirrorableSystemConfig,
systemConfigUnitDigests
)
return
}
if (!systemConfigExists) {
if (lastSyncedMirrorableSystemConfig.status !== 'valid') {
return
}
if (lastSyncedMirrorableSystemConfig.unitDigests === null) {
if (lastSyncedMirrorableSystemConfig.needsRewrite) {
writeLastSyncedMirrorableSystemCodexConfigDigestOnly(
lastSyncedMirrorableSystemConfig.digest
)
}
return
}
if (lastSyncedMirrorableSystemConfig.needsRewrite) {
// Why: a migrated legacy state can still prove the last system content;
// scrub legacy raw config without treating temporary absence as deletion.
writeLastSyncedMirrorableSystemCodexConfigDigestValue(
lastSyncedMirrorableSystemConfig.digest,
lastSyncedMirrorableSystemConfig.unitDigests
)
return
}
const runtimeConfig = readFileSync(runtimeConfigPath, 'utf-8')
const nextUnitDigests = getUnitDigestsAfterSystemConfigDeletion(
runtimeConfig,
lastSyncedMirrorableSystemConfig.unitDigests
)
const mergedConfig = mergeChangedSystemConfigUnitsIntoRuntime(
runtimeConfig,
systemConfigUnits,
lastSyncedMirrorableSystemConfig.unitDigests
)
if (mergedConfig !== runtimeConfig) {
writeFileAtomically(runtimeConfigPath, mergedConfig)
}
writeLastSyncedMirrorableSystemCodexConfigDigestValue(
getSystemCodexConfigDigest(mirrorableSystemConfig),
nextUnitDigests
)
return
}
if (lastSyncedMirrorableSystemConfig.status === 'missing') {
// Why: pre-state runtime configs may already contain Codex TUI preference
// changes written inside Orca's managed CODEX_HOME. Without a content
// baseline, preserve those ordinary prefs and only sync trust state.
const runtimeConfig = readFileSync(runtimeConfigPath, 'utf-8')
const mergedConfig = mergeSystemProjectTrustIntoRuntimeBaseline(runtimeConfig, systemConfig)
if (mergedConfig !== runtimeConfig) {
writeFileAtomically(runtimeConfigPath, mergedConfig)
}
writeLastSyncedMirrorableSystemCodexConfigDigest(
mirrorableSystemConfig,
systemConfigUnitDigests
)
return
}
if (lastSyncedMirrorableSystemConfig.status === 'invalid') {
const runtimeConfig = readFileSync(runtimeConfigPath, 'utf-8')
const mergedConfig = mergeSystemProjectTrustIntoRuntimeBaseline(runtimeConfig, systemConfig)
if (mergedConfig !== runtimeConfig) {
writeFileAtomically(runtimeConfigPath, mergedConfig)
}
// Why: corrupt sync state cannot prove the ordinary system settings were
// previously mirrored, so recover the baseline without overwriting TUI prefs.
writeLastSyncedMirrorableSystemCodexConfigDigest(
mirrorableSystemConfig,
systemConfigUnitDigests
)
return
}
if (lastSyncedMirrorableSystemConfig.unitDigests === null) {
const runtimeConfig = readFileSync(runtimeConfigPath, 'utf-8')
const currentMirrorableSystemConfigDigest = getSystemCodexConfigDigest(mirrorableSystemConfig)
const runtimeMirrorableConfigDigest = getSystemCodexConfigDigest(
getMirrorableSystemCodexConfig(runtimeConfig)
)
if (lastSyncedMirrorableSystemConfig.digest === currentMirrorableSystemConfigDigest) {
const mergedConfig = mergeSystemProjectTrustIntoRuntimeBaseline(runtimeConfig, systemConfig)
if (mergedConfig !== runtimeConfig) {
writeFileAtomically(runtimeConfigPath, mergedConfig)
}
writeLastSyncedMirrorableSystemCodexConfigDigest(
mirrorableSystemConfig,
systemConfigUnitDigests
)
return
}
if (lastSyncedMirrorableSystemConfig.digest === runtimeMirrorableConfigDigest) {
const mergedConfig = mergeChangedSystemConfigUnitsIntoRuntime(
runtimeConfig,
systemConfigUnits,
{}
)
if (mergedConfig !== runtimeConfig) {
writeFileAtomically(runtimeConfigPath, mergedConfig)
}
writeLastSyncedMirrorableSystemCodexConfigDigest(
mirrorableSystemConfig,
systemConfigUnitDigests
)
return
}
const mergedConfig = mergeSystemProjectTrustIntoRuntimeBaseline(runtimeConfig, systemConfig)
if (mergedConfig !== runtimeConfig) {
writeFileAtomically(runtimeConfigPath, mergedConfig)
}
writeLastSyncedMirrorableSystemCodexConfigDigest(
mirrorableSystemConfig,
systemConfigUnitDigests
)
return
}
const runtimeConfig = readFileSync(runtimeConfigPath, 'utf-8')
const mergedConfig = mergeSystemCodexConfigIntoRuntime(runtimeConfig, systemConfig)
const mergedConfig = mergeChangedSystemConfigUnitsIntoRuntime(
runtimeConfig,
systemConfigUnits,
lastSyncedMirrorableSystemConfig.unitDigests
)
if (mergedConfig !== runtimeConfig) {
writeFileAtomically(runtimeConfigPath, mergedConfig)
}
writeLastSyncedMirrorableSystemCodexConfigDigest(mirrorableSystemConfig, systemConfigUnitDigests)
}
function normalizeDeprecatedCodexHookFeatureFlag(config: string): string {
@@ -109,33 +265,390 @@ function normalizeFeatureSectionLines(lines: string[], start: number, end: numbe
}
}
function mergeSystemCodexConfigIntoRuntime(runtimeConfig: string, systemConfig: string): string {
type SystemConfigUnit = {
key: string
stateKey: string
digest: string
block: string
kind: 'ordinary' | 'project'
placement: 'top-level' | 'section'
}
function getSystemConfigUnits(config: string): SystemConfigUnit[] {
const ordinaryTopLevelUnits = getTopLevelTomlUnits(config).map((unit) =>
createSystemConfigUnit(`top:${unit.key}`, unit.block, 'ordinary', 'top-level')
)
const sectionIdentityCounts = new Map<string, number>()
const sectionUnits: SystemConfigUnit[] = []
for (const section of getTomlSections(config)) {
if (isRuntimeHookTrustTomlSection(section.header)) {
continue
}
const identityKey = getTomlSectionIdentityKey(section.header)
const occurrence = sectionIdentityCounts.get(identityKey) ?? 0
sectionIdentityCounts.set(identityKey, occurrence + 1)
sectionUnits.push(
createSystemConfigUnit(
`section:${identityKey}:${occurrence}`,
section.block,
isRuntimeProjectTomlSection(section.header) ? 'project' : 'ordinary',
'section'
)
)
}
return [...ordinaryTopLevelUnits, ...sectionUnits]
}
function createSystemConfigUnit(
key: string,
block: string,
kind: SystemConfigUnit['kind'],
placement: SystemConfigUnit['placement']
): SystemConfigUnit {
return {
key,
stateKey: getSystemCodexConfigDigest(key),
digest: getSystemCodexConfigDigest(normalizeTomlUnitForDigest(block)),
block,
kind,
placement
}
}
function getSystemConfigUnitDigestRecord(units: SystemConfigUnit[]): Record<string, string> {
return Object.fromEntries(units.map((unit) => [unit.stateKey, unit.digest]))
}
type TomlTopLevelUnit = {
key: string
block: string
}
function getTopLevelTomlUnits(config: string): TomlTopLevelUnit[] {
const lines = config.split('\n')
const firstSectionIndex = getTomlSections(config)[0]?.start ?? -1
const topLevelLines = firstSectionIndex === -1 ? lines : lines.slice(0, firstSectionIndex)
const units: TomlTopLevelUnit[] = []
let unitStart = -1
let unitKey: string | null = null
let multilineState: TomlMultilineState = { basic: false, literal: false }
for (let index = 0; index < topLevelLines.length; index += 1) {
const line = topLevelLines[index] ?? ''
const assignmentKey = isInsideTomlMultilineString(multilineState)
? null
: getTomlAssignmentKey(line)
if (assignmentKey !== null) {
if (unitStart !== -1 && unitKey !== null) {
units.push({
key: unitKey,
block: topLevelLines.slice(unitStart, index).join('\n')
})
}
unitStart = index
unitKey = assignmentKey
}
multilineState = updateTomlMultilineState(multilineState, line)
}
if (unitStart !== -1 && unitKey !== null) {
units.push({
key: unitKey,
block: topLevelLines.slice(unitStart).join('\n')
})
}
return units
}
function getTomlAssignmentKey(line: string): string | null {
let mode: TomlMultilineMode = null
let index = 0
while (index < line.length) {
if (mode === 'basic') {
if (line[index] === '\\') {
index += 2
continue
}
if (line[index] === '"') {
mode = null
}
index += 1
continue
}
if (mode === 'literal') {
if (line[index] === "'") {
mode = null
}
index += 1
continue
}
const char = line[index]
if (char === '#') {
return null
}
if (char === '=') {
const key = line.slice(0, index).trim()
return key.length > 0 ? key : null
}
if (char === '"') {
mode = 'basic'
} else if (char === "'") {
mode = 'literal'
}
index += 1
}
return null
}
function normalizeTomlUnitForDigest(block: string): string {
let multilineState: TomlMultilineState = { basic: false, literal: false }
const lines: string[] = []
for (const line of block.split('\n')) {
const normalizedLine = isInsideTomlMultilineString(multilineState)
? line.trim()
: normalizeTomlStructuralLineForDigest(line)
if (normalizedLine.length > 0) {
lines.push(normalizedLine)
}
multilineState = updateTomlMultilineState(multilineState, line)
}
return lines.join('\n')
}
function normalizeTomlStructuralLineForDigest(line: string): string {
const header = getTomlTableHeader(line)
const table = header ? parseTomlTableHeaderPath(header) : null
if (table) {
return getCanonicalTomlTableIdentity(table)
}
return stripTomlLineComment(line)
.trim()
.replace(/[ \t]*=[ \t]*/, ' = ')
}
function stripTomlLineComment(line: string): string {
let mode: TomlMultilineMode = null
let index = 0
while (index < line.length) {
if (mode === 'basic') {
if (line[index] === '\\') {
index += 2
continue
}
if (line[index] === '"') {
mode = null
}
index += 1
continue
}
if (mode === 'literal') {
if (line[index] === "'") {
mode = null
}
index += 1
continue
}
const char = line[index]
if (char === '#') {
return line.slice(0, index)
}
if (char === '"') {
mode = 'basic'
} else if (char === "'") {
mode = 'literal'
}
index += 1
}
return line
}
function mergeChangedSystemConfigUnitsIntoRuntime(
runtimeConfig: string,
systemUnits: SystemConfigUnit[],
previousUnitDigests: Record<string, string>
): string {
const runtimeUnits = getSystemConfigUnits(runtimeConfig)
const runtimeStateKeys = new Set(runtimeUnits.map((unit) => unit.stateKey))
const systemStateKeys = new Set(systemUnits.map((unit) => unit.stateKey))
const changedSystemStateKeys = new Set(
systemUnits
.filter(
(unit) =>
previousUnitDigests[unit.stateKey] !== unit.digest ||
(unit.kind === 'project' && !runtimeStateKeys.has(unit.stateKey))
)
.map((unit) => unit.stateKey)
)
const removedSystemStateKeys = new Set(
Object.keys(previousUnitDigests).filter((stateKey) => !systemStateKeys.has(stateKey))
)
const runtimeHookSections = getTomlSections(runtimeConfig)
.filter((section) => isRuntimeHookTrustTomlSection(section.header))
.map((section) => section.block)
const changedSystemUnitByStateKey = new Map(
systemUnits
.filter((unit) => changedSystemStateKeys.has(unit.stateKey))
.map((unit) => [
unit.stateKey,
unit.kind === 'project' ? getProjectUnitWithRuntimeOwnedFields(unit, runtimeUnits) : unit
])
)
const consumedChangedSystemStateKeys = new Set<string>()
const outputUnits: SystemConfigUnit[] = []
for (const runtimeUnit of runtimeUnits) {
const changedSystemUnit = changedSystemUnitByStateKey.get(runtimeUnit.stateKey)
if (changedSystemUnit) {
outputUnits.push(changedSystemUnit)
consumedChangedSystemStateKeys.add(runtimeUnit.stateKey)
continue
}
const previousDigest = previousUnitDigests[runtimeUnit.stateKey]
const shouldRemoveUnchangedSystemUnit =
previousDigest !== undefined &&
removedSystemStateKeys.has(runtimeUnit.stateKey) &&
previousDigest === runtimeUnit.digest
if (!shouldRemoveUnchangedSystemUnit) {
outputUnits.push(runtimeUnit)
}
}
outputUnits.push(
...[...changedSystemUnitByStateKey]
.filter(([stateKey]) => !consumedChangedSystemStateKeys.has(stateKey))
.map(([, unit]) => unit)
)
return joinTomlBlocks([
...outputUnits.filter((unit) => unit.placement === 'top-level').map((unit) => unit.block),
...outputUnits.filter((unit) => unit.placement === 'section').map((unit) => unit.block),
...runtimeHookSections
])
}
function getProjectUnitWithRuntimeOwnedFields(
systemUnit: SystemConfigUnit,
runtimeUnits: SystemConfigUnit[]
): SystemConfigUnit {
const runtimeUnit = runtimeUnits.find((unit) => unit.stateKey === systemUnit.stateKey)
if (!runtimeUnit) {
return systemUnit
}
return {
...systemUnit,
block: mergeProjectTrustAssignmentIntoRuntimeBlock(runtimeUnit.block, systemUnit.block)
}
}
function mergeProjectTrustAssignmentIntoRuntimeBlock(
runtimeBlock: string,
systemBlock: string
): string {
const systemTrustLine = getProjectTrustLine(systemBlock)
if (!systemTrustLine) {
return runtimeBlock
}
const lines = runtimeBlock.split('\n')
const trustLineIndexes = lines
.map((line, index) => (isProjectTrustAssignmentLine(line) ? index : -1))
.filter((index) => index !== -1)
if (trustLineIndexes.length === 0) {
lines.splice(1, 0, systemTrustLine)
return lines.join('\n')
}
lines[trustLineIndexes[0]!] = systemTrustLine
for (const index of trustLineIndexes.slice(1).reverse()) {
lines.splice(index, 1)
}
return lines.join('\n')
}
function getProjectTrustLine(block: string): string | null {
return block.split('\n').find((line) => isProjectTrustAssignmentLine(line)) ?? null
}
function isProjectTrustAssignmentLine(line: string): boolean {
return /^[ \t]*trust_level[ \t]*=/.test(line) && getProjectTrustLevel(`x = 1\n${line}\n`) !== null
}
function getUnitDigestsAfterSystemConfigDeletion(
runtimeConfig: string,
previousUnitDigests: Record<string, string>
): Record<string, string> {
return Object.fromEntries(
getSystemConfigUnits(runtimeConfig)
.map((unit) => [unit.stateKey, previousUnitDigests[unit.stateKey], unit.digest] as const)
.filter(
([, previousDigest, runtimeDigest]) =>
previousDigest !== undefined && previousDigest !== runtimeDigest
)
.map(([stateKey, previousDigest]) => [stateKey, previousDigest])
)
}
function getMirrorableSystemCodexConfig(systemConfig: string): string {
return joinTomlBlocks(
getSystemConfigUnits(systemConfig)
.filter((unit) => unit.kind === 'ordinary')
.map((unit) => unit.block)
)
}
function mergeSystemProjectTrustIntoRuntimeBaseline(
runtimeConfig: string,
systemConfig: string
): string {
const runtimeSections = getTomlSections(runtimeConfig)
const runtimeProjectHeaders = new Set(
runtimeSections
.filter((section) => isRuntimeProjectTomlSection(section.header))
.map((section) => getTomlSectionHeaderKey(section.header))
.map((section) => getTomlSectionIdentityKey(section.header))
)
const systemUntrustedProjectHeaders = new Set(
getTomlSections(systemConfig)
.filter((section) => isRuntimeProjectTomlSection(section.header))
.filter((section) => getProjectTrustLevel(section.block) === 'untrusted')
.map((section) => getTomlSectionHeaderKey(section.header))
const systemProjectSections = getTomlSections(systemConfig).filter((section) =>
isRuntimeProjectTomlSection(section.header)
)
// Why: ordinary Codex settings should mirror ~/.codex exactly; runtime hook
// trust and project trust are written under Orca's managed CODEX_HOME and
// must survive the copy unless the user explicitly revoked project trust in
// the system config.
const systemProjectSectionsByHeader = new Map(
systemProjectSections.map((section) => [getTomlSectionIdentityKey(section.header), section])
)
const systemProjectSectionsToAppend = systemProjectSections.filter(
(section) => !runtimeProjectHeaders.has(getTomlSectionIdentityKey(section.header))
)
const hasExplicitTrustToMerge = systemProjectSections.some(
(section) =>
runtimeProjectHeaders.has(getTomlSectionIdentityKey(section.header)) &&
getProjectTrustLevel(section.block) !== null
)
if (systemProjectSectionsToAppend.length === 0 && !hasExplicitTrustToMerge) {
return runtimeConfig
}
const systemExplicitTrustProjectHeaders = new Set(
systemProjectSections
.filter((section) => getProjectTrustLevel(section.block) !== null)
.map((section) => getTomlSectionIdentityKey(section.header))
)
const lines = runtimeConfig.split('\n')
const firstSectionIndex = runtimeSections[0]?.start ?? -1
const preamble =
firstSectionIndex === -1 ? runtimeConfig : lines.slice(0, firstSectionIndex).join('\n')
// Why: when the baseline is missing, Orca cannot safely decide whether
// ordinary settings changed in system or runtime config. Project trust is
// safety-sensitive, so still honor explicit system revocations.
return joinTomlBlocks([
stripRuntimeOwnedTomlSections(systemConfig, runtimeProjectHeaders),
...runtimeSections
.filter((section) => isRuntimePreservedTomlSection(section.header))
.filter(
(section) =>
!isRuntimeProjectTomlSection(section.header) ||
!systemUntrustedProjectHeaders.has(getTomlSectionHeaderKey(section.header))
)
.map((section) => section.block)
preamble,
...runtimeSections.map((section) => {
const identityKey = getTomlSectionIdentityKey(section.header)
if (
!isRuntimeProjectTomlSection(section.header) ||
!systemExplicitTrustProjectHeaders.has(identityKey)
) {
return section.block
}
const systemSection = systemProjectSectionsByHeader.get(identityKey)
return systemSection
? mergeProjectTrustAssignmentIntoRuntimeBlock(section.block, systemSection.block)
: section.block
}),
...systemProjectSectionsToAppend.map((section) => section.block)
])
}
@@ -167,7 +680,7 @@ function stripRuntimeOwnedTomlSections(
.filter(
(section) =>
!isRuntimeProjectTomlSection(section.header) ||
!runtimeProjectHeaders.has(getTomlSectionHeaderKey(section.header)) ||
!runtimeProjectHeaders.has(getTomlSectionIdentityKey(section.header)) ||
getProjectTrustLevel(section.block) === 'untrusted'
)
.map((section) => section.block)
@@ -212,20 +725,99 @@ function getTomlSections(config: string): TomlSection[] {
return sections
}
function isRuntimePreservedTomlSection(header: string): boolean {
return isRuntimeHookTrustTomlSection(header) || isRuntimeProjectTomlSection(header)
}
function isRuntimeHookTrustTomlSection(header: string): boolean {
return header.trimStart().startsWith('[hooks.state.')
const table = parseTomlTableHeaderPath(header)
return table?.parts[0] === 'hooks' && table.parts[1] === 'state' && table.parts.length > 2
}
function isRuntimeProjectTomlSection(header: string): boolean {
return header.trimStart().startsWith('[projects.')
const table = parseTomlTableHeaderPath(header)
return table?.parts[0] === 'projects' && table.parts.length > 1
}
function getTomlSectionHeaderKey(header: string): string {
return header.trim()
function getTomlSectionIdentityKey(header: string): string {
const table = parseTomlTableHeaderPath(header)
if (!table) {
return header.trim()
}
return getCanonicalTomlTableIdentity(table)
}
function getCanonicalTomlTableIdentity(table: { array: boolean; parts: string[] }): string {
if (table.parts[0] === 'projects' && table.parts.length > 1) {
return `project:${normalizeRuntimePathForComparison(table.parts.slice(1).join('.'))}`
}
return JSON.stringify({ array: table.array, parts: table.parts })
}
function parseTomlTableHeaderPath(header: string): { array: boolean; parts: string[] } | null {
const trimmed = header.trim()
const arrayMatch = /^\[\[\s*(.*?)\s*\]\]$/.exec(trimmed)
const tableMatch = /^\[\s*(.*?)\s*\]$/.exec(trimmed)
const keyPath = arrayMatch?.[1] ?? tableMatch?.[1]
if (keyPath === undefined) {
return null
}
const parts = splitTomlDottedKeyPath(keyPath)
.map((part) => parseTomlHeaderKeyPart(part.trim()))
.filter((part): part is string => part !== null)
return parts.length > 0 ? { array: arrayMatch !== null, parts } : null
}
function splitTomlDottedKeyPath(keyPath: string): string[] {
const parts: string[] = []
let mode: TomlMultilineMode = null
let partStart = 0
let index = 0
while (index < keyPath.length) {
if (mode === 'basic') {
if (keyPath[index] === '\\') {
index += 2
continue
}
if (keyPath[index] === '"') {
mode = null
}
index += 1
continue
}
if (mode === 'literal') {
if (keyPath[index] === "'") {
mode = null
}
index += 1
continue
}
if (keyPath[index] === '"') {
mode = 'basic'
} else if (keyPath[index] === "'") {
mode = 'literal'
} else if (keyPath[index] === '.') {
parts.push(keyPath.slice(partStart, index))
partStart = index + 1
}
index += 1
}
parts.push(keyPath.slice(partStart))
return parts
}
function parseTomlHeaderKeyPart(keyPart: string): string | null {
if (keyPart.startsWith('"') && keyPart.endsWith('"')) {
return parseTomlBasicStringValue(keyPart)
}
if (keyPart.startsWith("'") && keyPart.endsWith("'")) {
return keyPart.slice(1, -1)
}
return keyPart.length > 0 ? keyPart : null
}
function parseTomlBasicStringValue(value: string): string | null {
try {
return JSON.parse(value) as string
} catch {
return null
}
}
function getProjectTrustLevel(block: string): 'trusted' | 'untrusted' | null {
+151
View File
@@ -0,0 +1,151 @@
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { writeFileAtomically } from '../codex-accounts/fs-utils'
import { getOrcaManagedCodexHomePath } from './codex-home-paths'
type CodexConfigSyncState = {
lastMirrorableSystemConfigDigest: string
lastSystemConfigUnitDigests: Record<string, string>
}
type CodexConfigSyncStateRead =
| {
status: 'valid'
digest: string
unitDigests: Record<string, string> | null
needsRewrite: boolean
}
| {
status: 'legacy'
systemConfig: string
}
| {
status: 'missing'
}
| {
status: 'invalid'
}
const SYSTEM_CONFIG_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/
function getCodexConfigSyncStatePath(): string {
return join(dirname(getOrcaManagedCodexHomePath()), 'config-sync-state.json')
}
export function getSystemCodexConfigDigest(systemConfig: string): string {
return `sha256:${createHash('sha256').update(systemConfig).digest('hex')}`
}
export function readLastSyncedSystemCodexConfigState(): CodexConfigSyncStateRead {
try {
const parsed = JSON.parse(readFileSync(getCodexConfigSyncStatePath(), 'utf-8')) as unknown
const isStateObject = parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
const hasLegacySystemConfig = isStateObject && Object.hasOwn(parsed, 'lastSystemConfig')
const lastSystemConfig =
isStateObject &&
typeof (parsed as { lastSystemConfig?: unknown }).lastSystemConfig === 'string'
? (parsed as { lastSystemConfig: string }).lastSystemConfig
: null
const lastMirrorableSystemConfigDigest =
isStateObject &&
typeof (parsed as { lastMirrorableSystemConfigDigest?: unknown })
.lastMirrorableSystemConfigDigest === 'string'
? (parsed as CodexConfigSyncState).lastMirrorableSystemConfigDigest
: null
const legacySystemConfigDigest =
isStateObject &&
typeof (parsed as { lastSystemConfigDigest?: unknown }).lastSystemConfigDigest === 'string'
? (parsed as { lastSystemConfigDigest: string }).lastSystemConfigDigest
: null
const effectiveSystemConfigDigest = lastMirrorableSystemConfigDigest ?? legacySystemConfigDigest
const lastSystemConfigUnitDigests =
isStateObject &&
isValidDigestRecord(
(parsed as { lastSystemConfigUnitDigests?: unknown }).lastSystemConfigUnitDigests
)
? ((parsed as CodexConfigSyncState).lastSystemConfigUnitDigests ?? null)
: null
if (
effectiveSystemConfigDigest !== null &&
SYSTEM_CONFIG_DIGEST_PATTERN.test(effectiveSystemConfigDigest)
) {
return {
status: 'valid',
digest: effectiveSystemConfigDigest,
unitDigests: lastSystemConfigUnitDigests,
needsRewrite:
hasLegacySystemConfig ||
lastSystemConfigUnitDigests === null ||
legacySystemConfigDigest !== null
}
}
if (lastSystemConfig !== null) {
return {
status: 'legacy',
systemConfig: lastSystemConfig
}
}
} catch (error) {
return (error as NodeJS.ErrnoException).code === 'ENOENT'
? { status: 'missing' }
: { status: 'invalid' }
}
return { status: 'invalid' }
}
function isValidDigestRecord(value: unknown): value is Record<string, string> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return false
}
return Object.entries(value).every(
([key, digest]) =>
SYSTEM_CONFIG_DIGEST_PATTERN.test(key) &&
typeof digest === 'string' &&
SYSTEM_CONFIG_DIGEST_PATTERN.test(digest)
)
}
export function writeLastSyncedMirrorableSystemCodexConfigDigest(
mirrorableSystemConfig: string,
unitDigests: Record<string, string>
): void {
writeLastSyncedMirrorableSystemCodexConfigDigestValue(
getSystemCodexConfigDigest(mirrorableSystemConfig),
unitDigests
)
}
export function writeLastSyncedMirrorableSystemCodexConfigDigestValue(
digest: string,
unitDigests: Record<string, string>
): void {
if (!SYSTEM_CONFIG_DIGEST_PATTERN.test(digest)) {
throw new Error('Invalid Codex config digest')
}
if (!isValidDigestRecord(unitDigests)) {
throw new Error('Invalid Codex config unit digests')
}
writeConfigSyncState({
lastMirrorableSystemConfigDigest: digest,
lastSystemConfigUnitDigests: unitDigests
})
}
export function writeLastSyncedMirrorableSystemCodexConfigDigestOnly(digest: string): void {
if (!SYSTEM_CONFIG_DIGEST_PATTERN.test(digest)) {
throw new Error('Invalid Codex config digest')
}
writeConfigSyncState({ lastMirrorableSystemConfigDigest: digest })
}
function writeConfigSyncState(state: {
lastMirrorableSystemConfigDigest: string
lastSystemConfigUnitDigests?: Record<string, string>
}): void {
// Why: config.toml can contain provider credentials; a digest is enough to
// detect user edits without persisting a second copy of the config.
writeFileAtomically(getCodexConfigSyncStatePath(), `${JSON.stringify(state, null, 2)}\n`, {
mode: 0o600
})
}
+477
View File
@@ -0,0 +1,477 @@
/* eslint-disable max-lines -- Why: launch-home materialization needs path
safety, link/copy fallback, reconciliation, and cleanup in one place so
auth-only account isolation cannot drift across platforms. */
import {
cpSync,
existsSync,
lstatSync,
mkdirSync,
readFileSync,
readlinkSync,
readdirSync,
rmdirSync,
rmSync,
statSync,
symlinkSync,
unlinkSync,
writeFileSync
} from 'node:fs'
import { createHash } from 'node:crypto'
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
import { getOrcaManagedCodexHomePath } from './codex-home-paths'
const LAUNCH_HOME_MARKER = '.orca-managed-launch-home'
const LAUNCH_HOME_LINK_MARKERS_DIR = '.orca-launch-home-links'
const LAUNCH_HOME_MARKER_VERSION = 1
const SHARED_LAUNCH_ENTRY_NAMES = new Set([
'config.toml',
'hooks.json',
'history.jsonl',
'sessions',
'skills',
'plugins',
'plugin-state',
'profile-v2',
'themes',
'prompts'
])
const MUTABLE_SHARED_FILE_ENTRIES = new Set([
'config.toml',
'hooks.json',
'history.jsonl',
'profile-v2'
])
const MUTABLE_SHARED_DIRECTORY_ENTRIES = new Set(['sessions', 'plugin-state', 'profile-v2'])
type LaunchEntryMarker = {
version: number
sourcePath: string
mode: 'link' | 'copy'
targetDigest: string | null
sourceDigest: string | null
}
export function getOrcaCodexLaunchHomePath(accountId: string | null): string {
const launchHomePath = resolveOrcaCodexLaunchHomePath(accountId, { create: true })
mkdirSync(launchHomePath, { recursive: true })
return launchHomePath
}
export function ensureOrcaCodexLaunchHome(accountId: string | null): string {
const launchHomePath = getOrcaCodexLaunchHomePath(accountId)
writeLaunchHomeMarker(launchHomePath, accountId)
return launchHomePath
}
export function materializeOrcaCodexLaunchHome(accountId: string | null): string {
reconcileMutableLaunchHomeFilesIntoSharedHome()
const sharedHomePath = getOrcaManagedCodexHomePath()
const launchHomePath = getOrcaCodexLaunchHomePath(accountId)
writeLaunchHomeMarker(launchHomePath, accountId)
const sharedEntries = new Set<string>()
for (const entryName of listSharedLaunchEntryNames(sharedHomePath)) {
sharedEntries.add(entryName)
linkSharedEntryIntoLaunchHome(sharedHomePath, launchHomePath, entryName)
}
removeStaleLaunchHomeEntries(launchHomePath, sharedHomePath, sharedEntries)
return launchHomePath
}
export function removeOrcaCodexLaunchHome(accountId: string): void {
const launchHomePath = resolveOrcaCodexLaunchHomePath(accountId, { create: false })
if (!existsSync(launchHomePath)) {
return
}
const launchHomeStat = lstatSync(launchHomePath)
if (!launchHomeStat.isDirectory() || launchHomeStat.isSymbolicLink()) {
console.warn('[codex-home] Refusing to remove unexpected launch-home root:', launchHomePath)
return
}
if (!isMarkedLaunchHomeForAccount(launchHomePath, accountId)) {
// Why: older builds could write auth before the launch-home marker existed.
// Remove only the deterministic credential file, not an unmarked directory.
rmSync(join(launchHomePath, 'auth.json'), { force: true })
return
}
if (!isContainedPath(getOrcaCodexLaunchHostRootPath(), launchHomePath)) {
console.warn('[codex-home] Refusing to remove launch home outside host root:', launchHomePath)
return
}
rmSync(launchHomePath, { recursive: true, force: true })
}
function getOrcaCodexLaunchHostRootPath(): string {
return getOrcaCodexLaunchHostRootPathWithOptions({ create: true })
}
function getOrcaCodexLaunchHostRootPathWithOptions(options: { create: boolean }): string {
const rootPath = join(dirname(getOrcaManagedCodexHomePath()), 'launch', 'host')
if (options.create) {
mkdirSync(rootPath, { recursive: true })
}
return rootPath
}
function resolveOrcaCodexLaunchHomePath(
accountId: string | null,
options: { create: boolean }
): string {
return join(
getOrcaCodexLaunchHostRootPathWithOptions(options),
getLaunchSelectionSegment(accountId),
'home'
)
}
function getLaunchSelectionSegment(accountId: string | null): string {
if (accountId === null) {
return 'system'
}
return `account-${createHash('sha256').update(accountId).digest('hex').slice(0, 32)}`
}
function listSharedLaunchEntryNames(sharedHomePath: string): string[] {
try {
return readdirSync(sharedHomePath)
.filter((entryName) => SHARED_LAUNCH_ENTRY_NAMES.has(entryName))
.sort()
} catch {
return []
}
}
function linkSharedEntryIntoLaunchHome(
sharedHomePath: string,
launchHomePath: string,
entryName: string
): void {
const sourcePath = join(sharedHomePath, entryName)
const targetPath = join(launchHomePath, entryName)
const existingMarker = readLaunchEntryMarker(launchHomePath, entryName)
reconcileMutableLaunchEntryIfNeeded(sourcePath, targetPath, existingMarker)
if (!existsSync(sourcePath)) {
removeLaunchEntryIfOwned(targetPath, launchHomePath, entryName, sourcePath)
return
}
if (targetAlreadyPointsToSource(targetPath, sourcePath)) {
markLaunchEntry(launchHomePath, entryName, sourcePath, 'link')
return
}
const ownedTarget =
existingMarker?.sourcePath === sourcePath && targetExistsForLaunchRemoval(targetPath)
if (targetExistsForLaunchRemoval(targetPath) && !ownedTarget) {
return
}
if (ownedTarget) {
removeLaunchEntry(targetPath)
}
try {
const sourceStat = lstatSync(sourcePath)
symlinkSync(
sourcePath,
targetPath,
sourceStat.isDirectory() && process.platform === 'win32' ? 'junction' : undefined
)
markLaunchEntry(launchHomePath, entryName, sourcePath, 'link')
} catch (error) {
if (!copyFallbackAllowed(sourcePath, entryName)) {
console.warn('[codex-home] Failed to link shared Codex launch entry:', entryName, error)
return
}
try {
removeLaunchEntry(targetPath)
cpSync(sourcePath, targetPath, {
recursive: true,
force: false,
errorOnExist: true,
dereference: true
})
markLaunchEntry(launchHomePath, entryName, sourcePath, 'copy')
} catch {
console.warn('[codex-home] Failed to copy shared Codex launch entry:', entryName, error)
}
}
}
function copyFallbackAllowed(sourcePath: string, entryName: string): boolean {
if (entryName === 'hooks.json') {
return false
}
const sourceStat = lstatSync(sourcePath)
return !sourceStat.isDirectory() || !MUTABLE_SHARED_DIRECTORY_ENTRIES.has(entryName)
}
function isContainedPath(rootPath: string, candidatePath: string): boolean {
const relativePath = relative(resolve(rootPath), resolve(candidatePath))
return (
Boolean(relativePath) &&
relativePath !== '..' &&
!isAbsolute(relativePath) &&
!relativePath.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)
)
}
function reconcileMutableLaunchHomeFilesIntoSharedHome(): void {
const hostRootPath = getOrcaCodexLaunchHostRootPath()
let selectionEntries: string[]
try {
selectionEntries = readdirSync(hostRootPath)
} catch {
return
}
for (const selectionEntry of selectionEntries.sort()) {
const launchHomePath = join(hostRootPath, selectionEntry, 'home')
if (!existsSync(join(launchHomePath, LAUNCH_HOME_MARKER))) {
continue
}
reconcileMarkedMutableFiles(launchHomePath)
}
}
function reconcileMarkedMutableFiles(launchHomePath: string): void {
const markerDir = join(launchHomePath, LAUNCH_HOME_LINK_MARKERS_DIR)
let markerFiles: string[]
try {
markerFiles = readdirSync(markerDir)
} catch {
return
}
for (const markerFile of markerFiles.sort()) {
const entryName = markerFile.replace(/\.json$/, '')
const marker = readLaunchEntryMarker(launchHomePath, entryName)
if (!marker || !MUTABLE_SHARED_FILE_ENTRIES.has(entryName)) {
continue
}
reconcileMutableLaunchEntryIfNeeded(marker.sourcePath, join(launchHomePath, entryName), marker)
}
}
function reconcileMutableLaunchEntryIfNeeded(
sourcePath: string,
targetPath: string,
marker: LaunchEntryMarker | null
): void {
if (!marker || !MUTABLE_SHARED_FILE_ENTRIES.has(targetPath.split(/[\\/]/).at(-1) ?? '')) {
return
}
if (!targetExistsForLaunchRemoval(targetPath)) {
return
}
try {
if (lstatSync(targetPath).isSymbolicLink() || !statSync(targetPath).isFile()) {
return
}
const targetDigest = digestFile(targetPath)
if (targetDigest === marker.targetDigest) {
return
}
const sourceDigest = existsSync(sourcePath) ? digestFile(sourcePath) : null
if (
sourceDigest !== null &&
marker.sourceDigest !== null &&
sourceDigest !== marker.sourceDigest &&
statSync(sourcePath).mtimeMs > statSync(targetPath).mtimeMs
) {
return
}
mkdirSync(dirname(sourcePath), { recursive: true })
cpSync(targetPath, sourcePath, { force: true })
} catch (error) {
console.warn('[codex-home] Failed to reconcile launch-home Codex entry:', targetPath, error)
}
}
function removeStaleLaunchHomeEntries(
launchHomePath: string,
sharedHomePath: string,
sharedEntries: Set<string>
): void {
const markerDir = join(launchHomePath, LAUNCH_HOME_LINK_MARKERS_DIR)
let markerFiles: string[]
try {
markerFiles = readdirSync(markerDir)
} catch {
return
}
for (const markerFile of markerFiles) {
const entryName = markerFile.replace(/\.json$/, '')
if (!sharedEntries.has(entryName)) {
removeLaunchEntryIfOwned(
join(launchHomePath, entryName),
launchHomePath,
entryName,
join(sharedHomePath, entryName)
)
}
}
}
function removeLaunchEntryIfOwned(
targetPath: string,
launchHomePath: string,
entryName: string,
sourcePath: string
): void {
const marker = readLaunchEntryMarker(launchHomePath, entryName)
if (marker?.sourcePath !== sourcePath) {
return
}
removeLaunchEntry(targetPath)
rmSync(getLaunchEntryMarkerPath(launchHomePath, entryName), { force: true })
}
function removeLaunchEntry(targetPath: string): void {
if (!targetExistsForLaunchRemoval(targetPath)) {
return
}
try {
const stat = lstatSync(targetPath)
if (stat.isSymbolicLink()) {
try {
unlinkSync(targetPath)
} catch (error) {
if (process.platform !== 'win32') {
throw error
}
rmdirSync(targetPath)
}
return
}
rmSync(targetPath, { recursive: stat.isDirectory(), force: true })
} catch (error) {
console.warn('[codex-home] Failed to remove owned launch-home entry:', targetPath, error)
}
}
function targetExistsForLaunchRemoval(targetPath: string): boolean {
try {
lstatSync(targetPath)
return true
} catch {
return false
}
}
function writeLaunchHomeMarker(launchHomePath: string, accountId: string | null): void {
writeFileSync(
join(launchHomePath, LAUNCH_HOME_MARKER),
`${JSON.stringify({ version: LAUNCH_HOME_MARKER_VERSION, accountId }, null, 2)}\n`,
{ encoding: 'utf-8', mode: 0o600 }
)
}
function isMarkedLaunchHomeForAccount(launchHomePath: string, accountId: string): boolean {
try {
const parsed: unknown = JSON.parse(
readFileSync(join(launchHomePath, LAUNCH_HOME_MARKER), 'utf-8')
)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return false
}
const marker = parsed as { version?: unknown; accountId?: unknown }
return marker.version === LAUNCH_HOME_MARKER_VERSION && marker.accountId === accountId
} catch {
return false
}
}
function markLaunchEntry(
launchHomePath: string,
entryName: string,
sourcePath: string,
mode: 'link' | 'copy'
): void {
const markerPath = getLaunchEntryMarkerPath(launchHomePath, entryName)
mkdirSync(dirname(markerPath), { recursive: true })
writeFileSync(
markerPath,
`${JSON.stringify(
{
version: LAUNCH_HOME_MARKER_VERSION,
sourcePath,
mode,
sourceDigest: digestPathIfFile(sourcePath),
targetDigest: digestPathIfFile(join(launchHomePath, entryName))
} satisfies LaunchEntryMarker,
null,
2
)}\n`,
{ encoding: 'utf-8', mode: 0o600 }
)
}
function readLaunchEntryMarker(
launchHomePath: string,
entryName: string
): LaunchEntryMarker | null {
try {
const parsed: unknown = JSON.parse(
readFileSync(getLaunchEntryMarkerPath(launchHomePath, entryName), 'utf-8')
)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return null
}
const marker = parsed as Partial<LaunchEntryMarker>
if (
marker.version !== LAUNCH_HOME_MARKER_VERSION ||
typeof marker.sourcePath !== 'string' ||
(marker.mode !== 'link' && marker.mode !== 'copy')
) {
return null
}
return {
version: marker.version,
sourcePath: marker.sourcePath,
mode: marker.mode,
sourceDigest: typeof marker.sourceDigest === 'string' ? marker.sourceDigest : null,
targetDigest: typeof marker.targetDigest === 'string' ? marker.targetDigest : null
}
} catch {
return null
}
}
function targetAlreadyPointsToSource(targetPath: string, sourcePath: string): boolean {
try {
return (
lstatSync(targetPath).isSymbolicLink() &&
linkTargetsMatch(readlinkSync(targetPath), sourcePath)
)
} catch {
return false
}
}
function linkTargetsMatch(actualTarget: string, expectedTarget: string): boolean {
if (process.platform !== 'win32') {
return actualTarget === expectedTarget
}
return normalizeWindowsLinkTarget(actualTarget) === normalizeWindowsLinkTarget(expectedTarget)
}
function normalizeWindowsLinkTarget(linkTarget: string): string {
return linkTarget.replace(/^\\\\\?\\/, '').toLowerCase()
}
function getLaunchEntryMarkerPath(launchHomePath: string, entryName: string): string {
return join(launchHomePath, LAUNCH_HOME_LINK_MARKERS_DIR, `${entryName}.json`)
}
function digestPathIfFile(targetPath: string): string | null {
try {
if (!statSync(targetPath).isFile()) {
return null
}
return digestFile(targetPath)
} catch {
return null
}
}
function digestFile(targetPath: string): string {
return createHash('sha256').update(readFileSync(targetPath)).digest('hex')
}
+5 -5
View File
@@ -995,7 +995,7 @@ describe('CodexHookService', () => {
expect(runtimeToml).not.toContain(':stop:0:0')
})
it('mirrors system Codex config while preserving runtime hook trust on hook install', () => {
it('preserves runtime Codex prefs and hook trust on hook install without a sync baseline', () => {
const systemCodexHome = join(tmpHome, '.codex')
mkdirSync(systemCodexHome, { recursive: true })
writeFileSync(join(systemCodexHome, 'config.toml'), 'model = "system-model"\n', 'utf-8')
@@ -1019,12 +1019,12 @@ describe('CodexHookService', () => {
expect(status.state).toBe('installed')
const trustConfig = readFileSync(join(managedCodexHome, 'config.toml'), 'utf-8')
expect(trustConfig).toContain('model = "system-model"')
expect(trustConfig).toContain('model = "runtime-model"')
expect(trustConfig).toContain('[hooks.state."runtime-hook"]')
expect(trustConfig).toContain('enabled = false')
expect(trustConfig).toContain('trusted_hash = "sha256:runtime"')
expect(trustConfig).toContain(':permission_request:0:0')
expect(trustConfig).not.toContain('model = "runtime-model"')
expect(trustConfig).not.toContain('model = "system-model"')
})
it('repairs duplicate managed SessionStart trust tables on restart install', () => {
@@ -1112,9 +1112,9 @@ describe('CodexHookService', () => {
expect(status.state).toBe('installed')
const trustConfig = readFileSync(join(managedCodexHome, 'config.toml'), 'utf-8')
expect(trustConfig).toContain('model = "system-model"')
expect(trustConfig).toContain('model = "runtime-model"')
expect(trustConfig).toContain('[projects."/repo"]\ntrust_level = "untrusted"')
expect(trustConfig).toContain('[projects."/runtime-only"]\ntrust_level = "trusted"')
expect(trustConfig).not.toContain('model = "runtime-model"')
expect(trustConfig).not.toContain('model = "system-model"')
})
})