mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(agent-teams): run the tmux shim through the headless host's CLI, never the GUI binary (#33389)
* fix(agent-teams): run the tmux shim through the headless host's CLI, never the GUI binary On an orca serve host the runtime has no Electron resources directory, so the shim fell back to PATH and picked the AppImage's Electron binary, which booted Chromium and died on the SUID sandbox. Prefer the host's profile-scoped CLI launcher, and route agent-teams-tmux through the packaged CLI redirect so a direct Electron invocation never initializes the GUI. Fixes #33354 * fix(agent-teams): prefer a fresh CLI over a shim bin inherited from an older leader pane
This commit is contained in:
@@ -7,7 +7,8 @@ vi.mock('electron', () => ({ app: { commandLine: { getSwitchValue: () => '' } }
|
||||
|
||||
describe('CLI launch redirect command names', () => {
|
||||
it('routes every CLI root through command redirect or direct serve', () => {
|
||||
const roots = new Set(COMMAND_SPECS.map((spec) => spec.path[0]))
|
||||
// Why agent-teams-tmux: the CLI dispatches it before spec parsing, and the tmux shim runs it.
|
||||
const roots = new Set([...COMMAND_SPECS.map((spec) => spec.path[0]), 'agent-teams-tmux'])
|
||||
const options = {
|
||||
platform: 'linux',
|
||||
isPackaged: true,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, relative } from 'node:path'
|
||||
import { dirname, join, relative } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { installFakeAppEnvironment } from '../../../config/scripts/vitest-host-ports-setup'
|
||||
import { getOrcaCliCommandNameForPlatform } from '../../shared/orca-cli-command-name'
|
||||
import {
|
||||
buildClaudeAgentTeamsLaunchPlan,
|
||||
ensureClaudeAgentTeamsShimDir,
|
||||
@@ -96,6 +98,33 @@ describe('claude agent teams shim env', () => {
|
||||
expect(resolveClaudeAgentTeamsShimBin({ PATH: root })).toBe(cliPath)
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
"prefers a headless host's CLI launcher over a GUI binary earlier on PATH",
|
||||
async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-agent-teams-cli-'))
|
||||
roots.push(root)
|
||||
// Why: an AppImage puts its Electron binary, named like the CLI, first on PATH.
|
||||
const electron = join(root, getOrcaCliCommandNameForPlatform(process.platform))
|
||||
const launcher = join(root, 'cli', 'bin', 'orca')
|
||||
await mkdir(dirname(launcher), { recursive: true })
|
||||
for (const path of [electron, launcher]) {
|
||||
await writeFile(path, '#!/usr/bin/env sh\n', 'utf8')
|
||||
await chmod(path, 0o755)
|
||||
}
|
||||
installFakeAppEnvironment({ getCliLauncherPath: () => launcher })
|
||||
|
||||
expect(resolveClaudeAgentTeamsShimBin({ PATH: root })).toBe(launcher)
|
||||
// An older leader pane re-running `orca claude-teams` carries its stale team env.
|
||||
expect(
|
||||
resolveClaudeAgentTeamsShimBin({
|
||||
PATH: root,
|
||||
ORCA_AGENT_TEAMS_TEAM_ID: 'team-old',
|
||||
ORCA_AGENT_TEAMS_SHIM_BIN: electron
|
||||
})
|
||||
).toBe(launcher)
|
||||
}
|
||||
)
|
||||
|
||||
it('refuses to resolve a CLI through relative PATH entries or a bare override', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-agent-teams-cli-'))
|
||||
roots.push(root)
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
isDirectClaudeCommand,
|
||||
type ClaudeAgentTeamsMode
|
||||
} from '../../shared/claude-agent-teams-tmux-compat'
|
||||
import { getAppEnvironment, hasAppEnvironment } from '../../shared/app-environment'
|
||||
import { getOrcaCliCommandNameForPlatform } from '../../shared/orca-cli-command-name'
|
||||
import { getBundledLauncherPath } from '../cli/bundled-cli-launcher-path'
|
||||
import { resolvePathEnvKey } from '../pty/windows-path-segment-merge'
|
||||
|
||||
export type ClaudeAgentTeamsLaunchPlan = {
|
||||
@@ -65,18 +67,33 @@ export function resolveClaudeAgentTeamsShimBin(
|
||||
): string | null {
|
||||
// Why: Windows callers pass an env spelt `Path`; reading only `PATH` there would find no CLI at all.
|
||||
const pathValue = env[resolvePathEnvKey(env, process.platform)]
|
||||
const override = env.ORCA_AGENT_TEAMS_SHIM_BIN
|
||||
if (override) {
|
||||
// Why: a bare override name would be resolved by the shim's shell against its cwd, so qualify it or ignore it.
|
||||
const qualified = isAbsolute(override) ? override : findExecutableOnPath(override, pathValue)
|
||||
if (qualified) {
|
||||
return qualified
|
||||
}
|
||||
const rawOverride = env.ORCA_AGENT_TEAMS_SHIM_BIN
|
||||
// Why: a bare override name would be resolved by the shim's shell against its cwd, so qualify it or ignore it.
|
||||
const override = rawOverride
|
||||
? isAbsolute(rawOverride)
|
||||
? rawOverride
|
||||
: findExecutableOnPath(rawOverride, pathValue)
|
||||
: null
|
||||
// Why: with a team id the value was inherited from an older leader pane and may name a stale GUI binary.
|
||||
const inheritedFromTeam = Boolean(env.ORCA_AGENT_TEAMS_TEAM_ID)
|
||||
if (override && !inheritedFromTeam) {
|
||||
return override
|
||||
}
|
||||
const bundled = bundledLauncherPath()
|
||||
// Why first: a headless host has no resources dir, and the PATH fallback below can then find
|
||||
// the AppImage's Electron binary, which boots the GUI instead of the CLI.
|
||||
const hostLauncher = hasAppEnvironment() ? getAppEnvironment().getCliLauncherPath?.() : null
|
||||
if (hostLauncher && isAbsolute(hostLauncher) && isExecutableFile(hostLauncher)) {
|
||||
return hostLauncher
|
||||
}
|
||||
const bundled = process.resourcesPath
|
||||
? getBundledLauncherPath(process.platform, process.resourcesPath)
|
||||
: null
|
||||
if (bundled && isExecutableFile(bundled)) {
|
||||
return bundled
|
||||
}
|
||||
if (override) {
|
||||
return override
|
||||
}
|
||||
return (
|
||||
findExecutableOnPath(process.platform === 'win32' ? 'orca-dev.cmd' : 'orca-dev', pathValue) ??
|
||||
findExecutableOnPath(getOrcaCliCommandNameForPlatform(process.platform), pathValue)
|
||||
@@ -87,22 +104,6 @@ function defaultShimRoot(): string {
|
||||
return join(homedir(), '.orca', 'claude-agent-teams-bin')
|
||||
}
|
||||
|
||||
function bundledLauncherPath(): string | null {
|
||||
if (!process.resourcesPath) {
|
||||
return null
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
return join(process.resourcesPath, 'bin', 'orca')
|
||||
}
|
||||
if (process.platform === 'linux') {
|
||||
return join(process.resourcesPath, 'bin', 'orca-ide')
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
return join(process.resourcesPath, 'bin', 'orca.exe')
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function findExecutableOnPath(command: string, pathValue: string | undefined): string | null {
|
||||
for (const directory of pathValue?.split(delimiter) ?? []) {
|
||||
// Why: empty and relative PATH entries resolve against a cwd we do not control, which is the hijack we are avoiding.
|
||||
|
||||
@@ -3,6 +3,7 @@ export const CLI_COMMAND_NAMES = [
|
||||
'account',
|
||||
'agent',
|
||||
'agent-context',
|
||||
'agent-teams-tmux',
|
||||
'artifacts',
|
||||
'automations',
|
||||
'back',
|
||||
|
||||
Reference in New Issue
Block a user