mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* fix(terminal): fence daemon endpoint ownership * fix(terminal): clean failed daemon PID claims * fix(terminal): close daemon ownership review gaps * test(daemon): release startup IPC in boot smoke * test(daemon): mirror production stdio in boot smoke * fix(daemon): exit after rpc shutdown cleanup * fix(terminal): make the socket name the daemon endpoint authority The reported failure was a live daemon hosting PTYs that nothing could reach: terminals acknowledged input and never ran it, listings diverged from reality, and restarting the app never helped because the detached helper survived. The ownership fence added for it could not fire in the sequence that produces the split brain. libuv unlinks the pathname a server bound to when that server closes, with no ownership check. A daemon that lost its endpoint name therefore deleted whichever socket then sat at that path — including a live replacement's — stranding a daemon that still hosted every session. Bind a private same-directory name and hard-link it into place instead: libuv can only ever unlink our own bind name, the exclusive link is a kernel-enforced endpoint claim, and the canonical name is removed only under an inode ownership check. The bind name replaces the basename rather than extending it, so it cannot overflow sun_path. killStaleDaemon removed the PID record unconditionally immediately before every fork, so the exclusive PID claim was always uncontested at bind time. It also unlinked a live daemon's endpoint whenever a connect probe merely timed out, and treated a `ps` timeout as proof of PID recycling. Now only positive evidence of a dead endpoint authorizes reclaiming it, SIGKILL is confirmed rather than assumed, and a daemon that cannot be proven stopped keeps its record and endpoint while the launcher refuses to fork beside it. A daemon whose endpoint was taken over now retires itself, draining rather than killing, so an unreachable orphan stops being permanent. A repaired PID record re-derives entryPath, appVersion and the Linux incarnation markers from the authenticated owner instead of dropping them; without appVersion a healthy daemon read as a permanently stale bundle and, on Windows, went unpinned against daemon-host pruning. Repair failure now fails open — abandoning a healthy daemon over a pid file write cost every persistent terminal on the machine. Also: treat only ENOENT as an unclaimed record so a Windows file lock is not reported as an ownership conflict; settle start() before close() so an accepted connection cannot defer it forever; sweep abandoned claim and bind names; and type the endpoint-identity seam so a rename cannot silently disable the fence. Adds a real-process handover smoke that reproduces the failure with two daemons racing one endpoint, and wires it into the native-smoke job. * fix(daemon): retire only on proven endpoint ownership loss The ownership watchdog read a null identity for any stat failure, so a transient EACCES or EIO on the runtime directory would retire a daemon that was still serving every terminal on the machine. Distinguish "the entry is gone" from "the probe failed" and act only on the former. Also require the loss to persist across two polls: a replacement publishes by unlink-then-link, and a single observation can land in that gap. * fix(daemon): source repaired ownership metadata from the authenticated hello Adversarial review found three defects in the previous two commits. Re-deriving entryPath from the owner's command line truncated it at the first space. A command line is a single space-joined string, so `C:\Program Files\Orca\...` and `/Applications/Orca 2.app/...` came back as `"C:\Program` and `/Applications/Orca`. getDaemonLaunchIdentity treats a present entryPath as authoritative, so a healthy daemon read as `different_app_path` and was killed and re-forked — worse than the missing-metadata case the derivation was added to fix. Carry entryPath and appVersion as optional fields on the daemon hello identity instead: the daemon already has both from its own argv, and per docs/reference/remote-wire-compatibility.md a new optional field is safe because every reader falls back when it is absent. This also removes a synchronous `ps` spawn from the Electron main thread during startup. `start()` rolled back the PID record even when it never published one. Losing the endpoint link now runs that path, and the ownership-checked unlink briefly renames the incumbent's record aside — enough to strand a live daemon's ownership. Roll back only what we actually wrote. publishDaemonSocketPath read its identity from the canonical name after linking, so a concurrent unlink returned null: no ownership watchdog and no endpoint cleanup on any shutdown path. Read it from the bound name before linking, which shares the inode. Refusing to fork beside an unconfirmed daemon left the user with no daemon at all and no in-app recovery, since restart re-entered the same fence. We have just proved something answers the endpoint, so adopt it in degraded mode: live sessions keep working, fresh terminals run locally. SIGTERM is also individually guarded now — an EPERM fell into the blanket catch and reported "nothing alive", authorizing the very duplicate this fence exists to prevent. Also reset the ownership-loss streak on an inconclusive probe so the confirmations are consecutive, and sweep scratch names before the launch so a failed launch still reclaims them.
242 lines
11 KiB
YAML
242 lines
11 KiB
YAML
name: Computer-use e2e
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- '.github/workflows/computer-e2e.yml'
|
|
- 'config/electron-builder.config.cjs'
|
|
- 'config/scripts/build-computer-macos.mjs'
|
|
- 'config/scripts/build-windows-cli-launcher.mjs'
|
|
- 'config/scripts/build-windows-cli-launcher.test.mjs'
|
|
- 'config/scripts/computer-e2e-workflow.test.mjs'
|
|
- 'config/scripts/macos-computer-helper-owner-loss-benchmark.mjs'
|
|
- 'config/scripts/macos-computer-helper-owner-loss-group-recovery.test.mjs'
|
|
- 'config/scripts/macos-computer-helper-owner-loss-metrics.mjs'
|
|
- 'config/scripts/macos-computer-helper-owner-loss-processes.mjs'
|
|
- 'config/scripts/macos-computer-helper-owner-loss-processes.test.mjs'
|
|
- 'config/scripts/macos-computer-helper-owner-loss-trial-cleanup.mjs'
|
|
- 'config/scripts/computer-use-modifier-safety.test.mjs'
|
|
- 'config/scripts/computer-use-skill-guidance.test.mjs'
|
|
- 'config/scripts/computer-use-smoke.mjs'
|
|
- 'config/scripts/computer-use-smoke.test.mjs'
|
|
- 'config/scripts/daemon-boot-smoke.mjs'
|
|
- 'config/scripts/daemon-endpoint-handover-smoke.mjs'
|
|
- 'config/scripts/windows-daemon-workspace-close-repro.mjs'
|
|
- 'config/scripts/verify-computer-native.mjs'
|
|
# Why: the native-smoke job boots the built terminal daemon under plain
|
|
# Node, so any change to the daemon bundle graph or the main build must
|
|
# re-run it (the v1.4.129-rc.1 daemon outage shipped with green CI).
|
|
- 'electron.vite.config.ts'
|
|
- 'config/build-plugins/**'
|
|
- 'src/main/daemon/**'
|
|
- 'native/computer-use-macos/**'
|
|
- 'native/computer-use-linux/**'
|
|
- 'native/computer-use-windows/**'
|
|
- 'native/windows-cli-launcher/**'
|
|
- 'skills/computer-use/SKILL.md'
|
|
- 'src/cli/**'
|
|
- 'src/main/computer/**'
|
|
- 'src/main/runtime/rpc/dispatcher.ts'
|
|
- 'src/main/runtime/rpc/errors.ts'
|
|
- 'src/main/runtime/rpc/methods/computer*.ts'
|
|
- 'src/main/ssh/ssh-remote-cli-launcher.ts'
|
|
- 'src/main/ssh/ssh-remote-cli-launcher.test.ts'
|
|
- 'src/shared/computer-use-*.ts'
|
|
- 'tests/e2e/computer-linux.e2e.ts'
|
|
- 'tests/e2e/computer-mac.e2e.ts'
|
|
- 'tests/e2e/computer-mac-safari.e2e.ts'
|
|
- 'tests/e2e/computer-windows.e2e.ts'
|
|
- 'tests/e2e/computer-windows-store.e2e.ts'
|
|
- 'tests/e2e/helpers/computer-cli-driver.ts'
|
|
- 'tests/e2e/helpers/computer-driver.ts'
|
|
- 'tests/e2e/vitest.config.ts'
|
|
workflow_dispatch:
|
|
schedule:
|
|
- cron: '0 6 * * *'
|
|
|
|
jobs:
|
|
native-smoke:
|
|
if: github.event_name == 'pull_request'
|
|
permissions:
|
|
contents: read
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-22.04, windows-latest]
|
|
runs-on: ${{ matrix.os }}
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: package.json
|
|
- uses: pnpm/action-setup@v6
|
|
with:
|
|
run_install: false
|
|
- if: runner.os == 'Linux'
|
|
run: sudo apt-get update && sudo apt-get install -y python3 python3-gi gir1.2-atspi-2.0 at-spi2-core gedit xvfb xclip xdotool
|
|
# Why: pnpm's bundled node-gyp can ship gyp_main.py without execute
|
|
# permission on Linux runners; node-pty's install fallback then fails
|
|
# before this smoke job can exercise the native package.
|
|
- name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
|
|
if: runner.os == 'Linux'
|
|
run: |
|
|
npm install -g node-gyp@11.5.0
|
|
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
|
|
- run: pnpm install --frozen-lockfile
|
|
# Why: without --config, bare vitest ignores config/vitest.config.ts (there
|
|
# is no root config) and falls back to the 5s default timeout with no
|
|
# Windows worker cap, so the real csc.exe launcher-compile tests time out
|
|
# on hosted Windows. Use the shared config so this job matches pnpm test.
|
|
- run: >-
|
|
pnpm vitest run --config config/vitest.config.ts
|
|
config/scripts/build-windows-cli-launcher.test.mjs
|
|
src/main/ssh/ssh-remote-cli-launcher.test.ts
|
|
config/scripts/computer-e2e-workflow.test.mjs
|
|
config/scripts/macos-computer-helper-owner-loss-group-recovery.test.mjs
|
|
config/scripts/macos-computer-helper-owner-loss-processes.test.mjs
|
|
config/scripts/computer-use-modifier-safety.test.mjs
|
|
config/scripts/computer-use-skill-guidance.test.mjs
|
|
config/scripts/computer-use-smoke.test.mjs
|
|
src/main/computer/computer-provider-lifecycle.test.ts
|
|
src/main/computer/computer-provider-unavailable-message.test.ts
|
|
src/main/computer/sidecar-client.test.ts
|
|
src/main/computer/macos-native-provider-client.test.ts
|
|
src/main/computer/macos-native-provider-socket.test.ts
|
|
src/main/computer/macos-computer-use-permissions.test.ts
|
|
src/main/computer/macos-computer-use-permission-status.test.ts
|
|
src/main/computer/desktop-script-provider-client.test.ts
|
|
src/main/computer/desktop-script-provider-cache.test.ts
|
|
src/main/computer/desktop-script-provider-actions.test.ts
|
|
src/main/computer/desktop-script-provider-cache-lifecycle.test.ts
|
|
src/main/computer/desktop-script-provider-errors.test.ts
|
|
src/main/computer/desktop-script-provider-action-errors.test.ts
|
|
src/shared/computer-use-error-recovery.test.ts
|
|
src/shared/computer-use-key-spec.test.ts
|
|
src/cli/format.test.ts
|
|
src/cli/handlers/computer.test.ts
|
|
src/cli/handlers/computer-action-routing.test.ts
|
|
src/cli/handlers/computer-action-validation.test.ts
|
|
src/cli/handlers/computer-state-formatting.test.ts
|
|
src/cli/specs/computer.test.ts
|
|
src/cli/index.test.ts
|
|
src/main/runtime/rpc/dispatcher-computer-errors.test.ts
|
|
src/main/runtime/rpc/errors.test.ts
|
|
src/main/runtime/rpc/methods/computer.test.ts
|
|
src/main/runtime/rpc/methods/computer-actions.test.ts
|
|
src/cli/runtime/envelope-schema.test.ts
|
|
src/shared/remote-runtime-client.test.ts
|
|
- run: pnpm verify:computer-native
|
|
- run: pnpm build:cli
|
|
- run: pnpm build:electron-vite
|
|
# Why: boot the BUILT daemon-entry under plain Node the way production
|
|
# forks it. v1.4.129-rc.1 shipped a daemon that exited at module load
|
|
# (leaked electron require) while every other check passed; this fails
|
|
# the PR when the built daemon cannot start on ubuntu-22.04 / windows.
|
|
- name: Daemon boot smoke
|
|
run: node config/scripts/daemon-boot-smoke.mjs
|
|
# Why: a daemon that lost its endpoint name used to delete the replacement's
|
|
# socket on exit, stranding a live daemon that hosts PTYs nothing can reach.
|
|
# Only two real processes racing the same endpoint reproduce it.
|
|
- name: Daemon endpoint handover smoke
|
|
run: node config/scripts/daemon-endpoint-handover-smoke.mjs
|
|
# Why: workspace removal overlaps graceful renderer teardown with the
|
|
# forced main-process sweep; exercise that exact pair against real ConPTY.
|
|
- name: Windows daemon workspace-close repro
|
|
if: runner.os == 'Windows'
|
|
run: node config/scripts/windows-daemon-workspace-close-repro.mjs
|
|
mac-native-owner-smoke:
|
|
if: github.event_name == 'pull_request'
|
|
runs-on: macos-15
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: package.json
|
|
- uses: pnpm/action-setup@v6
|
|
with:
|
|
run_install: false
|
|
- run: pnpm install --frozen-lockfile
|
|
- name: Owner-loss benchmark process cleanup
|
|
run: >-
|
|
pnpm vitest run
|
|
config/scripts/macos-computer-helper-owner-loss-group-recovery.test.mjs
|
|
config/scripts/macos-computer-helper-owner-loss-processes.test.mjs
|
|
- name: Authenticated helper owner-loss smoke
|
|
run: pnpm bench:macos-computer-helper-owner-loss --expect reaped --trials 1
|
|
- name: Swift tests and signed universal helper verification
|
|
run: pnpm verify:computer-native
|
|
|
|
mac:
|
|
# macOS Accessibility and Screen Recording require user-granted TCC entries.
|
|
# Keep this on manual/scheduled permission-bearing runners instead of PR CI.
|
|
if: github.event_name != 'pull_request'
|
|
runs-on: macos-15
|
|
env:
|
|
ORCA_COMPUTER_E2E: '1'
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: package.json
|
|
- uses: pnpm/action-setup@v6
|
|
with:
|
|
run_install: false
|
|
- run: pnpm install --frozen-lockfile
|
|
- run: pnpm build:computer-macos
|
|
- run: pnpm verify:computer-native
|
|
- run: pnpm build:cli
|
|
- run: pnpm build:electron-vite
|
|
- run: pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-mac.e2e.ts tests/e2e/computer-mac-safari.e2e.ts
|
|
|
|
linux:
|
|
if: github.event_name != 'pull_request'
|
|
runs-on: ubuntu-22.04
|
|
env:
|
|
ORCA_COMPUTER_E2E: '1'
|
|
ACCESSIBILITY_ENABLED: '1'
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: package.json
|
|
- uses: pnpm/action-setup@v6
|
|
with:
|
|
run_install: false
|
|
- run: sudo apt-get update && sudo apt-get install -y build-essential python3 python3-gi gir1.2-atspi-2.0 gedit at-spi2-core xvfb xclip xdotool
|
|
# Why: keep scheduled Linux e2e on the same native install path as PR
|
|
# smoke and pr.yml's verify job.
|
|
- name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
|
|
run: |
|
|
npm install -g node-gyp@11.5.0
|
|
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
|
|
- run: pnpm install --frozen-lockfile
|
|
- run: pnpm verify:computer-native
|
|
- run: pnpm build:cli
|
|
- run: pnpm build:electron-vite
|
|
- run: xvfb-run --auto-servernum dbus-run-session -- pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-linux.e2e.ts
|
|
|
|
windows:
|
|
if: github.event_name != 'pull_request'
|
|
runs-on: windows-latest
|
|
env:
|
|
ORCA_COMPUTER_E2E: '1'
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: package.json
|
|
- uses: pnpm/action-setup@v6
|
|
with:
|
|
run_install: false
|
|
- run: pnpm install --frozen-lockfile
|
|
- run: pnpm verify:computer-native
|
|
- run: pnpm build:cli
|
|
- run: pnpm build:electron-vite
|
|
- run: pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-windows.e2e.ts tests/e2e/computer-windows-store.e2e.ts
|