Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/site/content/docs/remote-servers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,44 @@ orca-ide serve --pairing-address 100.64.1.20 --mobile-pairing

Keep the phone on the same tailnet, open Orca Mobile, choose **Pair**, and scan the terminal QR code or paste the printed link.

## Inspect the server from the CLI

Run `orca status --json` on the server (`orca-ide status --json` on Linux),
or select a paired runtime with `orca --environment <name> status --json`.
The CLI explicitly requests the optional `remoteServer` block. Routine RPC status
probes omit it. The block describes the server answering that request:

- `listener` reports `listening` with the actual bind `address` and `port`,
`not_listening`, or `disabled`. A wildcard address is a bind address, not a
connection URL. This does not test firewall/Tailscale reachability or retain a
previously advertised pairing URL.
- `grants` reports `total`, `pending`, `byScope`, and `entries`. Each entry has
`deviceId`, `name`, `scope`, `createdAt`, and `lastSeenAt`. Times are Unix
milliseconds; `lastSeenAt: null` means never used. A past connection is not
evidence that a client is connected now.
- `connectedClients` reports distinct pairing identities in `count`, authenticated
open sockets in `connectionCount`, and `byScope`. Its entries identify clients
and count their `direct` and `relay` connections. Multiple sockets from one
grant count as one client; separate grants on one physical computer count
separately. The requesting runtime connection is included. Network loss is
reflected when the existing connection/heartbeat handling detects it.

Both lists return up to 100 entries and indicate omitted entries with
`truncated: true`; totals still cover all observed records. This status endpoint
has no pagination or continuation for retrieving omitted entries. Names are limited to
160 characters and stripped of terminal controls. Tokens, keys, pairing URLs,
relay credentials, and push registrations are never included.

Diagnostics are available to the local CLI authenticated with this host's owner
token and to paired **runtime** clients. Mobile clients keep their existing
status response without these diagnostics. A section with
`state: "unavailable"` could not be observed; it does not mean zero clients or
grants. An absent `remoteServer` means an older host or an unsupported/unauthorized
path, including an SSH host's `orca` CLI (which reaches the controlling desktop
only with a scoped bridge credential) and the legacy in-process SSH bridge. It is
never filled with another host's data. Reading status does not create grants or
widen the listener.

## Desktop app or `orca serve`?

| | Desktop app on the server | `orca serve` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,15 +157,15 @@ describe('recorded requests against the host params contract', () => {
[
request('nothing.here', {}),
request('worktree.activate', {}),
request('status.get', { verbose: true }),
request('preflight.detectAgents', { verbose: true }),
request('terminal.close', { terminal: 'term-1', extra: 1 })
],
[{ method: 'terminal.close', path: '.gone', reason: 'made up' }]
)
expect(problems).toEqual([
expect.stringContaining('the host has no method nothing.here'),
expect.stringContaining('the host refuses it'),
expect.stringContaining('status.get takes no params; the host ignores these'),
expect.stringContaining('preflight.detectAgents takes no params; the host ignores these'),
expect.stringContaining('the host drops .extra'),
'terminal.close .gone: no recorded request sends it any more'
])
Expand Down
24 changes: 24 additions & 0 deletions src/cli/handlers/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,27 @@ describe('orca claude-teams CLI handler', () => {
expect(spawnEnv.ANTHROPIC_API_KEY).toBe('sk-ant-system')
})
})

it.each([true, false])('requests diagnostics only for JSON status (json=%s)', async (json) => {
const getCliStatus = vi.fn().mockResolvedValue({
id: 'status',
ok: true,
result: {
app: { running: false, pid: null },
runtime: { state: 'not_running', reachable: false, runtimeId: null },
graph: { state: 'not_running' }
},
_meta: { runtimeId: 'none' }
})
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the unreachable status fixture makes this handler use only getCliStatus.
const client = { getCliStatus } as unknown as RuntimeClient
const output = vi.spyOn(console, 'log').mockImplementation(() => {})
const exitCode = process.exitCode
try {
await CORE_HANDLERS.status({ client, json, flags: new Map(), cwd: '/tmp/repo' })
expect(getCliStatus).toHaveBeenCalledExactlyOnceWith({ includeRemoteServer: json })
} finally {
output.mockRestore()
process.exitCode = exitCode
}
})
2 changes: 1 addition & 1 deletion src/cli/handlers/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export const CORE_HANDLERS: Record<string, CommandHandler> = {
process.exitCode = exitCode
},
status: async ({ client, json }) => {
const result = await client.getCliStatus()
const result = await client.getCliStatus({ includeRemoteServer: json })
if (!json && !result.result.runtime.reachable) {
process.exitCode = 1
}
Expand Down
11 changes: 6 additions & 5 deletions src/cli/runtime-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,14 +319,15 @@ describe.skipIf(process.platform === 'win32')('RuntimeClient', () => {
it('openOrca waits for a reachable headless runtime to expose a desktop window', async () => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-client-'))
const endpoint = join(userDataPath, 'runtime.sock')
let statusRequests = 0
const requestParams: unknown[] = []
const server = createServer((socket) => {
sockets.add(socket)
socket.once('close', () => sockets.delete(socket))
socket.once('data', (data) => {
const request = JSON.parse(String(data).trim()) as { id: string }
statusRequests += 1
const available = statusRequests > 1
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this fixture reads requests emitted by RuntimeClient and asserts every probe's params.
const request = JSON.parse(String(data).trim()) as { id: string; params?: unknown }
requestParams.push(request.params)
const available = requestParams.length > 2
socket.write(
`${JSON.stringify({
id: request.id,
Expand Down Expand Up @@ -354,7 +355,7 @@ describe.skipIf(process.platform === 'win32')('RuntimeClient', () => {

expect(launchOrcaApp).toHaveBeenCalledOnce()
expect(status.result.app.desktopWindowStatus).toBe('available')
expect(statusRequests).toBeGreaterThan(1)
expect(requestParams).toEqual([undefined, undefined, undefined])
})

it('openOrca fails explicitly when the serve owner cannot promote safely', async () => {
Expand Down
14 changes: 10 additions & 4 deletions src/cli/runtime/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import type { PairingOffer } from '../../shared/pairing'
import { launchOrcaApp } from './launch'
import { getDefaultUserDataPath, readMetadata } from './metadata'
import { getCliStatus, projectRemoteAppStatus } from './status'
import { getCliStatus, projectRemoteAppStatus, type CliStatusOptions } from './status'
import { sendRequest } from './transport'
import { RuntimeClientError, RuntimeRpcFailureError, type RuntimeRpcSuccess } from './types'
import {
Expand Down Expand Up @@ -204,9 +204,12 @@ export class RuntimeClient {
return this.requestTimeoutMs
}

async getCliStatus(): Promise<RuntimeRpcSuccess<CliStatusResult>> {
async getCliStatus(options: CliStatusOptions = {}): Promise<RuntimeRpcSuccess<CliStatusResult>> {
if (this.remotePairing) {
const response = await this.call<RuntimeStatus>('status.get')
const response = await this.call<RuntimeStatus>(
'status.get',
options.includeRemoteServer ? { includeRemoteServer: true } : undefined
)
this.remoteCompat.noteVerifiedStatus(response.result)
const graphState = response.result.graphStatus
return {
Expand All @@ -218,6 +221,9 @@ export class RuntimeClient {
environment: this.environmentSelector ?? 'pairing-code'
},
app: projectRemoteAppStatus(response.result),
...(options.includeRemoteServer && response.result.remoteServer
? { remoteServer: response.result.remoteServer }
: {}),
runtime: {
state: graphState === 'ready' ? 'ready' : 'graph_not_ready',
reachable: true,
Expand All @@ -240,7 +246,7 @@ export class RuntimeClient {
_meta: response._meta
}
}
return getCliStatus(this.userDataPath)
return getCliStatus(this.userDataPath, options)
}

private async ensureOrchestrationContractCompatible(timeoutMs: number): Promise<void> {
Expand Down
123 changes: 72 additions & 51 deletions src/cli/runtime/status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,60 +30,81 @@ afterEach(async () => {
// Why: legacy runtime metadata compatibility only applies to local Unix socket
// metadata; Windows uses named pipes and cannot run this fixture directly.
describe.skipIf(process.platform === 'win32')('CLI runtime status', () => {
it('uses the legacy singular runtime transport when reporting status', async () => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-status-'))
const endpoint = join(userDataPath, 'runtime.sock')
const server = createServer((socket) => {
sockets.add(socket)
socket.once('close', () => sockets.delete(socket))
socket.once('data', (data) => {
const request = JSON.parse(String(data).trim()) as { id: string }
socket.write(
`${JSON.stringify({
id: request.id,
ok: true,
result: {
runtimeId: 'runtime-legacy',
rendererGraphEpoch: 1,
graphStatus: 'ready',
authoritativeWindowId: null,
liveTabCount: 0,
degradations: [
{
code: 'browser_unavailable',
capability: 'browser.headless.v1',
message: 'Browser automation is unavailable.'
}
]
},
_meta: { runtimeId: 'runtime-legacy' }
})}\n`
)
})
})
servers.add(server)
await new Promise<void>((resolve) => server.listen(endpoint, resolve))
writeFileSync(
getRuntimeMetadataPath(userDataPath),
JSON.stringify({
runtimeId: 'runtime-legacy',
pid: process.pid,
transport: { kind: 'unix', endpoint },
authToken: 'token',
startedAt: Date.now()
it.each([
undefined,
{
listener: { state: 'disabled' as const },
grants: { state: 'unavailable' as const },
connectedClients: { state: 'unavailable' as const }
}
])(
'preserves optional server diagnostics through legacy local metadata (%j)',
async (remoteServer) => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-status-'))
const endpoint = join(userDataPath, 'runtime.sock')
const requestParams: unknown[] = []
const server = createServer((socket) => {
sockets.add(socket)
socket.once('close', () => sockets.delete(socket))
socket.once('data', (data) => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture reads requests emitted by RuntimeClient; the diagnostic assertion requires its opt-in parameter.
const request = JSON.parse(String(data).trim()) as {
id: string
params?: { includeRemoteServer?: boolean }
}
requestParams.push(request.params)
socket.write(
`${JSON.stringify({
id: request.id,
ok: true,
result: {
runtimeId: 'runtime-legacy',
...(request.params?.includeRemoteServer && remoteServer ? { remoteServer } : {}),
rendererGraphEpoch: 1,
graphStatus: 'ready',
authoritativeWindowId: null,
liveTabCount: 0,
degradations: [
{
code: 'browser_unavailable',
capability: 'browser.headless.v1',
message: 'Browser automation is unavailable.'
}
]
},
_meta: { runtimeId: 'runtime-legacy' }
})}\n`
)
})
})
)
servers.add(server)
await new Promise<void>((resolve) => server.listen(endpoint, resolve))
writeFileSync(
getRuntimeMetadataPath(userDataPath),
JSON.stringify({
runtimeId: 'runtime-legacy',
pid: process.pid,
transport: { kind: 'unix', endpoint },
authToken: 'token',
startedAt: Date.now()
})
)

const status = await new RuntimeClient(userDataPath).getCliStatus()
const client = new RuntimeClient(userDataPath)
expect((await client.getCliStatus()).result).not.toHaveProperty('remoteServer')
const status = await client.getCliStatus({ includeRemoteServer: true })

expect(status.result.runtime).toMatchObject({
reachable: true,
connectionState: 'connected',
runtimeId: 'runtime-legacy',
state: 'ready',
degradations: [expect.objectContaining({ code: 'browser_unavailable' })]
})
})
expect(requestParams).toEqual([undefined, { includeRemoteServer: true }])
expect(status.result.remoteServer).toEqual(remoteServer)
expect(status.result.runtime).toMatchObject({
reachable: true,
connectionState: 'connected',
runtimeId: 'runtime-legacy',
state: 'ready',
degradations: [expect.objectContaining({ code: 'browser_unavailable' })]
})
}
)
})

// Why: `kill(pid, 0)` answers EPERM when the pid exists under another uid — an Orca the
Expand Down
15 changes: 13 additions & 2 deletions src/cli/runtime/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ import { isProcessRunning } from './runtime-pid-liveness'

export { projectRemoteAppStatus, resolveDesktopWindowStatus }

export type CliStatusOptions = { includeRemoteServer?: boolean }

export async function getCliStatus(
userDataPath: string
userDataPath: string,
options: CliStatusOptions = {}
): Promise<RuntimeRpcSuccess<CliStatusResult>> {
const metadata = tryReadMetadata(userDataPath)
const transport = metadata ? findTransport(metadata, 'unix', 'named-pipe') : null
Expand All @@ -38,13 +41,21 @@ export async function getCliStatus(
}

try {
const response = await sendRequest<RuntimeStatus>(metadata, 'status.get', undefined, 1000)
const response = await sendRequest<RuntimeStatus>(
metadata,
'status.get',
options.includeRemoteServer ? { includeRemoteServer: true } : undefined,
1000
)
if (response.ok === false) {
throw new RuntimeRpcFailureError(response)
}
const graphState = response.result.graphStatus
const desktopWindowStatus = resolveDesktopWindowStatus(response.result)
return buildCliStatusResponse({
...(options.includeRemoteServer && response.result.remoteServer
? { remoteServer: response.result.remoteServer }
: {}),
app: {
running: true,
pid: metadata.pid,
Expand Down
Loading