Files
orca/src/main/computer/desktop-script-provider-bridge.ts
NeilandOrca 46646d7ff1 chore(lint): upgrade oxlint to 1.71 + enable 7 new rules (autofixed backlog) (#6841)
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules

Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day
minimum-release-age supply-chain guard; nothing here needs it). The
bump is a no-op on the existing config.

Enable 3 error rules (backlog autofixed to zero in this commit) and
4 warn rules (surface signal without gating CI):

error (autofixed, behavior-preserving):
- unicorn/prefer-node-protocol        (~1531 sites: bare builtin -> node:)
- typescript/no-import-type-side-effects (~36: all-inline-type -> import type)
- unicorn/no-array-reverse            (19: copy-then-reverse -> toReversed)

warn (real signal, current fires are test-only/correct):
- unicorn/no-array-fill-with-reference-type  (aliasing footgun guard)
- typescript/no-unsafe-function-type         (bans bare Function type)
- unicorn/prefer-array-flat-map              (map().flat() -> flatMap())
- unicorn/prefer-regexp-test                 (.match() in bool ctx -> .test())

mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix
ran from root and covered mobile/ too.

Verification (all green): oxlint 0 errors (root+mobile+aux configs),
oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed,
builds (electron-vite + web + cli) succeed. node: rewrites confirmed to
skip embedded SSH/CLI string payloads (AST-only); all toReversed sites
verified to operate on fresh copies or write-once locals.

* chore(lint): bump mobile oxlint to 1.71 so inherited rules parse

mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which
lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since
mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile &&
oxlint' failed to parse the new rule. Bump mobile to match root (1.71).

Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit
pass, vitest 978 passed / 0 failed.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-06-29 22:38:29 -07:00

173 lines
5.6 KiB
TypeScript

import { execFile } from 'node:child_process'
import { RuntimeClientError } from './runtime-client-error'
import type { DesktopScriptPlatform } from './desktop-script-provider-paths'
const REQUEST_TIMEOUT_MS = 30_000
const FORCE_KILL_GRACE_MS = 1_000
export function execBridge(
platform: DesktopScriptPlatform,
scriptPath: string,
operationPath: string
): Promise<{ stdout: string; stderr: string }> {
const command = platform === 'windows' ? 'powershell.exe' : 'python3'
const args =
platform === 'windows'
? [
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'Bypass',
'-File',
scriptPath,
operationPath
]
: [scriptPath, operationPath]
return new Promise((resolve, reject) => {
let child: ReturnType<typeof execFile> | null = null
let settled = false
let forceKillTimer: ReturnType<typeof setTimeout> | null = null
const clearForceKillTimer = (): void => {
if (forceKillTimer) {
clearTimeout(forceKillTimer)
forceKillTimer = null
}
}
const finish = (
error: Error | null,
result?: { stdout: string; stderr: string },
options: { keepForceKillTimer?: boolean } = {}
): void => {
if (settled) {
return
}
settled = true
clearTimeout(timeout)
if (!options.keepForceKillTimer) {
clearForceKillTimer()
}
if (error) {
reject(error)
return
}
resolve(result ?? { stdout: '', stderr: '' })
}
// Why: native automation can hang inside platform APIs; reject promptly,
// then escalate cleanup if the bridge ignores the graceful termination.
const timeout = setTimeout(() => {
child?.kill('SIGTERM')
forceKillTimer = setTimeout(() => {
child?.kill('SIGKILL')
forceKillTimer = null
}, FORCE_KILL_GRACE_MS)
finish(
new RuntimeClientError(
'action_timeout',
`desktop provider timed out after ${REQUEST_TIMEOUT_MS}ms`
),
undefined,
{ keepForceKillTimer: true }
)
}, REQUEST_TIMEOUT_MS)
try {
child = execFile(
command,
args,
{
env: process.env,
maxBuffer: 20 * 1024 * 1024,
timeout: REQUEST_TIMEOUT_MS,
windowsHide: true
},
(error, stdout, stderr) => {
if (error) {
const message = stderr.trim() || stdout.trim() || error.message
finish(
error.killed
? new RuntimeClientError('action_timeout', message)
: mapBridgeError(message)
)
return
}
finish(null, { stdout, stderr })
}
)
child?.once('exit', clearForceKillTimer)
} catch (error) {
finish(mapBridgeError(error instanceof Error ? error.message : String(error)))
}
})
}
export function mapBridgeError(message: string): RuntimeClientError {
const text = message.trim() || 'desktop provider failed'
if (/appNotFound|app not found/i.test(text)) {
return new RuntimeClientError('app_not_found', text)
}
if (/appBlocked|app blocked/i.test(text)) {
return new RuntimeClientError('app_blocked', text)
}
if (
/unsupported capability|hotkey.*require|paste_text requires|GDK is required for non-character key synthesis/i.test(
text
)
) {
return new RuntimeClientError('unsupported_capability', text)
}
if (
/unsupported mouse button|unsupported scroll direction|unsupported (?:key|modifier)|windowId is not supported|must be a positive|must be a finite number|\b(?:x|y|from_x|from_y|to_x|to_y|pages|click_count|text|key|direction) is required\b/i.test(
text
)
) {
return new RuntimeClientError('invalid_argument', text)
}
if (/ModuleNotFoundError: No module named 'gi'|PyGObject|python3-gi/i.test(text)) {
return new RuntimeClientError(
'unsupported_capability',
'Linux Computer Use requires python3-gi and AT-SPI packages. Install python3-gi gir1.2-atspi-2.0 at-spi2-core, then retry.'
)
}
if (/not a valid secondary action|action.*not supported/i.test(text)) {
return new RuntimeClientError('action_not_supported', text)
}
if (/value is not settable|not settable/i.test(text)) {
return new RuntimeClientError('value_not_settable', text)
}
if (/stale element|fresh element index/i.test(text)) {
return new RuntimeClientError('element_not_found', text)
}
if (/windowStale|window stale/i.test(text)) {
return new RuntimeClientError('window_stale', text)
}
if (
/window_not_focused|keyboard input requires.*window.*focused|target window.*focused/i.test(text)
) {
return new RuntimeClientError('window_not_focused', text)
}
if (/screenshot_failed|screenshot.*failed|screen recording|payload cap/i.test(text)) {
return new RuntimeClientError('screenshot_failed', text)
}
if (
/window_not_found|No top-level(?: AT-SPI| UI Automation)? window|has no (?:on-screen |accessibility )?window|could not match accessibility window|unknown window(?:_index| id)?/i.test(
text
)
) {
return new RuntimeClientError('window_not_found', text)
}
if (/permission|desktop session|DBUS|XDG_RUNTIME_DIR|AT-SPI/i.test(text)) {
return new RuntimeClientError('permission_denied', text)
}
if (
/element_not_found|stale element|fresh element index|unknown element_index|element \d+ is stale|element indexes require|element \d+ changed since|element \d+ is not in the current cached snapshot/i.test(
text
)
) {
return new RuntimeClientError('element_not_found', text)
}
return new RuntimeClientError('accessibility_error', text)
}