Resolve conflicts in packages/cli/src/browser/session/close.rs and
packages/cli/Cargo.lock. Integrate main's 3-phase provider close with
our Windows Job Object chrome_job extraction in Phase 3.
WMI/PowerShell queries launched from the daemon process fail to enumerate
Chrome processes in some Windows contexts (DETACHED_PROCESS, non-interactive
sessions), causing kill loops to return count=0 immediately and leaving Chrome
helper processes alive after browser close.
Replace all WMI-based Chrome enumeration with Win32 kernel APIs that work
from any process context without COM/WMI initialization:
- chrome_reaper.rs: new enumerate_chrome_pids_for_dir() using
CreateToolhelp32Snapshot + NtQueryInformationProcess(ProcessCommandLineInformation)
to filter chrome.exe by --user-data-dir; terminate_pids_and_wait() uses
TerminateProcess + WaitForSingleObject. Removes ps_run_and_get_count,
build_kill_wait_ps, and kill_chrome_by_user_data_dir.
- close.rs / server.rs: simplified to kill_and_reap_async (main) followed by
kill_and_wait_for_chrome_by_user_data_dir_async (Win32 straggler sweep).
Win32 snapshots are unaffected by Chrome's helper re-parenting, so the
helpers-before-main ordering is no longer needed.
- start.rs: orphan recovery simplified to single
kill_and_wait_for_chrome_by_user_data_dir_async call.
- main.rs: add flush_and_exit() — flushes stdout before exit(1) to prevent
buffered JSON output from being lost when stdout is redirected (fully
buffered on Windows when piped to a file).
Adds windows-sys = "0.59" with Win32_Foundation + ToolHelp + Threading
features (already a transitive dep; no net new dependency resolution).
- Remove daemon.port after taskkill in restart_daemon_windows() so the
TCP-based is_pid_alive() check returns false immediately instead of
timing out for 5 seconds.
- Add daemon.lock to cleanup_stale_files_windows().
- Fix PowerShell -like pattern escaping in find_chrome_pids_for_dir():
backslash is literal in single-quoted strings, so doubling it caused
the pattern to never match Windows paths.
* [packages/cli]feat: add cloud browser provider support (-p flag)
Adds first-class support for launching cloud browser sessions through
4 providers (driver.dev, hyperbrowser, browseruse, browserless) via
the new `-p`/`--provider` flag, plus supporting registry, restart and
output plumbing.
Provider module
- New `browser/session/provider.rs` with typed `connect_provider` /
`close_provider_session` and per-provider env-var resolution.
- HTTP client built with bounded request (30s) and connect (10s)
timeouts so a hung control-plane can't stall the daemon. Cleanup
client uses tighter 10s/5s budgets.
- Hyperbrowser profile IDs are normalised to UUID v5 so non-UUID
identifiers (e.g. "user-42") map deterministically.
Cloud session lifecycle
- Registry tracks `provider` + `provider_session` so cleanup paths
can release paid sessions on close/restart/error.
- New `find_cloud_session_by_provider(provider, profile)` reuse path
in `start::execute` performs a `Target.getTargets` health probe
before reuse and removes stale entries on failure (mirrors the
existing endpoint reuse path).
- `execute_cloud` cleanup paths funnel through `fail_reserved_cloud_start`
/ `cleanup_provider_session_if_any` so all four error branches
release the provider session — `panic = "abort"` means we cannot
rely on Drop.
- Restart hybrid policy: stateless providers (driver.dev, browseruse)
reuse the original cdp_endpoint + headers so a rotated env var
cannot break a stable session; stateful providers (hyperbrowser,
browserless) re-mint via the connect path. Provider tag is
re-applied to the new entry so observability survives.
Errors and redaction
- New `CliError::ApiUnauthorized` (401/403), `ApiRateLimited` (429),
`ApiServerError` (5xx) variants with stable codes and actionable
hints; rate-limit and server errors are marked retryable.
- `redact_endpoint` now scrubs query parameters whose name matches a
secret key list (apiKey, token, password, …, case-insensitive) in
addition to long path segments. Provider WSS URLs carry the API
key in the query string, so the previous path-only redactor was
leaking credentials in success responses.
- 5 new redaction unit tests cover query, path, case and edge cases.
Config / CLI / output
- `BrowserConfig.provider` + `ACTIONBOOK_BROWSER_PROVIDER` env var
with cloud-mode auto-flip when a provider is set implicitly.
- `browser status` / `browser list` / start responses now surface
the provider name when present.
* [packages/cli]fix: forward provider env vars from CLI client to daemon
The daemon's std::env::var reads its own frozen environment (captured at
daemon-spawn time), not the user's current shell. This silently broke
provider config: rotating an API key in the shell never reached the daemon
until it was killed and restarted.
Add a per-request snapshot mechanism: the CLI client process collects all
DRIVER_DEV_*/HYPERBROWSER_*/BROWSER_USE_*/BROWSERLESS_* env vars via
collect_provider_env_from_process() and ships them in start::Cmd.provider_env
and restart::Cmd.provider_env (#[arg(skip)], serde default). The daemon
reads from this map instead of touching its own env.
For close/restart, snapshot the env onto ProviderSession.provider_env so
the daemon can talk to the provider control plane later, even when the
calling shell no longer has the keys exported.
* [packages/cli]fix: rewrite driver.dev as stateful provider with REST API
The previous implementation hardcoded wss://cdp.driver.dev as a "stateless"
provider, which DNS-resolves to NXDOMAIN. The real driver.dev is stateful:
mint a session via POST https://api.driver.dev/v1/browser/session and use
the per-session distributed cdpUrl returned in the response.
Changes:
- Replace DRIVER_DEV_WS_BASE constant with DRIVER_DEV_API_BASE
- Rewrite connect_driver_dev to POST /v1/browser/session with bearer auth
and parse {sessionId, cdpUrl}; supports country, nodeId, type, proxyUrl,
windowSize, profile body fields
- Add driver.dev case to close_provider_session (DELETE with sessionId)
- DRIVER_DEV_WS_URL escape hatch preserved for tests / private deployments
- Accept both DRIVER_DEV_API_KEY (Actionbook namespace) and DRIVER_API_KEY
(driver.dev's official docs name) via read_driver_dev_api_key
- Broaden PROVIDER_ENV_PREFIXES from "DRIVER_DEV_" to "DRIVER_" so the
fallback official-name keys also reach the daemon
- Add is_driver_dev_auth_failure body sniffer: driver.dev returns HTTP 500
with {"error":"Invalid consumer token"} for bad credentials, which the
generic 5xx → ApiServerError(retryable=true) mapping wrongly classifies.
Reclassify known auth bodies to ApiUnauthorized(retryable=false).
- 3 new unit tests
* [packages/cli]fix: filter dangerous URLs from preserved open_url on restart
restart preserves the registry's tab.url so the new session lands on the
same page. But the registry's tab.url is captured at session-launch time
and only refreshed by list-tabs — after a goto, the in-memory copy is
stale. Worse, driver.dev launches with a data:text/html,<title>... watermark
page that the L3 dangerous-protocol guard blocks on re-navigation.
Drop data:, javascript:, and about:blank from the preserved open_url so
the new session boots to the provider default and the user can goto again
from a clean state.
Also update the stale comment that listed driver.dev as a stateless
provider — after the API rewrite it has a control-plane session that needs
explicit cleanup.
* [packages/cli]fix: redact cdp_endpoint in reuse and local start paths
reuse_running_session emitted entry.ws_url verbatim, which for cloud sessions
contains the raw provider WSS URL with tokens embedded as query params
(e.g. Hyperbrowser's JWT). HYP-3 reproduced this in E2E by reusing a running
session — the JWT showed up plaintext in stdout.
execute_local was inconsistent for the same reason: local CDP URLs don't
carry secrets, but the rule should be uniform — every cdp_endpoint emission
must go through redact_endpoint().
Both sites now route through redact_endpoint(). All four cdp_endpoint JSON
emissions in start.rs are now redacted.
* Fix cloud provider session lifecycle
* [packages/cli]fix: address PR #507 cloud provider review comments
Bundle of PR #507 review fixes for cloud provider session lifecycle:
P1 — correctness / resource leaks
- start.rs: cleanup provider session on race with concurrent close
during startup (codex P1, start.rs:1064). When the placeholder
reserved at the top of execute_cloud is removed while we were busy
minting the remote session, the local provider_session handle was
dropped without stopping the remote browser. Now release the lock
and call cleanup_provider_session_if_any before returning
SESSION_NOT_FOUND.
- start.rs: close stale provider session before reconnecting on
failed health probe (codex P2, start.rs:314). A transient CDP hiccup
would orphan the previous remote session and spin up a duplicate.
- close.rs: introduce Closing state to close out the TOCTOU race
between Phase 1 (clone handle) and Phase 2 (HTTP PUT stop). A second
concurrent close now short-circuits with SESSION_CLOSING; on
provider stop failure the state reverts to the prior status so the
caller can retry.
- registry.rs: find_cloud_session_by_provider ignores WS_URL override
sessions (they have no provider_session handle); is_active() now
excludes both Closing and Closed.
P2 — output / configuration hygiene
- start.rs: endpoint_for_mode() emits local loopback CDP URLs
verbatim (so the caller can actually attach) while still redacting
cloud ws_urls that embed tokens.
- restart.rs: stateful provider restart keeps user-supplied CDP
headers (codex P2, restart.rs:177) instead of clearing them to
vec![], so sessions that needed custom headers for the initial
connect can also reconnect.
- goto.rs: refresh TabEntry.url/title in the registry after a
successful navigation so a restart-preserved open_url reflects the
navigated page, not the launch-time stub.
- Cargo.toml: release profile uses lto = "thin" (full LTO was
SIGKILLing the binary on macOS before commands reached the daemon).
P3 — observability / internals
- provider.rs: each connect_* helper now sets provider_env directly
when constructing ProviderSession, removing the post-hoc assignment
in connect_provider.
- provider.rs: is_driver_dev_auth_failure logs its classification
decision (ApiUnauthorized vs ApiServerError) with a 120-char body
snippet for easier triage.
Verified: cargo check --all-targets clean, cargo test --lib 335/335
passing. Pre-existing clippy warnings (collapsible_if, too_many_args)
are unrelated to this change.
* [packages/cli]style: fix fmt + clippy lints for CI
Addresses `Lint (fmt + clippy)` check failures on PR #507.
fmt: let cargo fmt rewrap a handful of long function signatures,
assert!() args, and closures to the project default width. No
semantic changes.
clippy:
- restart.rs:134 — collapse nested `if let Some(..)` / `if let Err(..)`
into a single `let`-chain condition.
- restart.rs:187 — replace the `if let Some(saved) / else` fallback
with `saved_provider_env.unwrap_or_default()`.
- start.rs:861 — collapse the same nested `if let` pattern in
`cleanup_provider_session_if_any`.
- start.rs:908 — annotate `execute_cloud` with
`#[allow(clippy::too_many_arguments)]`. All 8 arguments are
independent inputs resolved by the caller; folding them into a
struct would just be lint placation.
Verified locally: cargo fmt -- --check, cargo clippy --all-targets
-- -D warnings, and cargo test --lib (335/335) all clean.
* [packages/cli]docs: make -p/--provider self-documenting for agents
Before: \`-p <PROVIDER>\` rendered a single-line help string with no
list of valid values and no hint about the required auth env var.
Agents reading \`browser start --help\` had to guess a name, run the
command, read the runtime error, then guess the env var — 2–3 round
trips per provider.
After: the same field uses a \`PossibleValuesParser\` with per-value
help text, and the doc comment / \`after_help\` block spell out the
conflict and env-var rules up front. One \`--help\` read is enough
for an agent to pick a provider, know what to export, and see a
working command line.
Rendered output:
-p, --provider <PROVIDER>
Cloud browser provider (implies --mode cloud).
... mutual exclusion + stateful-restart note ...
Possible values:
- driver: driver.dev — requires DRIVER_DEV_API_KEY (or DRIVER_API_KEY)
- hyperbrowser: hyperbrowser.ai — requires HYPERBROWSER_API_KEY
- browseruse: browser-use.com — requires BROWSER_USE_API_KEY
Plus an after_help section with export-then-run examples for each
provider and a note about \`browser restart\` minting fresh remote
sessions for provider-backed IDs.
Implementation notes:
- Kept \`Cmd.provider\` as \`Option<String>\` deliberately. Switching to
a \`ValueEnum\` would have required touching \`config.rs\`,
\`restart.rs\`, registry lookups, and three in-repo test setups —
the string form survives round-tripping through IPC, TOML config,
and env merging with no conversion helpers.
- The \`browseruse\` alias for \`browser-use\` is preserved via
\`PossibleValue::aliases\`.
- \`normalize_provider_name\` still runs at \`execute()\` time so
daemon-side IPC input gets the same validation that clap now
enforces for CLI input.
- Clap now rejects invalid \`-p\` values at parse time with
\`[possible values: driver, hyperbrowser, browseruse]\` — the
runtime branch at \`execute()\` remains as an IPC safety net.
* [packages/cli]refactor: drop DRIVER_DEV_API_KEY alias, accept only DRIVER_API_KEY
driver.dev's official docs use the bare `DRIVER_API_KEY` name for the
auth credential. We previously accepted both `DRIVER_DEV_API_KEY` and
`DRIVER_API_KEY` as a transitional alias, but that exposed two names
for the same secret in every surface (--help, error hints, docs) and
confused agents reading the help output.
Keep only `DRIVER_API_KEY`:
- provider.rs: `read_driver_dev_api_key` no longer falls back to the
namespaced name; error message names only `DRIVER_API_KEY`.
- start.rs: after_help and PossibleValue help text drop the alias.
- error.rs: ApiUnauthorized hint names `DRIVER_API_KEY`.
- tests: rewritten to assert the retired name is rejected so a
future silent-fallback regression is caught.
`PROVIDER_ENV_PREFIXES` still includes `DRIVER_` (not `DRIVER_DEV_`),
so `DRIVER_API_KEY` and any `DRIVER_DEV_*` tuning knobs both survive
the CLI→daemon env forward.
* [root]chore: add changeset for CLI v1.4.0 (cloud provider support)
Minor bump for the `-p / --provider` cloud browser provider feature
(driver.dev, hyperbrowser.ai, browser-use.com) plus the stateful
session lifecycle, self-documenting --help, and the DRIVER_API_KEY
alias cleanup shipped on feature/cli-cloud-provider.
* test(daemon): add Windows daemon red coverage (ACT-897)
Add tests and CI configuration for Windows daemon TCP transport:
- config.rs: test_actionbook_home_uses_userprofile_when_home_unset — cross-platform
red test; fails until USERPROFILE fallback is implemented
- server.rs: port_path() stub (path-only) + #[cfg(all(test,windows))] unit tests for
is_daemon_running, send_sigterm, is_pid_alive Windows behaviour
- tests/e2e/windows_daemon.rs: 3 e2e tests ignored on non-Windows, run on Windows CI
- .github/workflows/e2e-windows.yml: windows-latest CI job for unit + e2e tests
* feat(daemon): add Windows TCP localhost IPC support (ACT-897)
Unix behaviour is unchanged (UnixListener/UnixStream, flock).
Windows now uses TCP localhost instead of the missing Unix socket:
server.rs:
- run_daemon(): bind TcpListener on 127.0.0.1:0, write ephemeral port to
daemon.port; use fs2::FileExt for cross-platform exclusive file locking
- is_daemon_running(): TCP connect-probe against daemon.port (Windows)
- send_sigterm(): taskkill /F /PID (Windows)
- is_pid_alive(): delegates to is_daemon_running() TCP probe (Windows)
- handle_connection_inner(): generic AsyncRead+AsyncWrite handler shared by
both platforms; Unix/Windows handle_connection() call it with the
appropriate stream type
- port_path(): daemon.port path (used both platforms for path derivation)
client.rs:
- DaemonClient (Windows): TcpStream-based connect/send_action
- auto_start_daemon_windows(): spawns daemon with DETACHED_PROCESS |
CREATE_NEW_PROCESS_GROUP flags so it survives parent exit
- wait/restart/cleanup helpers mirroring the Unix equivalents
config.rs:
- actionbook_home(): fall back to USERPROFILE when HOME is not set
(standard Windows home directory convention)
Cargo.toml:
- Add fs2 = "0.4" for cross-platform LockFileEx file locking
* style: apply cargo fmt to windows_daemon tests
* fix: serialize USERPROFILE test with config test_lock to prevent env var race
The test_actionbook_home_uses_userprofile_when_home_unset test manipulates
ACTIONBOOK_HOME and HOME env vars without acquiring the shared test_lock,
causing intermittent failures in other config tests that run concurrently.
* fix: gate Unix-only kill() FFI behind #[cfg(unix)] for Windows build
- start.rs: wrap orphan Chrome kill(pid, 0)/kill(pid, 9) in #[cfg(unix)],
add #[cfg(windows)] equivalent using taskkill
- chrome_reaper.rs: gate Duration/Instant imports behind #[cfg(unix)]
since they're only used in the Unix SIGTERM graceful shutdown path
* fix: make drop_session_entry test cross-platform for Windows CI
The test used Unix-only `sleep` and `kill -0` commands. Use `ping` for
a long-lived process on Windows and `tasklist` for liveness checks.
* ci: limit Windows e2e to lifecycle, tab management, and daemon tests
Run only windows_daemon, browser_lifecycle, and tab_management test
modules on Windows CI to avoid timeouts from tests not yet adapted
for Windows. Run sequentially with --test-threads=1.
* ci: narrow Windows e2e to windows_daemon + tab_management
browser_lifecycle tests hang on Windows (close_kills_chrome_process
stuck for 30min). These tests exercise daemon lifecycle edge cases
not yet adapted for Windows. Narrow scope to windows_daemon and
tab_management which cover core IPC and browser operations.
* ci: narrow Windows e2e to windows_daemon tests only
tab_management tests require full Chrome browser interaction and hang
on Windows CI. Only run our Windows-specific daemon IPC tests which
validate the TCP localhost approach without Chrome dependencies.
* fix: avoid pipe-inheritance hang in e2e harness on Windows
On Windows, assert_cmd pipes stdout/stderr. When the CLI spawns a
detached daemon, the daemon inherits those pipe handles via
CreateProcess(bInheritHandles=TRUE). Even after the CLI exits, the
daemon still holds write handles, blocking the test's read_to_end()
forever.
Fix: redirect stdout/stderr to temp files on Windows instead of pipes.
Also make SoloEnv::drop() cross-platform (taskkill on Windows).
* fix: add wait-timeout dev-dependency for Windows e2e harness
The Windows-specific timeout code in the e2e harness uses
wait_timeout::ChildExt directly. It was previously only available
transitively via assert_cmd, which isn't sufficient on Windows.
* fix: add Windows Chrome discovery and fix e2e test subcommand
- find_chrome(): Add Windows paths (Program Files, LOCALAPPDATA) and
use `where` instead of `which` for PATH lookup on Windows
- Fix session_status_works_over_tcp test: use 'status' subcommand
(not 'session-status' which doesn't exist)
* fix: use test env's ACTIONBOOK_HOME for daemon.port path assertion
The port file test was checking the global ~/.actionbook/daemon.port
but the test daemon writes to SoloEnv's isolated temp directory.
* [root]fix: release workflow — add workflow_dispatch and alpha npm tag
- Add workflow_dispatch trigger to allow manual releases from any branch
- Add --tag alpha for prerelease versions so alpha doesn't overwrite latest
- Remove stale CHANGELOG.md from 6 CLI platform packages (stuck at 0.8.0)
* [packages/cli]chore: bump version to 1.0.0-alpha.6