fix(terminal): run Orca's cmd.exe, path-named and setup-gated Codex launches without the shared server (#23933)

* fix(terminal): give plain shells and cmd.exe Codex launches --no-daemon

Plain bash, zsh and fish tabs were never wrapped, so a typed codex skipped the
shell function that adds --no-daemon. Wrap them (bash keeps its prompt and
DEBUG trap untouched unless Orca asked for command markers), add --no-daemon
host-side where no function can run (cmd.exe, path-named binaries), and move
new tabs to a v38 terminal daemon so they get the new wrappers.

* fix(terminal): keep plain bash a login shell and give the setup gate the codex function

Plain bash and Git Bash tabs launch exactly as before again: the rcfile
wrapper would have made every one a non-login shell. Plain tabs on the
user's configured shell args stay unwrapped on both transports. The
wait-for-setup gate's bash -lc now defines the codex function, so a
sequenced Codex launch gets --no-daemon from the binary it actually runs.

* fix(terminal): define the setup gate's codex function after setup finishes

Setup can be what puts codex on PATH, so defining the function before the
marker wait found no binary and skipped --no-daemon.

* refactor(terminal): fold the SSH/WSL guard into the Codex launch planner and bound the gate test

* fix(terminal): honour the pane's env deletions in the Codex opt-out check

Also pin the setup-gate test's fake codex ahead of path_helper's PATH.

* revert(terminal): launch plain zsh and fish tabs exactly as on main

Drops the always-wrap for plain zsh and fish, the configured-args guard
that only served it, and the v38 daemon bump: the daemon's launch configs
and generated wrappers are byte-identical to main again. Keeps the
host-side --no-daemon for cmd.exe and path-named launches and the setup
gate's codex function.
This commit is contained in:
Jinwoo Hong
2026-09-29 21:40:53 -04:00
committed by GitHub
parent aa68003362
commit 26bb7c23f1
20 changed files with 438 additions and 114 deletions
+1
View File
@@ -60,6 +60,7 @@ const CODEX_INDEX_HEAL_CONTRACT_PREFIXES = [
'src/main/codex/config-toml-trust',
'src/main/pty/codex-no-daemon-binary-contract',
'src/main/pty/codex-shell-launch-preflight',
'src/shared/codex-shell-function',
'src/main/codex/codex-index-heal-binary-contract',
'src/main/codex/codex-session-index-heal',
'src/main/codex/codex-app-server-session',
@@ -1,5 +1,5 @@
import { getPosixOmpShellWrapper } from '../pty/omp-shell-wrapper'
import { getPosixCodexShellLaunchPreflight } from '../pty/codex-shell-launch-preflight'
import { getPosixCodexShellLaunchPreflight } from '../../shared/codex-shell-function'
import { BASH_PROMPT_COMMAND_COMPOSITION_BLOCK } from '../bash-prompt-command-composition'
import { BASH_FEATURE_CHANNEL_BLOCK, SHELL_STARTUP_IDENTITY_MARKER_BLOCK } from '../shell-templates'
+1 -1
View File
@@ -6,7 +6,7 @@ import {
getPowerShellOsc133Bootstrap,
isPowerShellExecutableName
} from '../powershell-osc133-bootstrap'
import { getFishCodexShellLaunchPreflight } from '../pty/codex-shell-launch-preflight'
import { getFishCodexShellLaunchPreflight } from '../../shared/codex-shell-function'
import { getFishShellReadyInitCommand } from '../shell-templates'
import {
encodeShellStartupFeatures,
+11 -3
View File
@@ -68,8 +68,16 @@ export async function isCommandOnLocalPath(
command: string,
options: ResolveCommandOptions = {}
): Promise<boolean> {
return (await resolveCommandOnLocalPath(command, options)) !== null
}
/** The absolute path `isCommandOnLocalPath` found, or null. */
export async function resolveCommandOnLocalPath(
command: string,
options: ResolveCommandOptions = {}
): Promise<string | null> {
if (!command) {
return false
return null
}
const platform = options.platform ?? process.platform
const env = options.env ?? process.env
@@ -98,9 +106,9 @@ export async function isCommandOnLocalPath(
continue
}
if (await isExecutableFile(candidate, isWin)) {
return true
return candidate
}
}
}
return false
return null
}
+12 -2
View File
@@ -23,6 +23,7 @@ import { ptySizes } from '../delivery/visibility-state'
import { shouldSeedPreAttachPtySize } from '../delivery/attached-pty-size'
import { getStartupTerminalIngressIntent } from '../../terminal-startup-color-query-replies'
import { resolveConfiguredTerminalShellArgs } from '../configured-terminal-shell-args'
import { planCodexNoDaemonLaunch } from '../../../pty/codex-no-daemon-launch-command'
import type { PtyIpcSpawnState } from './spawn-state'
import { applyAgentWorkspaceTrustToSpawn } from '../../../agent-workspace-trust-spawn'
@@ -78,8 +79,17 @@ export async function buildPtyIpcSpawnOptions(
if (ctx.combinedEnvToDelete) {
ctx.spawnOptions.envToDelete = ctx.combinedEnvToDelete
}
if (ctx.launchCommand !== undefined) {
ctx.spawnOptions.command = ctx.launchCommand
const noDaemonLaunch = planCodexNoDaemonLaunch({
command: ctx.launchCommand,
executesOnThisHost: !args.connectionId && ctx.codexSelectionTarget.runtime !== 'wsl',
shellOverride: ctx.effectiveShellOverride,
env: ctx.spawnEnv,
envToDelete: ctx.combinedEnvToDelete,
cwd: ctx.cwd
})
const launchCommand = noDaemonLaunch ? await noDaemonLaunch : ctx.launchCommand
if (launchCommand !== undefined) {
ctx.spawnOptions.command = launchCommand
}
if (args.commandDelivery !== undefined) {
ctx.spawnOptions.commandDelivery = args.commandDelivery
+12 -2
View File
@@ -23,6 +23,7 @@ import { CLAUDE_AUTH_ENV_VARS } from '../../../claude-accounts/environment'
import { LEGACY_TERMINAL_SHIM_REMOTE_ENV_KEYS } from '../../../pty/legacy-terminal-shim-dir'
import { PI_PROCESS_OWNER_ENV_KEYS } from '../../../pty/pi-process-owner-env'
import { resolveConfiguredTerminalShellArgs } from '../configured-terminal-shell-args'
import { planCodexNoDaemonLaunch } from '../../../pty/codex-no-daemon-launch-command'
import { resolveStablePaneOwner } from '../pane/stable-owner'
import { getStartupTerminalIngressIntent } from '../../terminal-startup-color-query-replies'
import {
@@ -99,8 +100,17 @@ export async function buildRuntimePtySpawnOptions(
}
deleteRequestedEnvKeys(ctx.env, ctx.spawnOptions.envToDelete)
promoteAgentTeamsShimPath(ctx.env, ctx.requestedAgentTeamsPath)
if (ctx.launchCommand !== undefined) {
ctx.spawnOptions.command = ctx.launchCommand
const noDaemonLaunch = planCodexNoDaemonLaunch({
command: ctx.launchCommand,
executesOnThisHost: !args.connectionId && ctx.codexSelectionTarget.runtime !== 'wsl',
shellOverride: ctx.daemonShellOverride,
env: ctx.env,
envToDelete: ctx.spawnOptions.envToDelete,
cwd: ctx.cwd
})
const launchCommand = noDaemonLaunch ? await noDaemonLaunch : ctx.launchCommand
if (launchCommand !== undefined) {
ctx.spawnOptions.command = launchCommand
}
if (args.commandDelivery !== undefined) {
ctx.spawnOptions.commandDelivery = args.commandDelivery
+1 -1
View File
@@ -1,5 +1,5 @@
import { getPowerShellOmpShellWrapper } from './pty/omp-shell-wrapper'
import { getPowerShellCodexShellLaunchPreflight } from './pty/codex-shell-launch-preflight'
import { getPowerShellCodexShellLaunchPreflight } from '../shared/codex-shell-function'
export { encodePowerShellCommand } from '../shared/powershell-command-encoding'
/**
@@ -7,7 +7,7 @@
import { BASH_PROMPT_COMMAND_COMPOSITION_BLOCK } from '../bash-prompt-command-composition'
import { WSL_MANAGED_CLI_PATH_RESTORE } from '../wsl-managed-cli-path-restore'
import { getPosixOmpShellWrapper } from '../pty/omp-shell-wrapper'
import { getPosixCodexShellLaunchPreflight } from '../pty/codex-shell-launch-preflight'
import { getPosixCodexShellLaunchPreflight } from '../../shared/codex-shell-function'
import { getBashStartupCommandPromptBlock } from '../pty/posix-shell-startup-command'
import { BASH_FEATURE_CHANNEL_BLOCK, SHELL_STARTUP_IDENTITY_MARKER_BLOCK } from '../shell-templates'
import { SHELL_READY_MARKER_ESCAPED } from './local-pty-shell-ready-marker'
+1 -1
View File
@@ -10,7 +10,7 @@ import {
getPowerShellOsc133Bootstrap,
isPowerShellExecutableName
} from '../powershell-osc133-bootstrap'
import { getFishCodexShellLaunchPreflight } from '../pty/codex-shell-launch-preflight'
import { getFishCodexShellLaunchPreflight } from '../../shared/codex-shell-function'
import { POSIX_SHELL_STARTUP_COMMAND_ENV } from '../pty/posix-shell-startup-command'
import { getFishShellReadyInitCommand } from '../shell-templates'
import {
@@ -4,10 +4,10 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { promisify } from 'node:util'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { CODEX_SHARED_SERVER_ARGS } from './codex-shell-launch-preflight'
import { CODEX_SHARED_SERVER_ARGS } from '../../shared/codex-shell-function'
// Why: Orca's codex shell wrapper puts --no-daemon first for every subcommand but
// agents/queue (codex-shell-launch-preflight.ts). Its tests use a fake codex, so
// agents/queue (src/shared/codex-shell-function.ts). Its tests use a fake codex, so
// only the real binary can catch a renamed flag or a new subcommand rejecting it.
const execFileAsync = promisify(execFile)
@@ -0,0 +1,102 @@
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { planCodexNoDaemonLaunch, type LocalCodexLaunch } from './codex-no-daemon-launch-command'
const HELP_WITH_FLAG = 'Usage: codex [OPTIONS] [PROMPT]\n --no-daemon Run in-process\n'
const HELP_WITHOUT_FLAG = 'Usage: codex [OPTIONS] [PROMPT]\n --no-alt-screen\n'
const hostPlatform = process.platform
describe.skipIf(hostPlatform === 'win32')('planCodexNoDaemonLaunch', () => {
let dir: string
let codex: string
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'orca-codex-no-daemon-launch-'))
codex = writeCodex('codex', HELP_WITH_FLAG)
})
afterEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: hostPlatform })
vi.unstubAllEnvs()
rmSync(dir, { recursive: true, force: true })
})
function writeCodex(name: string, help: string): string {
const path = join(dir, name)
mkdirSync(dirname(path), { recursive: true })
writeFileSync(path, `#!/bin/sh\nprintf '%s' '${help}'\n`)
chmodSync(path, 0o755)
return path
}
function plan(command: string, overrides: Partial<LocalCodexLaunch> = {}) {
return planCodexNoDaemonLaunch({
command,
executesOnThisHost: true,
shellOverride: undefined,
env: {},
cwd: dir,
...overrides
})
}
it('adds --no-daemon once, right after a path-named codex', async () => {
await expect(plan(`${codex} --yolo 'fix the bug'`)).resolves.toBe(
`${codex} --no-daemon --yolo 'fix the bug'`
)
})
it.each([
['agents'],
['queue --thread T'],
['--no-daemon'],
['resume --no-daemon'],
['--remote unix://'],
['--remote=ws://h:1']
])('leaves `codex %s` alone: it needs the shared server or has the flag', (args) => {
expect(plan(`${codex} ${args}`)).toBeNull()
})
it('leaves SSH and WSL launches to the codex function on that host', () => {
expect(plan(`${codex} --yolo`, { executesOnThisHost: false })).toBeNull()
})
it('ignores an inherited opt-out the pane deletes', async () => {
vi.stubEnv('ORCA_CODEX_ISOLATE', '0')
await expect(plan(`${codex} --yolo`, { envToDelete: ['ORCA_CODEX_ISOLATE'] })).resolves.toBe(
`${codex} --no-daemon --yolo`
)
})
it('honours ORCA_CODEX_ISOLATE=0 from the pane env', () => {
expect(plan(`${codex} --yolo`, { env: { ORCA_CODEX_ISOLATE: '0' } })).toBeNull()
})
it('keeps the command when the binary predates --no-daemon', async () => {
const oldCodex = writeCodex('0.155/codex', HELP_WITHOUT_FLAG)
await expect(plan(`${oldCodex} --yolo`)).resolves.toBe(`${oldCodex} --yolo`)
})
it.each([['codex --yolo'], ['claude --yolo'], ['/opt/bin/codexx --yolo']])(
'leaves `%s` to the shell function or to another agent',
(command) => {
expect(plan(command)).toBeNull()
}
)
it('probes a bare codex on PATH for cmd.exe, which has no codex function', async () => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
writeCodex('codex.exe', HELP_WITH_FLAG)
await expect(
plan('codex --yolo', {
shellOverride: 'cmd.exe',
env: { PATH: dir, PATHEXT: '.exe' }
})
).resolves.toBe('codex --no-daemon --yolo')
})
})
@@ -0,0 +1,99 @@
import { isAbsolute, win32 as pathWin32 } from 'node:path'
import { runProcess } from '../../shared/child-process/run-process'
import { tokenizeStartupCommand } from '../../shared/tui-agent-startup-shell'
import { resolveLocalWindowsAgentStartupShell } from '../../shared/windows-terminal-shell'
import { resolveCommandOnLocalPath } from '../ipc/command-path-resolver'
import { CODEX_SHARED_SERVER_ARGS } from '../../shared/codex-shell-function'
const CODEX_EXECUTABLE = /^codex(\.(exe|cmd|bat|ps1))?$/i
const SHARED_SERVER_ARGS: ReadonlySet<string> = new Set(CODEX_SHARED_SERVER_ARGS)
// Why bounded: a hung binary must not hold the pane; a slow probe only costs the flag.
const HELP_PROBE_TIMEOUT_MS = 5_000
export type LocalCodexLaunch = {
command: string | undefined
/** False for SSH and WSL spawns: their shell's codex function probes on that host. */
executesOnThisHost: boolean
/** Shell the provider will launch; undefined means the platform default. */
shellOverride: string | undefined
/** Env the PTY gets on top of this process's own. */
env: Record<string, string | undefined> | undefined
/** Keys the provider removes from that merged env. */
envToDelete?: readonly string[]
cwd: string | undefined
}
/**
* The launch command with `--no-daemon` after the Codex executable, applying the
* shell codex function's rule (src/shared/codex-shell-function.ts) where that
* function never runs: cmd.exe defines none, and a path-named binary bypasses it.
* Everywhere else the function probes the binary the shell itself resolves after
* the user's startup files, which main cannot see. Null (synchronously) when
* nothing applies: an extra await tick would reorder the pane-spawn reservation
* races the spawn handlers arbitrate right after this.
*/
export function planCodexNoDaemonLaunch(launch: LocalCodexLaunch): Promise<string> | null {
const { command } = launch
if (!command || !launch.executesOnThisHost) {
return null
}
const shell =
resolveLocalWindowsAgentStartupShell({
platform: process.platform,
isRemote: false,
terminalWindowsShell: launch.shellOverride
}) ?? 'posix'
const parsed = tokenizeStartupCommand(command, shell)
const executableSpan = parsed.ok ? parsed.spans[0] : undefined
if (!parsed.ok || !executableSpan || executableSpan.divergesFromShell) {
return null
}
const [executable, ...args] = parsed.tokens
if (
!CODEX_EXECUTABLE.test(pathWin32.basename(executable)) ||
(shell !== 'cmd' && !isAbsolute(executable))
) {
return null
}
const env = { ...process.env, ...launch.env }
for (const key of launch.envToDelete ?? []) {
delete env[key]
}
if (
env.ORCA_CODEX_ISOLATE === '0' ||
args.some((arg) => SHARED_SERVER_ARGS.has(arg) || arg.startsWith('--remote='))
) {
return null
}
return supportsNoDaemon(executable, env, launch.cwd).then((supported) =>
supported
? `${command.slice(0, executableSpan.end)} --no-daemon${command.slice(executableSpan.end)}`
: command
)
}
// Why probe every launch: a cached answer goes stale across an upgrade, and 0.155 and older exit 2 on the flag.
async function supportsNoDaemon(
executable: string,
env: NodeJS.ProcessEnv,
cwd: string | undefined
): Promise<boolean> {
const program = isAbsolute(executable)
? executable
: await resolveCommandOnLocalPath(executable, { env, cwd })
if (!program) {
return false
}
try {
const help = await runProcess({
program,
args: ['--help'],
cwd,
env,
timeoutMs: HELP_PROBE_TIMEOUT_MS
})
return help.stdout.includes('--no-daemon')
} catch {
return false
}
}
@@ -13,12 +13,12 @@ import { tmpdir } from 'node:os'
import { delimiter, isAbsolute, join } from 'node:path'
import { execFileSync, spawnSync } from 'node:child_process'
import { afterEach, describe, expect, it } from 'vitest'
import { resolveCodexShellLaunchPreflightCommand } from './codex-shell-launch-preflight'
import {
getFishCodexShellLaunchPreflight,
getPosixCodexShellLaunchPreflight,
getPowerShellCodexShellLaunchPreflight,
resolveCodexShellLaunchPreflightCommand
} from './codex-shell-launch-preflight'
getPowerShellCodexShellLaunchPreflight
} from '../../shared/codex-shell-function'
import { fishRequirementViolation, resolveFishBinary } from '../../shared/fish-binary-requirement'
const roots: string[] = []
@@ -64,96 +64,3 @@ function isExecutableFileOnDisk(path: string, platform: NodeJS.Platform): boolea
return false
}
}
// Why --no-daemon: Codex 0.156+ otherwise shares one server per CODEX_HOME that runs every tab's
// hooks with the first tab's Orca env and dies with it (#22873). These args need that server or exit 2.
export const CODEX_SHARED_SERVER_ARGS = ['agents', 'queue', '--no-daemon', '--remote'] as const
const CODEX_SHARED_SERVER_ARG_PATTERN = `^(${CODEX_SHARED_SERVER_ARGS.join('|')}|--remote=.*)$`
export function getPosixCodexShellLaunchPreflight(): string {
return `# Why: a typed alias expands inside the shell, after pane launch prep.
# Why unalias inside the substitution: an alias named codex makes command -v
# report the alias text, and the subshell leaves the user's own alias intact.
# Why || : twice — zsh alone aborts inside the substitution, but every shell's
# assignment adopts its exit status, so an absent codex trips set -e in bash too.
__orca_codex_binary="$(unalias codex 2>/dev/null || :; command -v codex 2>/dev/null || :)"
if [[ -n "\${__orca_codex_binary:-}" && -x "\${__orca_codex_binary}" ]]; then
# Why the function reserved word: it suppresses alias expansion of the name,
# which otherwise rewrites this header at parse time and aborts the whole file.
function codex {
# Why local: zsh's warn_create_global warns for each global a function creates.
local __orca_codex_arg __orca_codex_isolate="\${ORCA_CODEX_ISOLATE:-1}"
if [[ -n "\${ORCA_CODEX_LAUNCH_PREFLIGHT:-}" && -x "\${ORCA_CODEX_LAUNCH_PREFLIGHT}" ]]; then
"\${ORCA_CODEX_LAUNCH_PREFLIGHT}" agent hooks prepare-codex >/dev/null 2>&1 || :
fi
for __orca_codex_arg in "$@"; do
case "$__orca_codex_arg" in ${CODEX_SHARED_SERVER_ARGS.join('|')}|--remote=*) __orca_codex_isolate=0 ;; esac
done
# Why probe every launch: a cached answer goes stale across an upgrade, and 0.155 and older exit 2 on the flag.
if [[ "$__orca_codex_isolate" != 0 ]]; then
case "$(command codex --help 2>/dev/null </dev/null)" in *--no-daemon*) set -- --no-daemon "$@" ;; esac
fi
command codex "$@"
}
fi
unset __orca_codex_binary
`
}
export function getFishCodexShellLaunchPreflight(): string {
return `# Why captured: an unquoted (type -t codex) expands to zero words when codex is
# absent, leaving "test = file" — fish then errors instead of failing closed.
# Quoting in place is not the fix; fish never substitutes inside double quotes.
set -l __orca_codex_type (type -t codex 2>/dev/null)
if test "$__orca_codex_type" = file
function codex
if test -x "$ORCA_CODEX_LAUNCH_PREFLIGHT"
command "$ORCA_CODEX_LAUNCH_PREFLIGHT" agent hooks prepare-codex >/dev/null 2>&1; or true
end
if test "$ORCA_CODEX_ISOLATE" != 0; and not string match -qr -- '${CODEX_SHARED_SERVER_ARG_PATTERN}' $argv; and command codex --help 2>/dev/null </dev/null | string match -q -- '*--no-daemon*'
set argv --no-daemon $argv
end
command codex $argv
end
end
set -e __orca_codex_type`
}
export function getPowerShellCodexShellLaunchPreflight(): string {
return `$orcaCodexCommand = Get-Command codex -ErrorAction SilentlyContinue | Select-Object -First 1
if ($orcaCodexCommand -and
$orcaCodexCommand.CommandType -in @("Application", "ExternalScript")) {
function Global:codex {
$orcaCodexExecutable = Get-Command codex -CommandType Application,ExternalScript -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $orcaCodexExecutable) {
Write-Error "codex executable not found"
$global:LASTEXITCODE = 127
return
}
$orcaCodexFlags = @()
# Why try/catch: under the user's $ErrorActionPreference = 'Stop', a failing prep or probe must not abort the launch.
if ($env:ORCA_CODEX_LAUNCH_PREFLIGHT) {
try {
& $env:ORCA_CODEX_LAUNCH_PREFLIGHT agent hooks prepare-codex *> $null
} catch {
}
}
if ($env:ORCA_CODEX_ISOLATE -ne '0' -and -not (@($args) -cmatch '${CODEX_SHARED_SERVER_ARG_PATTERN}')) {
try {
if ((& $orcaCodexExecutable.Source --help 2>$null) -match '--no-daemon') {
$orcaCodexFlags = @('--no-daemon')
}
} catch {
}
}
# Why: a native command inside a function never sees the function's pipeline input on its own.
if ($MyInvocation.ExpectingInput) {
$input | & $orcaCodexExecutable.Source @orcaCodexFlags @args
} else {
& $orcaCodexExecutable.Source @orcaCodexFlags @args
}
$global:LASTEXITCODE = $LASTEXITCODE
}
}
Remove-Variable orcaCodexCommand -ErrorAction SilentlyContinue`
}
+1 -1
View File
@@ -15,7 +15,7 @@ import {
getFishCodexShellLaunchPreflight,
getPosixCodexShellLaunchPreflight,
getPowerShellCodexShellLaunchPreflight
} from './codex-shell-launch-preflight'
} from '../../shared/codex-shell-function'
import { resolveFishBinary } from '../../shared/fish-binary-requirement'
const isWindows = process.platform === 'win32'
+1 -1
View File
@@ -28,7 +28,7 @@
*/
import { getPosixOmpShellWrapper } from './pty/omp-shell-wrapper'
import { WSL_MANAGED_CLI_PATH_RESTORE } from './wsl-managed-cli-path-restore'
import { getPosixCodexShellLaunchPreflight } from './pty/codex-shell-launch-preflight'
import { getPosixCodexShellLaunchPreflight } from '../shared/codex-shell-function'
import {
getZshShellReadyMarkerRegistrationBlock,
SHELL_STARTUP_IDENTITY_MARKER_BLOCK,
+1 -1
View File
@@ -1,7 +1,7 @@
import { readFileSync, statSync } from 'node:fs'
import { join } from 'node:path'
import { getPosixOmpShellWrapper } from '../main/pty/omp-shell-wrapper'
import { getPosixCodexShellLaunchPreflight } from '../main/pty/codex-shell-launch-preflight'
import { getPosixCodexShellLaunchPreflight } from '../shared/codex-shell-function'
import {
BASH_FEATURE_CHANNEL_BLOCK,
BASH_PROMPT_COMMAND_COMPOSITION_BLOCK,
+94
View File
@@ -0,0 +1,94 @@
// Orca's `codex` shell function for every shell family: runs launch prep and adds
// --no-daemon. Pure text, so any host that writes a shell script can embed it.
// Why --no-daemon: Codex 0.156+ otherwise shares one server per CODEX_HOME that runs every tab's
// hooks with the first tab's Orca env and dies with it (#22873). These args need that server or exit 2.
export const CODEX_SHARED_SERVER_ARGS = ['agents', 'queue', '--no-daemon', '--remote'] as const
const CODEX_SHARED_SERVER_ARG_PATTERN = `^(${CODEX_SHARED_SERVER_ARGS.join('|')}|--remote=.*)$`
export function getPosixCodexShellLaunchPreflight(): string {
return `# Why: a typed alias expands inside the shell, after pane launch prep.
# Why unalias inside the substitution: an alias named codex makes command -v
# report the alias text, and the subshell leaves the user's own alias intact.
# Why || : twice — zsh alone aborts inside the substitution, but every shell's
# assignment adopts its exit status, so an absent codex trips set -e in bash too.
__orca_codex_binary="$(unalias codex 2>/dev/null || :; command -v codex 2>/dev/null || :)"
if [[ -n "\${__orca_codex_binary:-}" && -x "\${__orca_codex_binary}" ]]; then
# Why the function reserved word: it suppresses alias expansion of the name,
# which otherwise rewrites this header at parse time and aborts the whole file.
function codex {
# Why local: zsh's warn_create_global warns for each global a function creates.
local __orca_codex_arg __orca_codex_isolate="\${ORCA_CODEX_ISOLATE:-1}"
if [[ -n "\${ORCA_CODEX_LAUNCH_PREFLIGHT:-}" && -x "\${ORCA_CODEX_LAUNCH_PREFLIGHT}" ]]; then
"\${ORCA_CODEX_LAUNCH_PREFLIGHT}" agent hooks prepare-codex >/dev/null 2>&1 || :
fi
for __orca_codex_arg in "$@"; do
case "$__orca_codex_arg" in ${CODEX_SHARED_SERVER_ARGS.join('|')}|--remote=*) __orca_codex_isolate=0 ;; esac
done
# Why probe every launch: a cached answer goes stale across an upgrade, and 0.155 and older exit 2 on the flag.
if [[ "$__orca_codex_isolate" != 0 ]]; then
case "$(command codex --help 2>/dev/null </dev/null)" in *--no-daemon*) set -- --no-daemon "$@" ;; esac
fi
command codex "$@"
}
fi
unset __orca_codex_binary
`
}
export function getFishCodexShellLaunchPreflight(): string {
return `# Why captured: an unquoted (type -t codex) expands to zero words when codex is
# absent, leaving "test = file" — fish then errors instead of failing closed.
# Quoting in place is not the fix; fish never substitutes inside double quotes.
set -l __orca_codex_type (type -t codex 2>/dev/null)
if test "$__orca_codex_type" = file
function codex
if test -x "$ORCA_CODEX_LAUNCH_PREFLIGHT"
command "$ORCA_CODEX_LAUNCH_PREFLIGHT" agent hooks prepare-codex >/dev/null 2>&1; or true
end
if test "$ORCA_CODEX_ISOLATE" != 0; and not string match -qr -- '${CODEX_SHARED_SERVER_ARG_PATTERN}' $argv; and command codex --help 2>/dev/null </dev/null | string match -q -- '*--no-daemon*'
set argv --no-daemon $argv
end
command codex $argv
end
end
set -e __orca_codex_type`
}
export function getPowerShellCodexShellLaunchPreflight(): string {
return `$orcaCodexCommand = Get-Command codex -ErrorAction SilentlyContinue | Select-Object -First 1
if ($orcaCodexCommand -and
$orcaCodexCommand.CommandType -in @("Application", "ExternalScript")) {
function Global:codex {
$orcaCodexExecutable = Get-Command codex -CommandType Application,ExternalScript -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $orcaCodexExecutable) {
Write-Error "codex executable not found"
$global:LASTEXITCODE = 127
return
}
$orcaCodexFlags = @()
# Why try/catch: under the user's $ErrorActionPreference = 'Stop', a failing prep or probe must not abort the launch.
if ($env:ORCA_CODEX_LAUNCH_PREFLIGHT) {
try {
& $env:ORCA_CODEX_LAUNCH_PREFLIGHT agent hooks prepare-codex *> $null
} catch {
}
}
if ($env:ORCA_CODEX_ISOLATE -ne '0' -and -not (@($args) -cmatch '${CODEX_SHARED_SERVER_ARG_PATTERN}')) {
try {
if ((& $orcaCodexExecutable.Source --help 2>$null) -match '--no-daemon') {
$orcaCodexFlags = @('--no-daemon')
}
} catch {
}
}
# Why: a native command inside a function never sees the function's pipeline input on its own.
if ($MyInvocation.ExpectingInput) {
$input | & $orcaCodexExecutable.Source @orcaCodexFlags @args
} else {
& $orcaCodexExecutable.Source @orcaCodexFlags @args
}
$global:LASTEXITCODE = $LASTEXITCODE
}
}
Remove-Variable orcaCodexCommand -ErrorAction SilentlyContinue`
}
@@ -0,0 +1,90 @@
import { spawn, spawnSync, type ChildProcess } from 'node:child_process'
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { createSequencedSetupAgentCommands } from './setup-agent-sequencing'
// Why: the POSIX gate evals the agent in a fresh `bash -lc`, which never reads
// Orca's shell wrapper, so it must carry the codex --no-daemon rule itself.
const roots: string[] = []
const gates: ChildProcess[] = []
afterEach(() => {
for (const gate of gates.splice(0)) {
gate.kill()
}
for (const root of roots.splice(0)) {
rmSync(root, { recursive: true, force: true })
}
})
function prepareGate(startupCommand: string) {
const root = mkdtempSync(join(tmpdir(), 'orca-gate-codex-'))
roots.push(root)
const bin = join(root, 'bin')
mkdirSync(bin)
// Why: `bash -l` reads /etc/profile, whose macOS path_helper moves the fixture bin
// behind /usr/local/bin and /opt/homebrew/bin; re-prepend it so a real codex never runs.
writeFileSync(join(root, '.bash_profile'), `export PATH=${JSON.stringify(bin)}:"$PATH"\n`)
const runner = join(root, 'setup-runner.sh')
const sequenced = createSequencedSetupAgentCommands({
runnerScriptPath: runner,
startupCommand,
platform: 'posix',
nonce: 'n1',
// Why: a failed assertion must not leave a gate polling for the default two hours.
waitTimeoutSeconds: 5
})
return {
sequenced,
// Why HOME and CODEX_HOME: `bash -l` must read no real profile, and nothing may touch ~/.codex.
env: { HOME: root, CODEX_HOME: root, PATH: `${bin}:/usr/bin:/bin`, ...sequenced.startupEnv },
finishSetup: (help: string) => {
const codex = join(bin, 'codex')
writeFileSync(
codex,
`#!/bin/sh\n[ "$1" = --help ] && { printf '%s\\n' '${help}'; exit 0; }\nprintf 'ARGV:%s\\n' "$*"\n`
)
chmodSync(codex, 0o755)
writeFileSync(`${runner}.n1.done`, 'n1:0\n')
}
}
}
function runGate(startupCommand: string, help: string, env: Record<string, string> = {}): string {
const gate = prepareGate(startupCommand)
gate.finishSetup(help)
return spawnSync('bash', ['-c', gate.sequenced.startupCommand], {
encoding: 'utf8',
env: { ...gate.env, ...env }
}).stdout
}
describe.skipIf(process.platform === 'win32')('sequenced setup gate runs codex', () => {
it('with --no-daemon when the binary supports it', () => {
expect(runGate('codex --yolo', '--no-daemon')).toBe('ARGV:--no-daemon --yolo\n')
})
it.each([
['an old binary', 'codex --yolo', '--no-alt-screen', {}],
['a subcommand that needs the shared server', 'codex agents', '--no-daemon', {}],
['the opt-out', 'codex --yolo', '--no-daemon', { ORCA_CODEX_ISOLATE: '0' }]
])('unchanged for %s', (_case, command, help, env) => {
expect(runGate(command, help, env)).toBe(`ARGV:${command.slice('codex '.length)}\n`)
})
it('with --no-daemon when setup is what installs codex', async () => {
const gate = prepareGate('codex --yolo')
const child = spawn('bash', ['-c', gate.sequenced.startupCommand], { env: gate.env })
gates.push(child)
let stdout = ''
child.stdout.on('data', (chunk: Buffer) => (stdout += chunk.toString()))
const exited = new Promise((resolve) => child.on('close', resolve))
// Why after spawn: the gate is already waiting when setup puts codex on PATH.
await new Promise((resolve) => setTimeout(resolve, 300))
gate.finishSetup('--no-daemon')
await exited
expect(stdout).toBe('ARGV:--no-daemon --yolo\n')
})
})
+4 -1
View File
@@ -1,4 +1,5 @@
import { encodePowerShellCommand } from './powershell-command-encoding'
import { getPosixCodexShellLaunchPreflight } from './codex-shell-function'
import {
nativeWindowsPathToPosixShellPath,
resolveSetupRunnerCommand,
@@ -128,7 +129,9 @@ function buildPosixStartupScript(
`rm -f ${marker} ${tmp} 2>/dev/null;`,
// Why: failure and timeout announce themselves; a silent success left
// "Waiting for setup..." as the pane's last line forever.
`if [ "$status" = "0" ]; then echo ${quotePosixArg(SETUP_COMPLETE_MESSAGE)} >&2; if [ -n "\${${SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV}:-}" ]; then eval "\$${SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV}"; exit "$?"; else ${startupSuccessCommand}; fi; fi;`,
// Why here and not first: setup may be what puts codex on PATH, and this
// `bash -lc` never reads Orca's shell wrapper, so it defines the function itself.
`if [ "$status" = "0" ]; then echo ${quotePosixArg(SETUP_COMPLETE_MESSAGE)} >&2;\n${getPosixCodexShellLaunchPreflight()}if [ -n "\${${SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV}:-}" ]; then eval "\$${SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV}"; exit "$?"; else ${startupSuccessCommand}; fi; fi;`,
'echo "Setup failed; skipping agent startup." >&2;',
'exit "${status:-1}";',
'fi;',