From b6c3661e1a2bc4eba6ea11a0fe5e756e1f84335a Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 27 Jun 2026 04:06:25 -0700 Subject: [PATCH] Guard Codex launches with managed network requirements --- ...codex-managed-network-requirements.test.ts | 83 ++++++++++++++++ .../codex-managed-network-requirements.ts | 95 +++++++++++++++++++ src/main/providers/local-pty-provider.test.ts | 44 ++++++++- src/main/providers/local-pty-provider.ts | 14 +++ 4 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 src/main/codex/codex-managed-network-requirements.test.ts create mode 100644 src/main/codex/codex-managed-network-requirements.ts diff --git a/src/main/codex/codex-managed-network-requirements.test.ts b/src/main/codex/codex-managed-network-requirements.test.ts new file mode 100644 index 00000000000..ef21d7ba8c7 --- /dev/null +++ b/src/main/codex/codex-managed-network-requirements.test.ts @@ -0,0 +1,83 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +const { execFileSyncMock, existsSyncMock, readFileSyncMock } = vi.hoisted(() => ({ + execFileSyncMock: vi.fn(), + existsSyncMock: vi.fn(), + readFileSyncMock: vi.fn() +})) + +vi.mock('node:child_process', () => ({ + execFileSync: execFileSyncMock +})) + +vi.mock('node:fs', () => ({ + existsSync: existsSyncMock, + readFileSync: readFileSyncMock +})) + +import { + buildCodexManagedNetworkRequirementsWarningCommand, + hasManagedCodexNetworkPermissionRequirement +} from './codex-managed-network-requirements' + +const REPRO_REQUIREMENTS = [ + 'default_permissions = "github_only"', + '', + '[permissions.github_only]', + 'extends = ":read-only"', + '', + '[permissions.github_only.network]', + 'enabled = true', + 'allow_local_binding = false', + '' +].join('\n') + +describe('codex managed network requirements', () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform') + + afterEach(() => { + if (originalPlatform) { + Object.defineProperty(process, 'platform', originalPlatform) + } + vi.clearAllMocks() + }) + + it('detects macOS MDM requirements with enabled permission-profile networking', () => { + Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' }) + execFileSyncMock.mockReturnValue(`${Buffer.from(REPRO_REQUIREMENTS).toString('base64')}\n`) + existsSyncMock.mockReturnValue(false) + + expect(hasManagedCodexNetworkPermissionRequirement()).toBe(true) + expect(execFileSyncMock).toHaveBeenCalledWith( + 'defaults', + ['read', 'com.openai.codex', 'requirements_toml_base64'], + expect.objectContaining({ encoding: 'utf-8' }) + ) + }) + + it('detects system requirements with enabled permission-profile networking', () => { + Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' }) + existsSyncMock.mockReturnValue(true) + readFileSyncMock.mockReturnValue(REPRO_REQUIREMENTS) + + expect(hasManagedCodexNetworkPermissionRequirement()).toBe(true) + }) + + it('ignores requirements without permission-profile network enablement', () => { + Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' }) + execFileSyncMock.mockReturnValue( + `${Buffer.from('[permissions.github_only.network]\\nenabled = false\\n').toString('base64')}\n` + ) + existsSyncMock.mockReturnValue(false) + + expect(hasManagedCodexNetworkPermissionRequirement()).toBe(false) + }) + + it('builds a shell-safe warning command', () => { + const command = buildCodexManagedNetworkRequirementsWarningCommand() + + expect(command).toContain('printf') + expect(command).toContain('managed Codex requirements') + expect(command).toContain('turn/start failed in TUI') + }) +}) diff --git a/src/main/codex/codex-managed-network-requirements.ts b/src/main/codex/codex-managed-network-requirements.ts new file mode 100644 index 00000000000..e078c6b076c --- /dev/null +++ b/src/main/codex/codex-managed-network-requirements.ts @@ -0,0 +1,95 @@ +import { execFileSync } from 'node:child_process' +import { existsSync, readFileSync } from 'node:fs' +import { join } from 'node:path' + +const MACOS_CODEX_PREFERENCES_DOMAIN = 'com.openai.codex' +const MACOS_REQUIREMENTS_KEY = 'requirements_toml_base64' +const UNIX_REQUIREMENTS_PATH = '/etc/codex/requirements.toml' +const WINDOWS_REQUIREMENTS_PATH = join( + process.env.ProgramData ?? 'C:\\ProgramData', + 'OpenAI', + 'Codex', + 'requirements.toml' +) + +const KNOWN_BAD_WARNING_LINES = [ + 'Orca did not start Codex because this Mac has managed Codex requirements with network.enabled = true in a permission profile.', + 'Codex CLI currently exits with "turn/start failed in TUI" for that managed network policy.', + 'Ask your Codex administrator to remove that network.enabled requirement or update Codex after the upstream fix lands.' +] as const + +export function buildCodexManagedNetworkRequirementsWarningCommand(): string { + return `printf '%s\\n' ${KNOWN_BAD_WARNING_LINES.map(shellQuote).join(' ')}` +} + +export function hasManagedCodexNetworkPermissionRequirement(): boolean { + return getManagedRequirementsTomlCandidates().some(hasNetworkEnabledPermissionProfile) +} + +function getManagedRequirementsTomlCandidates(): string[] { + const candidates: string[] = [] + const mdmRequirements = readMacosMdmRequirementsToml() + if (mdmRequirements) { + candidates.push(mdmRequirements) + } + + const filePath = process.platform === 'win32' ? WINDOWS_REQUIREMENTS_PATH : UNIX_REQUIREMENTS_PATH + const fileRequirements = readRequirementsFile(filePath) + if (fileRequirements) { + candidates.push(fileRequirements) + } + return candidates +} + +function readMacosMdmRequirementsToml(): string | null { + if (process.platform !== 'darwin') { + return null + } + try { + const encoded = execFileSync( + 'defaults', + ['read', MACOS_CODEX_PREFERENCES_DOMAIN, MACOS_REQUIREMENTS_KEY], + { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'ignore'], + timeout: 1000 + } + ).trim() + if (!encoded) { + return null + } + return Buffer.from(encoded, 'base64').toString('utf-8') + } catch { + return null + } +} + +function readRequirementsFile(path: string): string | null { + try { + return existsSync(path) ? readFileSync(path, 'utf-8') : null + } catch { + return null + } +} + +function hasNetworkEnabledPermissionProfile(toml: string): boolean { + let inPermissionNetworkSection = false + for (const rawLine of toml.split(/\r?\n/)) { + const line = rawLine.trim() + if (line.startsWith('[')) { + inPermissionNetworkSection = /^\[permissions\..+\.network\]\s*(?:#.*)?$/.test(line) + continue + } + if (!inPermissionNetworkSection) { + continue + } + if (/^enabled\s*=\s*true\s*(?:#.*)?$/i.test(line)) { + return true + } + } + return false +} + +function shellQuote(value: string): string { + return `'${value.replace(/'/g, "'\\''")}'` +} diff --git a/src/main/providers/local-pty-provider.test.ts b/src/main/providers/local-pty-provider.test.ts index 06f05fb3386..7ed0c27e5e3 100644 --- a/src/main/providers/local-pty-provider.test.ts +++ b/src/main/providers/local-pty-provider.test.ts @@ -8,7 +8,9 @@ const { mkdirSyncMock, writeFileSyncMock, spawnMock, - resolveAgentForegroundProcessMock + resolveAgentForegroundProcessMock, + hasManagedCodexNetworkPermissionRequirementMock, + buildCodexManagedNetworkRequirementsWarningCommandMock } = vi.hoisted(() => ({ existsSyncMock: vi.fn(), statSyncMock: vi.fn(), @@ -16,7 +18,9 @@ const { mkdirSyncMock: vi.fn(), writeFileSyncMock: vi.fn(), spawnMock: vi.fn(), - resolveAgentForegroundProcessMock: vi.fn() + resolveAgentForegroundProcessMock: vi.fn(), + hasManagedCodexNetworkPermissionRequirementMock: vi.fn(), + buildCodexManagedNetworkRequirementsWarningCommandMock: vi.fn() })) vi.mock('fs', () => ({ @@ -43,6 +47,12 @@ vi.mock('./agent-foreground-process', () => ({ resolveAgentForegroundProcess: resolveAgentForegroundProcessMock })) +vi.mock('../codex/codex-managed-network-requirements', () => ({ + hasManagedCodexNetworkPermissionRequirement: hasManagedCodexNetworkPermissionRequirementMock, + buildCodexManagedNetworkRequirementsWarningCommand: + buildCodexManagedNetworkRequirementsWarningCommandMock +})) + vi.mock('../wsl', () => ({ parseWslPath: (path: string) => { const match = path.match(/^\\\\wsl\.localhost\\([^\\]+)(.*)$/) @@ -96,6 +106,10 @@ describe('LocalPtyProvider', () => { resolveAgentForegroundProcessMock.mockImplementation( async (_pid: number, fallbackProcess: string | null) => fallbackProcess ) + hasManagedCodexNetworkPermissionRequirementMock.mockReturnValue(false) + buildCodexManagedNetworkRequirementsWarningCommandMock.mockReturnValue( + "printf '%s\\n' 'Codex managed network requirements are not supported'" + ) exitCb = undefined mockProc = { @@ -268,6 +282,32 @@ describe('LocalPtyProvider', () => { } }) + it('prints a diagnostic instead of launching Codex when managed network requirements would crash the TUI', async () => { + vi.useFakeTimers() + try { + Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' }) + hasManagedCodexNetworkPermissionRequirementMock.mockReturnValue(true) + + await provider.spawn({ + cols: 80, + rows: 24, + command: "codex '--dangerously-bypass-approvals-and-sandbox'" + }) + + const dataCallback = mockProc.onData.mock.calls[0]?.[0] as (data: string) => void + dataCallback('\x1b]777;orca-shell-ready\x07user@host % ') + await Promise.resolve() + vi.advanceTimersByTime(30) + await Promise.resolve() + + expect(mockProc.write).toHaveBeenCalledWith( + "printf '%s\\n' 'Codex managed network requirements are not supported'\n" + ) + } finally { + vi.useRealTimers() + } + }) + it('releases held marker-prefix bytes when local shell readiness times out', async () => { vi.useFakeTimers() const onData = vi.fn() diff --git a/src/main/providers/local-pty-provider.ts b/src/main/providers/local-pty-provider.ts index 7898285c2ac..c792eed6ad6 100644 --- a/src/main/providers/local-pty-provider.ts +++ b/src/main/providers/local-pty-provider.ts @@ -49,6 +49,10 @@ import { resolveAgentForegroundProcess } from './agent-foreground-process' import { getAgentForegroundContextPaths } from './agent-foreground-context-paths' import { recognizeAgentProcessFromCommandLine } from '../../shared/agent-process-recognition' import { shouldUseShellReadyStartupDelivery } from '../../shared/codex-startup-delivery' +import { + buildCodexManagedNetworkRequirementsWarningCommand, + hasManagedCodexNetworkPermissionRequirement +} from '../codex/codex-managed-network-requirements' const PANE_IDENTITY_ENV_KEYS = [ 'ORCA_PANE_KEY', @@ -476,6 +480,16 @@ export class LocalPtyProvider implements IPtyProvider { const isWslShell = Boolean(wslInfo) || pathWin32.basename(shellPath).toLowerCase() === 'wsl.exe' const launchWslDistro = wslInfo?.distro ?? worktreeWslContext?.distro ?? preferredWslContext?.distro ?? null + if ( + process.platform === 'darwin' && + args.command && + recognizeAgentProcessFromCommandLine(args.command)?.agent === 'codex' && + hasManagedCodexNetworkPermissionRequirement() + ) { + // Why: Codex 0.142 exits the TUI before the first turn when managed + // requirements enable a permission-profile network policy on macOS. + args.command = buildCodexManagedNetworkRequirementsWarningCommand() + } const finalEnv = this.opts.buildSpawnEnv ? this.opts.buildSpawnEnv(id, spawnEnv, { command: args.command,