Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
31e974d
chore(deps): add smol-toml as a direct dependency for checked Codex c…
brennanb2025 Sep 29, 2026
4faaadc
fix(codex): edit Codex config.toml through one parser-checked writer
brennanb2025 Sep 29, 2026
91cf10b
chore(deps): pin smol-toml exactly; its strictness is part of Codex c…
brennanb2025 Sep 29, 2026
0ad843c
fix(codex): mark Orca's verbatim copy of a broken ~/.codex so it is n…
brennanb2025 Sep 29, 2026
e75fde9
fix(codex): still trust the managed Codex home when ~/.codex cannot b…
brennanb2025 Sep 29, 2026
96de9db
fix(codex): report a refused Codex trust write once per file
brennanb2025 Sep 29, 2026
8378de4
fix(codex): replace the verbatim copy once ~/.codex is removed or emp…
brennanb2025 Sep 29, 2026
ad90e7e
chore(codex): remove unused unescapeTomlBasicString
brennanb2025 Sep 29, 2026
9d7d5f2
fix(codex): log each Codex config refusal once per kind
brennanb2025 Sep 29, 2026
f0bcb38
perf(codex): skip the full key-path parse for headers that cannot match
brennanb2025 Sep 29, 2026
2570e70
refactor(codex): leave a hand-broken ~/.codex to base mirror behavior…
brennanb2025 Sep 29, 2026
32b74b3
fix(codex): skip no-op hook trust edits and log a refused hook trust …
brennanb2025 Sep 30, 2026
417e69d
fix(codex): report a remote trust refusal again after a remote trust …
brennanb2025 Sep 30, 2026
1134881
fix(codex): keep managed trust for sub-table-only projects and re-rep…
brennanb2025 Sep 30, 2026
df27968
docs(codex): correct two stale comments about config sync and TOML pa…
brennanb2025 Sep 30, 2026
0b7eea8
fix(codex): treat a sub-table project as table-defined only when no d…
brennanb2025 Sep 30, 2026
c227bc7
refactor(codex): log each Codex config refusal once per session and n…
brennanb2025 Sep 30, 2026
efb8b33
Merge origin/main into codex-config-checked-writer
brennanb2025 Sep 30, 2026
4a5a570
fix(codex): count a project as table-defined only when headers define…
brennanb2025 Sep 30, 2026
c68497c
fix(codex): repair a broken remote config on Node 18 SSH hosts
brennanb2025 Sep 30, 2026
60dcc2e
test(codex): name the mirror sub-table case parameter by its role
brennanb2025 Sep 30, 2026
ad682c1
Merge origin/main (#23958) into codex-config-checked-writer
brennanb2025 Sep 30, 2026
01030ef
test(codex): pin reading quoted hooks.state field names
brennanb2025 Sep 30, 2026
b6afcb5
Merge origin/main into codex-config-checked-writer
brennanb2025 Oct 1, 2026
0262518
fix(codex): still sweep retired hooks from ~/.codex when the trust wr…
brennanb2025 Oct 1, 2026
4dcb620
fix(codex): wait to sweep retired hooks until config.toml can be read
brennanb2025 Oct 1, 2026
37a7f5e
build(cli): list the retired-hook sweep trust planner in the CLI project
brennanb2025 Oct 1, 2026
8d89699
test(codex): read the swept hooks.json without a type assertion
brennanb2025 Oct 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/packaged-runtime-node-modules.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const PACKAGED_RUNTIME_PACKAGE_ROOTS = [
// serve-sim (for CLI JS entry + closure + state/middleware + to make packaged require('serve-sim') + its internal relatives work; mirrors other runtime JS like ws/yaml/zod. Natives/dylibs still via extraResources + the node_modules/serve-sim copy in resources from builder. Client if added too.
'serve-sim',
'qrcode',
// smol-toml: Codex config.toml reads/validation in main and the CLI.
'smol-toml',
'ssh2',
'tweetnacl',
'ws',
Expand Down
89 changes: 89 additions & 0 deletions config/scripts/relay-codex-config-toml-bundle.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { build } from 'esbuild'
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { parse } from 'smol-toml'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'

// Why: relay hosts run the esbuild bundle with no node_modules, so the Codex config
// parser must be inlined; a runtime-resolved require only fails on a real SSH host.
let root
let applyRelayAgentWorkspaceTrust

beforeAll(async () => {
root = realpathSync.native(mkdtempSync(join(tmpdir(), 'orca-relay-codex-toml-')))
const outfile = join(root, 'bundle', 'agent-workspace-trust-spawn.js')
// Same options as build-relay.mjs uses for relay.js.
await build({
entryPoints: [resolve('src/relay/agent-workspace-trust-spawn.ts')],
outfile,
bundle: true,
platform: 'node',
target: 'node18',
format: 'cjs',
external: ['node-pty', '@parcel/watcher', 'electron'],
logLevel: 'silent'
})
;({ applyRelayAgentWorkspaceTrust } = createRequire(outfile)(outfile))
})

afterAll(() => {
rmSync(root, { recursive: true, force: true })
})

describe('relay bundle Codex trust write', () => {
it('loads outside any node_modules and edits a quoted ["projects"."…"] table in place', async () => {
const codexHome = join(root, 'codex-home')
const workspace = join(root, 'wt')
mkdirSync(codexHome)
mkdirSync(workspace)
const configPath = join(codexHome, 'config.toml')
writeFileSync(configPath, `["projects"."${workspace}"]\ntrust_level = "untrusted"\n`)

await applyRelayAgentWorkspaceTrust(
{ workspacePath: workspace },
'codex',
{ HOME: root, CODEX_HOME: codexHome },
{ wslShell: false }
)

expect(readFileSync(configPath, 'utf-8')).toBe(
`["projects"."${workspace}"]\ntrust_level = "trusted"\n`
)
})

it('repairs a duplicate an older Orca wrote using only Node 18 array methods', async () => {
const codexHome = join(root, 'codex-home-node18')
const workspace = join(root, 'wt-node18')
mkdirSync(codexHome)
mkdirSync(workspace)
const configPath = join(codexHome, 'config.toml')
writeFileSync(
configPath,
`["projects"."${workspace}"]\ntrust_level = "trusted"\n\n[projects."${workspace}"]\ntrust_level = "trusted"\n`
)
// Why: SSH hosts may run the relay on Node 18, which lacks the Node 20 copy-array methods.
const node20Methods = ['toReversed', 'toSorted', 'toSpliced', 'with']
const saved = node20Methods.map((name) => [name, Array.prototype[name]])
for (const name of node20Methods) {
delete Array.prototype[name]
}
try {
await applyRelayAgentWorkspaceTrust(
{ workspacePath: workspace },
'codex',
{ HOME: root, CODEX_HOME: codexHome },
{ wslShell: false }
)
} finally {
for (const [name, method] of saved) {
Array.prototype[name] = method
}
}

const repaired = readFileSync(configPath, 'utf-8')
expect(parse(repaired).projects[workspace].trust_level).toBe('trusted')
expect(repaired.match(/trust_level/g)).toHaveLength(1)
})
})
7 changes: 7 additions & 0 deletions config/tsconfig.cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,13 @@
"../src/main/codex/codex-app-server-record-reader.ts",
"../src/main/codex/codex-app-server-session.ts",
"../src/main/codex/codex-config-mirror.ts",
"../src/main/codex/codex-config-legacy-shared-home-mirror.ts",
"../src/main/codex/codex-config-mirror-merge.ts",
"../src/main/codex/codex-config-mirror-source.ts",
"../src/main/codex/codex-config-toml-checked-edit.ts",
"../src/main/codex/codex-config-toml-document.ts",
"../src/main/codex/codex-config-toml-repair.ts",
"../src/main/codex/codex-config-toml-structure.ts",
"../src/main/codex/codex-config-path-reference-rewrite.ts",
"../src/main/codex/codex-config-settings-preservation.ts",
"../src/main/codex/codex-config-settings-removal.ts",
Expand Down Expand Up @@ -153,6 +159,7 @@
"../src/main/codex/codex-real-home-hook-sweep.ts",
"../src/main/codex/codex-real-home-hook-withdrawal.ts",
"../src/main/codex/codex-real-home-hooks-json.ts",
"../src/main/codex/codex-retired-hook-sweep-trust.ts",
"../src/main/codex/codex-state-db.ts",
"../src/main/codex/codex-trust-identity.ts",
"../src/main/codex/codex-trust-config-mutation-queue.ts",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@
"react-i18next": "17.0.15",
"serve-sim": "0.1.40",
"sherpa-onnx": "1.12.37",
"smol-toml": "1.8.0",
"ssh2": "^1.17.0",
"tldts": "7.4.14",
"tweetnacl": "^1.0.3",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions src/main/agent-trust-presets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,38 @@ describe('markCodexProjectTrusted', () => {
}
})

// Why both orders: the lock order writes the managed home first, so each home must survive the other refusing.
it.each([
['the managed home', '~/.codex', 'system'],
['~/.codex', 'the managed home', 'runtime']
] as const)('still trusts %s when %s cannot be parsed', async (_trusted, _broken, brokenHome) => {
const workspace = mkdtempSync(join(tmpdir(), 'orca-codex-ws-'))
try {
const homes = {
system: join(testState.fakeHomeDir, '.codex'),
runtime: join(testState.userDataDir, 'codex-runtime-home', 'home')
}
const trustedHome = brokenHome === 'system' ? homes.runtime : homes.system
mkdirSync(homes[brokenHome], { recursive: true })
const brokenConfig = 'model = "o3"\nmodel_reasoning_effort = \n'
writeFileSync(join(homes[brokenHome], 'config.toml'), brokenConfig, 'utf-8')

await expect(
markCodexProjectTrusted(workspace, getLocalCodexTrustConfigFiles(testState.fakeHomeDir))
).rejects.toMatchObject({
name: 'CodexConfigTomlEditRefusedError',
reason: 'input-invalid'
})

expect(readFileSync(join(homes[brokenHome], 'config.toml'), 'utf-8')).toBe(brokenConfig)
expect(readFileSync(join(trustedHome, 'config.toml'), 'utf-8')).toContain(
`[projects."${escapeTomlBasicString(realpathSync.native(workspace))}"]`
)
} finally {
rmSync(workspace, { recursive: true, force: true })
}
})

it('preserves existing config keys and updates an existing project block', async () => {
const workspace = mkdtempSync(join(tmpdir(), 'orca-codex-ws-'))
const realpath = realpathSync.native(workspace)
Expand Down
17 changes: 16 additions & 1 deletion src/main/agent-trust-presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,23 @@ export function markCodexProjectTrusted(
const write = configFiles.reduceRight<() => Promise<void>>(
(inner, configFile) => () => runExclusivelyForCodexTrustConfig(configFile, inner),
async () => {
// Why: a config.toml Orca refuses to edit must not also leave Codex's other homes untrusted.
const failures: unknown[] = []
for (const configFile of configFiles) {
upsertProjectTrustLevel(configFile, absPath, 'trusted')
try {
upsertProjectTrustLevel(configFile, absPath, 'trusted')
} catch (error) {
failures.push(error)
}
}
if (failures.length === 1) {
throw failures[0]
}
if (failures.length > 1) {
throw new AggregateError(
failures,
'Orca could not mark the project trusted in several Codex homes'
)
}
}
)
Expand Down
80 changes: 80 additions & 0 deletions src/main/codex/codex-config-legacy-shared-home-mirror.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { join } from 'node:path'
import {
recoverInterruptedGuardedFileOperation,
writeFileAtomicallyIfUnchanged
} from '../codex-accounts/fs-utils'
import { observeAgentStateFile } from './codex-path-observation'
import { getOrcaManagedCodexHomePath, getSystemCodexHomePath } from './codex-home-paths'
import type { CodexSettingsPromotionHomes } from './config-settings-promotion'
import { applyCodexDaemonSocketGuard } from './codex-daemon-socket-path-guard'
import { refuseUnreadableCodexConfigResult } from './codex-config-toml-checked-edit'
import { mergeSystemCodexConfigIntoRuntime } from './codex-config-mirror-merge'
import {
prepareSystemConfigForFreshRuntimeMirror,
prepareSystemConfigForRuntimeMirror,
resolveCodexConfigMirrorSourceDirectory
} from './codex-config-mirror-source'

/**
* Refreshes the retired shared home for PTYs that survived real-home rollout.
*
* This is deliberately one-way: a retained PTY may hold pre-rollout settings,
* so treating that home as a promotion source could overwrite the live config.
*/
export function syncSystemConfigIntoLegacySharedCodexHome(
homes: CodexSettingsPromotionHomes = {
runtimeHomePath: getOrcaManagedCodexHomePath(),
systemHomePath: getSystemCodexHomePath()
}
): void {
const systemConfigPath = join(homes.systemHomePath, 'config.toml')
const runtimeConfigPath = join(homes.runtimeHomePath, 'config.toml')
recoverInterruptedGuardedFileOperation(runtimeConfigPath)
const systemConfigObservation = observeAgentStateFile(systemConfigPath)
if (systemConfigObservation.kind === 'indeterminate') {
throw systemConfigObservation.error
}
const rawSystemConfig =
systemConfigObservation.kind === 'present' ? systemConfigObservation.value : ''
const runtimeConfigObservation = observeAgentStateFile(runtimeConfigPath)
if (runtimeConfigObservation.kind === 'indeterminate') {
throw runtimeConfigObservation.error
}
const runtimeConfigBeforeMirror =
runtimeConfigObservation.kind === 'present' ? runtimeConfigObservation.value : null
// Why: a missing cloud-synced source is not proof the user cleared config.
let mirroredRuntimeConfig = runtimeConfigBeforeMirror ?? ''
if (rawSystemConfig.trim() !== '') {
const sourceConfigDir = resolveCodexConfigMirrorSourceDirectory(homes.systemHomePath)
// The retired home has no ownership baseline; its entire MCP root stays canonical.
mirroredRuntimeConfig =
runtimeConfigBeforeMirror !== null
? mergeSystemCodexConfigIntoRuntime(
runtimeConfigBeforeMirror,
prepareSystemConfigForRuntimeMirror(rawSystemConfig, sourceConfigDir),
sourceConfigDir,
new Set(),
true
)
: prepareSystemConfigForFreshRuntimeMirror(rawSystemConfig, sourceConfigDir)
}
// Why: retained pre-rollout panes still use this home, so a refresh must keep the daemon guard.
const nextRuntimeConfig = applyCodexDaemonSocketGuard(
mirroredRuntimeConfig,
homes.runtimeHomePath
)
if (
(runtimeConfigBeforeMirror ?? '') === nextRuntimeConfig ||
refuseUnreadableCodexConfigResult({
configPath: runtimeConfigPath,
result: nextRuntimeConfig,
inputs: [rawSystemConfig, runtimeConfigBeforeMirror],
context: 'Skipped mirroring the Codex config into a managed home'
})
) {
return
}
// Why: stage first, then compare immediately before replace so a retained
// Codex trust write during mirror preparation wins.
writeFileAtomicallyIfUnchanged(runtimeConfigPath, runtimeConfigBeforeMirror, nextRuntimeConfig)
}
Loading
Loading