aboutsummaryrefslogtreecommitdiff
path: root/src/cdp/discovery.ts
AgeCommit message (Collapse)Author
2026-10-17Start a local Chrome when the target is an explicit local and none answersIgor Soarez
Dedicated profile (~/.chrome-cdp-profile), macOS only, never for the castle default. BU_LOCAL_CHROME_LAUNCH=off disables it.
2026-10-15fix(cdp): give the castle default 1.5s, not 5s, inside upstream's connect budgetIgor Soarez
2026-10-14feat(cdp): /browser-target to switch browsers without leaving piIgor Soarez
Switching between the shared browser and this machine's own previously meant leaving pi and relaunching with BU_CDP_HTTP — and, less obviously, killing the daemon first, or the switch silently did not happen. /browser-target show the current target /browser-target local this machine's browser /browser-target default the built-in default /browser-target host:port somewhere else Precedence becomes env > stored > default. The environment still wins so a one-off `BU_CDP_HTTP=… pi` is never silently overridden by a choice made days ago; when it is set, the command says so rather than pretending to obey. Clearing falls back to the default rather than to local — "use the usual browser", not "go local". The daemon is the reason this needed more than a settings write. It is machine-wide, resolves its target once per process, and holds the WebSocket to the old browser, so re-reading the store would leave tool calls going to the old browser while /browser-status reported the new one. The command therefore replaces the process: the daemon now honours an inbound control/shutdown (the action already existed in the protocol, previously only sent daemon→client), and stopDaemon() asks for it and polls until the process is really gone before ensureDaemon() respawns it against the new target. /browser-status now reads the store too, so it cannot disagree with what the tools will do. Persistence mirrors the profile pin: atomic temp-file-plus-rename, and every read failure degrades to "nothing stored" so a corrupt file falls back to the default instead of breaking browser control. It is a separate file from the pin, so damage to one cannot cost you the other. The stored value stays in BU_CDP_HTTP's vocabulary so there is exactly one resolution path. 62 tests: the full precedence table including the stored tier, that a bad value is blamed on the source that set it rather than sending you hunting through shell config, that the command only accepts values resolution can actually use, and eight ways of damaging the store file.
2026-10-14feat(cdp): default to the shared browser, and say so in /browser-statusIgor Soarez
Two follow-ups to BU_CDP_HTTP. Default the endpoint. The remote host is now a built-in default (DEFAULT_REMOTE_CDP), so nothing has to export BU_CDP_HTTP just to get the usual setup. The variable becomes an override rather than a requirement. Precedence is deliberately asymmetric, in src/cdp/endpoint.ts: BU_CDP_HTTP=host:port that endpoint; unreachable is a hard error BU_CDP_HTTP=local|off upstream behaviour, this machine's own browser unset the default, falling back to local when unreachable An explicitly configured endpoint is an instruction, so failing to reach it must not quietly automate a different browser - that would drive the operator's own signed-in Chrome instead of the shared one. The built-in default is only a preference, so a laptop away from the network still gets the local browser the extension was originally written for. Resolution is pure, with no I/O, so the status command can report the configured intent without touching the network and the daemon can resolve the same way in its own process. /browser-status no longer reports a bare "Disconnected". The connection is lazy - nothing dials the browser until the first browser_* tool runs - so a fresh session always showed a red light that reads as a fault. It now distinguishes "not connected yet, connects on first use" from "target unreachable", and names the target with a reachability check, so the common confusion answers itself. 26 tests for the resolution table, covering the aliases, malformed values that must be rejected rather than coerced, and that the default constant is itself well-formed - if it ever stopped parsing, every machine would silently fall back to local.
2026-10-14feat(cdp): BU_CDP_HTTP to attach to a browser on another hostIgor Soarez
Upstream can only drive a browser on localhost. The pi extension always goes through the daemon (src/index.ts uses createDaemonTransport), the daemon resolves Chrome with discoverWsUrl() (src/daemon/bridge.ts), and that probe is hardcoded to 127.0.0.1 (src/cdp/discovery.ts). BU_CDP_WS is read only by the in-process client (src/client.ts), which that path never touches, so it has no effect on the extension - the only workaround is relaying the remote browser onto localhost first. BU_CDP_HTTP="host:port" points at the remote CDP HTTP endpoint. It is resolved inside discoverEndpoint(), so the daemon and the client both honour it. Deliberately an HTTP endpoint rather than a WebSocket URL: it is re-resolved on every discovery call, so the browser UUID is re-read after the remote browser restarts. Chrome re-mints that UUID on every launch, so a pinned WebSocket URL goes stale as soon as the remote browser is recycled. Also skips performSetup's local-process precondition when BU_CDP_HTTP is set: isBrowserRunning() enumerates local processes and can never see a remote browser. Step 4 still fails loudly if it is unreachable. No userDataDir is returned for remote endpoints, since the profile registry lives on the remote host - profile enumeration degrades to the pre-profile behaviour rather than reading the wrong machine's Local State.
2026-10-11fix(daemon): stop importing typebox in the daemon process (#38)Aman Kumar
pi installs packages with dev and peer dependencies omitted and supplies typebox only to the extension host. The daemon is a separate Node process whose import graph reached typebox via src/cdp/discovery.ts, so from a pi-managed install it crashed with "Cannot find module 'typebox'" before binding its socket. Parse /json/version with the package's own guards, and add a test that keeps every package reachable from the daemon a runtime dependency. Fixes #34 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-08-02fix: make --browser-debug-clicks and BU_CDP_WS actually workamankumarsingh77
Both were documented and inert. --browser-debug-clicks was registered as a flag and never read; the only real switch was the undocumented BH_DEBUG_CLICKS env var. The flag now feeds util/debug.ts, which the click tool consults, and the env var keeps working as an equivalent. BU_CDP_WS was read in the client and handed to transport.connect(), but the daemon transport discards that argument and always dials the daemon socket — and the daemon's own discovery never looked at the variable, so setting it did nothing. The override now lives in discoverEndpoint, which is what both processes go through, and the client's redundant read is gone. The variable must be set before the daemon spawns. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor: tidy the whitespace the comment removal left behindamankumarsingh77
Drops leading blank lines where a file header was removed, collapses double blank lines, and closes the empty catch/handler blocks whose only content had been a comment. Whitespace only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor(cdp): strip comment noiseamankumarsingh77
Removes module banners, section dividers, and comments restating the code. Keeps one-line notes for the window.open/browserContextId constraint, the seed-tab claim, the unverifiable-pin reuse rule, the SeedTargetProvider rationale, the deliberate dialog retention, the discovery-time browser quirks, and the guarded CDP decoder assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor: close the remaining unguarded type assertionsamankumarsingh77
Types the event-queue iterator's TReturn as undefined so the terminal result needs no cast; declares the isError field the pi runtime reads by duck-typing; binds socket to a local const in the daemon transport; replaces the @ts-ignore on the optional sharp import with a non-literal specifier so src/ still typechecks where sharp was not installed. Adds src/util/guards.ts as the single home for narrowing over unknown — isRecord plus scalar/array readers — and converts the two private isObject copies in cdp/types.ts and daemon/protocol.ts along with every inline `as Record<string, unknown>` in domains/js.ts and domains/render.ts. Gives Accessibility.getFullAXTree a real result schema, closing the last unvalidated CDP boundary: only nodeId is required, every object carries additionalProperties, and each AX value stays Unknown, so the tree decodes as a whole or fails as a whole rather than being filtered. Guards the five tool-result `details` readers, the daemon attach response, the persisted session state, the browser-namespace flag, and the fs errno narrowing, each feeding an already-present failure path. The daemon-transport socket binding is not a bug fix: send() runs synchronously inside sendWithTimeout's promise executor, so socket cannot be reassigned between the check and the write. It removes the assertion and makes the write immune to a later refactor that adds an await. Two assertions survive by design and are documented in the task report: the never->unknown re-widening in util/tool.ts, which the type-erased tool registry makes unavoidable, and the AsyncFunction constructor lookup in domains/js.ts, which JavaScript exposes no typed binding for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor: validate JSON at every parse boundaryamankumarsingh77
Ten sites cast or implicitly typed JSON.parse output; JSON.parse("null") would have satisfied all of them. parseJson returns a Result and validates against a typebox schema. js.ts keeps unknown — user script output has no fixed shape. daemon/bridge.ts assigned JSON.parse straight to CdpRawMessage with no guard at all; it now uses isCdpRawMessage like the transport does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25feat: pin the agent to a chosen browser profile (#16)Aman Kumar
* feat: pin the agent to a chosen browser profile (#15) Harness tabs were created with a bare Target.createTarget, which places them in Chrome's defaultBrowserContextId — and that value follows window focus. Which profile the agent acted as therefore depended on which browser window the user last clicked, so the same task could run as a work account on one run and a personal account on the next. /browser-profile lists the connected browser's profiles as "Name (email)" and pins one to ~/.pi/agent/browser-harness.json, so the choice outlives the session. The same picker appears on first browser_setup when nothing is pinned; with no interactive UI, or on cancel, setup continues with a note and the previous behavior. Chrome offers no direct route to another profile: Target.createTarget rejects a foreign browserContextId, and openerId does not inherit the opener's context. The window is opened through the browser's own command line (--profile-directory, handed to the running browser by Chromium's ProcessSingleton) and identified by a unique file:// sentinel page, since a bare about:blank#token argument is discarded. Further tabs come from window.open evaluated with userGesture: true — the only CDP-reachable way to place a tab in a non-default browser context — correlated by a unique token so concurrent isolated tabs stay distinct. A failed handshake is reported, never silently redirected to another profile. All four tab-creation sites now funnel through cdp/target-factory, so open_urls and the isolated tabs behind web_search / read_page cannot run with a different profile's cookies than the visible tabs. Also fixed along the way: snap Chromium is discovered, Windows uses %LOCALAPPDATA%, browsers launched with an explicit --user-data-dir are found, the running browser is chosen by ranking candidates against the connected user-data-dir rather than taking the first process found, and an installed-but-closed browser no longer reads as running. CI runs the profile suites and a real-browser end-to-end test on ubuntu, macOS, and Windows — profile discovery is the one part of the harness whose behavior genuinely differs per OS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: single-flight harness window creation Isolated tabs run concurrently (browser_read_page is registered unserialized), so several could each find "no harness window" and create one — two blank windows before this feature, two LAUNCHED profile windows after it. ensureHarnessWindow now shares one in-flight creation per client. Sharing introduces a second hazard: freshlyCreated is a claim on the seed tab, and if every waiter received it, they would all drive the same tab. Only the caller that started the creation keeps the claim; the rest open their own tab beside it. Covered by test/manual/profile-concurrency-test.ts (five concurrent isolated tabs, pinned and unpinned: distinct targets, one window, correct profile), wired into CI on Linux only — the coordination is pure JS and identical across platforms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: tilde expansion differs by purpose on Windows CI on windows-latest caught this: join("~", "x") yields "~\x" there, and expandTilde only handled "~/". The two uses are not the same, so they no longer share a helper. The agent dir stays byte-compatible with pi's own expandTildePath ("~/" only, every platform) — being more lenient would put the pin file somewhere pi never looks. $CHROME_USER_DATA_DIR is ours to interpret, so it accepts "~\" as well, which is what a Windows user would type. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13feat: support Brave + Chrome channels, fix stale DevToolsActivePort ↵Aman Kumar
discovery (#13) * feat: support Brave + Chrome channels, fix stale DevToolsActivePort discovery Broadens browser detection and hardens CDP endpoint discovery, based on the investigation and approach in #4 (Brave Beta on macOS with a leftover Chrome DevToolsActivePort file). Browser detection (src/setup.ts, src/cdp/discovery.ts): - checkChromeRunning() now recognizes Brave (macOS "brave browser", Linux brave/brave-browser, Windows brave.exe), alongside the existing Chrome/ Chromium/Edge detection. Also excludes gpu/updater sub-processes that can linger after a browser quits. - profileDirs() gains Brave (Stable/Beta/Nightly/Dev) and Chrome Beta/Dev/ Canary/Chromium profile paths across macOS, Linux, and Windows. Stale DevToolsActivePort handling (src/cdp/discovery.ts): Previously discovery trusted the first readable DevToolsActivePort file. A browser that has quit leaves its file behind, so if another browser later binds the same port (e.g. 9222), discovery returned a WS URL carrying the dead browser's UUID against the live browser's server, and the connection failed. discoverWsUrl() now: - collects all readable candidates instead of committing to the first; - when candidates share a port, keeps the most-recently-written file (mtime); - skips candidates whose port isn't live via a fast single-shot probe (no 30s waitForPort block on stale files); - asks each live browser for its canonical webSocketDebuggerUrl via /json/version, falling back to the file's WS path when that endpoint is disabled (some browsers only expose it via --remote-debugging-port); - falls back to probing well-known ports (9222, plus BU_CDP_PORTS) when no profile file is readable — covering sandboxed harnesses (EPERM/EACCES) and non-default install locations. Setup guidance updated to mention brave://inspect / edge://inspect and the --remote-debugging-port launch flag. Verified: typecheck clean; queryLiveWsUrl / mtime-dedup / fallback-port logic exercised against a mock /json/version server; checkChromeRunning() matches the single main process against live macOS ps output, excluding all helpers. Closes #4 Co-authored-by: Trevor Whitney <trevorjwhitney@gmail.com> * fix: harden discovery against malformed port files + stale-then-fallback Addresses two issues raised in review of the discovery rewrite: - Validate the port parsed from DevToolsActivePort before building a candidate. A truncated/corrupt file (non-numeric or out-of-range first line) yielded NaN, and net.connect throws ERR_SOCKET_BAD_PORT synchronously for NaN / <=0 / >=65536 — which escaped discoverWsUrl as an unhandled rejection instead of surfacing as a Result error. Such entries are now skipped. - Fall back to well-known ports (9222 / BU_CDP_PORTS) after all discovered candidates prove stale. Previously the fallback ran only when no port file was readable, so a leftover file for a quit browser caused a ~30s waitForPort block and failure without ever trying a live fallback port. Ports already covered by a candidate are skipped to avoid re-probing. --------- Co-authored-by: Trevor Whitney <trevorjwhitney@gmail.com>
2026-05-02fix(cdp): clear socket timer on probe success; surface non-ENOENT readFile ↵amankumarsingh77
errors
2026-05-02feat(cdp): port DevToolsActivePort discovery to Resultamankumarsingh77