mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
fix(cli): negotiate agent launches with paired runtimes (#32029)
* fix(cli): negotiate agent launches with paired runtimes * refactor(cli): share paired client capability list across request paths Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { SKILL_INSTALL_RESULT_V2_CAPABILITY } from '../../shared/skill-install-capability'
|
||||
import { AGENT_LAUNCH_RUNTIME_CAPABILITY } from '../../shared/agent-launch-runtime-capability'
|
||||
import { createServer, type Server } from 'node:http'
|
||||
import { mkdtempSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
@@ -89,7 +90,8 @@ describe('CLI remote WebSocket transport', () => {
|
||||
WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY,
|
||||
AUTOMATION_OWNER_FENCING_RUNTIME_CAPABILITY,
|
||||
REPO_SEARCH_QUALIFIED_REFS_RUNTIME_CAPABILITY,
|
||||
TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY
|
||||
TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY,
|
||||
AGENT_LAUNCH_RUNTIME_CAPABILITY
|
||||
]
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PairedRuntimeConnection } from '../../shared/paired-runtime-connection'
|
||||
import type { RuntimeOrchestrationEnvelope } from '../../shared/runtime-rpc-envelope'
|
||||
import { RemoteRuntimeClientError } from '../../shared/remote-runtime-client-error'
|
||||
import { AGENT_LAUNCH_RUNTIME_CAPABILITY } from '../../shared/agent-launch-runtime-capability'
|
||||
import {
|
||||
sendRemoteRuntimeRequest,
|
||||
sendRemoteRuntimeRequestWithStatusPreflight
|
||||
@@ -8,6 +9,9 @@ import {
|
||||
import type { RuntimeStatus } from '../../shared/runtime-types'
|
||||
import { RuntimeClientError, type RuntimeRpcResponse } from './types'
|
||||
|
||||
// Paired CLI callers are not exempt from the host's agent.launch gate like the local socket is.
|
||||
const CLI_CLIENT_CAPABILITIES = [AGENT_LAUNCH_RUNTIME_CAPABILITY] as const
|
||||
|
||||
export async function sendWebSocketRequest<TResult>(
|
||||
connection: PairedRuntimeConnection,
|
||||
method: string,
|
||||
@@ -16,7 +20,15 @@ export async function sendWebSocketRequest<TResult>(
|
||||
envelope?: RuntimeOrchestrationEnvelope
|
||||
): Promise<RuntimeRpcResponse<TResult>> {
|
||||
try {
|
||||
return await sendRemoteRuntimeRequest<TResult>(connection, method, params, timeoutMs, envelope)
|
||||
return await sendRemoteRuntimeRequest<TResult>(
|
||||
connection,
|
||||
method,
|
||||
params,
|
||||
timeoutMs,
|
||||
envelope,
|
||||
undefined,
|
||||
CLI_CLIENT_CAPABILITIES
|
||||
)
|
||||
} catch (error) {
|
||||
if (error instanceof RemoteRuntimeClientError) {
|
||||
throw new RuntimeClientError(error.code, error.message, error.data)
|
||||
@@ -40,7 +52,8 @@ export async function sendWebSocketRequestWithStatusPreflight<TResult>(
|
||||
params,
|
||||
timeoutMs,
|
||||
validateStatus,
|
||||
envelope
|
||||
envelope,
|
||||
CLI_CLIENT_CAPABILITIES
|
||||
)
|
||||
} catch (error) {
|
||||
if (error instanceof RemoteRuntimeClientError) {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { RuntimeClient } from '../../src/cli/runtime/client'
|
||||
import type { AgentLaunchResult } from '../../src/shared/agent-launch-intent'
|
||||
import { openTestAgentSessionRecordStore } from '../../src/main/runtime/agent-session-record-store-test-harness'
|
||||
import { OrcaRuntimeService } from '../../src/main/runtime/orca-runtime'
|
||||
import { OrcaRuntimeRpcServer } from '../../src/main/runtime/runtime-rpc'
|
||||
import {
|
||||
runtimeStub,
|
||||
setAgentLaunchRecordStore
|
||||
} from '../../src/main/runtime/rpc/methods/agent-launch.test-fixture'
|
||||
import { AGENT_LAUNCH_METHODS } from '../../src/main/runtime/rpc/methods/agent-launch'
|
||||
import { STATUS_METHODS } from '../../src/main/runtime/rpc/methods/status'
|
||||
import { closeTestJournalHostDatabase } from '../../src/main/native-chat/agent-session-journal/journal-host-database-test-support'
|
||||
|
||||
const TARGETS = [
|
||||
{ kind: 'existing', worktree: 'id:wt-7' },
|
||||
{ kind: 'create-worktree', create: { repo: 'id:repo-1', name: 'paired-task' } },
|
||||
{ kind: 'create-folder-workspace', create: { projectGroupId: 'group-1' } }
|
||||
] as const
|
||||
|
||||
describe.each([
|
||||
{ method: 'agent.launch', path: 'preflight' },
|
||||
{ method: 'agent.launch', path: 'verified-status' },
|
||||
{ method: 'agent.launchReplay', path: 'preflight' },
|
||||
{ method: 'agent.launchReplay', path: 'verified-status' }
|
||||
])('paired CLI $method via $path', ({ method, path }) => {
|
||||
it.each(TARGETS)('launches and replays $kind without a second agent', async (target) => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-paired-cli-launch-'))
|
||||
const store = await openTestAgentSessionRecordStore(directory)
|
||||
setAgentLaunchRecordStore(store)
|
||||
const boundary = runtimeStub({
|
||||
settings: {},
|
||||
terminalPaneKey: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d:3f2504e0-4f89-41d3-9a0c-0305e82c3301',
|
||||
startupTerminalPaneKey:
|
||||
'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d:3f2504e0-4f89-41d3-9a0c-0305e82c3301',
|
||||
lineCarriesPrompt: true
|
||||
})
|
||||
const runtime = Object.assign(new OrcaRuntimeService(), boundary, {
|
||||
selectCreatedMobileSessionTabForClient: vi.fn(() => true)
|
||||
})
|
||||
const server = new OrcaRuntimeRpcServer({
|
||||
runtime,
|
||||
userDataPath: directory,
|
||||
methods: [...STATUS_METHODS, ...AGENT_LAUNCH_METHODS],
|
||||
enableWebSocket: true,
|
||||
wsPort: 0
|
||||
})
|
||||
try {
|
||||
await server.start()
|
||||
const offer = server.createPairingOffer({ name: 'paired-cli', scope: 'runtime' })
|
||||
if (!offer.available) {
|
||||
throw new Error('pairing unavailable')
|
||||
}
|
||||
const client = new RuntimeClient(directory, 5_000, offer.pairingUrl, null)
|
||||
if (path === 'verified-status') {
|
||||
await client.getCliStatus()
|
||||
}
|
||||
const params = {
|
||||
agent: 'claude',
|
||||
target,
|
||||
presentation: 'background',
|
||||
operationId: `${Date.now()}-000000000000000000000000000000ef`,
|
||||
prompt: { text: 'Reply with OK', delivery: 'submit' }
|
||||
}
|
||||
const first = await client.call<AgentLaunchResult>(method, params)
|
||||
expect(first.result).toMatchObject({
|
||||
outcome: { kind: 'terminal' },
|
||||
worktreeId:
|
||||
target.kind === 'existing'
|
||||
? 'wt-7'
|
||||
: target.kind === 'create-worktree'
|
||||
? 'wt-new'
|
||||
: 'folder:fw-new',
|
||||
prompt: { delivery: 'submit', outcome: 'handed-to-terminal' }
|
||||
})
|
||||
const replay = await client.call<AgentLaunchResult>(method, params)
|
||||
expect(replay.result).toEqual(first.result)
|
||||
if (target.kind === 'create-worktree') {
|
||||
expect(boundary.createManagedWorktree).toHaveBeenCalledTimes(1)
|
||||
expect(boundary.createTerminal).not.toHaveBeenCalled()
|
||||
} else {
|
||||
expect(boundary.createTerminal).toHaveBeenCalledTimes(1)
|
||||
expect(boundary.createTerminal).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({ startupAgent: 'claude', startupPrompt: 'Reply with OK' })
|
||||
)
|
||||
}
|
||||
expect(boundary.createFolderWorkspace).toHaveBeenCalledTimes(
|
||||
target.kind === 'create-folder-workspace' ? 1 : 0
|
||||
)
|
||||
} finally {
|
||||
await server.stop()
|
||||
setAgentLaunchRecordStore(null)
|
||||
closeTestJournalHostDatabase(directory)
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user