Frame 09Open sourceAgentsNov 2025

A browser agent that never sees the password and writes up what it did

Browser Autopilot takes a plain-English task, drives a real Chromium page through a LangGraph loop, and hands the run to a second agent that turns it into a screenshot tutorial. I wrote it as a single file to test two ideas: that a model can log in without ever holding a secret, and that an agent should leave a readable record of what it touched.

Stack
Python, LangGraph, Playwright (sync), Pydantic, gpt-5-mini through LangChain
Scope
Sole author, one file
The 30-second version

What could go wrong, and how we would know

Two things go wrong with browser agents. They loop, clicking the same dead element until the budget runs out. And they leak, because the easiest way to log in is to paste the password into the prompt.

The loop is handled with a counter. Every action appends a line to the history; failures are prefixed FAILED and shown back to the model as "failed", so it sees its own misses. consecutive_failures resets on success. At 2 the prompt gets a warning suggesting Escape or a direct goto; at 4 the loop exits. Every exit path, including the aborts, routes into the documentation agent, so a stuck run still produces a write-up of how far it got.

The leak is handled by never giving the model the value. The action schema has credential_site and credential_type fields, both enums. The model says, in effect, "fill this index with the github password"; execute_action builds the key GITHUB_PASSWORD, reads it with os.getenv, types it, and prints [HIDDEN]. Usernames log as three characters and asterisks. A missing variable raises, which counts as a failed step rather than a silent fallback.

Why it holds up

A failure counter fed back into the prompt: warning at 2 consecutive failures, abort at 4, plus a 15-step budget and a LangGraph recursion limit of 50.

What it cannot tell: the model sets task_complete itself and nothing checks the claim. A confident "done" on the wrong page ends the run.

What I built

Four nodes. get_page_state runs a script in the page that collects every link, button, input, select, textarea and role="button" or role="link" element, drops the invisible ones, and only then assigns an index. The order matters: the number the model returns has to point at the element it saw. Each element carries a unique selector when it has an id, data-testid or input name, and its center coordinates either way.

plan_action sends a JPEG screenshot, the request, the history and the element list to gpt-5-mini bound to a Pydantic PlaywrightAction, so the reply is one of five actions (click, fill, press, goto, wait_for_user) plus a one-line reason. Past 60 interactive elements the list is trimmed: inputs first, then up to 40 named elements, then a short tail.

execute_action uses the selector when it exists and is visible, and clicks the recorded coordinates when it does not. wait_for_user blocks on input() so a human can clear 2FA or a CAPTCHA. After every successful action it stores a PNG plus the action, reason and URL for the documentation agent.

ChoseOverBecauseCost
Symbolic credential reference in the action schemaSecrets in the prompt, or a hard-coded login stepThe model only needs to know that a github password exists, not its valueOne enum entry per site; only github today, so a Notion sign-in falls to the human
Index assigned after the visibility filter, with a selector and coordinates per elementRaw DOM indices, or letting the model write selectorsThe index the model returns must map to the element it sawNo selector without an id, data-testid or name; the rest get a coordinate click
Pydantic structured output for both agentsParsing free textAn invalid action fails at the schema, not mid-clickFive action types only; no scroll, hover or select

The documentation agent, and the four runs

After the loop, generate_documentation sends the request, the full action history and every post-action PNG to the same model bound to a TutorialOutput schema: title, introduction, steps that each reference a screenshot index, and a conclusion. The prompt says to keep only the essential states, "typically 3-7 screenshots", to skip failed actions and retries, and to merge micro-actions. export_tutorial writes tutorial.md, tutorial.json and only the selected PNGs into a timestamped folder.

Four runs are committed:

Sixteen screenshots in all. None are embedded here: they were captured inside my own accounts and show a GitHub username and a Notion workspace name. That is also the limit of the indirection. It keeps the password out of the prompt and the logs, not the username or anything the page renders afterwards, and those frames go to the documentation model and into the repo.

Outcome, and what it does not prove

The four runs show the loop completing real tasks on live sites and the second agent producing readable tutorials with sensible step boundaries. They do not show reliability: there is no success rate, no run that hit the abort path is committed, and the curated tutorial drops failures by design, so the repo holds a clean-path record rather than an audit log. The full history is printed to stdout and never saved.

What I would do differently

Persist the unfiltered history and every captured frame next to the tutorial; pin requirements.txt; write a test for the element cap, because others[:20 - len(inputs)] goes negative once a page has more than 20 inputs, and a negative slice keeps everything except the last few, the opposite of a cap; and gate task_complete on a second look at the page instead of trusting the planner's own claim.

Honest notes

Sources README main.py tutorials/ Scope: I (sole author) Verified 3 Sep 2026

Elsewhere