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:
Neil
2026-10-24 21:35:18 -07:00
committed by GitHub
parent 1b54aba056
commit c25fad76df
4 changed files with 59 additions and 27 deletions
@@ -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)
+25 -24
View File
@@ -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.
+1
View File
@@ -3,6 +3,7 @@ export const CLI_COMMAND_NAMES = [
'account',
'agent',
'agent-context',
'agent-teams-tmux',
'artifacts',
'automations',
'back',