Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,35 @@ project in `.picklab/config.json`:
This does not block an explicitly requested screenshot command. Screenshot
pixels cannot be redacted; see [SECURITY.md](SECURITY.md#recorded-evidence-and-screenshots).

### Supervised pause and human takeover

```sh
picklab watch --session <id> --control # pause the agent, take a temporary writable viewer
picklab takeover status --session <id> # check whether a session is under human control
```

`picklab watch --control` pauses PickLab-managed agent input for a session,
grants a temporary writable VNC viewer for a human, and hands control back
with a fresh screenshot and an evidence record once the viewer closes (or the
terminal is interrupted). Unlike `--vnc-control`'s persistent writable
session, control here is leased: while a human holds it, every desktop input
tool (`desktop_click`/`move`/`scroll`/`drag`/`double_click`/`type`/`key`),
`desktop_launch` (a newly launched client could otherwise grab input focus),
and every DevTools relay request fail closed with a stable busy error —
`takeover_status` (MCP) / `picklab takeover status` (CLI) let an agent check
before retrying, and `request_user_input` is the recommended way to ask a
human to run it. `desktop_screenshot` is the only desktop tool left ungated
(read-only).

The lease is a 30-second TTL, heartbeat-renewed-every-5-seconds record in the
session's state directory. Closing the viewer, an interrupted terminal, or a
PickLab crash all release it and revert VNC to read-only. A crash of the
`watch --control` process itself is reclaimed *actively*, not only the next
time something else happens to touch the session: a detached watchdog
process, spawned alongside the takeover and immune to a `SIGKILL` of its
parent, polls the lease and stops a stale writable VNC on its own — writable
VNC does not survive its lease going stale, whichever side crashes.

### Concurrent sessions

Each session gets its own isolated display or emulator, so several agents and projects can run labs side by side. When a command or tool is called without an explicit session id, the default resolves per project: only running sessions created for the same project directory are considered. Pass `session` ids (CLI: `--session <id>`) to target a specific lab, including one belonging to another project.
Expand Down Expand Up @@ -215,7 +244,8 @@ picklab agents add --name my-agent --mcp-command "picklab mcp serve"
| --- | --- |
| Setup | `doctor`, `init`, `setup lab-user`, `setup android` |
| Sessions | `session create`, `session status [id]`, `session destroy <id\|--all>` |
| Watch | `watch [--session <id>]` |
| Watch | `watch [--session <id>] [--control]` |
| Takeover | `takeover status [--session <id>]` |
| Desktop | `desktop launch <cmd>`, `desktop screenshot`, `desktop click <x> <y>`, `desktop move <x> <y>`, `desktop scroll <deltaX> <deltaY>`, `desktop drag <fromX> <fromY> <toX> <toY>`, `desktop double-click <x> <y>`, `desktop type <text>`, `desktop key <keys>` |
| Android | `android start`, `android install-apk <apk>`, `android launch-app <pkg>`, `android screenshot`, `android tap <x> <y>`, `android type <text>`, `android back`, `android home`, `android ui-tree`, `android logcat`, `android adb [args...]` |
| Artifacts | `artifacts list`, `artifacts open <runId>`, `artifacts report [runId]` |
Expand All @@ -225,7 +255,7 @@ picklab agents add --name my-agent --mcp-command "picklab mcp serve"

Session types: `desktop` (Xvfb, optional VNC), `android` (emulator on the dedicated AVD), `desktop+android`, and `browser` (isolated headed Chrome with loopback CDP). Most commands accept `--json` for machine-readable output and `--project-dir` to target another project.

`session create --vnc` is read-only. When a human must enter a password, API key, or OTP directly into the lab app, `--vnc-control` creates an explicitly writable VNC session instead. Pause agent input while using it; coordinated human takeover is tracked separately.
`session create --vnc` is read-only. `--vnc-control` creates an explicitly writable VNC session up front and does not coordinate with agent input — pause agent activity yourself while using it. For a coordinated, leased handoff instead, use `picklab watch --control` (see [Supervised pause and human takeover](#supervised-pause-and-human-takeover)), which fails agent input closed for the lease's duration and hands back a fresh screenshot automatically.

Scroll deltas are integer wheel steps: positive `deltaY` scrolls down, negative up; positive `deltaX` scrolls right, negative left (put negative values after `--`, e.g. `picklab desktop scroll -- 0 -3`). `desktop scroll` accepts `--at <x,y>` to position the pointer first; `desktop drag` accepts `--button` and `--duration <ms>`; `desktop double-click` accepts `--button` and `--interval <ms>`.
`picklab watch [--session <id>]` attaches a normal host-side VNC window to an
Expand Down Expand Up @@ -265,12 +295,13 @@ reported as suppressed for an explicitly writable `--vnc-control` session.

## MCP surface

`picklab mcp serve` exposes 26 tools over stdio:
`picklab mcp serve` exposes 27 tools over stdio:

- Sessions: `session_create`, `session_status`, `session_destroy`
- Desktop: `desktop_launch`, `desktop_screenshot`, `desktop_click`, `desktop_move`, `desktop_scroll`, `desktop_drag`, `desktop_double_click`, `desktop_type`, `desktop_key`
- Desktop: `desktop_launch`, `desktop_screenshot`, `desktop_click`, `desktop_move`, `desktop_scroll`, `desktop_drag`, `desktop_double_click`, `desktop_type`, `desktop_key` — all fail closed with a busy error while a human lease is active except `desktop_screenshot` (read-only). `desktop_launch` is gated too: a newly launched client can grab input focus on the shared display, which is exactly what the lease protects against.
- Android: `android_start`, `android_install_apk`, `android_launch_app`, `android_screenshot`, `android_tap`, `android_type`, `android_back`, `android_home`, `android_get_ui_tree`, `android_logcat`, `android_run_adb`
- Artifacts: `artifact_list`, `artifact_report`
- Takeover: `takeover_status` — check whether a session is under human control (see [Supervised pause and human takeover](#supervised-pause-and-human-takeover)); read-only, always safe to call
- User: `request_user_input` — ask the human a question (via MCP elicitation when the client supports it) and wait for the answer; never used for secrets

Resources, addressable as `picklab://` URIs:
Expand Down Expand Up @@ -308,7 +339,7 @@ A TypeScript monorepo. `@pickforge/picklab` is the published package; the rest a
- All user inputs are spawned as argument arrays — never interpolated into shell strings.
- The DevTools relay validates the installed upstream package name, exact version, declared bin, and confined real path before spawning Node with an argument array. Its browser URL is always derived as `http://127.0.0.1:<session-cdp-port>`.
- Relay stdout is protocol-only. A pending JSON-RPC record is capped at 16 MiB. Upstream diagnostic lines are capped at 64 KiB, redacted, and forwarded only to stderr; an over-limit line is dropped with a safe notice. Upstream update checks and usage statistics are disabled.
- VNC binds to loopback only by default: `x11vnc` is started with `-localhost`, so the server listens on `127.0.0.1` and is not reachable from the network. Tunnel over SSH for remote access. Normal `--vnc` and `picklab watch` observation is server-enforced read-only (`-viewonly`); viewer exit never stops the session or its Xvfb/VNC processes. `--vnc-control` is an explicit writable escape hatch for human secret entry and does not yet coordinate with agent input.
- VNC binds to loopback only by default: `x11vnc` is started with `-localhost`, so the server listens on `127.0.0.1` and is not reachable from the network. Tunnel over SSH for remote access. Normal `--vnc` and `picklab watch` observation is server-enforced read-only (`-viewonly`); viewer exit never stops the session or its Xvfb/VNC processes. `--vnc-control` is an explicit, persistent writable escape hatch for human secret entry and does not coordinate with agent input. `picklab watch --control` is the coordinated alternative: an atomic, TTL-bounded lease gates a temporary writable VNC server, and every agent desktop-input call (including `desktop_launch`, which could otherwise grab input focus on the shared display) and DevTools relay request fails closed (a live human lease is checked immediately before delivery) for as long as it is held. A crash on either side is reclaimed actively — the controlling process force-ends on the first failed lease renewal (never waiting for the viewer to close) and carries a hard deadline timer at the lease's `expiresAt` as a backstop; a detached watchdog process, immune to a `SIGKILL` of its parent, independently polls and stops a stale writable VNC. Writable VNC never outlives its lease in wall-clock terms, on any exit path.
- Artifacts are redacted by default: logcat output strips tokens and secrets before it is stored or returned. Only `android adb` is raw, and it says so.
- Evidence timelines persist only allowlisted metadata; typed values become length/type metadata, and network headers, bodies, and URL queries are dropped. Static HTML reports escape page-controlled text and use a no-script, no-network CSP.
- Screenshot files contain raw pixels and cannot be redacted. Avoid explicit captures on screens containing secrets, and use `evidence.enabled: false` when an action timeline is not appropriate. See [SECURITY.md](SECURITY.md#recorded-evidence-and-screenshots).
Expand Down
89 changes: 89 additions & 0 deletions docs/releases/UNRELEASED.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ release description, then reset it after the release is published.
- Added `picklab watch` plus configurable manual/automatic VNC viewer attachment
for running desktop-capable sessions. VNC remains loopback-only and read-only
by default; writable access requires the explicit `--vnc-control` path.
- **Added supervised pause and human takeover (`picklab watch --control`).**
Pauses PickLab-managed agent input for a session, atomically acquires a
30-second-TTL / 5-second-heartbeat human lease, switches the session's VNC
to a temporary writable server for the duration, and hands control back —
reverting VNC to read-only, recording a fresh screenshot, and releasing the
lease — on every exit path: normal viewer close, terminal cancellation
(SIGINT/SIGTERM), a failed lease renewal, or a PickLab crash (recovered the
next time the session's VNC is touched). While a lease is held, every
desktop input call (CLI and MCP: click/move/scroll/drag/double-click/type/
key) and every DevTools relay `tools/call` request fails closed with a
stable busy error, checked immediately before delivery — never delivered
concurrently with human input. New `takeover_status` MCP tool and
`picklab takeover status` CLI command report lease state (agent-active,
human-active, or a stale/recoverable lease) without side effects.
`request_user_input`'s secret guidance now points at this flow instead of
the persistent, uncoordinated `--vnc-control`. See the README's
"Supervised pause and human takeover" section. (pickforge/picklab#21)
- Added desktop mouse move, scroll, drag, and double-click controls to both the
CLI and MCP server.
- Added end-to-end computer-use evidence recording for desktop, Android, and
Expand Down Expand Up @@ -117,6 +134,31 @@ release description, then reset it after the release is published.
`cancelled` executor status instead of a generic failure.
- Added a framing-aware DevTools NDJSON relay with protocol validation,
backpressure, bounded diagnostics, redacted failures, and evidence hooks.
- Added the human-lease/agent-permit storage primitives backing supervised
takeover (`packages/core/src/takeover.ts`): an atomic (`wx`) `human.lease.json`
per session directory, short-lived per-action agent permit files under
`permits/`, and a fail-closed `withAgentPermit` (acquire permit → recheck
lease → execute only if clear → release in `finally`). Lease acquisition
publishes the lease before draining permits that predate it (sweeping ones
owned by a dead process), so writable VNC is never granted while an agent
action from before the lease could still be in flight. Staleness is
identity-or-TTL based (`isHumanLeaseStale`); a second live acquisition
attempt fails fast (`HumanLeaseHeldError`, one winner, no queueing). Desktop-
linux's `takeover.ts` (`startHumanTakeover`/`endHumanTakeover`/
`renewHumanTakeover`/`recoverStaleHumanLease`) layers VNC-mode switching,
fresh-screenshot capture, and lifecycle evidence (`takeover_start`/
`takeover_return`/`takeover_timeout`/`takeover_cancelled`/
`takeover_recovered`) on top; `ensureSessionVnc` (plain `watch`) now
self-heals a crash-orphaned writable VNC left by a stale lease instead of
refusing outright, while still refusing while a live lease holds it. The
DevTools relay gained a generic NDJSON `intercept` hook (answers a request
directly on the response stream instead of forwarding it to the child) used
for the same busy-error contract; its writes are funneled through a shared
write queue (`createJsonRpcWriteQueue`) alongside the normal child-response
pump so a busy rejection and an in-flight forwarded response can never
interleave on the wire. A blocked relay request is not recorded as an
evidence action, unlike a blocked MCP desktop-input tool call — the relay
has no equivalent per-call evidence lifecycle to hook.
- Added atomic, crash-recoverable evidence journals, active-run ownership,
truncation markers, report publication, and symlink-safe resource access.
- Hardened Android and evidence cleanup around process identity, atomic writes,
Expand Down Expand Up @@ -185,6 +227,39 @@ release description, then reset it after the release is published.
changing production code) and can occasionally need extra wall-clock time
under the same full-parallel load — a scheduling artifact, not a logic
defect, and it stays within the pre-existing 43-45 Darwin noise band above.
- New `core/test/takeover.test.ts` (15 tests): atomic lease acquisition/
staleness/renewal/release, permit drain (including sweeping a permit owned
by a dead process), `withAgentPermit` fail-closed and mid-flight-
invalidation behavior, `getTakeoverStatus`, and a real separate-process
(`bun`-spawned) race proving two concurrent lease claims yield exactly one
winner.
- New `desktop-linux/test/takeover.test.ts` (11 tests): end-to-end
start/end-takeover VNC mode switching against real spawned fake-x11vnc
processes and real port listening (only the two `/proc`-dependent identity
functions are mocked, following `destroy.test.ts`'s existing pattern, so
this runs on Darwin too) — asserts the exact `-viewonly` flag sequence,
evidence transitions, lease renewal/expiry, crash recovery (stale lease +
orphaned writable VNC), and `ensureSessionVnc`'s self-heal vs. still-refuse-
while-live-lease branches. Real x11vnc/Xvfb hardware validation deferred.
- New `browser/test/ndjson.test.ts` and `devtools-mcp.test.ts` intercept
coverage: diversion to `interceptDestination` without invoking `hook` or
forwarding to the child, the `interceptDestination` requirement, an
end-to-end relay test proving an intercepted `tools/call` never reaches the
spawned child process, and `createTakeoverBusyIntercept` unit coverage
(blocks only `tools/call` with an id while a live lease exists; passes
through notifications, other methods, and once the lease clears).
- New `cli/test/watch-control.test.ts` (5, mocked desktop-linux takeover
functions), `cli/test/takeover-status.test.ts` (3), and
`cli/test/desktop-input-takeover.test.ts` (1, proves the CLI rejects
desktop input without ever invoking `xdotool` while a lease is held) —
cover `--control`'s reason selection (return/cancelled/timeout), the
`--control` + `waitForViewerExit: false` guard, and the no-viewer-available
path.
- New `mcp-server/test/takeover.test.ts` (3): `takeover_status` across
agent-active/human-active/stale, and `desktop_click`/`desktop_type`/
`desktop_key` failing closed with the busy error while a lease is held,
using a synthetic session record (no real Xvfb/xdotool needed for the
fail-closed path).
- `bun run build`

### Not tested yet
Expand All @@ -201,6 +276,20 @@ release description, then reset it after the release is published.
- Live remote SSH-tunnel smoke test.
- Tag-triggered npm publish and draft-release creation (runs only after merge and
tag push).
- Live supervised takeover proof on real hardware: a genuine `picklab watch
--control` against real Xvfb/x11vnc, taking control in an actual VNC
viewer, typing, and returning control, on a Linux desktop. All takeover
logic is proven with real spawned processes and real port listening except
the two `/proc`-only identity checks (mocked identically to the existing
`destroy.test.ts` pattern); this is a hardware/CI gap, not a logic gap.
- A bare `kill -TERM <picklab-pid>` (not the whole foreground process group)
arriving while a real interactive VNC viewer window is still open: the
takeover's SIGTERM handler marks the session for a "cancelled" reason but
cannot itself close the already-open viewer (`openVncViewer`'s API has no
cancel handle), so cleanup only runs once the viewer exits on its own or a
process-group-wide SIGINT (the normal terminal Ctrl+C path, which reaches
the viewer child directly) closes both. Documented, not implemented;
extending `openVncViewer` with a cancel handle is a reasonable follow-up.

### Release blockers

Expand Down
Loading
Loading