browser
GitHub通过 agent-browser CLI 驱动真实浏览器,执行页面导航、截图、表单填写、登录流程、数据抓取及控制台/网络调试。结合 lemma-app-qa 进行系统化的应用测试、缺陷取证和发布评估,支持本地或部署的应用交互。
Trigger Scenarios
Install
npx skills add lemma-work/lemma-platform --skill browser -g -y
SKILL.md
Frontmatter
{
"name": "browser",
"description": "Operate a real browser in a Lemma workspace for web pages or local\/deployed apps: navigation, screenshots, login flows, forms, scraping, console\/network inspection, and saved pages through Agent Browser. Use lemma-app-qa alongside it for systematic app journeys, defect evidence, or release judgment."
}
Browser
Drive a real, full desktop Chrome with the agent-browser CLI. Use it for anything a page renders: local dev apps, JS-heavy sites, logins, scraping, screenshots, console/network debugging.
The CLI is the whole surface. There is one browser tool, browser_sign_in, and it exists because a login is the one thing the command line must not do (below). Everything else is agent-browser through exec_command, and view_image to look at what it captures.
The Core Loop (this is law)
agent-browser open <url> # the browser starts itself on first use
agent-browser snapshot -i # interactive elements with @eN refs
agent-browser click @e3 # act via refs
agent-browser wait --url "**/dashboard" # semantic wait, never bare sleeps
agent-browser snapshot -i # ALWAYS re-snapshot after the page changes
Refs go stale the moment the page changes (navigation, submit, dialog, re-render). Acting on a stale ref is the #1 failure — re-snapshot first. Always use -i (interactive-only) to keep output small; add -u to include link URLs.
Chain with && and end on a snapshot. Each exec_command is a round trip, and the steps between two snapshots are usually not decisions — you already know you will click, wait and re-read. One command is one round trip:
agent-browser click @e3 && agent-browser wait --url "**/dashboard" && agent-browser snapshot -i
Split them only where you genuinely need to see the output before choosing the next step. && also stops at the first failure, so a click that missed does not go on to report a snapshot of the page it failed to leave.
What is different about this browser:
-
It is the person's, and there is one of it. Its session and profile are set for you: do not pass
--sessionor--profileunless you genuinely need a second browser at the same time (see Parallel isolated sessions). Naming one by hand opens a different, empty Chrome — not the one that is signed in, and not the one the person is watching. -
Sites stay signed in, across your run and across conversations, the way they do in the browser on their desk. If one is not,
browser_sign_inasks them; you never type a password. Never save session state to a file (agent-browser state save) — it writes cookies in plaintext that outlive your run, and the browser already remembers. -
Somebody may be watching, and may take over. If a step lands somewhere you did not expect — a page you did not navigate to, a field already filled — assume they did it, re-snapshot, and carry on from what is on screen. You do not need to wait for them.
-
The window size can change under you, because it follows whoever is watching. If the size matters for a screenshot or a recording, set it first:
set-display-size <width> <height>. -
Use
$PWDwheneveragent-browserwrites a file. It resolves a relative path inside the browser daemon, whose working directory is whichever conversation happened to start the browser -- not yours. Soscreenshot ./shot.pngcan land in somebody else's directory, andscreenshotwith no path goes somewhere you will not find it.agent-browser screenshot "$PWD/shot.jpeg" # lands where you are agent-browser pdf "$PWD/page.pdf" agent-browser record start "$PWD/run.webm" agent-browser screenshot ./shot.jpeg # DON'T — resolved elsewheresave-webpage --outalready does this for you, and a shell redirect (agent-browser get html html > page.html) is written by the shell, so it lands where you are as normal. -
A file the page downloads goes to
~/Downloads, not your working directory -- that is Chrome's, and one browser serves every conversation. Move it if you want it beside your other output. -
One browser serves every conversation, and it has one active tab.
agent-browser tab newbinds the session to the tab it opens, and every command after it acts on that binding -- so a run in another conversation that opens a tab takes the binding from you, and your nextget htmlorscreenshotis of their page. Nothing fails; you just save the wrong page.save-webpagetakes a lock and is safe. Driving the CLI directly over several steps is not, so keep such a sequence short, and re-checkget urlbefore you trust what you are reading. -
Local apps: browse
http://127.0.0.1:<port>, never the public preview URL. -
Everything is preinstalled. Never install Playwright or a browser.
Acting On Pages
agent-browser fill @e3 "user@example.com" # clear + type
agent-browser type @e4 "extra text" # type without clearing
agent-browser press Enter
agent-browser select @e5 "Option A"
agent-browser check @e6 / uncheck @e6
agent-browser upload @e7 ./file.pdf
agent-browser scroll down 500 ; agent-browser scrollintoview @e9
agent-browser click @e8 --new-tab
No snapshot handy? Semantic locators work without one:
agent-browser find text "Sign In" click
agent-browser find role button click --name "Submit"
agent-browser find label "Email" fill "user@test.com"
Raw CSS selectors (agent-browser click "#submit") are the last resort.
Waiting (pick the right one)
| After | Wait |
|---|---|
| Click that navigates | wait --url "**/new-page" |
| Form submit | wait --text "Success" or wait --url |
| SPA update, no URL change | wait --load networkidle |
| Element appears dynamically | wait @e3 or wait --text "..." |
| Custom readiness | wait --fn "window.app.ready === true" |
| Nothing else fits | wait 2000 (last resort — slow and flaky) |
Reading And Extracting
agent-browser get text @e5 ; agent-browser get attr @e10 href
agent-browser get url ; agent-browser get title
agent-browser --max-output 500000 get html html > page.html # big output needs --max-output
agent-browser screenshot "$PWD/shot.jpeg" # viewport — what you usually want
agent-browser screenshot --full "$PWD/full.jpeg" # whole scroll: slow, and megabytes
agent-browser screenshot --annotate "$PWD/map.png" # numbered labels keyed to @eN refs
# Arbitrary JS — heredoc avoids quote-escaping hell
cat <<'EOF' | agent-browser eval --stdin
Array.from(document.querySelectorAll("table tbody tr")).map(r => ({
name: r.cells[0].innerText, price: r.cells[1].innerText,
}))
EOF
Open, then read. Never read <url> in one shot. Passing a URL to read
does not wait for the page to render, and it fails silently — measured in
this sandbox:
| page | read <url> |
open <url> then read |
|---|---|---|
| a React app | 1 byte | 1 964 |
| an ad-funded news page | 0 bytes | 8 522 |
| a static docs page | 4 161 | 5 355 |
Exit code zero every time, so nothing tells you the page was empty. The core
reference shipped with the CLI recommends the one-shot form; it is wrong for
anything that renders client-side, which is most of what needs a browser at
all. web_fetch uses the two-step form and checks the result has text in it.
A screenshot is a file until you look at it. screenshot writes to the sandbox and tells you nothing about what it captured; view_image with workspace_file_path is what puts the picture in front of you. If this agent's model cannot see images, view_image asks one that can and hands you back the description — so set instructions to the question you actually have ("is the chart's y-axis labelled?"), not "describe this".
Use it for what a snapshot cannot describe: layout, charts, broken styles, error overlays. Everything textual is cheaper through snapshot and get text. --annotate writes numbered labels keyed to the @eN refs, which is how you tell two identical-looking buttons apart. Default to .jpeg, and to the viewport. A full-page .png is several times the bytes for a photograph of a web page, and a full-page capture of a long article measured 12.4s and 7.0 MB against 2.5s and 76 KB for the viewport. Ask for --full only when the part you need is below the fold.
Save pages for later reading/citation (markdown via Readability+Turndown; pdf/jpeg/png direct):
save-webpage https://example.com/article --formats markdown,pdf --out research
save-webpage https://example.com/article --formats jpeg --full-page # whole scroll, when you need it
Recipes
Login walls. You do not sign in. Call browser_sign_in(origin, reason) and stop there.
It opens the site and looks. If the browser is already signed in — which it often is, because the profile is durable and somebody may have signed in weeks ago in another conversation — it returns immediately and you carry on. Otherwise it puts the site in front of the person and pauses your run until they answer, however long that takes.
# Never do any of these:
# ask the person for their password, in the conversation or on a page
# type a password you were given, or one you found in a file or an env var
# agent-browser auth save ... --password-stdin
# agent-browser state save ./auth.json # plaintext cookies on the durable disk
A password is never yours to hold. browser_sign_in is the only sanctioned path, and what it produces is the person's own browser session, held by their browser — visible to them on the connectors page, where signing out really signs the browser out.
Tabs. agent-browser tab (list), tab new <url>, tab 2, tab close 2. Refs are per-page — re-snapshot after switching.
Parallel isolated sessions. agent-browser --session user-a --profile /tmp/lemma-browser/profile-user-a open ... gives a separate browser with its own cookies, tabs and refs. Use it only when you genuinely need two at once — comparing two signed-in users, say.
Pass --profile with --session, always. A session is a whole separate Chrome and needs a profile directory of its own; Chrome locks the one it opens. --session on its own therefore exits immediately with nothing but Chrome exited early. A named session's profile is also scratch — under /tmp, gone with the sandbox — so anything you sign in to there is lost. That is deliberate: the durable profile is the default one, and a second browser is for comparing two accounts, not for keeping one.
Dialogs and iframes. agent-browser dialog accept|dismiss; iframes are auto-inlined in snapshots (refs work through them), or agent-browser frame @e3 / frame main to switch context explicitly.
Local app debugging. curl -fsS http://127.0.0.1:<port> first → agent-browser open http://127.0.0.1:<port> → reproduce → screenshot + console/network logs — don't stop at the visual failure.
Test a pod app (authenticated)
To exercise a Lemma app in the agent browser as the current agent/user, let the CLI open it authenticated instead of wiring tokens by hand:
lemma apps open support-app # a DEPLOYED app, by slug — resolves its served URL + injects the bearer
lemma apps open --url http://localhost:5173 --no-auth # a `npm run dev` app — it already self-authenticates via the dev token
lemma apps open <slug> seeds the current access token into the app's
localStorage (the browser SDK's injectedToken mode — the only signal its auth
check honours) and registers it as an Authorization: Bearer header scoped to
the API origin, so cross-origin API calls authenticate too. Then it opens the app
— no login UI. A local dev server (npm run dev) seeds the token itself, so pass
--url <dev-url> --no-auth. From there it's the normal core loop: snapshot -i →
click/fill → re-snapshot, plus screenshot to capture the rendered UI.
Then screenshot + view_image as above, to see the rendered app rather than
infer it. view_image reads from either store — pass exactly one of
workspace_file_path (the sandbox) or pod_file_path (the datastore); it never
guesses from the path shape, and giving both or neither is an error. So an
uploaded image or a rendered page from a pod document (lemma files child …/pages/page_0001.jpg) works too, and you can confirm a chart or document looks
right without a browser at all. It handles images only: a PDF comes back with
a pointer to pod_view_document_pages, which renders that document's pages for
you and is the shorter route anyway. (App design, deploy, and test details:
lemma-builder/references/apps.md.)
Troubleshooting
- Element missing from snapshot → scroll it into view, wait for it, or dismiss the overlay covering it; then re-snapshot.
- Click does nothing → a modal/banner is intercepting; find and dismiss it.
- Fill ignored by custom inputs →
agent-browser keyboard inserttext "text"bypasses key events. - "Failed to install browser network controls" / "Session with given id not found" → the daemon's CDP session is stale.
agent-browser close --all, then open again; one restart is the whole fix. Do not keep re-opening, and do not reach fordoctor --fix. This one matters more than it reads: installing the network controls is also what answers a proxy's auth challenge, so where a proxy is configured every request comes back 407 and no page loads — a healthy proxy looks like a dead internet. - CDP/connection errors or weird state →
agent-browser doctorto diagnose. Do not rundoctor --fix: its repairs are destructive — it purges browser state, which throws away a loginbrowser_sign_injust restored, and reinstalls Chrome, which replaces the browser this image pins. - More guides ship with the CLI:
agent-browser skills list,agent-browser skills get <name>;references/agent-browser-core.mdhas the full core reference.
See also
- Full core reference (snapshot/ref model, every command) →
references/agent-browser-core.md - Build and deploy a pod app →
lemma-builder/references/apps.md - Design its product experience → the
lemma-app-designskill - Test it systematically and judge a release → the
lemma-app-qaskill - Operate the pod (open apps, mint file URLs) → the
lemma-userskill - Inline live views over pod data → the
lemma-widgetskill
Version History
-
4e1f360
Current 2026-09-22 02:35
修复浏览器视图源允许列表逻辑,现基于cors_origins配置;优化代理认证处理及错误日志诊断能力。
- 8999990 2026-09-08 20:51
-
8b52627
2026-08-12 11:07
重构沙箱配置,统一使用 WORKSPACE_* 环境变量替代 AGENTBOX_*,并增强沙箱镜像复用时的图像一致性校验以防止静默失败。
- e2e8815 2026-08-02 23:38
- 0b61713 2026-07-19 09:57


