Files
orca/src/main/ssh/ssh-remote-orca-cli.ts
T
OrcaWinandm4air 7aa91f730a fix(cli): check the runtime source before sending (#27206)
* fix(cli): check the runtime source before sending

A terminal Orca launches now carries which Orca launched it
(ORCA_RUNTIME_SOURCE_ID = the profile's installation id,
ORCA_RUNTIME_SOURCE_INCARNATION = the launching runtime id). Before a
request is sent, the CLI compares that with the runtime the profile's
runtime record names. Same runtime: sent as before. Different runtime:
a read-only status.get asks which profile it serves; the same profile (a
restart or update) is accepted, anything else is refused before the
request reaches it.

The vars cross into WSL through WSLENV. The SSH bridge stamps the
bridging runtime itself rather than trusting the remote env. The
existing orchestration compatibility stamp is untouched.

* fix(orcad): drop an inherited runtime source at startup

* fix(cli): an explicit switch to another profile is not fenced

The stamp also carries the launching runtime's userData path. A CLI that
dials a different profile than the one that launched its terminal, and
not the platform default an unpinned terminal falls back to, was pointed
there on purpose (orca-dev from a packaged Orca's terminal), so the
fence does not apply.

* refactor(cli): reuse the shared userData path comparison

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 03:19:14 -07:00

319 lines
13 KiB
TypeScript

import type { CliStatusResult, RuntimeStatus } from '../../shared/runtime-types'
import { runtimeHostConnectionState } from '../../shared/runtime-host-connection-state'
import { projectRemoteAppStatus } from '../../shared/cli-app-status-projection'
import { randomUUID } from 'node:crypto'
import type { RuntimeOrchestrationEnvelope } from '../../shared/runtime-rpc-envelope'
import { ORCHESTRATION_CONTRACT_VERSION } from '../../shared/protocol-version'
import type { RpcResponse } from '../runtime/rpc/core'
import { RpcDispatcher } from '../runtime/rpc/dispatcher'
import { ALL_RPC_METHODS } from '../runtime/rpc/methods'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import { getRuntimeSourceStamp } from '../runtime/host-descriptor'
import {
HostCliUnavailableError,
runHostOrcaCliPassthrough,
type HostCliPassthroughOptions,
type RemoteOrcaCliRequest,
type RemoteOrcaCliResult
} from './ssh-remote-cli-host-passthrough'
import { RemoteCliArgumentError, type ParsedRemoteCli } from './ssh-remote-cli-argument-error'
import {
optionalRemoteCliNumber,
optionalRemoteCliString,
parseRemoteCliArgs,
readRemoteRetryRequestFlag,
requiredRemoteCliString,
resolveRemoteCliHandle
} from './ssh-remote-cli-args'
import { buildRemoteCliError } from './ssh-remote-cli-error-response'
import { getRemoteLinearHelp, tryDispatchRemoteLinearCli } from './ssh-remote-linear-cli'
import {
getRemoteOrchestrationPayload,
resolveRemoteOrchestrationSender
} from './ssh-remote-orchestration-send'
import { formatInProcessRemoteCliResult } from './ssh-remote-cli-in-process-result'
import { remoteCliOrchestrationEvidence } from './ssh-remote-orchestration-post-output'
export type { RemoteOrcaCliRequest, RemoteOrcaCliResult } from './ssh-remote-cli-host-passthrough'
// Why: these commands run a foreground/interactive process attached to the
// caller's TTY (or a local tmux pane), which a buffered one-shot relay bridge
// cannot host. Everything else routes through the full host CLI.
const HOST_INTERACTIVE_COMMANDS: Record<string, string> = {
serve:
'orca serve starts a foreground headless Orca server and cannot run through the SSH relay bridge. Run it directly on the machine that should host Orca.',
'claude-teams':
'orca claude-teams starts an interactive Claude Code session and cannot run through the SSH relay bridge. Run it in a terminal on the Orca host machine.',
'agent-teams-tmux':
'orca agent-teams-tmux is a tmux pane shim for the Orca host machine and cannot run through the SSH relay bridge.',
'account add':
'orca account add runs an interactive agent login and cannot run through the buffered SSH relay bridge. Run it directly in a terminal on the Orca host machine.'
}
export async function runRemoteOrcaCli(
runtime: OrcaRuntimeService,
request: RemoteOrcaCliRequest,
passthroughOptions?: HostCliPassthroughOptions
): Promise<RemoteOrcaCliResult> {
const parsed = parseRemoteCliArgs(request.argv)
const json = parsed.flags.has('json')
const command = parsed.commandPath.join(' ')
const interactiveMessage =
HOST_INTERACTIVE_COMMANDS[command] ?? HOST_INTERACTIVE_COMMANDS[parsed.commandPath[0] ?? '']
if (interactiveMessage && !parsed.flags.has('help')) {
if (json) {
return {
stdout: `${JSON.stringify(buildRemoteCliError(interactiveMessage, 'unsupported_over_ssh'), null, 2)}\n`,
stderr: '',
exitCode: 1
}
}
return { stdout: '', stderr: `${interactiveMessage}\n`, exitCode: 1 }
}
if (command === 'orchestration check' || command === 'orchestration ask') {
// Why: compatibility ACKs must wait until relay stdout is observable; a host CLI child can only flush into main's capture pipe.
return await runLegacyRemoteOrcaCli(
runtime,
request,
parsed,
json,
new HostCliUnavailableError('output-ordered orchestration bridge required')
)
}
let passthroughFailure: HostCliUnavailableError | null = null
try {
return await runHostOrcaCliPassthrough(request, {
runtimeSource: getRuntimeSourceStamp(runtime),
...passthroughOptions
})
} catch (err) {
if (!(err instanceof HostCliUnavailableError)) {
throw err
}
// Why: fall back to the legacy in-process command switch below so the
// historical read-only/orchestration surface keeps working even when the
// bundled CLI entry cannot be launched on this install.
passthroughFailure = err
}
return await runLegacyRemoteOrcaCli(runtime, request, parsed, json, passthroughFailure)
}
async function runLegacyRemoteOrcaCli(
runtime: OrcaRuntimeService,
request: RemoteOrcaCliRequest,
parsed: ParsedRemoteCli,
json: boolean,
passthroughFailure: HostCliUnavailableError
): Promise<RemoteOrcaCliResult> {
const dispatcher = new RpcDispatcher({
runtime,
methods: ALL_RPC_METHODS,
callerScope: request.callerScope
})
const help = getRemoteLinearHelp(parsed)
if (help) {
return { stdout: `${help}\n`, stderr: '', exitCode: 0 }
}
try {
const response = await dispatchRemoteCli(
dispatcher,
parsed,
request.env,
request.stdin,
passthroughFailure.message,
request.runtimeAuthority
)
return formatInProcessRemoteCliResult(parsed, request.env, response, json)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const code =
err instanceof RemoteCliArgumentError
? err.code
: err instanceof Error && 'code' in err && typeof err.code === 'string'
? err.code
: 'runtime_error'
if (json) {
return {
stdout: `${JSON.stringify(buildRemoteCliError(message, code), null, 2)}\n`,
stderr: '',
exitCode: 1
}
}
return { stdout: '', stderr: `${message}\n`, exitCode: 1 }
}
}
async function dispatchRemoteCli(
dispatcher: RpcDispatcher,
parsed: ParsedRemoteCli,
env: Record<string, string>,
stdin: string | undefined,
passthroughFailureReason: string,
runtimeAuthority: RemoteOrcaCliRequest['runtimeAuthority']
): Promise<RpcResponse> {
const command = parsed.commandPath.join(' ')
const orchestrationCompatibilityEvidence = remoteCliOrchestrationEvidence(env, runtimeAuthority)
const compatibilityEnvelope: RuntimeOrchestrationEnvelope = {
compatibilityInvocationId: randomUUID(),
orchestrationRequestId:
readRemoteRetryRequestFlag(parsed.flags) ??
(command === 'orchestration check' || command === 'orchestration ask'
? randomUUID()
: undefined),
orchestrationCompatibilityEvidence
}
const linearResponse = await tryDispatchRemoteLinearCli(dispatcher, parsed, env, stdin)
if (linearResponse) {
return linearResponse
}
switch (command) {
case 'status': {
const response = await call(dispatcher, 'status.get')
if (!response.ok) {
return response
}
const status = response.result as RuntimeStatus
const cliStatus: CliStatusResult = {
target: { kind: 'environment', environment: 'ssh' },
// Why: this answers for the Orca host the caller reached over SSH, not for the caller's
// machine. It used to report running:true unconditionally, which claimed a desktop app
// even for a headless `serve`; share the same projection the paired-server path uses so
// both transports answer the question the same way (STA-4792 defect 4).
app: projectRemoteAppStatus(status),
runtime: {
state: status.graphStatus === 'ready' ? 'ready' : 'graph_not_ready',
reachable: true,
connectionState: runtimeHostConnectionState({ hasStatusEntry: true, status }),
runtimeId: status.runtimeId,
// Why: `status.get` ran in-process on the execution host, so these ARE that host's
// capabilities; dropping them made `--shell` report an outdated host instead of SSH.
...(status.capabilities ? { capabilities: status.capabilities } : {})
},
graph: { state: status.graphStatus }
}
return { ...response, result: cliStatus }
}
case 'terminal list':
return await call(dispatcher, 'terminal.list', {
worktree: optionalRemoteCliString(parsed.flags, 'worktree'),
limit: optionalRemoteCliNumber(parsed.flags, 'limit'),
// Why: agent JSON calls dominate; topology stays available through an explicit opt-in.
includeVisualLayouts:
!parsed.flags.has('json') || parsed.flags.has('include-visual-layouts')
})
case 'orchestration send': {
const type = optionalRemoteCliString(parsed.flags, 'type')
return await call(
dispatcher,
'orchestration.send',
{
from: resolveRemoteOrchestrationSender(parsed.flags, env, type),
to: optionalRemoteCliString(parsed.flags, 'to'),
subject: requiredRemoteCliString(parsed.flags, 'subject'),
body: optionalRemoteCliString(parsed.flags, 'body'),
type,
priority: optionalRemoteCliString(parsed.flags, 'priority'),
threadId: optionalRemoteCliString(parsed.flags, 'thread-id'),
payload: getRemoteOrchestrationPayload(parsed.flags),
// Why: the legacy in-process bridge must preserve the same pane
// authority as the full host CLI passthrough.
senderPaneKey: env.ORCA_PANE_KEY || undefined
},
compatibilityEnvelope
)
}
case 'orchestration check':
return await call(
dispatcher,
'orchestration.check',
{
terminal: resolveRemoteCliHandle(parsed.flags, env, 'terminal'),
terminalPaneKey: parsed.flags.has('terminal')
? undefined
: env.ORCA_PANE_KEY || undefined,
unread: parsed.flags.has('unread') ? true : parsed.flags.has('peek') ? false : undefined,
peek: parsed.flags.has('peek') ? true : undefined,
all: parsed.flags.has('all') ? true : undefined,
types: optionalRemoteCliString(parsed.flags, 'types'),
format: parsed.flags.has('format') ? true : undefined,
inject: parsed.flags.has('inject') ? true : undefined,
compatibilityCliCommand: 'orca',
run: optionalRemoteCliString(parsed.flags, 'run'),
ack: optionalRemoteCliString(parsed.flags, 'ack'),
wait: parsed.flags.has('wait') ? true : undefined,
timeoutMs: optionalRemoteCliNumber(parsed.flags, 'timeout-ms')
},
compatibilityEnvelope
)
case 'orchestration ask':
return await call(
dispatcher,
'orchestration.ask',
{
to: optionalRemoteCliString(parsed.flags, 'to'),
question: optionalRemoteCliString(parsed.flags, 'question'),
resume: optionalRemoteCliString(parsed.flags, 'resume'),
options: optionalRemoteCliString(parsed.flags, 'options'),
timeoutMs: optionalRemoteCliNumber(parsed.flags, 'timeout-ms'),
from: resolveRemoteCliHandle(parsed.flags, env, 'from'),
run: optionalRemoteCliString(parsed.flags, 'run'),
compatibilityCliCommand: 'orca'
},
compatibilityEnvelope
)
case 'orchestration reply':
return await call(
dispatcher,
'orchestration.reply',
{
id: requiredRemoteCliString(parsed.flags, 'id'),
body: requiredRemoteCliString(parsed.flags, 'body'),
from: resolveRemoteCliHandle(parsed.flags, env, 'from')
},
compatibilityEnvelope
)
case 'orchestration inbox':
return await call(
dispatcher,
'orchestration.inbox',
{
limit: optionalRemoteCliNumber(parsed.flags, 'limit'),
terminal: optionalRemoteCliString(parsed.flags, 'terminal')
},
compatibilityEnvelope
)
default:
// Why: only reachable when the full host CLI could not be launched;
// include that root cause so users can fix the install instead of
// assuming the command family is unsupported over SSH.
throw new Error(
`Unsupported SSH Orca CLI command: ${command} (full Orca CLI bridge unavailable: ${passthroughFailureReason})`
)
}
}
async function call(
dispatcher: RpcDispatcher,
method: string,
params?: Record<string, unknown>,
envelope?: RuntimeOrchestrationEnvelope
): Promise<RpcResponse> {
return await dispatcher.dispatch({
id: `remote-cli-${Date.now()}`,
authToken: 'remote-cli',
method,
params,
orchestrationContractVersion: method.startsWith('orchestration.')
? ORCHESTRATION_CONTRACT_VERSION
: undefined,
orchestrationRequestId: envelope?.orchestrationRequestId,
compatibilityInvocationId:
envelope?.orchestrationRequestId ?? envelope?.compatibilityInvocationId,
orchestrationCompatibilityEvidence: envelope?.orchestrationCompatibilityEvidence
})
}