mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 08:04:05 +00:00
fix(codex): heal WSL hooks before typed launches
This commit is contained in:
@@ -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: 10_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']
|
||||
|
||||
@@ -207,6 +207,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: 10_000 }
|
||||
)
|
||||
} 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 {
|
||||
|
||||
@@ -4,7 +4,9 @@ import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
prepareManagedCodexHomeBeforeShellLaunch,
|
||||
resolveManagedCodexShellPreflightHome
|
||||
prepareManagedWslCodexHomeBeforeShellLaunch,
|
||||
resolveManagedCodexShellPreflightHome,
|
||||
resolveManagedWslCodexShellPreflightTarget
|
||||
} from './managed-home-shell-preflight'
|
||||
|
||||
const roots: string[] = []
|
||||
@@ -160,3 +162,84 @@ describe('managed Codex shell preflight', () => {
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('managed WSL Codex shell preflight', () => {
|
||||
const home = '/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', () => {
|
||||
expect(resolveManagedWslCodexShellPreflightTarget(env)).toEqual({
|
||||
runtimeHomePath:
|
||||
'\\\\wsl.localhost\\Ubuntu-24.04\\home\\jin\\.local\\share\\orca\\codex-runtime-home\\home',
|
||||
wslDistro: 'Ubuntu-24.04'
|
||||
})
|
||||
})
|
||||
|
||||
it('installs once through the WSL runtime-home lane while hooks are enabled', () => {
|
||||
const status = {
|
||||
agent: 'codex' as const,
|
||||
state: 'installed' as const,
|
||||
configPath: 'config.toml',
|
||||
managedHooksPresent: true,
|
||||
detail: null
|
||||
}
|
||||
const install = vi.fn(() => status)
|
||||
|
||||
expect(prepareManagedWslCodexHomeBeforeShellLaunch({ env, hooksEnabled: true, install })).toBe(
|
||||
status
|
||||
)
|
||||
expect(install).toHaveBeenCalledExactlyOnceWith(
|
||||
'\\\\wsl.localhost\\Ubuntu-24.04\\home\\jin\\.local\\share\\orca\\codex-runtime-home\\home',
|
||||
{ runtime: 'wsl', wslDistro: 'Ubuntu-24.04' }
|
||||
)
|
||||
|
||||
expect(
|
||||
prepareManagedWslCodexHomeBeforeShellLaunch({ env, hooksEnabled: false, install })
|
||||
).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) => {
|
||||
expect(resolveManagedWslCodexShellPreflightTarget(candidate)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,11 +2,20 @@ import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
||||
import { join, relative, resolve, sep } from 'node:path'
|
||||
import type { AgentHookInstallStatus } from '../../shared/agent-hook-types'
|
||||
import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path'
|
||||
import { toWindowsWslPath } from '../../shared/wsl-paths'
|
||||
import { wslCodexRuntimeHomeForGuestHome } from '../pty/codex-home-wsl-env'
|
||||
import type { CodexWslRuntimeHookTarget } from './codex-wsl-hook-install-plan'
|
||||
import { codexHookService } from './hook-service'
|
||||
|
||||
type ShellPreflightEnvironment = {
|
||||
CODEX_HOME?: string
|
||||
ORCA_CODEX_HOME?: string
|
||||
WSL_DISTRO_NAME?: string
|
||||
}
|
||||
|
||||
export type ManagedWslCodexShellPreflightTarget = {
|
||||
runtimeHomePath: string
|
||||
wslDistro: string
|
||||
}
|
||||
|
||||
function pathsEqual(left: string, right: string): boolean {
|
||||
@@ -104,3 +113,67 @@ export async function prepareManagedCodexHomeBeforeShellLaunch(args: {
|
||||
}
|
||||
return (args.install ?? ((home) => codexHookService.install(home)))(runtimeHomePath)
|
||||
}
|
||||
|
||||
function isAbsolutePosixPathWithoutDotSegments(path: string): boolean {
|
||||
const segments = path.split('/').slice(1)
|
||||
return (
|
||||
path.startsWith('/') &&
|
||||
!path.startsWith('//') &&
|
||||
!path.includes('\\') &&
|
||||
segments.every((segment) => segment && segment !== '.' && segment !== '..')
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveManagedWslCodexShellPreflightTarget(
|
||||
env: ShellPreflightEnvironment
|
||||
): 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 runtimeHomeSuffix = wslCodexRuntimeHomeForGuestHome('')
|
||||
if (!codexHome.endsWith(runtimeHomeSuffix)) {
|
||||
return null
|
||||
}
|
||||
const guestHome = codexHome.slice(0, -runtimeHomeSuffix.length)
|
||||
if (
|
||||
!guestHome ||
|
||||
!isAbsolutePosixPathWithoutDotSegments(guestHome) ||
|
||||
wslCodexRuntimeHomeForGuestHome(guestHome) !== codexHome
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return { runtimeHomePath: toWindowsWslPath(codexHome, wslDistro), wslDistro }
|
||||
}
|
||||
|
||||
export function prepareManagedWslCodexHomeBeforeShellLaunch(args: {
|
||||
env: ShellPreflightEnvironment
|
||||
hooksEnabled: boolean
|
||||
install?: (
|
||||
runtimeHomePath: string,
|
||||
target: CodexWslRuntimeHookTarget
|
||||
) => AgentHookInstallStatus | null
|
||||
}): AgentHookInstallStatus | null {
|
||||
if (!args.hooksEnabled) {
|
||||
return null
|
||||
}
|
||||
const target = resolveManagedWslCodexShellPreflightTarget(args.env)
|
||||
if (!target) {
|
||||
return null
|
||||
}
|
||||
const install =
|
||||
args.install ??
|
||||
((home: string, hookTarget: CodexWslRuntimeHookTarget) =>
|
||||
codexHookService.installForRuntimeHome(home, hookTarget))
|
||||
return install(target.runtimeHomePath, { runtime: 'wsl', wslDistro: target.wslDistro })
|
||||
}
|
||||
|
||||
@@ -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,27 @@ describe('Codex shell launch preflight command', () => {
|
||||
).toBe(launcherPath)
|
||||
})
|
||||
|
||||
it.skipIf(process.platform !== 'win32')(
|
||||
'carries the packaged Windows launcher as a guest-absolute WSL path',
|
||||
() => {
|
||||
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'
|
||||
})
|
||||
).toMatch(/^\/mnt\/[a-z]\//)
|
||||
}
|
||||
)
|
||||
|
||||
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,7 +385,6 @@ 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()
|
||||
@@ -364,6 +408,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 +446,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 +455,7 @@ describe('Codex shell launch preflight command', () => {
|
||||
expect(
|
||||
resolveCodexShellLaunchPreflightCommand({
|
||||
...options,
|
||||
isPackaged: true,
|
||||
isPackaged: options.isPackaged ?? true,
|
||||
userDataPath,
|
||||
resourcesPath,
|
||||
platform: 'darwin'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { accessSync, constants, statSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { toLinuxPath } from '../../shared/wsl-paths'
|
||||
import { getBundledLauncherPath } from '../cli/bundled-cli-launcher-path'
|
||||
|
||||
const DEV_LAUNCHER_DIR = ['cli', 'bin']
|
||||
@@ -29,7 +30,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 +43,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: WSL executes the verified Windows launcher through interop; its guest path crosses WSLENV untranslated.
|
||||
return platform === 'win32' && options.isPackaged ? toLinuxPath(candidate) : null
|
||||
}
|
||||
|
||||
function isExecutableFileOnDisk(path: string, platform: NodeJS.Platform): boolean {
|
||||
@@ -65,7 +73,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 +86,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
|
||||
|
||||
@@ -84,7 +84,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/u')
|
||||
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')
|
||||
|
||||
@@ -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/u',
|
||||
'ORCA_PANE_KEY/u',
|
||||
'ORCA_TAB_ID/u',
|
||||
'ORCA_WORKTREE_ID/u',
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { OrcaRuntimeService } from '../../orca-runtime'
|
||||
import { isStreamingMethod, type RpcContext } from '../core'
|
||||
|
||||
const { installForRuntimeHomeMock } = vi.hoisted(() => ({
|
||||
installForRuntimeHomeMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../../../codex/hook-service', () => ({
|
||||
codexHookService: { installForRuntimeHome: installForRuntimeHomeMock }
|
||||
}))
|
||||
|
||||
import { AGENT_HOOK_METHODS } from './agent-hooks'
|
||||
|
||||
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(() => {
|
||||
installForRuntimeHomeMock.mockReset()
|
||||
})
|
||||
|
||||
it('installs the pane-selected WSL home once and returns its status', async () => {
|
||||
const status = { agent: 'codex', state: 'installed' }
|
||||
installForRuntimeHomeMock.mockReturnValue(status)
|
||||
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'
|
||||
})
|
||||
|
||||
expect(method.handler(params, { runtime: runtimeWithSettings() })).toBe(status)
|
||||
expect(installForRuntimeHomeMock).toHaveBeenCalledExactlyOnceWith(
|
||||
'\\\\wsl.localhost\\Ubuntu-24.04\\home\\jin\\.local\\share\\orca\\codex-runtime-home\\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'
|
||||
})
|
||||
|
||||
expect(
|
||||
method.handler(params, { runtime: runtimeWithSettings(enabled, disabledTuiAgents) })
|
||||
).toBeNull()
|
||||
expect(installForRuntimeHomeMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects non-local callers', async () => {
|
||||
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'
|
||||
})
|
||||
|
||||
expect(() =>
|
||||
method.handler(params, {
|
||||
runtime: runtimeWithSettings(),
|
||||
clientKind: 'runtime'
|
||||
} as RpcContext)
|
||||
).toThrow(/only available to the local Orca CLI/)
|
||||
expect(installForRuntimeHomeMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('propagates an attempted installer failure', () => {
|
||||
installForRuntimeHomeMock.mockImplementation(() => {
|
||||
throw 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'
|
||||
})
|
||||
|
||||
expect(() => method.handler(params, { runtime: runtimeWithSettings() })).toThrow(
|
||||
'install failed'
|
||||
)
|
||||
expect(installForRuntimeHomeMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
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-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: (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 prepareManagedWslCodexHomeBeforeShellLaunch({
|
||||
env: {
|
||||
CODEX_HOME: params.codexHome,
|
||||
ORCA_CODEX_HOME: params.orcaCodexHome,
|
||||
WSL_DISTRO_NAME: params.wslDistro
|
||||
},
|
||||
hooksEnabled:
|
||||
settings.agentStatusHooksEnabled && !settings.disabledTuiAgents.includes('codex')
|
||||
})
|
||||
}
|
||||
})
|
||||
]
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user