mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 00:03:47 +00:00
Guard Codex launches with managed network requirements
This commit is contained in:
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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, "'\\''")}'`
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user