Files
orca/src/main/runtime/rpc/methods/computer.ts
T
Neil b187d55e15 fix(computer-use): cap every macOS permission helper launch path at one per 5 s (#34390) (#36150)
* fix(computer-use): replace the macOS permission status gate with one 5s launch floor (#34390)

Follows up #36028. The earlier gate grew background/explicit flags, epochs,
backoff and a setup pgrep path that reviewers could not make correct.

- Status: one probe at a time and one 5s launch floor shared by every reader, so
  every path stays at <=12 helper launches/min. Readers inside the floor join the
  running probe or ride one trailing probe that fires only if someone waits.
  Only grants are cached (until the floor expires); denials and errors are never
  served, so a grant shows within one floor locally and on web. No app epochs:
  without a denial cache they bought nothing. Off macOS the gate is skipped.
- Setup: one launch per 5s across every permission, claimed synchronously; a
  request inside the window is queued (latest wins) and reported as
  launchQueued. Kill-and-relaunch stays so a click always re-shows the helper.
  Setup reuses the gate's in-flight/cached/last result instead of probing.
- Reset checks helper availability from the bundle and invalidates after any
  tccutil attempt, including partial failures.
- Remove the background flag from preload/IPC/renderer/web, the app-activity
  binding, and the sidecar permission_denied invalidation.

* fix(computer-use): close the remaining gaps in the permission launch floor (#34390)

- Clear the trailing timer whenever any probe launches and re-check the floor
  when it fires, so a late timer can never launch inside the floor.
- Setup reuses a denial only until the floor expires.
- A reader that joins a probe older than 300ms and gets a denial rides the next
  probe, so a grant shows within about one floor even after a stale web/RPC probe.
- Setup requests are numbered on arrival, including ones that coalesce, so the
  latest request wins (A, B, A launches A); a queued launch already covered by a
  newer launch is dropped. Reset and will-quit cancel any queued launch.
- The settings pane says "Opening…" and the CLI says queued for queued launches.

* fix(computer-use): measure permission launch floors from real spawns and cancel setup on every host (#34390)

- Both 5s floors use performance.now, so a wall-clock step cannot stretch or skip them.
- The status floor counts from the later of claim and the actual `open` spawn, and
  the setup window from after its spawn, so a slow mkdtemp or pkill can't allow a
  13th spawn in a 60s window.
- Setup cancellation registers through AppEnvironment quit handlers (desktop,
  headless Electron serve, orcad) and orcad's shutdown trigger; reset and
  shutdown bump a generation so an Open still waiting on status never launches.
- Reset cancels queued setup before its first await.
- A setup request leaves the coalescing map when it launches, so a later request
  for the same permission queues and wins (latest-wins across microtasks).
- A failed probe clears the remembered denial.

* fix(computer-use): drop setup coalescing and gate setup on a host-wide shutdown flag (#34390)

- Remove the per-permission setupInFlight map. Every Open goes through the 5s
  window and seq-ordered queue; a queued request with the same args that arrived
  before the last spawn was already shown by it, so concurrent same-permission
  Opens still spawn once. This fixes stale entries after reset turning the next
  Open into a no-op or a cancelled one.
- A cancelled Open (reset or shutdown) rejects with an error instead of returning
  launchedHelper: false, which every client rendered as "setup complete".
- Replace the per-host cancel calls with src/main/host-shutdown-state.ts, set
  synchronously by Electron will-quit and orcad's shutdown admission; setup
  checks it before every spawn, including the queued timer.
- Track the status cache's expiry from the probe that produced it, so starting a
  new probe cannot revive an expired grant.
- The queued setup timer re-checks its deadline and re-arms when early, and both
  timers round delays up; a throwing spawn there is logged, not uncaught.

* fix(computer-use): keep setup latest-wins after slow probes and surface cancelled setups (#34390)

- Record the launching request's seq; an older request that resumes or is queued
  after a newer one spawned is dropped, so latest-wins holds when requests
  resume out of order behind a probe.
- orcad sets the host shutdown flag on every cleanup entry, including failed
  startup.
- The web facade only falls back for hosts without computer.permissions; other
  failures, such as a cancelled Open, reject so the pane and onboarding show them.
- The cancellation check runs before an untargeted Open can report everything granted.
- Opens that arrive mid-reset wait for it, so reset's helper cleanup cannot kill
  what they open.
- Queued wording now says a newer request or a reset may replace it.

* fix(computer-use): wait out a reset before an Open reads permission status (#34390)
2026-10-29 16:33:52 -07:00

160 lines
4.2 KiB
TypeScript

import {
callComputerSidecarAction,
callComputerSidecarCapabilities,
callComputerSidecarListApps,
callComputerSidecarListWindows,
callComputerSidecarSnapshot,
resetComputerSidecarForTest
} from '../../../computer/sidecar-client'
import { defineMethod } from '../core'
import {
Click,
ComputerObserveTarget,
ComputerPermissions,
Drag,
Hotkey,
ListApps,
ListWindows,
PasteText,
PerformSecondaryAction,
PressKey,
Scroll,
SetValue,
TypeText
} from '../../../../shared/rpc-contract/computer-schemas-params'
import {
ComputerCapabilitiesParams,
ComputerPermissionsStatusParams
} from '../../../../shared/rpc-contract/computer-params'
export function resetComputerSessionsForTest(): void {
resetComputerSidecarForTest()
}
export const COMPUTER_METHODS = [
defineMethod({
name: 'computer.capabilities',
permission: 'workspace',
params: ComputerCapabilitiesParams,
handler: async () => {
return await callComputerSidecarCapabilities()
}
}),
defineMethod({
name: 'computer.listApps',
permission: 'desktop-control',
params: ListApps,
handler: async () => {
return await callComputerSidecarListApps()
}
}),
defineMethod({
name: 'computer.permissions',
permission: 'desktop-control',
params: ComputerPermissions,
handler: async (params) => {
const { openComputerUsePermissions } =
await import('../../../computer/macos-computer-use-permissions')
return openComputerUsePermissions(params.id)
}
}),
defineMethod({
name: 'computer.permissionsStatus',
permission: 'workspace',
params: ComputerPermissionsStatusParams,
handler: async () => {
const { computerUsePermissionStatusGate } =
await import('../../../computer/macos-computer-use-permission-status-gate')
return computerUsePermissionStatusGate.read()
}
}),
defineMethod({
name: 'computer.listWindows',
permission: 'desktop-control',
params: ListWindows,
handler: async (params) => {
return await callComputerSidecarListWindows(params)
}
}),
defineMethod({
name: 'computer.getAppState',
permission: 'desktop-control',
params: ComputerObserveTarget,
handler: async (params) => {
return await callComputerSidecarSnapshot(params)
}
}),
defineMethod({
name: 'computer.click',
permission: 'desktop-control',
params: Click,
handler: async (params) => {
return await callComputerSidecarAction('click', params)
}
}),
defineMethod({
name: 'computer.performSecondaryAction',
permission: 'desktop-control',
params: PerformSecondaryAction,
handler: async (params) => {
return await callComputerSidecarAction('performSecondaryAction', params)
}
}),
defineMethod({
name: 'computer.scroll',
permission: 'desktop-control',
params: Scroll,
handler: async (params) => {
return await callComputerSidecarAction('scroll', params)
}
}),
defineMethod({
name: 'computer.drag',
permission: 'desktop-control',
params: Drag,
handler: async (params) => {
return await callComputerSidecarAction('drag', params)
}
}),
defineMethod({
name: 'computer.typeText',
permission: 'desktop-control',
params: TypeText,
handler: async (params) => {
return await callComputerSidecarAction('typeText', params)
}
}),
defineMethod({
name: 'computer.pressKey',
permission: 'desktop-control',
params: PressKey,
handler: async (params) => {
return await callComputerSidecarAction('pressKey', params)
}
}),
defineMethod({
name: 'computer.hotkey',
permission: 'desktop-control',
params: Hotkey,
handler: async (params) => {
return await callComputerSidecarAction('hotkey', params)
}
}),
defineMethod({
name: 'computer.pasteText',
permission: 'desktop-control',
params: PasteText,
handler: async (params) => {
return await callComputerSidecarAction('pasteText', params)
}
}),
defineMethod({
name: 'computer.setValue',
permission: 'desktop-control',
params: SetValue,
handler: async (params) => {
return await callComputerSidecarAction('setValue', params)
}
})
]