From abb289d11b92be9e707c720ce5b7dde4d4fc2c86 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:36:57 -0700 Subject: [PATCH] Add reset capability for computer-use permissions New `computer.permissionsReset` RPC method allows resetting all permissions to not-granted state. Includes underlying platform support, web client integration, and feature interaction tracking. --- src/main/ipc/computer-use-permissions.test.ts | 45 +++++++++++--- .../dispatcher-feature-interactions.test.ts | 8 ++- src/main/runtime/rpc/methods/computer.test.ts | 25 +++++++- src/main/runtime/rpc/methods/computer.ts | 9 +++ .../rpc/runtime-feature-interaction.ts | 3 +- .../preload-api/web-host-capability-api.ts | 22 ++++--- ...eload-api-computer-use-permissions.test.ts | 62 +++++++++++++++++++ 7 files changed, 156 insertions(+), 18 deletions(-) create mode 100644 src/renderer/src/web/web-preload-api-computer-use-permissions.test.ts diff --git a/src/main/ipc/computer-use-permissions.test.ts b/src/main/ipc/computer-use-permissions.test.ts index fea36c778d8..bff1cbe112d 100644 --- a/src/main/ipc/computer-use-permissions.test.ts +++ b/src/main/ipc/computer-use-permissions.test.ts @@ -1,11 +1,16 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -const { handleMock, openComputerUsePermissionsMock, getComputerUsePermissionStatusMock } = - vi.hoisted(() => ({ - handleMock: vi.fn(), - openComputerUsePermissionsMock: vi.fn(), - getComputerUsePermissionStatusMock: vi.fn() - })) +const { + handleMock, + openComputerUsePermissionsMock, + getComputerUsePermissionStatusMock, + resetComputerUsePermissionsMock +} = vi.hoisted(() => ({ + handleMock: vi.fn(), + openComputerUsePermissionsMock: vi.fn(), + getComputerUsePermissionStatusMock: vi.fn(), + resetComputerUsePermissionsMock: vi.fn() +})) vi.mock('electron', () => ({ ipcMain: { @@ -15,7 +20,8 @@ vi.mock('electron', () => ({ vi.mock('../computer/macos-computer-use-permissions', () => ({ getComputerUsePermissionStatus: getComputerUsePermissionStatusMock, - openComputerUsePermissions: openComputerUsePermissionsMock + openComputerUsePermissions: openComputerUsePermissionsMock, + resetComputerUsePermissions: resetComputerUsePermissionsMock })) import { registerComputerUsePermissionHandlers } from './computer-use-permissions' @@ -25,6 +31,7 @@ describe('registerComputerUsePermissionHandlers', () => { handleMock.mockReset() getComputerUsePermissionStatusMock.mockReset() openComputerUsePermissionsMock.mockReset() + resetComputerUsePermissionsMock.mockReset() }) it('launches the computer-use helper setup', async () => { @@ -68,4 +75,28 @@ describe('registerComputerUsePermissionHandlers', () => { await expect(registration![1]()).resolves.toBe(result) expect(getComputerUsePermissionStatusMock).toHaveBeenCalledWith() }) + + it('resets all computer-use permissions', async () => { + const result = { + platform: 'darwin', + helperAppPath: '/Applications/Orca Computer Use.app', + helperUnavailableReason: null, + bundleId: 'com.stablyai.orca.computer-use', + permissions: [ + { id: 'accessibility', status: 'not-granted' }, + { id: 'screenshots', status: 'not-granted' } + ] + } + resetComputerUsePermissionsMock.mockReturnValue(result) + + registerComputerUsePermissionHandlers() + + const registration = handleMock.mock.calls.find( + ([channel]) => channel === 'computerUsePermissions:reset' + ) + expect(registration).toBeTruthy() + + await expect(registration![1]()).resolves.toBe(result) + expect(resetComputerUsePermissionsMock).toHaveBeenCalledWith() + }) }) diff --git a/src/main/runtime/rpc/dispatcher-feature-interactions.test.ts b/src/main/runtime/rpc/dispatcher-feature-interactions.test.ts index d0f1e6fbc39..5d25e351acc 100644 --- a/src/main/runtime/rpc/dispatcher-feature-interactions.test.ts +++ b/src/main/runtime/rpc/dispatcher-feature-interactions.test.ts @@ -123,6 +123,11 @@ const METHODS = [ params: z.object({}), handler: () => ({ permissions: [] }) }), + defineMethod({ + name: 'computer.permissionsReset', + params: z.object({}), + handler: () => ({ permissions: [] }) + }), defineMethod({ name: 'computer.click', params: z.object({}), @@ -162,13 +167,14 @@ describe('RpcDispatcher feature interactions', () => { await dispatcher.dispatch(makeRequest('computer.permissions')) await dispatcher.dispatch(makeRequest('computer.permissionsStatus')) + await dispatcher.dispatch(makeRequest('computer.permissionsReset')) await dispatcher.dispatch(makeRequest('browser.profileImportFromBrowser')) await dispatcher.dispatch(makeRequest('browser.profileList')) await dispatcher.dispatch(makeRequest('browser.profileClearDefaultCookies')) expect(runtime.recordFeatureInteraction).toHaveBeenCalledWith('computer-use-setup') expect(runtime.recordFeatureInteraction).toHaveBeenCalledWith('cookie-import') - expect(runtime.recordFeatureInteraction).toHaveBeenCalledTimes(2) + expect(runtime.recordFeatureInteraction).toHaveBeenCalledTimes(3) }) it('does not record failed runtime methods', async () => { diff --git a/src/main/runtime/rpc/methods/computer.test.ts b/src/main/runtime/rpc/methods/computer.test.ts index 6a01fac7d0a..e506d3f74e3 100644 --- a/src/main/runtime/rpc/methods/computer.test.ts +++ b/src/main/runtime/rpc/methods/computer.test.ts @@ -10,7 +10,8 @@ const computerMocks = vi.hoisted(() => ({ callComputerSidecarSnapshot: vi.fn(), resetComputerSidecarForTest: vi.fn(), openComputerUsePermissions: vi.fn(), - getComputerUsePermissionStatus: vi.fn() + getComputerUsePermissionStatus: vi.fn(), + resetComputerUsePermissions: vi.fn() })) vi.mock('../../../computer/sidecar-client', () => ({ @@ -24,7 +25,8 @@ vi.mock('../../../computer/sidecar-client', () => ({ vi.mock('../../../computer/macos-computer-use-permissions', () => ({ openComputerUsePermissions: computerMocks.openComputerUsePermissions, - getComputerUsePermissionStatus: computerMocks.getComputerUsePermissionStatus + getComputerUsePermissionStatus: computerMocks.getComputerUsePermissionStatus, + resetComputerUsePermissions: computerMocks.resetComputerUsePermissions })) import { COMPUTER_METHODS, resetComputerSessionsForTest } from './computer' @@ -39,6 +41,7 @@ describe('computer RPC methods', () => { computerMocks.resetComputerSidecarForTest.mockReset() computerMocks.openComputerUsePermissions.mockReset() computerMocks.getComputerUsePermissionStatus.mockReset() + computerMocks.resetComputerUsePermissions.mockReset() resetComputerSessionsForTest() computerMocks.resetComputerSidecarForTest.mockClear() }) @@ -57,6 +60,7 @@ describe('computer RPC methods', () => { 'computer.pasteText', 'computer.performSecondaryAction', 'computer.permissions', + 'computer.permissionsReset', 'computer.permissionsStatus', 'computer.pressKey', 'computer.scroll', @@ -121,6 +125,23 @@ describe('computer RPC methods', () => { expect(computerMocks.getComputerUsePermissionStatus).toHaveBeenCalledWith() }) + it('resets all computer-use permissions', async () => { + const result = { + platform: 'darwin', + helperAppPath: '/Applications/Orca Computer Use.app', + helperUnavailableReason: null, + bundleId: 'com.stablyai.orca.computer-use', + permissions: [ + { id: 'accessibility', status: 'not-granted' }, + { id: 'screenshots', status: 'not-granted' } + ] + } + computerMocks.resetComputerUsePermissions.mockReturnValue(result) + + await expect(call('computer.permissionsReset', {})).resolves.toBe(result) + expect(computerMocks.resetComputerUsePermissions).toHaveBeenCalledWith() + }) + it('lists windows through the sidecar', async () => { const result = { app: { name: 'Finder', bundleId: 'com.apple.finder', pid: 100 }, diff --git a/src/main/runtime/rpc/methods/computer.ts b/src/main/runtime/rpc/methods/computer.ts index 1f928b97afe..386d454c379 100644 --- a/src/main/runtime/rpc/methods/computer.ts +++ b/src/main/runtime/rpc/methods/computer.ts @@ -61,6 +61,15 @@ export const COMPUTER_METHODS: RpcMethod[] = [ return getComputerUsePermissionStatus() } }), + defineMethod({ + name: 'computer.permissionsReset', + params: z.object({}), + handler: async () => { + const { resetComputerUsePermissions } = + await import('../../../computer/macos-computer-use-permissions') + return resetComputerUsePermissions() + } + }), defineMethod({ name: 'computer.listWindows', params: ListWindows, diff --git a/src/main/runtime/rpc/runtime-feature-interaction.ts b/src/main/runtime/rpc/runtime-feature-interaction.ts index 23f28001fcd..494aab8c140 100644 --- a/src/main/runtime/rpc/runtime-feature-interaction.ts +++ b/src/main/runtime/rpc/runtime-feature-interaction.ts @@ -25,7 +25,8 @@ export function getRuntimeFeatureInteractionId( if (method.startsWith('emulator.')) { return null } - if (method === 'computer.permissions') { + // Opening setup and resetting grants are both permission-management flows. + if (method === 'computer.permissions' || method === 'computer.permissionsReset') { return 'computer-use-setup' } if ( diff --git a/src/renderer/src/web/preload-api/web-host-capability-api.ts b/src/renderer/src/web/preload-api/web-host-capability-api.ts index 3834d664d87..15aabacc2ac 100644 --- a/src/renderer/src/web/preload-api/web-host-capability-api.ts +++ b/src/renderer/src/web/preload-api/web-host-capability-api.ts @@ -4,6 +4,7 @@ import type { RefreshAgentsResult } from '../../../../preload/api-types' import type { + ComputerUsePermissionResetResult, ComputerUsePermissionSetupResult, ComputerUsePermissionStatusResult } from '../../../../shared/computer-use-permissions-types' @@ -132,13 +133,20 @@ export function createComputerUsePermissionsApi(): NonNullable< nextStep: 'Computer-use permissions are managed on the Orca server.' })), reset: () => - Promise.resolve({ - platform: getBrowserPlatform(), - helperAppPath: null, - helperUnavailableReason: 'web_client', - bundleId: null, - permissions: [] - }) + callRuntimeResult( + 'computer.permissionsReset', + {}, + 15_000 + ).catch( + () => + ({ + platform: getBrowserPlatform(), + helperAppPath: null, + helperUnavailableReason: 'web_client', + bundleId: null, + permissions: [] + }) satisfies ComputerUsePermissionResetResult + ) } } diff --git a/src/renderer/src/web/web-preload-api-computer-use-permissions.test.ts b/src/renderer/src/web/web-preload-api-computer-use-permissions.test.ts new file mode 100644 index 00000000000..bc27740c836 --- /dev/null +++ b/src/renderer/src/web/web-preload-api-computer-use-permissions.test.ts @@ -0,0 +1,62 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { RuntimeRpcResponse } from '../../../shared/runtime-rpc-envelope' +import { + installBrowserGlobals, + writeStoredRuntimeEnvironment +} from './web-preload-api-test-harness' + +describe('web UI computer-use permission reset', () => { + beforeEach(() => { + vi.resetModules() + }) + + afterEach(() => { + vi.unstubAllGlobals() + vi.doUnmock('./web-runtime-client') + }) + + it('proxies permission reset for paired web clients', async () => { + const calls: { method: string; params: unknown }[] = [] + vi.doMock('./web-runtime-client', () => ({ + WebRuntimeClient: class { + call(method: string, params: unknown): Promise> { + calls.push({ method, params }) + return Promise.resolve({ + id: method, + ok: true, + result: + method === 'computer.permissionsReset' + ? { + platform: 'darwin', + helperAppPath: '/Applications/Orca Computer Use.app', + helperUnavailableReason: null, + bundleId: 'com.stablyai.orca.computer-use', + permissions: [ + { id: 'accessibility', status: 'not-granted' }, + { id: 'screenshots', status: 'not-granted' } + ] + } + : {}, + _meta: { runtimeId: 'runtime-1' } + }) + } + + close(): void {} + } + })) + + const globals = installBrowserGlobals('Linux') + writeStoredRuntimeEnvironment(globals.storage) + const { installWebPreloadApi } = await import('./web-preload-api') + installWebPreloadApi() + + await expect(globals.window.api.computerUsePermissions.reset()).resolves.toMatchObject({ + bundleId: 'com.stablyai.orca.computer-use', + permissions: [ + { id: 'accessibility', status: 'not-granted' }, + { id: 'screenshots', status: 'not-granted' } + ] + }) + expect(calls).toEqual([{ method: 'computer.permissionsReset', params: {} }]) + }) +})