From 1d5d02e39634612f6a73cb17a2e79ca12098a0ce Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:40:48 -0700 Subject: [PATCH] Resolve explicitly configured command aliases for workers (#24648) * feat(orchestration): resolve explicitly configured command aliases * docs(orchestration): explain configured command aliases * fix(orchestration): validate configured aliases with target shell grammar * fix(orchestration): use actual shell and refuse assignment-only aliases * test: preserve typed calls in configured worker target checks Replace Reflect.apply with the existing typed prototype call pattern so the unchanged regression cases pass the anti-slop lint gate. --- .../orchestration-configured-agent-aliases.md | 22 +++++ ...a-runtime-get-terminal-interactive-wait.ts | 40 +++++++++ ...hestration-configured-agent-target.test.ts | 86 +++++++++++++++++++ .../configured-worker-agent-selector.test.ts | 80 +++++++++++++++++ .../configured-worker-agent-selector.ts | 51 +++++++++++ .../orchestration/federation/federation.ts | 4 +- .../worker/local-worker-start.ts | 22 ++++- .../worker-configured-agent-preflight.ts | 36 ++++++++ .../worker/worker-start-validation.ts | 10 ++- 9 files changed, 345 insertions(+), 6 deletions(-) create mode 100644 docs/reference/orchestration-configured-agent-aliases.md create mode 100644 src/main/runtime/orchestration-configured-agent-target.test.ts create mode 100644 src/main/runtime/orchestration/configured-worker-agent-selector.test.ts create mode 100644 src/main/runtime/orchestration/configured-worker-agent-selector.ts create mode 100644 src/main/runtime/rpc/methods/orchestration/worker/worker-configured-agent-preflight.ts diff --git a/docs/reference/orchestration-configured-agent-aliases.md b/docs/reference/orchestration-configured-agent-aliases.md new file mode 100644 index 00000000000..2dc95ccd100 --- /dev/null +++ b/docs/reference/orchestration-configured-agent-aliases.md @@ -0,0 +1,22 @@ +# Configured command aliases for orchestration workers + +A worker can use the name of a direct executable configured in **Settings → Agents**. +Choose the built-in agent whose command-line interface the executable implements, +then set that agent's command override to the executable name or quoted full path. +For example, configure Codex's command as `codex-fugu`, then select it with +`orca orchestration worker-start --agent codex-fugu` and the normal placement options. + +The execution host resolves its own configuration. Launch receipts use the canonical +agent (`codex` in this example), and model/effort handling reuses that agent's existing +launch rules. A receipt records applied launch preferences; it does not prove provider +entitlement, successful generation, or an arbitrary vendor's model selection behavior. + +Aliases require a single executable token. Commands containing interpreter arguments, +environment assignments, or shell wrappers are not aliases. Multiple built-in agents +configured with the same executable name are ambiguous and require the canonical agent +ID. Disabled launchers remain disabled. An unconfigured name is refused even if it is +on PATH; Orca cannot infer a compatible launch interface from a process name. + +The same rule applies to folder workspaces and git worktrees. For a remote worker, +configure the command on its execution host. Older hosts may refuse aliases they do not +support. Configuring a command does not create new status producers or grant permissions. diff --git a/src/main/runtime/orca-runtime-get-terminal-interactive-wait.ts b/src/main/runtime/orca-runtime-get-terminal-interactive-wait.ts index 80796cb28bb..82969ecb1da 100644 --- a/src/main/runtime/orca-runtime-get-terminal-interactive-wait.ts +++ b/src/main/runtime/orca-runtime-get-terminal-interactive-wait.ts @@ -15,6 +15,10 @@ import { selectExactWorkerProviderSession } from './orchestration/worker-provide import type { TuiAgent } from '../../shared/tui-agent' import { isTuiAgentEnabled } from '../../shared/tui-agent-selection' import { OrchestrationError } from './orchestration/orchestration-error' +import { resolveLocalWindowsAgentStartupShell } from '../../shared/windows-terminal-shell' +import { resolveStartupShell, type AgentStartupShell } from '../../shared/tui-agent-startup-shell' +import { isTuiAgent } from '../../shared/tui-agent-config' +import { resolveConfiguredWorkerAgent } from './orchestration/configured-worker-agent-selector' export class OrcaRuntimeWithGetTerminalInteractiveWait extends OrcaRuntimeWithAdoptTerminalOrphansFromInventory { async getTerminalInteractiveWait( @@ -183,6 +187,42 @@ export class OrcaRuntimeWithGetTerminalInteractiveWait extends OrcaRuntimeWithAd }) } + resolveOrchestrationAgentLauncher( + selector: string, + platform: NodeJS.Platform = process.platform, + shell?: AgentStartupShell + ): TuiAgent | undefined { + return resolveConfiguredWorkerAgent( + selector, + this.store?.getSettings().agentCmdOverrides ?? {}, + platform, + shell + ) + } + + async resolveOrchestrationAgentLauncherForTarget( + selector: string, + target: { repo?: string; worktree?: string } + ): Promise { + if (isTuiAgent(selector)) { + return selector + } + const repo = target.repo ? await this.resolveRepoSelector(target.repo) : null + const workspace = repo + ? { repo, path: repo.path, connectionId: repo.connectionId } + : await this.resolveTerminalWorkspaceLaunchScope(target.worktree) + const platform = this.getAgentLaunchPlatformForWorkspace(workspace) + const shell = resolveStartupShell( + platform, + resolveLocalWindowsAgentStartupShell({ + platform, + isRemote: Boolean(workspace.connectionId), + terminalWindowsShell: this.store?.getSettings().terminalWindowsShell + }) + ) + return this.resolveOrchestrationAgentLauncher(selector, platform, shell) + } + validateOrchestrationAgentLauncher(agent: TuiAgent): void { const settings = this.store?.getSettings() if (!settings) { diff --git a/src/main/runtime/orchestration-configured-agent-target.test.ts b/src/main/runtime/orchestration-configured-agent-target.test.ts new file mode 100644 index 00000000000..ff656e50d92 --- /dev/null +++ b/src/main/runtime/orchestration-configured-agent-target.test.ts @@ -0,0 +1,86 @@ +import type { AgentStartupShell } from '../../shared/tui-agent-startup-shell' +import { describe, expect, it, vi } from 'vitest' +import { OrcaRuntimeService } from './orca-runtime' +import { resolveConfiguredWorkerAgent } from './orchestration/configured-worker-agent-selector' + +it.each(['folder', 'ssh', 'wsl'] as const)( + 'resolves the %s target platform before interpreting an alias', + async (kind) => { + const scope = { + path: kind === 'wsl' ? '\\\\wsl.localhost\\Ubuntu\\repo' : '/opt/repo', + connectionId: kind === 'ssh' ? 'ssh-1' : null + } + const context = { + resolveTerminalWorkspaceLaunchScope: vi.fn(async () => scope), + getAgentLaunchPlatformForWorkspace: vi.fn(() => 'linux' as const), + resolveOrchestrationAgentLauncher: vi.fn( + (selector: string, platform: NodeJS.Platform, shell?: AgentStartupShell) => + resolveConfiguredWorkerAgent( + selector, + { opencode: '/opt/My\\ Agent/opencode-private' }, + platform, + shell + ) + ) + } + const result = + await OrcaRuntimeService.prototype.resolveOrchestrationAgentLauncherForTarget.call( + context, + 'opencode-private', + { worktree: 'id:workspace' } + ) + expect(result).toBe('opencode') + expect(context.getAgentLaunchPlatformForWorkspace).toHaveBeenCalledWith(scope) + expect(context.resolveOrchestrationAgentLauncher).toHaveBeenCalledWith( + 'opencode-private', + 'linux', + 'posix' + ) + } +) + +describe('canonical worker agent target', () => { + it('keeps canonical selectors authoritative without probing another host', async () => { + const resolve = vi.fn() + expect( + await OrcaRuntimeService.prototype.resolveOrchestrationAgentLauncherForTarget.call( + { resolveTerminalWorkspaceLaunchScope: resolve }, + 'opencode', + { worktree: 'id:remote' } + ) + ).toBe('opencode') + expect(resolve).not.toHaveBeenCalled() + }) +}) + +it('resolves local Windows shell settings rather than assuming PowerShell', async () => { + const context = { + store: { getSettings: () => ({ terminalWindowsShell: 'bash.exe' }) }, + resolveTerminalWorkspaceLaunchScope: vi.fn(async () => ({ + path: 'C:\\repo', + connectionId: null + })), + getAgentLaunchPlatformForWorkspace: vi.fn(() => 'win32' as const), + resolveOrchestrationAgentLauncher: vi.fn( + (selector: string, platform: NodeJS.Platform, shell?: AgentStartupShell) => + resolveConfiguredWorkerAgent( + selector, + { opencode: '/c/Agent\\ Directory/opencode-private.exe' }, + platform, + shell + ) + ) + } + expect( + await OrcaRuntimeService.prototype.resolveOrchestrationAgentLauncherForTarget.call( + context, + 'opencode-private', + { worktree: 'id:workspace' } + ) + ).toBe('opencode') + expect(context.resolveOrchestrationAgentLauncher).toHaveBeenCalledWith( + 'opencode-private', + 'win32', + 'posix' + ) +}) diff --git a/src/main/runtime/orchestration/configured-worker-agent-selector.test.ts b/src/main/runtime/orchestration/configured-worker-agent-selector.test.ts new file mode 100644 index 00000000000..16ae957da0a --- /dev/null +++ b/src/main/runtime/orchestration/configured-worker-agent-selector.test.ts @@ -0,0 +1,80 @@ +import { expect, it } from 'vitest' +import { resolveConfiguredWorkerAgent } from './configured-worker-agent-selector' + +it('reuses the configured built-in grammar for a direct vendor executable', () => { + expect(resolveConfiguredWorkerAgent('codex-fugu', { codex: 'codex-fugu' })).toBe('codex') + expect(resolveConfiguredWorkerAgent('claude-fugu', { claude: 'claude-fugu' })).toBe('claude') +}) +it('keeps canonical IDs authoritative even when their command has an alias', () => { + expect(resolveConfiguredWorkerAgent('codex', { codex: 'codex-fugu' })).toBe('codex') +}) +it('requires explicit configuration instead of guessing a PATH command grammar', () => { + expect(resolveConfiguredWorkerAgent('codex-fugu', {})).toBeUndefined() + expect(resolveConfiguredWorkerAgent('node', { codex: 'node vendor.js' })).toBeUndefined() +}) +it('recognizes quoted native and Windows paths without changing the configured command', () => { + expect( + resolveConfiguredWorkerAgent( + 'opencode-private', + { + opencode: "'/tmp/agent directory/opencode-private'" + }, + 'darwin' + ) + ).toBe('opencode') + expect( + resolveConfiguredWorkerAgent( + 'codex-fugu', + { + codex: '"C:\\Agent Directory\\codex-fugu.exe"' + }, + 'win32' + ) + ).toBe('codex') +}) +it('refuses ambiguous aliases instead of selecting a different provider grammar', () => { + expect(() => + resolveConfiguredWorkerAgent('vendor', { + codex: 'vendor', + claude: 'vendor' + }) + ).toThrow('multiple launchers') +}) + +it.each([ + 'echo;/tmp/opencode-private', + 'echo&&/tmp/opencode-private', + '$(echo /tmp)/opencode-private', + '`echo /tmp`/opencode-private' +])('refuses shell-divergent override %s', (opencode) => { + expect(resolveConfiguredWorkerAgent('opencode-private', { opencode }, 'linux')).toBeUndefined() +}) +it('uses target POSIX grammar for an escaped-space guest executable', () => { + const command = '/opt/My\\ Agent/opencode-private' + expect(resolveConfiguredWorkerAgent('opencode-private', { opencode: command }, 'linux')).toBe( + 'opencode' + ) + expect( + resolveConfiguredWorkerAgent('opencode-private', { opencode: command }, 'win32') + ).toBeUndefined() +}) + +it('refuses an assignment without an executable', () => { + expect( + resolveConfiguredWorkerAgent( + 'opencode-private', + { opencode: 'FOO=/tmp/opencode-private' }, + 'linux' + ) + ).toBeUndefined() +}) +it('uses the actual native Windows Git Bash grammar', () => { + expect( + resolveConfiguredWorkerAgent( + 'opencode-private', + { opencode: '/c/Agent\\ Directory/opencode-private.exe' }, + 'win32', + 'posix' + ) + ).toBe('opencode') +}) diff --git a/src/main/runtime/orchestration/configured-worker-agent-selector.ts b/src/main/runtime/orchestration/configured-worker-agent-selector.ts new file mode 100644 index 00000000000..959eb66c55c --- /dev/null +++ b/src/main/runtime/orchestration/configured-worker-agent-selector.ts @@ -0,0 +1,51 @@ +import { extractLeadingEnvAssignments } from '../../../shared/command-environment' +import { getCommandTokenPathBasename } from '../../../shared/command-token-scanner' +import type { TuiAgent } from '../../../shared/tui-agent' +import { isTuiAgent } from '../../../shared/tui-agent-config' +import { + resolveStartupShell, + type AgentStartupShell, + tokenizeStartupCommand +} from '../../../shared/tui-agent-startup-shell' +import { OrchestrationError } from './orchestration-error' + +export function resolveConfiguredWorkerAgent( + selector: string, + overrides: Partial>, + platform: NodeJS.Platform = process.platform, + shell?: AgentStartupShell +): TuiAgent | undefined { + if (isTuiAgent(selector)) { + return selector + } + const matches: TuiAgent[] = [] + for (const [agent, command] of Object.entries(overrides)) { + if (!isTuiAgent(agent) || !command) { + continue + } + const parsed = tokenizeStartupCommand(command, resolveStartupShell(platform, shell)) + // A command wrapper cannot attest which CLI grammar its arguments implement. + if ( + !parsed.ok || + parsed.tokens.length !== 1 || + parsed.spans.some((span) => span.divergesFromShell) + ) { + continue + } + if (extractLeadingEnvAssignments(parsed.tokens).env) { + continue + } + const executable = parsed.tokens[0] + const name = getCommandTokenPathBasename(executable).replace(/\.(?:exe|cmd|bat)$/i, '') + if (name === selector) { + matches.push(agent) + } + } + if (matches.length > 1) { + throw new OrchestrationError( + 'agent_unconfigured', + `Agent command ${selector} is configured for multiple launchers. Use a canonical agent ID.` + ) + } + return matches[0] +} diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federation.ts b/src/main/runtime/rpc/methods/orchestration/federation/federation.ts index ca63c1fb46d..ee0033e8fbd 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federation.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federation.ts @@ -21,7 +21,7 @@ import { } from './federation-setup' import { FederationAttachStartParams } from './federation-start-schema' import { failFederatedAttachmentWithReceipt } from './federation-start-receipt' -import { prepareFederationAttachmentWorkerStart } from '../worker/worker-start-validation' +import { prepareFederationConfiguredWorkerStart } from '../worker/worker-configured-agent-preflight' import { isWorkerStartTimeoutWithinTimerLimit, resolveWorkerStartReadinessTimeoutMs @@ -54,7 +54,7 @@ export const ORCHESTRATION_FEDERATION_ATTACH_METHODS = [ ) } const createsWorktree = params.worktree === 'new-top-level' - const { agent, launch } = prepareFederationAttachmentWorkerStart({ + const { agent, launch } = await prepareFederationConfiguredWorkerStart({ params, createsWorktree, runtime diff --git a/src/main/runtime/rpc/methods/orchestration/worker/local-worker-start.ts b/src/main/runtime/rpc/methods/orchestration/worker/local-worker-start.ts index 7e10274435b..f276dc736cb 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/local-worker-start.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/local-worker-start.ts @@ -1,3 +1,4 @@ +import { resolveWorkerConfiguredAgentParams } from './worker-configured-agent-preflight' import type { OrcaRuntimeService } from '../../../../orca-runtime' import { describeTerminalWaitBlockedReason } from '../../../../../../shared/terminal-wait-blocked-reason-legacy-alias' import type { OrchestrationDb } from '../../../../orchestration/db' @@ -54,7 +55,26 @@ export async function startLocalWorker(args: { const coordinatorPane = coordinator?.paneKey ?? null const requestedWorktree = params.worktree ?? 'current' const createsWorktree = requestedWorktree === 'new-child' || requestedWorktree === 'new-top-level' - const { agent, launch } = prepareLocalWorkerStart({ params, createsWorktree, runtime }) + const launchParams = await resolveWorkerConfiguredAgentParams(runtime, params, async () => { + const callerWorkspaceId = await resolveDispatchCallerWorktreeId( + runtime, + params.from, + callerSession + ) + const parent = createsWorktree + ? await runtime.showManagedWorktree(`id:${callerWorkspaceId}`) + : undefined + return createsWorktree + ? { repo: params.repo ?? parent?.repoId } + : { + worktree: requestedWorktree === 'current' ? `id:${callerWorkspaceId}` : requestedWorktree + } + }) + const { agent, launch } = prepareLocalWorkerStart({ + params: launchParams, + createsWorktree, + runtime + }) const coordinatorWorktreeId = await resolveDispatchCallerWorktreeId( runtime, diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-configured-agent-preflight.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-configured-agent-preflight.ts new file mode 100644 index 00000000000..a61282c6d08 --- /dev/null +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-configured-agent-preflight.ts @@ -0,0 +1,36 @@ +import type { OrcaRuntimeService } from '../../../../orca-runtime' +import { isTuiAgent } from '../../../../../../shared/tui-agent-config' +import { OrchestrationError } from '../../../../orchestration/orchestration-error' +import { prepareFederationAttachmentWorkerStart } from './worker-start-validation' + +type WorkerAgentTarget = { repo?: string; worktree?: string } + +export async function resolveWorkerConfiguredAgentParams( + runtime: OrcaRuntimeService, + params: T, + resolveTarget: () => Promise +): Promise { + if (!params.agent || isTuiAgent(params.agent)) { + return params + } + const agent = await runtime.resolveOrchestrationAgentLauncherForTarget( + params.agent, + await resolveTarget() + ) + if (!agent) { + throw new OrchestrationError( + 'agent_unconfigured', + 'A configured single-executable agent alias is required.' + ) + } + return { ...params, agent } +} + +export async function prepareFederationConfiguredWorkerStart( + args: Parameters[0] +) { + const params = await resolveWorkerConfiguredAgentParams(args.runtime, args.params, async () => + args.createsWorktree ? { repo: args.params.repo } : { worktree: args.params.worktree } + ) + return prepareFederationAttachmentWorkerStart({ ...args, params }) +} diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-start-validation.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-start-validation.ts index 1ecce8e557f..1f80eebcb34 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-start-validation.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-start-validation.ts @@ -40,7 +40,7 @@ export function validateFederatedWorkerStartPlacement( '--terminal reuses an existing agent and cannot combine with --agent.' ) } - if (!params.terminal && (!params.agent || !isTuiAgent(params.agent))) { + if (!params.terminal && !params.agent) { throw new OrchestrationError( 'agent_unconfigured', 'A configured --agent is required when remote worker-start creates a terminal.' @@ -139,10 +139,14 @@ function resolveWorkerStartAgent(args: { effort?: string missingAgentMessage: string }): { agent: TuiAgent | undefined; launch: WorkerStartLaunch } { - if (!args.terminal && (!args.agent || !isTuiAgent(args.agent))) { + const agent = args.agent + ? isTuiAgent(args.agent) + ? args.agent + : args.runtime.resolveOrchestrationAgentLauncher?.(args.agent) + : undefined + if (!args.terminal && !agent) { throw new OrchestrationError('agent_unconfigured', args.missingAgentMessage) } - const agent = args.agent as TuiAgent | undefined if (agent) { args.runtime.validateOrchestrationAgentLauncher(agent) return {