mirror of
https://github.com/stablyai/orca.git
synced 2026-10-06 08:02:28 +00:00
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.
This commit is contained in:
@@ -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.
|
||||
@@ -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<TuiAgent | undefined> {
|
||||
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) {
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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<Record<TuiAgent, string>>,
|
||||
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]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
+36
@@ -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<T extends { agent?: string }>(
|
||||
runtime: OrcaRuntimeService,
|
||||
params: T,
|
||||
resolveTarget: () => Promise<WorkerAgentTarget>
|
||||
): Promise<T> {
|
||||
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<typeof prepareFederationAttachmentWorkerStart>[0]
|
||||
) {
|
||||
const params = await resolveWorkerConfiguredAgentParams(args.runtime, args.params, async () =>
|
||||
args.createsWorktree ? { repo: args.params.repo } : { worktree: args.params.worktree }
|
||||
)
|
||||
return prepareFederationAttachmentWorkerStart({ ...args, params })
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user