Commit Graph

22 Commits

Author SHA1 Message Date
github-actions[bot] f416dff24f chore: version packages (#519)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-14 11:33:14 +08:00
github-actions[bot] a49caeadc3 chore: version packages (#512) 2026-04-12 20:22:53 +08:00
mcfn 6faea9940a Merge origin/main into feat/expand-windows-e2e
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.
2026-04-12 19:30:40 +08:00
mcfn 9c0b2c2cf5 fix(windows): replace WMI/PowerShell with Win32 API for Chrome process cleanup
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).
2026-04-12 17:05:52 +08:00
mcfn 94eea61490 fix(windows): resolve daemon restart timeout and Chrome PID detection
- 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.
2026-04-12 12:29:04 +08:00
github-actions[bot] 35247f649e chore: version packages (#509)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-12 11:39:58 +08:00
sen 5bddf13420 [packages/cli] Improve cloud provider session lifecycle (#507)
* [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.
2026-04-12 11:00:56 +08:00
Junliang Feng 11e71d9d03 feat(daemon): add Windows TCP localhost IPC support (ACT-897) (#508)
* 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.
2026-04-12 10:06:24 +08:00
Asen's Agent 55e5f6421f chore: version packages (cli v1.3.1)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 17:10:33 +08:00
github-actions[bot] b5d699687a chore: version packages (#500)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-11 16:28:13 +08:00
github-actions[bot] 805bb8f171 chore: version packages (#487)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-09 20:43:41 +08:00
github-actions[bot] c59638d50a chore: version packages (#484)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-07 23:20:31 +08:00
Asen's Agent 350f4fa8db chore: version packages (cli v1.0.2)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 14:32:01 +08:00
mcfn.feng 8aa6116cbd [packages/cli]chore: bump version to 1.0.1 (npm rejected 1.0.0 re-publish) 2026-04-02 23:34:40 +08:00
github-actions[bot] 8b55277dad chore: version packages 2026-04-02 14:27:45 +00:00
mcfn.feng 00a9b8ef14 [feat] update version 2026-04-02 20:02:06 +08:00
mcfn.feng 66b732b84e [packages/cli]chore: bump version to 1.0.0-alpha.10 2026-04-02 17:49:20 +08:00
mcfn.feng ca4303a73d [packages/cli]chore: bump version to 1.0.0-alpha.9 2026-04-02 17:23:37 +08:00
mcfn.feng 4f833ac38a [feat] update version. 2026-04-02 17:08:04 +08:00
Junliang Feng 34f262220d fix: release workflow — add workflow_dispatch and alpha npm tag (#453)
* [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
2026-04-02 15:18:28 +08:00
mcfn.feng 5e7a636721 [feat] update version. 2026-04-02 02:53:23 +08:00
mcfn.feng 58545129de [root]refactor: remove actionbook-rs, promote cli-v2 to packages/cli
- Delete packages/actionbook-rs/ (legacy 0.x CLI)
- Delete old packages/cli/ (JS npm wrapper for 0.x)
- Rename packages/cli-v2/ → packages/cli/ (git mv)
- Merge npm wrapper files (bin/actionbook.js, scripts/postinstall.js) into new cli/
- Update package.json: name → @actionbookdev/cli, private → false, add npm distribution fields
- Update sync-versions.js: point Cargo.toml/Cargo.lock sync at packages/cli/
- Update release.yml: build from packages/cli/ instead of packages/actionbook-rs/
- Update test.yml: paths and working-directory from cli-v2 → cli
- Update CLAUDE.md, CONTRIBUTING.md, docs, issue templates
- Delete stale changeset (fix-screenshot-timeout.md)
- Keep 6 platform binary packages unchanged
2026-04-02 00:46:12 +08:00