blob: e3bb78c965df36ead2f063535cb6ea67a836c8f5 [file] [view]
# Computer Use Process-Restart E2E
## Goal
Prove that a Computer Use observation from one live application process cannot
authorize an action after the same canonical application path is relaunched as
a new process.
The expected lifetime follows the recovered Codex boundary:
```text
canonical application path
+ current live process instance
```
Conversation or transport continuity must not extend an observation across a
real target-process restart.
## Incident And Investigation
The first real-machine harness attempts exposed several independent test
assumptions before reaching the process boundary:
1. A fresh worktree had no workspace `node_modules`, so downstream packages
could not resolve `@maka/core`. Running `npm ci` established the same
dependency layout used by CI.
2. The ignored cua-driver artifact was absent in the new worktree. The launcher
now runs `prepare:cua-driver` and then verifies the pinned artifact before
starting the fixture.
3. WKWebView AX nodes can appear late and can be mirrored with duplicate labels
and different frames. The restart gate therefore uses the native
`CUA Lab Coordinate Target`; OOP behavior remains covered by the dedicated
guarded E2E.
4. Activating the fixture made the test pass but stole the user's focus.
`open -g` avoided activation but kept the app hidden from the driver's
on-screen window set. The final fixture mode uses LaunchServices background
launch plus `unhideWithoutActivation`, `moveToActiveSpace`, and
`orderFrontRegardless` so the window is on-screen without becoming the
frontmost application.
5. Runtime tool failures are projected through model-visible text rather than a
top-level `error` property. The gate verifies both the backend typed outcome
and the model-visible `target_missing` result.
These were harness defects or environment prerequisites. None dispatched an
action before the target-process identity gate was reached.
## Real Sequence
The launcher and harness keep one cua-driver backend and one Runtime tool
instance alive across five target restarts:
```text
repeat 5 times with one backend/Runtime instance:
-> observe current PID/window
-> bind coordinate action
-> terminate current app
-> launch same canonical app path
-> require globally new host PID and WebContent PID
-> attempt old observation
-> require target_missing, no dispatch, mutation 0 -> 0
-> clear session
-> observe new PID/window
-> execute fresh native AX set_value action
-> if visible, require exact readback on the new process
-> if covered by the user's window, require target_occluded and zero mutation
-> require cua-driver generations stable and restartAttempts == 0
```
The launcher owns sleep prevention, fixture cleanup, bounded child
termination, and private temporary handshake/report files. The synthetic app
is launched with `CUA_LAB_BACKGROUND=1`. In that mode the fixture orders its
window visible without activating the application or making it the user's
frontmost app. A continuous Swift sentinel allows the user to switch among
their own applications, type, and move the pointer, but fails immediately if
the synthetic fixture ever becomes frontmost or the screen locks. Cleanup
preserves the user's current application; restoration is only an emergency
path if the fixture itself stole focus.
The fixture is still a real visible AppKit window. Background launch avoids
explicit activation, but window creation and `orderFrontRegardless` can remain
noticeable and can perturb WindowServer responsiveness. This is an attended
release test, not a zero-disturbance background-run proof.
Physical user pointer movement is allowed and reported as observation data.
When the user's window occludes the target, the stronger non-interference
proof is that the backend emits no dispatch and both target and decoy mutation
remain zero.
## Historical Pixel Result
Before compatibility input was disabled, the July 14, 2026 five-round
no-focus pixel soak proved:
```text
restart rounds: 5
distinct host PIDs: 6
distinct WebContent PIDs: 6
old observation per round:
outcome: target_missing
native dispatch: none
target mutation: 0 -> 0
fresh observation:
background px success: 4 rounds
fail_closed_occluded: 1 round
decoy mutation: always 0 -> 0
cua-driver service:
action generation: 1 throughout
capture generation: 0 throughout
restartAttempts: 0 throughout
desktop concurrency:
fixture became frontmost: never
user pointer moved: 151.2 logical points
user app switching: allowed
```
The occluded round proved:
```text
outcome: target_occluded
native dispatch: none
target mutation: 0 -> 0
decoy mutation: 0 -> 0
```
The test command is:
```bash
npm run e2e:computer-use-process-restart
```
## Current AX-Only Result
The replacement soak uses AX `set_value`, not pixel input. The corrected
five-round run proved:
```text
restart rounds: 5
old observation:
target_missing: 5/5
native dispatch: 0
fresh observation:
AX set_value + readback: 5/5
cua-driver service:
action generation: 1 throughout
capture generation: 0 throughout
restartAttempts: 0 throughout
desktop concurrency:
fixture became frontmost: never
user pointer moved: 217.1 logical points
user mouse/keyboard: reported normal
```
The first AX-only attempt had already confirmed stale-process rejection and a
successful fresh AX mutation, but used the wrong external oracle: direct
AXValue mutation does not invoke the fixture's Cocoa callback that writes
`state.json`. The final run instead verified the fresh AX observation returned
by Runtime.
## Remaining Boundary
This proves ordinary process restart isolation. It does not force the operating
system to reuse the old numeric PID. PID-reuse safety still requires either:
- a native atomic process-instance identity exposed by the executor; or
- a deterministic driver/host fixture that can substitute a new process under
the same PID-shaped identity.
The current fail-closed old-window lookup is sufficient for the observed real
restart, but it is not evidence for deliberate PID reuse.