Merge remote-tracking branch 'origin/main' into brennanb2025/sta-3919-mobile-picker-lag

This commit is contained in:
Brennan Benson
2026-08-27 13:18:54 -07:00
31 changed files with 917 additions and 54 deletions
+28
View File
@@ -92,6 +92,7 @@ describe('agent hooks CLI handler', () => {
})
afterEach(() => {
vi.unstubAllEnvs()
vi.restoreAllMocks()
rmSync(userDataPath, { recursive: true, force: true })
})
@@ -139,6 +140,33 @@ describe('agent hooks CLI handler', () => {
})
})
it('forwards WSL pane routing to the runtime exactly once without using the host installer', async () => {
const home = '/home/jin/.local/share/orca/codex-runtime-home/home'
vi.stubEnv('CODEX_HOME', home)
vi.stubEnv('ORCA_CODEX_HOME', home)
vi.stubEnv('WSL_DISTRO_NAME', 'Ubuntu-24.04')
callMock.mockResolvedValue({ result: { state: 'installed' } })
await main(['agent', 'hooks', 'prepare-codex'], userDataPath)
expect(callMock).toHaveBeenCalledExactlyOnceWith(
'agentHooks.prepareCodexForWslPane',
{ codexHome: home, orcaCodexHome: home, wslDistro: 'Ubuntu-24.04' },
{ timeoutMs: 50_000 }
)
expect(prepareManagedCodexHomeBeforeShellLaunchMock).not.toHaveBeenCalled()
})
it('fails open when WSL runtime preparation is unavailable', async () => {
vi.stubEnv('CODEX_HOME', '/home/jin/.local/share/orca/codex-runtime-home/home')
vi.stubEnv('ORCA_CODEX_HOME', '/home/jin/.local/share/orca/codex-runtime-home/home')
vi.stubEnv('WSL_DISTRO_NAME', 'Ubuntu')
callMock.mockRejectedValue(new Error('method_not_found'))
await expect(main(['agent', 'hooks', 'prepare-codex'], userDataPath)).resolves.toBeUndefined()
expect(prepareManagedCodexHomeBeforeShellLaunchMock).not.toHaveBeenCalled()
})
it('honors Codex-specific disablement when the runtime is unavailable', async () => {
const state = getDefaultPersistedState(userDataPath)
state.settings.disabledTuiAgents = ['codex']
+19
View File
@@ -28,6 +28,9 @@ type AgentHookCommandResult = {
statuses: AgentHookInstallStatus[]
}
// Covers managed-home verification, WSL identity, trust grant, and bounded app-server reap.
const WSL_CODEX_PREPARE_TIMEOUT_MS = 50_000
function getDataPath(): string {
const userDataPath = getDefaultUserDataPath()
const indexPath = join(userDataPath, 'orca-profile-index.json')
@@ -207,6 +210,22 @@ async function setAgentHooksEnabled(
export const AGENT_HOOK_HANDLERS: Record<string, CommandHandler> = {
'agent hooks prepare-codex': async ({ client }) => {
if (process.env.WSL_DISTRO_NAME?.trim()) {
try {
await client.call(
'agentHooks.prepareCodexForWslPane',
{
codexHome: process.env.CODEX_HOME ?? '',
orcaCodexHome: process.env.ORCA_CODEX_HOME ?? '',
wslDistro: process.env.WSL_DISTRO_NAME
},
{ timeoutMs: WSL_CODEX_PREPARE_TIMEOUT_MS }
)
} catch {
// Best effort: old or unavailable runtimes must not block Codex launch.
}
return
}
const settings = await readHookSettings(client)
await prepareManagedCodexHomeBeforeShellLaunch({
userDataPath: getDefaultUserDataPath(),
@@ -72,7 +72,7 @@ fi
# 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_LAUNCH_PREFLIGHT:-}" && -n "${__orca_codex_binary:-}" && -x "${__orca_codex_binary}" ]]; then
if [[ -n "${ORCA_CODEX_LAUNCH_PREFLIGHT:-}" && -x "${ORCA_CODEX_LAUNCH_PREFLIGHT}" && -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 {
@@ -2,7 +2,7 @@ function __orca_shell_ready_marker --on-event fish_prompt
builtin printf "\033]777;orca-shell-ready\007"
functions -e __orca_shell_ready_marker
end
if test -n "$ORCA_CODEX_LAUNCH_PREFLIGHT"; and test (type -t codex 2>/dev/null) = file
if test -x "$ORCA_CODEX_LAUNCH_PREFLIGHT"; and test (type -t codex 2>/dev/null) = file
function codex
command "$ORCA_CODEX_LAUNCH_PREFLIGHT" agent hooks prepare-codex >/dev/null 2>&1; or true
command codex $argv
@@ -110,7 +110,7 @@ __orca_deferred_init() {
# 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_LAUNCH_PREFLIGHT:-}" && -n "${__orca_codex_binary:-}" && -x "${__orca_codex_binary}" ]]; then
if [[ -n "${ORCA_CODEX_LAUNCH_PREFLIGHT:-}" && -x "${ORCA_CODEX_LAUNCH_PREFLIGHT}" && -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 {
@@ -75,7 +75,7 @@ fi
# 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_LAUNCH_PREFLIGHT:-}" && -n "${__orca_codex_binary:-}" && -x "${__orca_codex_binary}" ]]; then
if [[ -n "${ORCA_CODEX_LAUNCH_PREFLIGHT:-}" && -x "${ORCA_CODEX_LAUNCH_PREFLIGHT}" && -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 {
@@ -2,7 +2,7 @@ function __orca_shell_ready_marker --on-event fish_prompt
builtin printf "\033]777;orca-shell-ready\007"
functions -e __orca_shell_ready_marker
end
if test -n "$ORCA_CODEX_LAUNCH_PREFLIGHT"; and test (type -t codex 2>/dev/null) = file
if test -x "$ORCA_CODEX_LAUNCH_PREFLIGHT"; and test (type -t codex 2>/dev/null) = file
function codex
command "$ORCA_CODEX_LAUNCH_PREFLIGHT" agent hooks prepare-codex >/dev/null 2>&1; or true
command codex $argv
@@ -110,7 +110,7 @@ __orca_deferred_init() {
# 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_LAUNCH_PREFLIGHT:-}" && -n "${__orca_codex_binary:-}" && -x "${__orca_codex_binary}" ]]; then
if [[ -n "${ORCA_CODEX_LAUNCH_PREFLIGHT:-}" && -x "${ORCA_CODEX_LAUNCH_PREFLIGHT}" && -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 {
@@ -284,7 +284,7 @@ describe('remote hook service installers', () => {
hooks: Record<string, { hooks: { command: string }[] }[]>
}
expect(hooks.hooks.Stop?.[0]?.hooks?.[0]?.command).toContain(
'/home/dev/.orca/agent-hooks/codex-hook.sh'
'/home/dev/.local/share/orca/codex-runtime-home/home/.orca/agent-hooks/codex-hook.sh'
)
expect(fs.files.get(`${runtimeHome}/config.toml`)).toContain(
`${runtimeHome}/hooks.json:stop:0:0`
+24 -7
View File
@@ -12,19 +12,23 @@ import {
type ManagedHookDetectionSettings
} from './managed-hook-detection-commands'
import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer'
import { wslCodexRuntimeHomeForGuestHome } from '../pty/codex-home-wsl-env'
import { WSL_HOOK_FS_METHODS, type WslFsResult } from '../../shared/wsl-hook-relay-contract'
import type { AgentHookInstallStatus } from '../../shared/agent-hook-types'
/** Run the shared remote hook installers against a WSL guest over the relay's fs bridge. */
export async function installWslGuestHooks(options: {
mux: SshChannelMultiplexer
guestHome: string
codexHomePath: string | null
distro: string
installHooks: typeof installRemoteManagedAgentHooks
settings: ManagedHookDetectionSettings
warn: (message: string) => void
/** Canonical runtime-host installer for redirected Codex homes. */
installCodex: (runtimeHomePath: string, distro: string) => Promise<AgentHookInstallStatus | null>
}): Promise<void> {
const { mux, guestHome, distro, installHooks, settings, warn } = options
const { mux, guestHome, codexHomePath, distro, installHooks, settings, warn, installCodex } =
options
let agents
try {
const detected = (await mux.request('preflight.detectAgents', {
@@ -42,12 +46,25 @@ export async function installWslGuestHooks(options: {
if (agents.length === 0) {
return
}
if (agents.includes('codex') && codexHomePath) {
try {
const status = await installCodex(codexHomePath, distro)
if (status?.state === 'error') {
warn(
`[agent-hooks] WSL Codex hook install for '${distro}' failed: ${status.detail ?? 'unknown error'}`
)
}
} catch (error) {
warn(
`[agent-hooks] WSL Codex hook install for '${distro}' failed: ${error instanceof Error ? error.message : String(error)}`
)
}
}
// Codex is redirected into the runtime home and must use the canonical
// runtime-host writer above; the relay adapter owns all other agents.
const remoteAgents = agents.filter((agent) => agent !== 'codex')
const results = await installHooks(createWslHookSftpAdapter(mux), guestHome, {
agents,
// WSL Codex launches use Orca's managed runtime CODEX_HOME, not ~/.codex.
// Keep relay hooks in that active home so status callbacks are received.
codexHomeDir: wslCodexRuntimeHomeForGuestHome(guestHome),
deferTrustUntilConfigToml: true
agents: remoteAgents
})
const failed = results.filter((r) => r.state === 'error').length
if (failed > 0) {
@@ -9,6 +9,8 @@ import { agentHookServer } from './server'
import type { ManagedHookDetectionSettings } from './managed-hook-detection-commands'
import { installRemoteManagedAgentHooks } from './remote-managed-hook-installers'
import { getOpenCodePluginSource } from '../opencode/hook-service'
import { codexHookService } from '../codex/hook-service'
import type { AgentHookInstallStatus } from '../../shared/agent-hook-types'
import type { PluginSources } from '../../relay/plugin-overlay'
import {
isWslDistroRunning,
@@ -61,6 +63,7 @@ export type WslHookRelayManagerDeps = {
waitForSentinel: typeof waitForWslRelaySentinel
ingest: (envelope: Record<string, unknown>, connectionId: string) => void
installHooks: typeof installRemoteManagedAgentHooks
installCodex: (runtimeHomePath: string, distro: string) => Promise<AgentHookInstallStatus | null>
managedHookSettings: () => ManagedHookDetectionSettings
/** Plugin source strings shipped to the guest relay so an Orca update needn't redeploy the relay bundle. */
pluginSources: () => PluginSources
@@ -102,6 +105,11 @@ export const defaultWslHookRelayDeps: WslHookRelayManagerDeps = {
connectionId
),
installHooks: installRemoteManagedAgentHooks,
installCodex: (runtimeHomePath, distro) =>
codexHookService.installForRuntimeHomeSerialized(runtimeHomePath, {
runtime: 'wsl',
wslDistro: distro
}),
managedHookSettings: () => null,
// Why: only OpenCode is in scope for WSL now; the payload shape stays identical to SSH so Pi/OMP are additive later.
pluginSources: () => ({ opencodePluginSource: getOpenCodePluginSource() }),
@@ -6,13 +6,14 @@ import type { ManagedHookDetectionSettings } from './managed-hook-detection-comm
import type { installRemoteManagedAgentHooks } from './remote-managed-hook-installers'
import { requestGuestOpenCodeOverlayDir } from './wsl-guest-plugin-install'
import { installWslGuestHooks } from './wsl-hook-fs-adapter'
import { REINSTALL_MIN_INTERVAL_MS } from './wsl-hook-relay-deps'
import { REINSTALL_MIN_INTERVAL_MS, type WslHookRelayManagerDeps } from './wsl-hook-relay-deps'
import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer'
import type { PluginSources } from '../../relay/plugin-overlay'
/** Structural slice of WslHookRelayManagerDeps — only what an install pass uses. */
type GuestInstallDeps = {
installHooks: typeof installRemoteManagedAgentHooks
installCodex: WslHookRelayManagerDeps['installCodex']
managedHookSettings: () => ManagedHookDetectionSettings
pluginSources: () => PluginSources
warn: (message: string) => void
@@ -23,6 +24,7 @@ type GuestInstallState = {
distro: string
mux?: SshChannelMultiplexer
guestHome?: string
codexHomePath?: string
opencodeOverlayDir?: string
lastInstallAt?: number
}
@@ -37,8 +39,10 @@ export async function runWslRelayGuestInstall(
await installWslGuestHooks({
mux,
guestHome,
codexHomePath: state.codexHomePath ?? null,
distro: state.distro,
installHooks: deps.installHooks,
installCodex: deps.installCodex,
settings: deps.managedHookSettings(),
warn: deps.warn
})
@@ -13,7 +13,8 @@ import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import { AgentHookServer } from './server'
import { WslHookRelayManager } from './wsl-hook-relay-manager'
import { wslCodexRuntimeHomeForGuestHome } from '../pty/codex-home-wsl-env'
import { createManagedHookLocalFilesystem } from './managed-hook-local-filesystem'
import { codexHookService } from '../codex/hook-service'
const BUNDLE_DIR = join(process.cwd(), 'out', 'relay', 'wsl')
const BUNDLE_JS = join(BUNDLE_DIR, 'wsl-agent-hook-relay.js')
@@ -75,6 +76,7 @@ describe.skipIf(process.platform === 'win32')(
const server = orcaServer
const warns: string[] = []
const codexHome = join(fakeHome, '.local', 'share', 'orca', 'codex-runtime-home', 'home')
manager = new WslHookRelayManager({
platform: () => 'win32',
remoteHooksEnabled: () => true,
@@ -102,6 +104,11 @@ describe.skipIf(process.platform === 'win32')(
envelope as Parameters<AgentHookServer['ingestRemote']>[0],
connectionId
),
installCodex: (runtimeHomePath) =>
codexHookService.installRemote(createManagedHookLocalFilesystem(), fakeHome, {
codexHomeDir: runtimeHomePath,
deferTrustUntilConfigToml: true
}),
managedHookSettings: () => ({
agentCmdOverrides: {
claude: process.execPath,
@@ -112,15 +119,17 @@ describe.skipIf(process.platform === 'win32')(
transientRetryDelayMs: 1
})
manager.ensureForDistro('LiveDistro')
manager.ensureForDistro('LiveDistro', codexHome)
// Waiting on Codex's artifact (not Claude's, which is written first) keeps the
// assertions behind the still-running 14-agent installer loop.
const codexHome = wslCodexRuntimeHomeForGuestHome(fakeHome)
await vi.waitFor(() => expect(existsSync(join(codexHome, 'hooks.json'))).toBe(true), {
timeout: 15_000
})
expect(existsSync(join(fakeHome, '.claude', 'settings.json'))).toBe(true)
await vi.waitFor(
() => expect(existsSync(join(fakeHome, '.claude', 'settings.json'))).toBe(true),
{ timeout: 15_000 }
)
const claudeScript = readFileSync(
join(fakeHome, '.orca', 'agent-hooks', 'claude-hook.sh'),
'utf8'
@@ -147,6 +147,8 @@ describe('WslHookRelayManager', () => {
// hosts — installHooks is mocked here, so the fs bridge only ever serves
// the wslfs.home request and never touches the real filesystem.
const home = '/home/wsl-test-user'
const codexHome =
'\\\\wsl.localhost\\Ubuntu\\home\\wsl-test-user\\.local\\share\\orca\\codex-runtime-home\\home'
const opencodeOverlayDir = `${home}/.orca-relay/opencode-overlays/deadbeefcafe`
let harnesses: GuestHarness[]
@@ -229,6 +231,13 @@ describe('WslHookRelayManager', () => {
waitForSentinel: vi.fn(async () => guestTransport()),
ingest: vi.fn(),
installHooks: vi.fn(async () => []),
installCodex: vi.fn(async () => ({
agent: 'codex' as const,
state: 'installed' as const,
configPath: `${home}/.local/share/orca/codex-runtime-home/home/hooks.json`,
managedHooksPresent: true,
detail: null
})),
managedHookSettings: () => null,
pluginSources: () => ({ opencodePluginSource: '// opencode plugin source' }),
warn: vi.fn(),
@@ -240,14 +249,14 @@ describe('WslHookRelayManager', () => {
it('starts one relay per distro, installs hooks, exposes the guest endpoint path, and forwards envelopes', async () => {
const { manager, deps } = createManager({})
manager.ensureForDistro('Ubuntu')
manager.ensureForDistro('Ubuntu')
manager.ensureForDistro('Ubuntu', codexHome)
manager.ensureForDistro('Ubuntu', codexHome)
await vi.waitFor(() => expect(deps.installHooks).toHaveBeenCalledTimes(1))
expect(deps.spawnRelay).toHaveBeenCalledTimes(1)
expect(deps.installCodex).toHaveBeenCalledWith(codexHome, 'Ubuntu')
// Codex is owned by the canonical runtime-host writer, not the relay adapter.
expect(deps.installHooks).toHaveBeenCalledWith(expect.anything(), home, {
agents: ['codex'],
codexHomeDir: `${home}/.local/share/orca/codex-runtime-home/home`,
deferTrustUntilConfigToml: true
agents: []
})
expect(manager.getGuestEndpointFilePath('Ubuntu')).toBe(
@@ -272,9 +281,23 @@ describe('WslHookRelayManager', () => {
manager.disposeAll()
})
it('reinstalls into a newly resolved runtime home without restarting the relay', async () => {
const { manager, deps } = createManager({})
manager.ensureForDistro('Ubuntu', codexHome)
await vi.waitFor(() => expect(deps.installCodex).toHaveBeenCalledTimes(1))
const nextHome = codexHome.replace('codex-runtime-home', 'codex-accounts\\account-2')
manager.ensureForDistro('ubuntu', nextHome)
await vi.waitFor(() => expect(deps.installCodex).toHaveBeenCalledTimes(2))
expect(deps.installCodex).toHaveBeenLastCalledWith(nextHome, 'Ubuntu')
expect(deps.spawnRelay).toHaveBeenCalledTimes(1)
manager.disposeAll()
})
it('ships the OpenCode plugin to the guest and exposes the overlay dir', async () => {
const { manager } = createManager({})
manager.ensureForDistro('Ubuntu')
manager.ensureForDistro('Ubuntu', codexHome)
await vi.waitFor(() => expect(manager.getOpenCodeOverlayDir('Ubuntu')).toBe(opencodeOverlayDir))
manager.disposeAll()
})
@@ -397,7 +420,7 @@ describe('WslHookRelayManager', () => {
it('stops live relays and refuses to revive them once agent status hooks are switched off', async () => {
const settings = { agentStatusHooksEnabled: true }
const { manager, deps } = createManager({ managedHookSettings: () => settings })
manager.ensureForDistro('Ubuntu')
manager.ensureForDistro('Ubuntu', codexHome)
await vi.waitFor(() => expect(deps.installHooks).toHaveBeenCalledTimes(1))
settings.agentStatusHooksEnabled = false
@@ -414,6 +437,8 @@ describe('WslHookRelayManager', () => {
settings.agentStatusHooksEnabled = true
manager.resumeStoppedRelays()
await vi.waitFor(() => expect(deps.spawnRelay).toHaveBeenCalledTimes(2))
await vi.waitFor(() => expect(deps.installCodex).toHaveBeenCalledTimes(2))
expect(deps.installCodex).toHaveBeenLastCalledWith(codexHome, 'Ubuntu')
manager.disposeAll()
})
+27 -8
View File
@@ -29,6 +29,10 @@ import {
WSL_HOOK_FS_METHODS,
wslHookRelayEndpointFilePath
} from '../../shared/wsl-hook-relay-contract'
import {
recordManagedWslCodexHome,
wslRuntimeHomePathsEqual
} from '../codex/managed-wsl-codex-home-registry'
type DistroState = {
/** Original casing for wsl.exe argv and breadcrumbs; map keys are lowercased. */
@@ -37,6 +41,7 @@ type DistroState = {
child?: ChildProcessWithoutNullStreams
mux?: SshChannelMultiplexer
guestHome?: string
codexHomePath?: string
guestEndpointFilePath?: string
opencodeOverlayDir?: string
failures: number
@@ -52,7 +57,7 @@ export class WslHookRelayManager {
private recovery: WslRelayRecovery
private states = new Map<string, DistroState>()
/** Distros a hooks-off teardown stopped, so re-enabling can put them back. */
private stoppedByHooksOff = new Set<string>()
private stoppedByHooksOff = new Map<string, string | undefined>()
private defaultDistro: string | null = null
private disposed = false
private warnedBundleMissing = false
@@ -64,7 +69,7 @@ export class WslHookRelayManager {
warn: (message) => this.deps.warn(message),
isDisposed: () => this.disposed,
isCurrent: (state) => this.states.get(wslHookRelayStateKey(state.distro)) === state,
restart: (distro) => this.ensureForDistro(distro),
restart: (distro) => this.ensureForDistro(distro, this.stateFor(distro)?.codexHomePath),
dropState: (state) => {
// Why: identity-guarded — a fresh ensure() may own this key by now;
// deleting by key alone would orphan its live relay child.
@@ -81,11 +86,11 @@ export class WslHookRelayManager {
}
/** Fire-and-forget from every WSL PTY spawn-env build; errors breadcrumb. */
ensureForDistro(distro: string | null): void {
ensureForDistro(distro: string | null, codexHomePath?: string | null): void {
if (this.disposed || !isWslHookRelayAllowed(this.deps)) {
return
}
void this.ensureInternal(distro).catch((err) => {
void this.ensureInternal(distro, codexHomePath ?? undefined).catch((err) => {
const detail = err instanceof Error ? err.message : String(err)
this.deps.warn(`[agent-hooks] WSL hook relay ensure failed: ${detail}`)
})
@@ -118,7 +123,7 @@ export class WslHookRelayManager {
state.mux?.dispose()
state.child?.kill()
if (!permanent) {
this.stoppedByHooksOff.add(state.distro)
this.stoppedByHooksOff.set(state.distro, state.codexHomePath)
}
}
this.states.clear()
@@ -129,26 +134,39 @@ export class WslHookRelayManager {
resumeStoppedRelays(): void {
const distros = [...this.stoppedByHooksOff]
this.stoppedByHooksOff.clear()
for (const distro of distros) {
for (const [distro, codexHomePath] of distros) {
void this.deps
.isDistroRunning(distro)
.then((running) => {
if (running) {
this.ensureForDistro(distro)
this.ensureForDistro(distro, codexHomePath)
}
})
.catch(() => undefined)
}
}
private async ensureInternal(requestedDistro: string | null): Promise<void> {
private async ensureInternal(
requestedDistro: string | null,
requestedCodexHomePath?: string
): Promise<void> {
const distro = requestedDistro ?? (await this.resolveDefaultDistro())
if (!distro || this.disposed) {
return
}
const key = wslHookRelayStateKey(distro)
const existing = this.states.get(key)
if (requestedCodexHomePath) {
recordManagedWslCodexHome(distro, requestedCodexHomePath)
}
if (existing) {
if (
requestedCodexHomePath &&
!wslRuntimeHomePathsEqual(existing.codexHomePath, requestedCodexHomePath)
) {
existing.codexHomePath = requestedCodexHomePath
existing.lastInstallAt = 0
}
if (existing.phase === 'running') {
void maybeRerunWslRelayGuestInstall(this.deps, existing)
return
@@ -184,6 +202,7 @@ export class WslHookRelayManager {
// Why: instance-keyed and on the distro's persistent fs, so it outlives a relay
// crash — dropping it would blank status on panes spawned mid-relaunch.
opencodeOverlayDir: existing?.opencodeOverlayDir,
codexHomePath: requestedCodexHomePath ?? existing?.codexHomePath,
cooldownUntil: 0
}
this.states.set(key, state)
@@ -108,4 +108,36 @@ describe('runExclusivelyForCodexTrustConfig', () => {
await queued
expect(secondStarted).toBe(true)
})
it('coalesces WSL UNC aliases without folding the case-sensitive Linux path', async () => {
const aliasGate = deferred()
let aliasStarted = false
const blockedAlias = runExclusivelyForCodexTrustConfig(
String.raw`\\wsl.localhost\Ubuntu\home\Alice\.codex\config.toml`,
() => aliasGate.promise
)
const queuedAlias = runExclusivelyForCodexTrustConfig(
String.raw`\\wsl$\ubuntu\home\Alice\.codex\config.toml`,
() => {
aliasStarted = true
return Promise.resolve()
}
)
await Promise.resolve()
expect(aliasStarted).toBe(false)
let distinctStarted = false
await runExclusivelyForCodexTrustConfig(
String.raw`\\wsl.localhost\Ubuntu\home\alice\.codex\config.toml`,
async () => {
distinctStarted = true
}
)
expect(distinctStarted).toBe(true)
aliasGate.resolve()
await blockedAlias
await queuedAlias
expect(aliasStarted).toBe(true)
})
})
@@ -16,6 +16,7 @@ import {
type CodexTrustEntry
} from './config-toml-trust'
import {
CodexHookService,
_internals,
createCodexWslRuntimeHookInstallPlan,
type CodexWslRuntimeHookInstallPlan
@@ -82,7 +83,67 @@ function expectedManagedCommand(scriptPath: string): string {
}
describe('Codex WSL runtime hook install', () => {
it('plans WSL hook files with Linux command and trust paths', async () => {
it('coalesces aliases of one runtime home without blocking independent homes', async () => {
const service = new CodexHookService()
const releases: (() => void)[] = []
const started: string[] = []
vi.spyOn(service, 'installForRuntimeHome').mockImplementation(async (runtimeHomePath) => {
started.push(runtimeHomePath!)
await new Promise<void>((resolve) => releases.push(resolve))
return null
})
const firstHome = '\\\\wsl$\\Ubuntu\\home\\Alice\\.local\\share\\orca\\codex-runtime-home\\home'
const alias = firstHome.replace('\\\\wsl$', '\\\\wsl.localhost')
const independent = firstHome.replace('\\Alice\\', '\\Bob\\')
const first = service.installForRuntimeHomeSerialized(firstHome)
const second = service.installForRuntimeHomeSerialized(alias)
const third = service.installForRuntimeHomeSerialized(independent)
await vi.waitFor(() => expect(started).toEqual([firstHome, independent]))
expect(second).toBe(first)
releases.splice(0).forEach((release) => release())
await Promise.all([first, second, third])
expect(started).toEqual([firstHome, independent])
})
it('does not coalesce one drive-backed home across WSL distros', async () => {
const service = new CodexHookService()
const started: string[] = []
vi.spyOn(service, 'installForRuntimeHome').mockImplementation(async (_runtimeHome, target) => {
started.push(target?.wslDistro ?? '')
return null
})
const home = 'D:\\wsl-home\\.local\\share\\orca\\codex-runtime-home\\home'
await Promise.all([
service.installForRuntimeHomeSerialized(home, { runtime: 'wsl', wslDistro: 'Ubuntu' }),
service.installForRuntimeHomeSerialized(home, { runtime: 'wsl', wslDistro: 'Debian' })
])
expect(started).toEqual(['Ubuntu', 'Debian'])
})
it('keeps case-distinct Linux runtime homes on separate queues', async () => {
const service = new CodexHookService()
const started: string[] = []
vi.spyOn(service, 'installForRuntimeHome').mockImplementation(async (runtimeHomePath) => {
started.push(runtimeHomePath!)
return null
})
const upper =
'\\\\wsl.localhost\\Ubuntu\\home\\Alice\\.local\\share\\orca\\codex-runtime-home\\home'
const lower = upper.replace('\\Alice\\', '\\alice\\')
await Promise.all([
service.installForRuntimeHomeSerialized(upper),
service.installForRuntimeHomeSerialized(lower)
])
expect(started).toEqual([upper, lower])
})
it('plans WSL hook files with Linux command and trust paths', () => {
const runtimeHome =
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\.local\\share\\orca\\codex-runtime-home\\home'
+39 -4
View File
@@ -1075,6 +1075,7 @@ export class CodexHookService {
}
private readonly wslReconciliationGeneration = new Map<string, number>()
private readonly wslInstallsInFlight = new Map<string, Promise<AgentHookInstallStatus | null>>()
private supersedeWslReconciliation(runtimeHomePath: string | null | undefined): number {
if (!runtimeHomePath) {
@@ -1171,6 +1172,30 @@ export class CodexHookService {
}
}
installForRuntimeHomeSerialized(
runtimeHomePath: string | null | undefined,
target?: CodexWslRuntimeHookTarget
): Promise<AgentHookInstallStatus | null> {
if (!runtimeHomePath) {
return Promise.resolve(null)
}
const targetKey = target?.runtime === 'wsl' ? target.wslDistro?.trim().toLowerCase() : ''
const key = `${getWslReconciliationKey(runtimeHomePath)}\0${targetKey ?? ''}`
const active = this.wslInstallsInFlight.get(key)
if (active) {
return active
}
const install = this.installForRuntimeHome(runtimeHomePath, target)
this.wslInstallsInFlight.set(key, install)
const clear = (): void => {
if (this.wslInstallsInFlight.get(key) === install) {
this.wslInstallsInFlight.delete(key)
}
}
void install.then(clear, clear)
return install
}
refreshRuntimeUserHooksForRuntimeHome(
runtimeHomePath: string | null | undefined,
target?: CodexWslRuntimeHookTarget
@@ -1486,7 +1511,13 @@ export class CodexHookService {
options?.codexHomeDir?.replace(/\/$/, '') ?? `${remoteHome.replace(/\/$/, '')}/.codex`
const remoteConfigPath = `${codexHomeBase}/hooks.json`
const remoteTomlPath = `${codexHomeBase}/config.toml`
const remoteScriptPath = `${remoteHome.replace(/\/$/, '')}/.orca/agent-hooks/codex-hook.sh`
// Redirected WSL homes must use the same script location and command shape
// as the runtime installer; two representations of one hooks.json race
// into stale trust keys. Plain SSH keeps its guest-home script contract.
const redirectedCodexHome = options?.codexHomeDir?.replace(/\/$/, '')
const remoteScriptPath = redirectedCodexHome
? `${redirectedCodexHome}/.orca/agent-hooks/codex-hook.sh`
: `${remoteHome.replace(/\/$/, '')}/.orca/agent-hooks/codex-hook.sh`
try {
const config = await readHooksJsonRemote(sftp, remoteConfigPath)
if (!config) {
@@ -1499,7 +1530,9 @@ export class CodexHookService {
}
}
const command = wrapPosixHookCommand(remoteScriptPath)
const command = redirectedCodexHome
? wrapReadablePosixHookCommand(remoteScriptPath)
: wrapPosixHookCommand(remoteScriptPath)
const nextHooks = { ...config.hooks }
const managedEvents = new Set<string>(CODEX_EVENTS)
const isManagedCommand = createManagedCommandMatcher('codex-hook.sh')
@@ -1523,11 +1556,13 @@ export class CodexHookService {
const definition: HookDefinition = {
hooks: [buildManagedCommandHook(command)]
}
nextHooks[eventName] = [...cleaned, definition]
nextHooks[eventName] = redirectedCodexHome
? [definition, ...cleaned]
: [...cleaned, definition]
trustEntries.push({
sourcePath: remoteConfigPath,
eventLabel: CODEX_EVENT_LABEL[eventName],
groupIndex: cleaned.length,
groupIndex: redirectedCodexHome ? 0 : cleaned.length,
handlerIndex: 0,
command,
timeoutSec: MANAGED_HOOK_TIMEOUT_SECONDS
@@ -2,10 +2,25 @@ import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
const { wslRealpathMock } = vi.hoisted(() => ({
wslRealpathMock: vi.fn(async (path: string) => path)
}))
vi.mock('node:fs/promises', () => ({ realpath: wslRealpathMock }))
import {
prepareManagedCodexHomeBeforeShellLaunch,
resolveManagedCodexShellPreflightHome
} from './managed-home-shell-preflight'
import {
prepareManagedWslCodexHomeBeforeShellLaunch,
resolveManagedWslCodexShellPreflightTarget
} from './managed-wsl-home-shell-preflight'
import {
_internals as managedWslHomeRegistryInternals,
recordManagedWslCodexHome
} from './managed-wsl-codex-home-registry'
const roots: string[] = []
@@ -16,6 +31,8 @@ function makeRoot(): string {
}
afterEach(() => {
wslRealpathMock.mockClear()
managedWslHomeRegistryInternals.clearRecordedManagedWslCodexHomes()
for (const root of roots.splice(0)) {
rmSync(root, { recursive: true, force: true })
}
@@ -160,3 +177,123 @@ describe('managed Codex shell preflight', () => {
).toBeNull()
})
})
describe('managed WSL Codex shell preflight', () => {
const home = '/home/jin/.local/share/orca/codex-runtime-home/home'
const runtimeHome =
'\\\\wsl.localhost\\Ubuntu-24.04\\home\\jin\\.local\\share\\orca\\codex-runtime-home\\home'
const env = {
CODEX_HOME: home,
ORCA_CODEX_HOME: home,
WSL_DISTRO_NAME: 'Ubuntu-24.04'
}
it('targets the pane-selected managed home through its UNC twin', () => {
recordManagedWslCodexHome('Ubuntu-24.04', runtimeHome)
expect(resolveManagedWslCodexShellPreflightTarget(env)).toEqual({
runtimeHomePath: runtimeHome,
wslDistro: 'Ubuntu-24.04'
})
})
it('reconstructs a validated managed home when a restarted runtime has no record', () => {
expect(resolveManagedWslCodexShellPreflightTarget(env)).toEqual({
runtimeHomePath: runtimeHome,
wslDistro: 'Ubuntu-24.04'
})
})
it('installs once through the WSL runtime-home lane while hooks are enabled', async () => {
recordManagedWslCodexHome('Ubuntu-24.04', runtimeHome)
const status = {
agent: 'codex' as const,
state: 'installed' as const,
configPath: 'config.toml',
managedHooksPresent: true,
detail: null
}
const install = vi.fn(() => status)
await expect(
prepareManagedWslCodexHomeBeforeShellLaunch({ env, hooksEnabled: true, install })
).resolves.toBe(status)
expect(install).toHaveBeenCalledExactlyOnceWith(runtimeHome, {
runtime: 'wsl',
wslDistro: 'Ubuntu-24.04'
})
await expect(
prepareManagedWslCodexHomeBeforeShellLaunch({ env, hooksEnabled: false, install })
).resolves.toBeNull()
expect(install).toHaveBeenCalledTimes(1)
})
it.each([
[
'a user home',
{ ...env, CODEX_HOME: '/home/jin/.codex', ORCA_CODEX_HOME: '/home/jin/.codex' }
],
['unequal routing markers', { ...env, ORCA_CODEX_HOME: `${home}-other` }],
[
'a parent traversal',
{
...env,
CODEX_HOME: `/home/jin/../jin${home.slice('/home/jin'.length)}`,
ORCA_CODEX_HOME: `/home/jin/../jin${home.slice('/home/jin'.length)}`
}
],
[
'a dot segment',
{
...env,
CODEX_HOME: `/home/./jin${home.slice('/home/jin'.length)}`,
ORCA_CODEX_HOME: `/home/./jin${home.slice('/home/jin'.length)}`
}
],
[
'a host path',
{ ...env, CODEX_HOME: 'C:\\Users\\jin\\.codex', ORCA_CODEX_HOME: 'C:\\Users\\jin\\.codex' }
],
['a missing distro', { ...env, WSL_DISTRO_NAME: '' }],
['a distro path escape', { ...env, WSL_DISTRO_NAME: 'Ubuntu\\..\\host' }],
[
'a repeated separator',
{
...env,
CODEX_HOME: home.replace('/jin/', '/jin//'),
ORCA_CODEX_HOME: home.replace('/jin/', '/jin//')
}
]
])('rejects %s', (_label, candidate) => {
recordManagedWslCodexHome('Ubuntu-24.04', runtimeHome)
expect(resolveManagedWslCodexShellPreflightTarget(candidate)).toBeNull()
})
it('preserves a recorded runtime spelling for a managed account home', () => {
const directHome = '/home/jin/.local/share/orca/codex-accounts/account-1/home'
const directRuntimeHome =
'\\\\wsl$\\Ubuntu-24.04\\home\\jin\\.local\\share\\orca\\codex-accounts\\account-1\\home'
recordManagedWslCodexHome('Ubuntu-24.04', directRuntimeHome)
expect(
resolveManagedWslCodexShellPreflightTarget({
CODEX_HOME: directHome,
ORCA_CODEX_HOME: directHome,
WSL_DISTRO_NAME: 'ubuntu-24.04'
})
).toEqual({ runtimeHomePath: directRuntimeHome, wslDistro: 'ubuntu-24.04' })
})
it('never authorizes a WSL system home even when it was offered by a PTY lane', () => {
const systemHome = '/home/jin/.codex'
recordManagedWslCodexHome('Ubuntu-24.04', '\\\\wsl.localhost\\Ubuntu-24.04\\home\\jin\\.codex')
expect(
resolveManagedWslCodexShellPreflightTarget({
CODEX_HOME: systemHome,
ORCA_CODEX_HOME: systemHome,
WSL_DISTRO_NAME: 'Ubuntu-24.04'
})
).toBeNull()
})
})
@@ -0,0 +1,115 @@
import { win32 as pathWin32 } from 'node:path'
import {
foldWslUncPathCaseInsensitiveParts,
parseWslUncPath,
toWindowsWslPath
} from '../../shared/wsl-paths'
type ManagedWslCodexHome = {
runtimeHomePath: string
linuxHomePath: string
}
const managedHomesByDistro = new Map<string, Map<string, ManagedWslCodexHome>>()
function distroKey(distro: string): string {
return distro.trim().toLowerCase()
}
export function isAbsolutePosixPathWithoutDotSegments(value: string): boolean {
const segments = value.split('/').slice(1)
return (
value.startsWith('/') &&
!value.startsWith('//') &&
!value.includes('\\') &&
segments.every((segment) => segment && segment !== '.' && segment !== '..')
)
}
function isOrcaManagedWslCodexHome(linuxHomePath: string): boolean {
const segments = linuxHomePath.split('/').filter(Boolean)
const orcaIndex = segments.findIndex(
(segment, index) =>
segment === 'orca' && segments[index - 1] === 'share' && segments[index - 2] === '.local'
)
if (orcaIndex === -1) {
return false
}
const tail = segments.slice(orcaIndex + 1)
return (
(tail.length === 2 && tail[0] === 'codex-runtime-home' && tail[1] === 'home') ||
(tail.length === 3 && tail[0] === 'codex-accounts' && Boolean(tail[1]) && tail[2] === 'home')
)
}
function linuxHomeForRuntimePath(runtimeHomePath: string, distro: string): string | null {
const wsl = parseWslUncPath(runtimeHomePath)
if (wsl) {
return wsl.distro.toLowerCase() === distroKey(distro) ? wsl.linuxPath : null
}
const drive = runtimeHomePath.match(/^([A-Za-z]):[/\\](.*)$/)
return drive ? `/mnt/${drive[1].toLowerCase()}/${drive[2].replace(/\\/g, '/')}` : null
}
export function recordManagedWslCodexHome(distro: string, runtimeHomePath: string): void {
const normalizedDistro = distro.trim()
const linuxHomePath = linuxHomeForRuntimePath(runtimeHomePath, normalizedDistro)
if (
!normalizedDistro ||
/[\\/\r\n]/.test(normalizedDistro) ||
!linuxHomePath ||
!isAbsolutePosixPathWithoutDotSegments(linuxHomePath) ||
!isOrcaManagedWslCodexHome(linuxHomePath)
) {
return
}
const homes = managedHomesByDistro.get(distroKey(normalizedDistro)) ?? new Map()
homes.set(linuxHomePath, { runtimeHomePath, linuxHomePath })
managedHomesByDistro.set(distroKey(normalizedDistro), homes)
}
export function resolveRecordedManagedWslCodexHome(
distro: string,
linuxHomePath: string
): string | null {
if (
!distro.trim() ||
/[\\/\r\n]/.test(distro) ||
!isAbsolutePosixPathWithoutDotSegments(linuxHomePath)
) {
return null
}
return managedHomesByDistro.get(distroKey(distro))?.get(linuxHomePath)?.runtimeHomePath ?? null
}
export function resolveManagedWslCodexHome(distro: string, linuxHomePath: string): string | null {
if (
!distro.trim() ||
/[\\/\r\n]/.test(distro) ||
!isAbsolutePosixPathWithoutDotSegments(linuxHomePath) ||
!isOrcaManagedWslCodexHome(linuxHomePath)
) {
return null
}
return (
resolveRecordedManagedWslCodexHome(distro, linuxHomePath) ??
toWindowsWslPath(linuxHomePath, distro)
)
}
export function wslRuntimeHomePathsEqual(left: string | undefined, right: string): boolean {
if (!left) {
return false
}
const leftWsl = foldWslUncPathCaseInsensitiveParts(left)
const rightWsl = foldWslUncPathCaseInsensitiveParts(right)
if (leftWsl || rightWsl) {
return leftWsl !== null && leftWsl === rightWsl
}
return pathWin32.normalize(left).toLowerCase() === pathWin32.normalize(right).toLowerCase()
}
export const _internals = {
clearRecordedManagedWslCodexHomes: () => managedHomesByDistro.clear(),
isOrcaManagedWslCodexHome
}
@@ -0,0 +1,75 @@
import { realpath } from 'node:fs/promises'
import type { AgentHookInstallStatus } from '../../shared/agent-hook-types'
import { withTimeout } from '../../shared/promise-timeout-fallback'
import type { CodexWslRuntimeHookTarget } from './codex-wsl-hook-install-plan'
import { codexHookService } from './hook-service'
import {
isAbsolutePosixPathWithoutDotSegments,
resolveManagedWslCodexHome,
wslRuntimeHomePathsEqual
} from './managed-wsl-codex-home-registry'
type WslShellPreflightEnvironment = {
CODEX_HOME?: string
ORCA_CODEX_HOME?: string
WSL_DISTRO_NAME?: string
}
const WSL_MANAGED_HOME_REALPATH_TIMEOUT_MS = 5_000
export type ManagedWslCodexShellPreflightTarget = {
runtimeHomePath: string
wslDistro: string
}
export function resolveManagedWslCodexShellPreflightTarget(
env: WslShellPreflightEnvironment
): ManagedWslCodexShellPreflightTarget | null {
const codexHome = env.CODEX_HOME?.trim()
const orcaCodexHome = env.ORCA_CODEX_HOME?.trim()
const wslDistro = env.WSL_DISTRO_NAME?.trim()
if (
!codexHome ||
codexHome !== orcaCodexHome ||
!wslDistro ||
/[\\/\r\n]/.test(wslDistro) ||
!isAbsolutePosixPathWithoutDotSegments(codexHome)
) {
return null
}
const runtimeHomePath = resolveManagedWslCodexHome(wslDistro, codexHome)
return runtimeHomePath ? { runtimeHomePath, wslDistro } : null
}
export async function prepareManagedWslCodexHomeBeforeShellLaunch(args: {
env: WslShellPreflightEnvironment
hooksEnabled: boolean
install?: (
runtimeHomePath: string,
target: CodexWslRuntimeHookTarget
) => AgentHookInstallStatus | Promise<AgentHookInstallStatus | null> | null
}): Promise<AgentHookInstallStatus | null> {
if (!args.hooksEnabled) {
return null
}
const target = resolveManagedWslCodexShellPreflightTarget(args.env)
if (!target) {
return null
}
const realHome = await withTimeout(
realpath(target.runtimeHomePath),
WSL_MANAGED_HOME_REALPATH_TIMEOUT_MS,
null
)
if (!realHome || !wslRuntimeHomePathsEqual(realHome, target.runtimeHomePath)) {
return null
}
const install =
args.install ??
((home: string, hookTarget: CodexWslRuntimeHookTarget) =>
codexHookService.installForRuntimeHomeSerialized(home, hookTarget))
return await install(target.runtimeHomePath, {
runtime: 'wsl',
wslDistro: target.wslDistro
})
}
@@ -7,6 +7,7 @@ import { LocalPtyProvider } from '../providers/local-pty-provider'
import { __resetPersistedWindowsPathCacheForTests } from '../pty/windows-environment-path'
import { __setWindowsPathRegistryLoaderForTests } from '../pty/windows-path-registry-reader'
import { hasLiveClaudePtys, markClaudePtySpawned } from '../claude-accounts/live-pty-gate'
import { wslHookRelayManager } from '../agent-hooks/wsl-hook-relay-manager'
import { registerPtyHandlers, buildPtyHostEnv, clearProviderPtyState } from './pty'
vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock()))
@@ -57,6 +58,32 @@ describe('registerPtyHandlers', () => {
const { handlers, mainWindow, spawnAndGetEnv, withBundledCli } = setupPtyIpcSuite()
describe('spawn environment', () => {
it('passes the PTY-resolved Codex home to the WSL relay lane', () => {
const runtimeHome =
'\\\\wsl.localhost\\Ubuntu\\home\\jin\\.local\\share\\orca\\codex-runtime-home\\home'
const ensureForDistro = vi
.spyOn(wslHookRelayManager, 'ensureForDistro')
.mockImplementation(() => {})
try {
buildPtyHostEnv(
'pty-wsl',
{},
{
isPackaged: true,
userDataPath: '/tmp/orca-user-data',
selectedCodexHomePath: runtimeHome,
isWsl: true,
wslDistro: 'Ubuntu',
agentStatusHooksEnabled: true
}
)
expect(ensureForDistro).toHaveBeenCalledExactlyOnceWith('Ubuntu', runtimeHome)
} finally {
ensureForDistro.mockRestore()
}
})
it('refreshes the outer Windows PATH for a WSL spawn without forwarding it', async () => {
const originalPlatform = process.platform
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
+1 -1
View File
@@ -122,7 +122,7 @@ export function buildPtyHostEnv(
if (opts.isWsl === true) {
// Why: hook POSTs to 127.0.0.1 die inside WSL's NAT namespace; use the guest-resident relay's endpoint instead of the Windows one.
const distro = opts.wslDistro ?? null
wslHookRelayManager.ensureForDistro(distro)
wslHookRelayManager.ensureForDistro(distro, opts.selectedCodexHomePath)
const guestEndpoint = wslHookRelayManager.getGuestEndpointFilePath(distro)
if (guestEndpoint) {
baseEnv.ORCA_AGENT_HOOK_ENDPOINT = guestEndpoint
@@ -162,13 +162,13 @@ describe.skipIf(!bashAvailable)('Codex preflight under a profile-rewritten PATH'
expect(readFileSync(fixture.codexMarker, 'utf-8').trim()).toBe('--alias-flag --version')
})
// Why: pins the defect itself, so a regression back to an unqualified name fails here.
it('would run the impostor if the preflight carried an unqualified command name', () => {
// Why: an unqualified preflight is rejected so profile PATH entries cannot hijack it.
it('skips an unqualified preflight value while still launching codex', () => {
const fixture = buildFixture()
launchCodexThroughRcfile(fixture, 'orca')
expect(existsSync(fixture.hijackMarker)).toBe(true)
expect(existsSync(fixture.hijackMarker)).toBe(false)
expect(existsSync(fixture.intendedMarker)).toBe(false)
expect(existsSync(fixture.codexMarker)).toBe(true)
})
@@ -170,6 +170,30 @@ describe.skipIf(process.platform === 'win32')('Codex shell launch preflight', ()
)
})
it('does not trigger a preflight outside an Orca terminal', () => {
const root = mkdtempSync(join(tmpdir(), 'orca-codex-plain-shell-'))
roots.push(root)
const bin = join(root, 'bin')
const marker = join(root, 'preflight-ran')
mkdirSync(bin)
writeExecutable(join(bin, 'codex'), '#!/bin/sh\nprintf launched\n')
writeExecutable(join(bin, 'orca-test'), `#!/bin/sh\nprintf ran > ${JSON.stringify(marker)}\n`)
const output = execFileSync(
'/bin/bash',
['--noprofile', '--norc', '-c', `${getPosixCodexShellLaunchPreflight()}\ncodex`],
{
encoding: 'utf-8',
env: {
PATH: `${bin}${delimiter}${process.env.PATH ?? ''}`
}
}
)
expect(output.trim()).toBe('launched')
expect(existsSync(marker)).toBe(false)
})
// Why a loop over it.each: an early `return` reported green on hosts without the
// shell, so the zsh half silently never ran on Linux. skipIf reports it as a skip.
for (const [shell, strict] of [
@@ -319,6 +343,24 @@ describe('Codex shell launch preflight command', () => {
).toBe(launcherPath)
})
it('carries the packaged Windows launcher for WSLENV path translation', () => {
const { userDataPath, resourcesPath } = makeCliRoot()
const launcherPath = join(resourcesPath, 'bin', 'orca.exe')
writeExecutable(launcherPath, '#!/bin/sh\nexit 0\n')
expect(
resolveCodexShellLaunchPreflightCommand({
hooksEnabled: true,
isPackaged: true,
isWsl: true,
managedHomePath: '/home/jin/.local/share/orca/codex-runtime-home/home',
userDataPath,
resourcesPath,
platform: 'win32'
})
).toBe(launcherPath)
})
it('never returns an unqualified command name that a profile-rewritten PATH could hijack', () => {
const { userDataPath, resourcesPath } = makeCliRoot()
writeExecutable(join(resourcesPath, 'bin', 'orca'), '#!/bin/sh\nexit 0\n')
@@ -340,16 +382,12 @@ describe('Codex shell launch preflight command', () => {
it.each([
{ label: 'the launcher file is missing', create: null },
{ label: 'the launcher is not executable', create: 0o644 },
{ label: 'the launcher path is a directory', create: 'directory' as const }
])('skips the preflight when $label', (config) => {
const { userDataPath, resourcesPath } = makeCliRoot()
const launcherPath = join(resourcesPath, 'bin', 'orca')
if (config.create === 'directory') {
mkdirSync(launcherPath)
} else if (config.create !== null) {
writeFileSync(launcherPath, '#!/bin/sh\nexit 0\n')
chmodSync(launcherPath, config.create)
}
expect(
@@ -364,6 +402,27 @@ describe('Codex shell launch preflight command', () => {
).toBeNull()
})
it.skipIf(process.platform === 'win32')(
'skips the preflight when the launcher is not executable',
() => {
const { userDataPath, resourcesPath } = makeCliRoot()
const launcherPath = join(resourcesPath, 'bin', 'orca')
writeFileSync(launcherPath, '#!/bin/sh\nexit 0\n')
chmodSync(launcherPath, 0o644)
expect(
resolveCodexShellLaunchPreflightCommand({
hooksEnabled: true,
isPackaged: true,
managedHomePath: '/managed/home',
userDataPath,
resourcesPath,
platform: 'darwin'
})
).toBeNull()
}
)
it('skips the preflight when the packaged build exposes no resources root', () => {
const { userDataPath } = makeCliRoot()
@@ -381,7 +440,7 @@ describe('Codex shell launch preflight command', () => {
it.each([
{ hooksEnabled: false, isWsl: false, managedHomePath: '/managed/home' },
{ hooksEnabled: true, isWsl: true, managedHomePath: '/managed/home' },
{ hooksEnabled: true, isWsl: true, managedHomePath: '/managed/home', isPackaged: false },
{ hooksEnabled: true, isWsl: false, managedHomePath: null }
])('does not enable an unsupported preflight for %o', (options) => {
const { userDataPath, resourcesPath } = makeCliRoot()
@@ -390,7 +449,7 @@ describe('Codex shell launch preflight command', () => {
expect(
resolveCodexShellLaunchPreflightCommand({
...options,
isPackaged: true,
isPackaged: options.isPackaged ?? true,
userDataPath,
resourcesPath,
platform: 'darwin'
+11 -4
View File
@@ -29,7 +29,7 @@ export type CodexShellLaunchPreflightCommandOptions = {
export function resolveCodexShellLaunchPreflightCommand(
options: CodexShellLaunchPreflightCommandOptions
): string | null {
if (!options.hooksEnabled || options.isWsl || !options.managedHomePath) {
if (!options.hooksEnabled || !options.managedHomePath) {
return null
}
const platform = options.platform ?? process.platform
@@ -42,7 +42,14 @@ export function resolveCodexShellLaunchPreflightCommand(
...DEV_LAUNCHER_DIR,
platform === 'win32' ? `${DEV_COMMAND_NAME}.cmd` : DEV_COMMAND_NAME
)
return candidate && isExecutableFileOnDisk(candidate, platform) ? candidate : null
if (!candidate || !isExecutableFileOnDisk(candidate, platform)) {
return null
}
if (!options.isWsl) {
return candidate
}
// Why: WSLENV /p translates the verified Windows launcher with the distro's configured automount root.
return platform === 'win32' && options.isPackaged ? candidate : null
}
function isExecutableFileOnDisk(path: string, platform: NodeJS.Platform): boolean {
@@ -65,7 +72,7 @@ export function getPosixCodexShellLaunchPreflight(): string {
# 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_LAUNCH_PREFLIGHT:-}" && -n "\${__orca_codex_binary:-}" && -x "\${__orca_codex_binary}" ]]; then
if [[ -n "\${ORCA_CODEX_LAUNCH_PREFLIGHT:-}" && -x "\${ORCA_CODEX_LAUNCH_PREFLIGHT}" && -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 {
@@ -78,7 +85,7 @@ unset __orca_codex_binary
}
export function getFishCodexShellLaunchPreflight(): string {
return `if test -n "$ORCA_CODEX_LAUNCH_PREFLIGHT"; and test (type -t codex 2>/dev/null) = file
return `if test -x "$ORCA_CODEX_LAUNCH_PREFLIGHT"; and test (type -t codex 2>/dev/null) = file
function codex
command "$ORCA_CODEX_LAUNCH_PREFLIGHT" agent hooks prepare-codex >/dev/null 2>&1; or true
command codex $argv
+1 -1
View File
@@ -85,7 +85,7 @@ describe('addOrcaWslInteropEnv', () => {
expect(env.WSLENV).toContain('ORCA_TERMINAL_HANDLE/u')
expect(env.WSLENV).toContain('ORCA_USER_DATA_PATH/p')
expect(env.WSLENV).toContain('ORCA_CLI_COMMAND/u')
expect(env.WSLENV).not.toContain('ORCA_CODEX_LAUNCH_PREFLIGHT')
expect(env.WSLENV).toContain('ORCA_CODEX_LAUNCH_PREFLIGHT/p')
expect(env.WSLENV).toContain('ORCA_OMP_STATUS_EXTENSION/p')
expect(env.WSLENV).not.toContain('ORCA_PRIME_AGENT_STATUS_EXTENSION')
expect(env.WSLENV).toContain('ORCA_PANE_KEY/u')
+1
View File
@@ -77,6 +77,7 @@ export function addOrcaWslInteropEnv(env: Record<string, string>): void {
// and it cannot derive the hash segment from ORCA_USER_DATA_PATH alone.
'ORCA_SHELL_READY_ROOT/p',
'ORCA_CLI_COMMAND/u',
'ORCA_CODEX_LAUNCH_PREFLIGHT/p',
'ORCA_PANE_KEY/u',
'ORCA_TAB_ID/u',
'ORCA_WORKTREE_ID/u',
@@ -0,0 +1,145 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { OrcaRuntimeService } from '../../orca-runtime'
import { isStreamingMethod, type RpcContext } from '../core'
const { installForRuntimeHomeSerializedMock, realpathMock } = vi.hoisted(() => ({
installForRuntimeHomeSerializedMock: vi.fn(),
realpathMock: vi.fn()
}))
vi.mock('../../../codex/hook-service', () => ({
codexHookService: { installForRuntimeHomeSerialized: installForRuntimeHomeSerializedMock }
}))
vi.mock('node:fs/promises', () => ({ realpath: realpathMock }))
import { AGENT_HOOK_METHODS } from './agent-hooks'
import {
_internals as managedWslHomeRegistryInternals,
recordManagedWslCodexHome
} from '../../../codex/managed-wsl-codex-home-registry'
const LINUX_HOME = '/home/jin/.local/share/orca/codex-runtime-home/home'
const RUNTIME_HOME =
'\\\\wsl.localhost\\Ubuntu-24.04\\home\\jin\\.local\\share\\orca\\codex-runtime-home\\home'
function prepareMethod() {
const method = AGENT_HOOK_METHODS.find(
(candidate) => candidate.name === 'agentHooks.prepareCodexForWslPane'
)
if (!method || isStreamingMethod(method)) {
throw new Error('Missing agentHooks.prepareCodexForWslPane request method')
}
return method
}
function runtimeWithSettings(enabled = true, disabledTuiAgents: string[] = []): OrcaRuntimeService {
return {
getClientSettings: vi.fn(() => ({
agentStatusHooksEnabled: enabled,
disabledTuiAgents
}))
} as unknown as OrcaRuntimeService
}
describe('agent hook RPC methods', () => {
beforeEach(() => {
installForRuntimeHomeSerializedMock.mockReset()
realpathMock.mockReset()
realpathMock.mockImplementation(async (path: string) => path)
managedWslHomeRegistryInternals.clearRecordedManagedWslCodexHomes()
recordManagedWslCodexHome('Ubuntu-24.04', RUNTIME_HOME)
})
it('installs the pane-selected WSL home once and returns its status', async () => {
const status = { agent: 'codex', state: 'installed' }
installForRuntimeHomeSerializedMock.mockResolvedValue(status)
const method = prepareMethod()
const params = method.params!.parse({
codexHome: LINUX_HOME,
orcaCodexHome: LINUX_HOME,
wslDistro: 'Ubuntu-24.04'
})
await expect(method.handler(params, { runtime: runtimeWithSettings() })).resolves.toBe(status)
expect(installForRuntimeHomeSerializedMock).toHaveBeenCalledExactlyOnceWith(RUNTIME_HOME, {
runtime: 'wsl',
wslDistro: 'Ubuntu-24.04'
})
})
it.each([
[false, []],
[true, ['codex']]
])('does not install when hooks are disabled (%s, %j)', async (enabled, disabledTuiAgents) => {
const method = prepareMethod()
const params = method.params!.parse({
codexHome: '/home/jin/.local/share/orca/codex-runtime-home/home',
orcaCodexHome: '/home/jin/.local/share/orca/codex-runtime-home/home',
wslDistro: 'Ubuntu-24.04'
})
await expect(
method.handler(params, { runtime: runtimeWithSettings(enabled, disabledTuiAgents) })
).resolves.toBeNull()
expect(installForRuntimeHomeSerializedMock).not.toHaveBeenCalled()
})
it.each(['runtime', 'mobile'] as const)('rejects non-local %s callers', async (clientKind) => {
const method = prepareMethod()
const params = method.params!.parse({
codexHome: '/home/jin/.local/share/orca/codex-runtime-home/home',
orcaCodexHome: '/home/jin/.local/share/orca/codex-runtime-home/home',
wslDistro: 'Ubuntu-24.04'
})
await expect(
method.handler(params, {
runtime: runtimeWithSettings(),
clientKind
} as RpcContext)
).rejects.toThrow(/only available to the local Orca CLI/)
expect(installForRuntimeHomeSerializedMock).not.toHaveBeenCalled()
})
it('propagates an attempted installer failure', async () => {
installForRuntimeHomeSerializedMock.mockRejectedValue(new Error('install failed'))
const method = prepareMethod()
const params = method.params!.parse({
codexHome: '/home/jin/.local/share/orca/codex-runtime-home/home',
orcaCodexHome: '/home/jin/.local/share/orca/codex-runtime-home/home',
wslDistro: 'Ubuntu-24.04'
})
await expect(method.handler(params, { runtime: runtimeWithSettings() })).rejects.toThrow(
'install failed'
)
expect(installForRuntimeHomeSerializedMock).toHaveBeenCalledOnce()
})
it('rejects a managed-looking home that resolves through a symlink', async () => {
realpathMock.mockResolvedValue(
'\\\\wsl.localhost\\Ubuntu-24.04\\home\\jin\\outside-managed-home'
)
const method = prepareMethod()
const params = method.params!.parse({
codexHome: LINUX_HOME,
orcaCodexHome: LINUX_HOME,
wslDistro: 'Ubuntu-24.04'
})
await expect(method.handler(params, { runtime: runtimeWithSettings() })).resolves.toBeNull()
expect(installForRuntimeHomeSerializedMock).not.toHaveBeenCalled()
})
it('rejects malformed distro names at the RPC schema', () => {
const method = prepareMethod()
expect(() =>
method.params!.parse({
codexHome: '/home/jin/.local/share/orca/codex-runtime-home/home',
orcaCodexHome: '/home/jin/.local/share/orca/codex-runtime-home/home',
wslDistro: 'Ubuntu\\..\\host'
})
).toThrow()
})
})
@@ -0,0 +1,38 @@
import { z } from 'zod'
import { prepareManagedWslCodexHomeBeforeShellLaunch } from '../../../codex/managed-wsl-home-shell-preflight'
import { defineMethod, type RpcMethod } from '../core'
const PrepareCodexForWslPaneParams = z
.object({
codexHome: z.string().max(4_096),
orcaCodexHome: z.string().max(4_096),
wslDistro: z
.string()
.trim()
.min(1)
.max(255)
.regex(/^[^\\/\r\n]+$/)
})
.strict()
export const AGENT_HOOK_METHODS: readonly RpcMethod[] = [
defineMethod({
name: 'agentHooks.prepareCodexForWslPane',
params: PrepareCodexForWslPaneParams,
handler: async (params, { runtime, clientKind }) => {
if (clientKind !== undefined) {
throw new Error('Codex hook preparation is only available to the local Orca CLI.')
}
const settings = runtime.getClientSettings()
return await prepareManagedWslCodexHomeBeforeShellLaunch({
env: {
CODEX_HOME: params.codexHome,
ORCA_CODEX_HOME: params.orcaCodexHome,
WSL_DISTRO_NAME: params.wslDistro
},
hooksEnabled:
settings.agentStatusHooksEnabled && !settings.disabledTuiAgents.includes('codex')
})
}
})
]
+2
View File
@@ -43,12 +43,14 @@ import { PAIRING_METHODS } from './pairing'
import { UPDATER_METHODS } from './updater'
import { AGENT_SESSION_METHODS } from './agent-session'
import { ARTIFACT_METHODS } from './artifacts'
import { AGENT_HOOK_METHODS } from './agent-hooks'
// Why: a flat manifest keeps registration order explicit and provides one
// grep-point for "what methods does the RPC server expose?" — useful when
// auditing the security boundary or wiring new CLI commands.
export const ALL_RPC_METHODS: readonly RpcAnyMethod[] = [
...STATUS_METHODS,
...AGENT_HOOK_METHODS,
...AI_VAULT_METHODS,
...ARTIFACT_METHODS,
...AUTOMATION_METHODS,