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
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import { realpath } from 'node:fs/promises'
import { homedir } from 'node:os'
import {
DEFAULT_REPO_BADGE_COLOR,
EventEmitter,
Expand Down Expand Up @@ -49,7 +51,7 @@ describe('OrcaRuntimeService', () => {
})

it('browses runtime server directories before projects are added', async () => {
const tempRoot = await mkdtemp(join(tmpdir(), 'orca-runtime-browse-'))
const tempRoot = await mkdtemp(join(homedir(), 'orca-runtime-browse-'))
Comment thread
pullfrog[bot] marked this conversation as resolved.
try {
await mkdir(join(tempRoot, 'zeta'))
await mkdir(join(tempRoot, 'alpha'))
Expand All @@ -58,7 +60,7 @@ describe('OrcaRuntimeService', () => {

const result = await runtime.browseServerDir(tempRoot)

expect(result.resolvedPath).toBe(tempRoot)
expect(result.resolvedPath).toBe(await realpath(tempRoot))
expect(result.pathFlavor).toBe(process.platform === 'win32' ? 'win32' : 'posix')
expect(result.entries).toEqual([
{ name: 'alpha', isDirectory: true, isSymlink: false },
Expand All @@ -70,6 +72,15 @@ describe('OrcaRuntimeService', () => {
}
})

it('refuses to browse a path outside the home directory', async () => {
const runtime = new OrcaRuntimeService(store)
const outsideHome = process.platform === 'win32' ? 'C:\\Windows' : '/etc'

await expect(runtime.browseServerDir(outsideHome)).rejects.toThrow(
'Directory browsing is limited to the home directory.'
)
})

it.runIf(process.platform === 'win32')('lists drive roots for a server-root browse', async () => {
const runtime = new OrcaRuntimeService(store)

Expand Down
44 changes: 44 additions & 0 deletions src/main/runtime/rpc/methods/client-ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,50 @@ describe('client UI RPC methods', () => {
expect((update as { prBotAuthorOverrides: string[] }).prBotAuthorOverrides).toHaveLength(500)
})

it('lets the local unix socket persist agent launch env and args', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
updateClientSettings: vi.fn(() => ({}))
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: CLIENT_UI_METHODS })

const response = await dispatcher.dispatch(
makeRequest('settings.update', {
agentDefaultEnv: { claude: { PATH: '/evil' } },
agentDefaultArgs: { claude: '--debug' }
})
)

expect(runtime.updateClientSettings).toHaveBeenCalledWith({
agentDefaultEnv: { claude: { PATH: '/evil' } },
agentDefaultArgs: { claude: '--debug' }
})
expect(response).toMatchObject({ ok: true })
})

it('strips agentDefaultEnv and agentDefaultArgs from paired settings.update', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
updateClientSettings: vi.fn(() => ({}))
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: CLIENT_UI_METHODS })

for (const clientKind of ['mobile', 'runtime'] as const) {
vi.mocked(runtime.updateClientSettings).mockClear()
const response = await dispatcher.dispatch(
makeRequest('settings.update', {
agentDefaultEnv: { claude: { PATH: '/evil' } },
agentDefaultArgs: { claude: '--debug' },
compactWorktreeCards: true
}),
{ clientKind }
)

expect(runtime.updateClientSettings).toHaveBeenCalledWith({ compactWorktreeCards: true })
expect(response).toMatchObject({ ok: true })
}
})

it('routes bot-author deltas to the runtime-owned atomic update', async () => {
const settings = { prBotAuthorOverrides: ['alice', 'bob'] }
const runtime = {
Expand Down
11 changes: 8 additions & 3 deletions src/main/runtime/rpc/methods/client-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@ export const CLIENT_UI_METHODS = [
defineMethod({
name: 'settings.update',
params: SettingsUpdate,
handler: async (params, { runtime }) => ({
settings: await runtime.updateClientSettings(params)
})
handler: async (params, { runtime, clientKind }) => {
const updates = { ...params }
if (clientKind !== undefined) {
delete updates.agentDefaultEnv
delete updates.agentDefaultArgs
}
return { settings: await runtime.updateClientSettings(updates) }
}
}),
defineMethod({
name: 'settings.getTerminalQuickCommands',
Expand Down
63 changes: 63 additions & 0 deletions src/main/runtime/rpc/methods/computer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,27 @@ vi.mock('../../../computer/macos-computer-use-permissions', () => ({

import { COMPUTER_METHODS, resetComputerSessionsForTest } from './computer'

const PAIRED_COMPUTER_METHOD_CASES = [
{ name: 'computer.capabilities', params: {} },
{ name: 'computer.listApps', params: {} },
{ name: 'computer.permissions', params: { id: 'accessibility' } },
{ name: 'computer.permissionsStatus', params: {} },
{ name: 'computer.listWindows', params: { app: 'Finder' } },
{ name: 'computer.getAppState', params: { app: 'Finder' } },
{ name: 'computer.click', params: { app: 'Finder', elementIndex: 0 } },
{
name: 'computer.performSecondaryAction',
params: { app: 'Finder', elementIndex: 0, action: 'Raise' }
},
{ name: 'computer.scroll', params: { app: 'Finder', elementIndex: 0, direction: 'down' } },
{ name: 'computer.drag', params: { app: 'Finder', fromX: 1, fromY: 2, toX: 3, toY: 4 } },
{ name: 'computer.typeText', params: { app: 'Finder', text: 'hello' } },
{ name: 'computer.pressKey', params: { app: 'Finder', key: 'Return' } },
{ name: 'computer.hotkey', params: { app: 'Finder', key: 'CmdOrCtrl+L' } },
{ name: 'computer.pasteText', params: { app: 'Finder', text: 'hello' } },
{ name: 'computer.setValue', params: { app: 'Finder', elementIndex: 0, value: 'x' } }
] as const

describe('computer RPC methods', () => {
beforeEach(() => {
computerMocks.callComputerSidecarAction.mockReset()
Expand Down Expand Up @@ -246,6 +267,48 @@ describe('computer RPC methods', () => {
findMethod('computer.pasteText').params!.safeParse({ app: 'Finder', text }).success
).toBe(true)
})

it.each(PAIRED_COMPUTER_METHOD_CASES)(
'rejects paired-device calls to $name',
async ({ name, params }) => {
const method = findMethod(name)
const parsed = method.params ? method.params.parse(params) : undefined
const runtime = { getRuntimeId: () => 'runtime-1' } as never

for (const clientKind of ['mobile', 'runtime'] as const) {
await expect(method.handler(parsed, { runtime, clientKind })).rejects.toThrow(
/only available on the Orca host runtime/
)
}

expect(computerMocks.callComputerSidecarAction).not.toHaveBeenCalled()
expect(computerMocks.callComputerSidecarCapabilities).not.toHaveBeenCalled()
expect(computerMocks.callComputerSidecarListApps).not.toHaveBeenCalled()
expect(computerMocks.callComputerSidecarListWindows).not.toHaveBeenCalled()
expect(computerMocks.callComputerSidecarSnapshot).not.toHaveBeenCalled()
expect(computerMocks.openComputerUsePermissions).not.toHaveBeenCalled()
expect(computerMocks.getComputerUsePermissionStatus).not.toHaveBeenCalled()
}
)

it('allows local-socket computer.click and computer.typeText', async () => {
computerMocks.callComputerSidecarAction.mockResolvedValue({ ok: true })

await expect(call('computer.click', { app: 'Finder', elementIndex: 0 })).resolves.toEqual({
ok: true
})
await expect(call('computer.typeText', { app: 'Finder', text: 'hello' })).resolves.toEqual({
ok: true
})
expect(computerMocks.callComputerSidecarAction).toHaveBeenNthCalledWith(1, 'click', {
app: 'Finder',
elementIndex: 0
})
expect(computerMocks.callComputerSidecarAction).toHaveBeenNthCalledWith(2, 'typeText', {
app: 'Finder',
text: 'hello'
})
})
})

function findMethod(name: string) {
Expand Down
55 changes: 39 additions & 16 deletions src/main/runtime/rpc/methods/computer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
callComputerSidecarSnapshot,
resetComputerSidecarForTest
} from '../../../computer/sidecar-client'
import { defineMethod } from '../core'
import { defineMethod, type RpcContext } from '../core'
import {
Click,
ComputerObserveTarget,
Expand All @@ -31,25 +31,36 @@ export function resetComputerSessionsForTest(): void {
resetComputerSidecarForTest()
}

const COMPUTER_HOST_ONLY_MESSAGE = 'Computer use is only available on the Orca host runtime.'

function assertHostOnlyClient(clientKind: RpcContext['clientKind'], message: string): void {
if (clientKind !== undefined) {
throw new Error(message)
}
}

export const COMPUTER_METHODS = [
defineMethod({
name: 'computer.capabilities',
params: ComputerCapabilitiesParams,
handler: async () => {
handler: async (_params, { clientKind }) => {
assertHostOnlyClient(clientKind, COMPUTER_HOST_ONLY_MESSAGE)
return await callComputerSidecarCapabilities()
}
}),
defineMethod({
name: 'computer.listApps',
params: ListApps,
handler: async () => {
handler: async (_params, { clientKind }) => {
assertHostOnlyClient(clientKind, COMPUTER_HOST_ONLY_MESSAGE)
return await callComputerSidecarListApps()
}
}),
defineMethod({
name: 'computer.permissions',
params: ComputerPermissions,
handler: async (params) => {
handler: async (params, { clientKind }) => {
assertHostOnlyClient(clientKind, COMPUTER_HOST_ONLY_MESSAGE)
const { openComputerUsePermissions } =
await import('../../../computer/macos-computer-use-permissions')
return openComputerUsePermissions(params.id)
Expand All @@ -58,7 +69,8 @@ export const COMPUTER_METHODS = [
defineMethod({
name: 'computer.permissionsStatus',
params: ComputerPermissionsStatusParams,
handler: async () => {
handler: async (_params, { clientKind }) => {
assertHostOnlyClient(clientKind, COMPUTER_HOST_ONLY_MESSAGE)
const { getComputerUsePermissionStatus } =
await import('../../../computer/macos-computer-use-permissions')
return getComputerUsePermissionStatus()
Expand All @@ -67,77 +79,88 @@ export const COMPUTER_METHODS = [
defineMethod({
name: 'computer.listWindows',
params: ListWindows,
handler: async (params) => {
handler: async (params, { clientKind }) => {
assertHostOnlyClient(clientKind, COMPUTER_HOST_ONLY_MESSAGE)
return await callComputerSidecarListWindows(params)
}
}),
defineMethod({
name: 'computer.getAppState',
params: ComputerObserveTarget,
handler: async (params) => {
handler: async (params, { clientKind }) => {
assertHostOnlyClient(clientKind, COMPUTER_HOST_ONLY_MESSAGE)
return await callComputerSidecarSnapshot(params)
}
}),
defineMethod({
name: 'computer.click',
params: Click,
handler: async (params) => {
handler: async (params, { clientKind }) => {
assertHostOnlyClient(clientKind, COMPUTER_HOST_ONLY_MESSAGE)
return await callComputerSidecarAction('click', params)
}
}),
defineMethod({
name: 'computer.performSecondaryAction',
params: PerformSecondaryAction,
handler: async (params) => {
handler: async (params, { clientKind }) => {
assertHostOnlyClient(clientKind, COMPUTER_HOST_ONLY_MESSAGE)
return await callComputerSidecarAction('performSecondaryAction', params)
}
}),
defineMethod({
name: 'computer.scroll',
params: Scroll,
handler: async (params) => {
handler: async (params, { clientKind }) => {
assertHostOnlyClient(clientKind, COMPUTER_HOST_ONLY_MESSAGE)
return await callComputerSidecarAction('scroll', params)
}
}),
defineMethod({
name: 'computer.drag',
params: Drag,
handler: async (params) => {
handler: async (params, { clientKind }) => {
assertHostOnlyClient(clientKind, COMPUTER_HOST_ONLY_MESSAGE)
return await callComputerSidecarAction('drag', params)
}
}),
defineMethod({
name: 'computer.typeText',
params: TypeText,
handler: async (params) => {
handler: async (params, { clientKind }) => {
assertHostOnlyClient(clientKind, COMPUTER_HOST_ONLY_MESSAGE)
return await callComputerSidecarAction('typeText', params)
}
}),
defineMethod({
name: 'computer.pressKey',
params: PressKey,
handler: async (params) => {
handler: async (params, { clientKind }) => {
assertHostOnlyClient(clientKind, COMPUTER_HOST_ONLY_MESSAGE)
return await callComputerSidecarAction('pressKey', params)
}
}),
defineMethod({
name: 'computer.hotkey',
params: Hotkey,
handler: async (params) => {
handler: async (params, { clientKind }) => {
assertHostOnlyClient(clientKind, COMPUTER_HOST_ONLY_MESSAGE)
return await callComputerSidecarAction('hotkey', params)
}
}),
defineMethod({
name: 'computer.pasteText',
params: PasteText,
handler: async (params) => {
handler: async (params, { clientKind }) => {
assertHostOnlyClient(clientKind, COMPUTER_HOST_ONLY_MESSAGE)
return await callComputerSidecarAction('pasteText', params)
}
}),
defineMethod({
name: 'computer.setValue',
params: SetValue,
handler: async (params) => {
handler: async (params, { clientKind }) => {
assertHostOnlyClient(clientKind, COMPUTER_HOST_ONLY_MESSAGE)
return await callComputerSidecarAction('setValue', params)
}
})
Expand Down
Loading