Find Chrome beyond fixed paths; add eval/screenshot/wait/navigate verbs - #3
Open
sai-v-ch wants to merge 2 commits into
Open
Find Chrome beyond fixed paths; add eval/screenshot/wait/navigate verbs#3sai-v-ch wants to merge 2 commits into
sai-v-ch wants to merge 2 commits into
Conversation
Browser discovery was five hardcoded absolute paths, so any install
outside them -- Chrome for Testing, a user-local build, /usr/local/bin,
Homebrew, Nix -- was unreachable and launch failed with "not found" on a
machine that had Chrome. Discovery now falls back to a $PATH lookup, and
--binary / $CHROME_AGENT_BINARY name a browser explicitly. An override is
authoritative: a bad path errors and names itself rather than silently
launching a different browser than the caller asked for.
Add four verbs over the existing one-shot channel, for the operations
that are painful expressed raw:
eval JS from an argument, --file, or stdin -- no JSON-inside-shell
quoting -- printing the value rather than the CDP envelope.
Promises are awaited; a page exception exits 1.
screenshot decodes the base64 and writes the file, with --full-page and
--selector clipping.
wait blocks until a CDP event fires, replacing the fixed sleeps
the guide already tells agents not to use. scripts/cdp-wait.py
stays the answer for events that may fire before the wait.
navigate waits for the load and reports the main document's HTTP
status, which Page.navigate never returns -- a 404 and a 200
are otherwise indistinguishable without inspecting the DOM.
These are wrappers, not a layer: the raw form still reaches every method,
and nothing is validated against a bundled schema.
Target attachment is now shared: page_ops.attached_page_session is the one
place that resolves a target and attaches a flattened session, and the
one-shot path uses it too, so every entry point has identical selection
and isolation semantics.
An unregistered leading token stays in the verb's argument list, so the expression or URL is consumed as the verb's own argument and the next token trips the unknown-option branch -- reporting a bad expression when the actual cause is a mistyped or already-stopped instance name.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four changes, all from friction hit while driving
chrome-agentthrough a real session (setting the tool up on a fresh Ubuntu box, then using it to research an article and drive a browser-based HDL playground).Every console block below is a verbatim transcript, captured from this branch against Chrome 151 on Linux.
1. Chrome discovery beyond the five hardcoded paths
find_chrome_binary()checked five absolute paths and nothing else — no$PATH, no env var, no flag. On a machine where Chrome lives anywhere else (Chrome for Testing, a user-local install,/usr/local/bin, Homebrew, Nix),launchfails with "not found" on a system that has Chrome.Discovery now falls back to a
$PATHlookup after the absolute paths, and--binary PATH/$CHROME_AGENT_BINARYname a browser explicitly.An override is authoritative — a bad path errors and names itself rather than silently launching a different browser than the caller asked for. Falling back there would be worse than failing: fingerprint and profile expectations silently differ from what was requested.
$CHROME_AGENT_BINARYtakes the same path when no flag is given. Existing behaviour is unchanged when neither override is set: the platform's absolute paths still win, so a$PATHshim can never displace a system install.2.
wait— an event barrier instead ofsleepThe README tells agents on non-Monitor harnesses not to fall back to fixed sleeps, then ships the answer as
scripts/cdp-wait.py, which isn't in the package. Without a CLI verb, the practical result issleep 5between every step (which is what I ended up writing, repeatedly, before this).--contains SUBSTRINGnarrows a match to events whose JSON holds every given substring, and the+Eventform is accepted soattachandwaitsubscribe alike.Scope is stated in the docs rather than fudged:
waitopens its own session, so it can only match events that fire after it subscribes —scripts/cdp-wait.pyover a backgroundedattachstream remains the answer for events that may fire first.3.
navigate— a load barrier and the HTTP statusPage.navigatereturns{frameId, loaderId, isDownload}at commit time. It carries no status, so a 404 or a bot-block page is indistinguishable from real content until you inspect the DOM, and any following command races the load.This cost me six wrong guesses in one session: probing category slugs to find an article, every miss returned a plausible-looking result and a page whose
document.titlewas just the site name.Same shape from
Page.navigatefor the first two; differentstatus, which is the whole point. (Frame and loader ids truncated here for width; the real output prints them in full.)--wait load|domcontentloaded|none.--wait nonereports"loaded": nullrather than claiming a load it never observed; a load that doesn't finish within--timeoutprints the result and exits 1.4.
evalandscreenshot— the plumbing taxRuntime.evaluatemeans JavaScript inside a JSON string inside a shell argument. Anything non-trivial dies of quoting — during the session that produced this PR I abandoned the shell entirely and drove the CLI from a Python wrapper just to build the arguments.Page.captureScreenshotreturns base64 that every caller decodes by hand.evalprints the value — string as itself, anything else as JSON — becauseresult.result.valueis essentially never what the caller wants;--jsonkeeps the full envelope. Promises are awaited. A thrown page exception goes to stderr and exits 1, so an error can't be mistaken for a result.screenshotprints the path it wrote (a human-readable line with a byte count when stdout is a TTY).These are wrappers, not a layer
Nothing here gates the protocol. Every verb is a thin composition of the same primitives, the raw
<instance> Domain.methodform still reaches everything, and no new schema validation is introduced — the "tracks the running browser, not its own version" property is untouched.Structural note
page_ops.attached_page_sessionis now the single place that resolves a page target and attaches a flattened session, and the existing one-shot path was migrated onto it rather than left as a parallel copy — that's most of the churn incli.py, and it means every entry point shares identical target-selection and isolation semantics.One consequence worth reviewing:
resolve_portreaches the registry through the module (registry.lookup) rather than a from-import, sotest_one_shot_ambiguous_target_clean_error, which monkeypatcheschrome_agent.registry.lookup, keeps working unchanged.Testing
190 passed— the 162 existing tests plus 28 new ones (22 intests/test_page_ops.py, 6 discovery tests intests/test_launcher.py).Two existing stubs were updated for the new
find_chrome_binarysignature (lambda: None→lambda binary=None: None) intests/test_launcher.pyandtests/test_cli.py.New tests assert observable contracts, not plumbing: a bad
--binarydoesn't fall back; the$PATHfallback never displaces a system install;evalunwraps values, awaits promises, and raises on page exceptions;--selectorproduces an image of exactly the element's size (120×60 for a 120×60 div);--full-pageis taller than the viewport capture;waitreturns the event, andNoneon timeout;navigatereports status 200, returns"loaded": nullfor--wait none, and raises on a refused navigation.Every verb was also exercised outside the suite against a real headed Chrome 151, which is where the transcripts above come from.
Docs
README.mdandAGENTS.mdupdated. Sincesrc/chrome_agent/AGENTS.mdsymlinks to the root guide,chrome-agent guideships the new verbs automatically. The screenshot entry in the guide's "Commands and what they return" section now points at the verb instead of the hand-rolled base64 decode one-liner.