fix(codex): preserve remote env and complete real-home cleanup

This commit is contained in:
Brennan Benson
2026-07-15 15:16:39 -07:00
parent 1fcdc9a32a
commit b552e719be
4 changed files with 110 additions and 22 deletions
@@ -5,8 +5,10 @@ import type * as Os from 'node:os'
import { join } from 'node:path'
import { wrapPosixHookCommand } from '../agent-hooks/installer-utils'
import {
computeTrustKey,
computeTrustedHash,
parseTrustKey,
readHookTrustEntries,
upsertHookTrustEntries,
type CodexTrustEntry
} from './config-toml-trust'
@@ -17,6 +19,7 @@ import {
writeCodexTrustGrantLedgerHome
} from './codex-trust-grant-ledger'
import type { CodexHookTrustGrantRequest } from './codex-app-server-client'
import { getCodexHookTrustSignature } from './codex-hook-identity'
const { getPathMock, homedirMock } = vi.hoisted(() => ({
getPathMock: vi.fn<(name: string) => string>(),
@@ -29,7 +32,7 @@ vi.mock('os', async (importOriginal) => {
return { ...actual, homedir: homedirMock }
})
import { CodexHookService } from './hook-service'
import { CodexHookService, getCodexManagedHookInstallMaterial } from './hook-service'
let tmpHome: string
let userDataDir: string
@@ -136,6 +139,42 @@ describe('CodexHookService app-server trust grant lane', () => {
expect(readFileSync(join(managedHome, 'config.toml'))).toEqual(firstToml)
})
it('retries ledger-proven real-home trust cleanup after the hook is already gone', () => {
prepareSystemHome()
const systemHome = join(tmpHome, '.codex')
const hooksPath = join(systemHome, 'hooks.json')
const configPath = join(systemHome, 'config.toml')
const material = getCodexManagedHookInstallMaterial()
const trustedHash = 'sha256:codex-real-home-stop'
const entry: CodexTrustEntry = {
sourcePath: hooksPath,
eventLabel: 'stop',
groupIndex: 0,
handlerIndex: 0,
command: material.command,
timeoutSec: 10,
trustedHash
}
const trustKey = computeTrustKey(entry)
writeFileSync(hooksPath, `${JSON.stringify({ hooks: {} }, null, 2)}\n`)
upsertHookTrustEntries(configPath, [entry])
writeCodexTrustGrantLedgerHome(systemHome, {
binary: null,
entries: {
[trustKey]: {
signature: getCodexHookTrustSignature(entry),
trustedHash
}
}
})
installCodexLikeGrantRunner()
expect(new CodexHookService().install().state).toBe('installed')
expect(readHookTrustEntries(configPath).has(trustKey)).toBe(false)
expect(readCodexTrustGrantLedgerHome(systemHome)).toBeNull()
})
it('does not accept a ledger hash after the recorded Codex binary stamp changes', () => {
prepareSystemHome()
installCodexLikeGrantRunner()
+29 -2
View File
@@ -228,7 +228,10 @@ function collectManagedTrustEntries(
return entries
}
function removeMatchingTrustEntries(configPath: string, entries: readonly CodexTrustEntry[]): void {
function removeSelfComputedMatchingTrustEntries(
configPath: string,
entries: readonly CodexTrustEntry[]
): void {
if (entries.length === 0) {
return
}
@@ -573,6 +576,17 @@ function dedupeHookDefinitions(definitions: readonly HookDefinition[]): HookDefi
})
}
function removeSystemManagedHookTrustEntries(systemHomePath: string, hooksJsonPath: string): void {
removeCodexManagedHookTrustEntries({
tomlPath: getSystemCodexConfigTomlPath(),
runtimeHomePath: systemHomePath,
sourcePath: hooksJsonPath,
command: getManagedCommand(getManagedScriptPath()),
managedEventLabels: CODEX_MANAGED_EVENT_LABELS,
timeoutSec: MANAGED_HOOK_TIMEOUT_SECONDS
})
}
function cleanupLegacySystemManagedHooks(): void {
if (systemCodexHomeHookSweepSuppressed()) {
return
@@ -583,8 +597,14 @@ function cleanupLegacySystemManagedHooks(): void {
return
}
const systemHomePath = getSystemCodexHomePath()
const hasRecordedRealHomeGrant =
readCodexTrustGrantLedgerHomeForReconciliation(systemHomePath) !== null
const config = readHooksJson(legacyConfigPath)
if (!config?.hooks) {
if (hasRecordedRealHomeGrant) {
removeSystemManagedHookTrustEntries(systemHomePath, legacyConfigPath)
}
return
}
@@ -623,8 +643,15 @@ function cleanupLegacySystemManagedHooks(): void {
// Why: this is the user's system hooks file, not Orca's runtime copy.
// Remove only stale Orca hook entries and preserve other managers' metadata.
writeHooksJson(legacyConfigPath, { ...config, hooks: nextHooks })
// Why: stale dev/version entries can reference an older managed script
// path that is not represented by the current grant ledger.
removeSelfComputedMatchingTrustEntries(getSystemCodexConfigTomlPath(), trustEntries)
}
if (removedManagedHook || hasRecordedRealHomeGrant) {
// Why: the ledger recognizes Codex-computed hashes and remains a retry
// marker if a prior cleanup removed hooks.json but could not update TOML.
removeSystemManagedHookTrustEntries(systemHomePath, legacyConfigPath)
}
removeMatchingTrustEntries(getSystemCodexConfigTomlPath(), trustEntries)
}
function stripLegacyManagedProfileBlock(content: string): string {
+25 -7
View File
@@ -2383,7 +2383,12 @@ describe('registerPtyHandlers', () => {
it('does NOT inject host-local env on SSH spawns (connectionId set)', async () => {
const sshSpawn = vi.fn(
async (_opts: { env: Record<string, string>; paneKey?: string; tabId?: string }) => ({
async (_opts: {
env: Record<string, string>
envToDelete?: string[]
paneKey?: string
tabId?: string
}) => ({
id: 'ssh-pty'
})
)
@@ -2420,7 +2425,8 @@ describe('registerPtyHandlers', () => {
undefined,
(() => ({
httpProxyUrl: 'http://proxy.example:8080',
httpProxyBypassRules: 'localhost'
httpProxyBypassRules: 'localhost',
codexSystemDefaultRealHomeEnabled: true
})) as never,
undefined,
store as never
@@ -2459,6 +2465,10 @@ describe('registerPtyHandlers', () => {
expect(env.HTTPS_PROXY).toBeUndefined()
expect(env.NO_PROXY).toBeUndefined()
expect(env.FOO).toBe('bar')
// Why: real-home routing is host-only. A null local-home resolver on
// SSH must not become a request to alter the remote Codex environment.
expect(spawnOptions.envToDelete ?? []).not.toContain('CODEX_HOME')
expect(spawnOptions.envToDelete ?? []).not.toContain('ORCA_CODEX_HOME')
expect(spawnOptions.paneKey).toBe(makePaneKey('tab-1', leafId))
expect(spawnOptions.tabId).toBe('tab-1')
expect(openCodeBuildPtyEnvMock).not.toHaveBeenCalled()
@@ -5635,9 +5645,11 @@ describe('registerPtyHandlers', () => {
}
const savedRemoteHooks = process.env.ORCA_FEATURE_REMOTE_AGENT_HOOKS
process.env.ORCA_FEATURE_REMOTE_AGENT_HOOKS = '0'
const remoteSpawn = vi.fn(async (_opts: { env?: Record<string, string> }) => ({
id: 'ssh:ssh-runtime-env@@relay-pty'
}))
const remoteSpawn = vi.fn(
async (_opts: { env?: Record<string, string>; envToDelete?: string[] }) => ({
id: 'ssh:ssh-runtime-env@@relay-pty'
})
)
registerSshPtyProvider('ssh-runtime-env', {
spawn: remoteSpawn,
write: vi.fn(),
@@ -5683,7 +5695,10 @@ describe('registerPtyHandlers', () => {
mainWindow as never,
runtime as never,
undefined,
undefined,
(() => ({
agentStatusHooksEnabled: false,
codexSystemDefaultRealHomeEnabled: true
})) as never,
undefined,
store as never
)
@@ -5705,11 +5720,14 @@ describe('registerPtyHandlers', () => {
persistHostSessionBinding: true
})
const env = remoteSpawn.mock.calls[0]?.[0].env
const spawnOptions = remoteSpawn.mock.calls[0]?.[0]
const env = spawnOptions.env
expect(env).toMatchObject({ FOO: 'bar' })
expect(env?.ORCA_PANE_KEY).toBeUndefined()
expect(env?.ORCA_TAB_ID).toBeUndefined()
expect(env?.ORCA_WORKTREE_ID).toBeUndefined()
expect(spawnOptions.envToDelete ?? []).not.toContain('CODEX_HOME')
expect(spawnOptions.envToDelete ?? []).not.toContain('ORCA_CODEX_HOME')
expect(store.upsertSshRemotePtyLease).toHaveBeenCalledWith(
expect.objectContaining({
targetId: 'ssh-runtime-env',
+16 -12
View File
@@ -3093,12 +3093,14 @@ export function registerPtyHandlers(
isDaemonHostSpawn &&
shouldSkipCodexHomeEnvForWindowsShell(daemonShellOverride, cwd) &&
!selectedCodexHomePath
const stripInheritedOrcaCodexHome = shouldStripInheritedOrcaCodexHome({
target: codexSelectionTarget,
selectedCodexHomePath,
skipCodexHomeEnv,
settings: getSettings?.()
})
const stripInheritedOrcaCodexHome =
isDaemonHostSpawn &&
shouldStripInheritedOrcaCodexHome({
target: codexSelectionTarget,
selectedCodexHomePath,
skipCodexHomeEnv,
settings: getSettings?.()
})
if (isDaemonHostSpawn && sessionId) {
if (!isSafePtySessionId(sessionId, app.getPath('userData'))) {
throw new Error('Invalid PTY session id')
@@ -3974,12 +3976,14 @@ export function registerPtyHandlers(
isDaemonHostSpawn &&
shouldSkipCodexHomeEnvForWindowsShell(effectiveShellOverride, cwd) &&
!selectedCodexHomePath
const stripInheritedOrcaCodexHome = shouldStripInheritedOrcaCodexHome({
target: codexSelectionTarget,
selectedCodexHomePath,
skipCodexHomeEnv,
settings: getSettings?.()
})
const stripInheritedOrcaCodexHome =
isDaemonHostSpawn &&
shouldStripInheritedOrcaCodexHome({
target: codexSelectionTarget,
selectedCodexHomePath,
skipCodexHomeEnv,
settings: getSettings?.()
})
if (isDaemonHostSpawn) {
if (effectiveSessionId === undefined) {
// Should be unreachable: the expression above returns a string when