Skip to the content.

Browser sessions

A stage can drive a real browser that stays logged in. The browser tool is agent-browser. The agent uses it through the bash tool and the bundled browser skill.

Install it first: npm i -g agent-browser && agent-browser install.

Concepts

Profile folder: $STAGEFLOW_HOME/browser/<scope>/<profile>/ (default ~/.stageflow/browser/local/<profile>/), owner-only permissions. It is never in the project checkout.

YAML

A stage declares browser: with profile, headed, allow_domains, login_url, and check. The agent-browser requirement is added for you. Field reference: YAML catalog: Stage browser. The login check and human login stage are in Login check stage and Human login stage.

A browser field with no profile gives a throw-away browser. It takes no lease, shares no login, and its profile is deleted when it closes.

Login pattern

Check, human login, then work. The work can be one stage or several parallel stages.

  1. check has browser.check. The Host opens the check page with the profile and computes { logged_in, url }. The stage reports it.
  2. login runs only when logged_in is false. The Host opens a browser the operator can see (a window on the Host's screen, or the live view in the console). The stage asks the operator with a confirm gate. After the operator confirms, the Host checks again (verify: browser_login). A wrong confirm repeats the stage.
  3. work stages use the saved login. Parallel work stages share it in their own tabs.

Use the join topology. Only the check to login edge has an if on logged_in. The edges check to work and login to work have no if (for every work stage). If check to work also has an if, work is silently skipped when the login stage runs. The work input schema must fit the output of both parents.

Runnable walkthrough: examples/browser-session/. Fixtures: tests/fixtures/pipelines/browser-login-check.pipeline.yaml and browser-human-login.pipeline.yaml.

Shared browser and tabs {#shared-browser-and-tabs}

A profile is leased to one run, from the first stage that needs it until the run ends. All stages of that run share the lease and one Chrome. Stages of the same run never block each other, including parallel stages.

The example has two parallel work stages that share one login: examples/browser-session/.

Hand-over between stages

The shared cookies and login are the hand-over. A later stage is logged in with no extra step. Passing one exact open page to the next stage is out of scope. To hand over a page, put its URL in the envelope and open it in the next stage.

Queueing and lease

A stage of a different run that needs the profile waits until the holder run is success, failed, cancelled, or abandoned. It shows a "waiting for browser profile" message that names the holder run. The wait covers the whole run, not one stage. A waiting stage does not use one of its run's active stage slots. The lease stays held while a gate waits for the operator. Leases of dead runs are reclaimed at Host start and while waiting. Waiting has no time limit.

Stages with no profile take no lease and can run in parallel.

Teardown and orphan sweep

When a stage succeeds, fails, times out, or is cancelled, the Host closes that stage's tab and its session. The shared browser and the other stages stay up. When the run ends, fails, is cancelled, or is abandoned, the Host closes the shared browser gracefully and releases the lease. It does not rely on the stage worker, which can be killed. The browser stays open while a stage waits at a gate.

When the Host starts, it closes browsers and stage sessions whose run no longer exists and removes their leftover socket files.

If the shared browser dies in the middle of a run, the Host restarts it on the next stage start and points the stage sessions at the new address. Pages open in the dead browser are lost. Cookies are not, because they are in the profile.

CDP port risk

The shared browser listens on a CDP port bound to 127.0.0.1 with no token. While a run is active, any local process can drive that browser. This is acceptable on a single-user laptop. A hosted multi-user service must run the browser in a sandbox and must not use this local port.

Headed and headless

Headed is the default. The operator can watch, and all stages that share a profile look like one device to the site. Set headed: false per stage for CI or background work. Use one mode for all stages that share a profile, because headless Chrome reports a different user agent. A human login stage always gets a visible browser, whatever headed says: a window on a Host with a screen, or the live view on a Host without one.

To watch any running browser stage, use Watch browser on the stage in the run page. It is a read-only live view: the server refuses input from it, it follows popups, closing it does not affect the stage, and it ends when the stage ends.

On Linux the Host needs DISPLAY or WAYLAND_DISPLAY, or Xvfb on PATH (see Linux hosts with no screen), for a headed browser. With none of them, a stage that did not ask for a login window quietly runs headless. macOS and Windows always count as having a screen.

Live view {#live-view}

Live view lets the operator log in to a browser that runs on a Host with no screen, or on another machine. The console shows the Host's browser as a picture on a canvas in the gate, and sends the operator's clicks and keys back to it. Nothing is installed on the operator's side.

What each Host shows

The Host decides how a human login gate is handed over. YAML does not choose it, and launch options are not set in YAML.

Host What the operator sees at the gate
Desktop (macOS, Windows, Linux with DISPLAY or WAYLAND_DISPLAY) A browser window on the Host's screen (local_window). Nothing changes: the console only asks to confirm.
Linux with no screen, started with the console (sf ui or sf mcp) The live view in the console gate (live_view). The browser runs without a window on the Host.
No screen and no live view (for example a run started only from the CLI, or a host that cannot stream) The login stage fails before the agent starts: "A visible browser is needed for login, but this Host has no screen and no live view." In Docker the message adds a hint to log in on a machine with a screen first.

The live view needs the process that serves the console routes: sf ui, or the MCP service (sf mcp) that serves the console API. A CLI run without a console never offers it. The gate carries a stable path (/api/runs/<run>/stages/<stage>/live-view), never a token; the console asks the Host for a short-lived ticket when the operator opens the gate.

Using it during a login gate

  1. Open the run in the console. The gate shows a line "Log in in the browser view below, then confirm.", the page address, and the browser picture.
  2. Click in the picture to focus a field, then type. Tab, Enter, Backspace, the mouse wheel and right click work. Paste with the keyboard shortcut. Long text is sent in small batches, so a long paste takes a moment.
  3. If the site opens a popup (for example "Sign in with a provider"), the view follows the popup. When the popup closes, the view returns to the page that opened it. The opener still receives the result.
  4. When the page shows you are logged in, press Accept. The Host then re-checks the login itself (verify: browser_login). If it still sees a logged-out page, the stage runs again with a new gate, and the live view works there too.
  5. Accepting, rejecting, cancelling or ending the run closes the live view and revokes its ticket. Input sent after that is refused.

If typing and clicking do nothing (a known issue after submitting some login forms), open "Page seems stuck?" under the picture and press Reopen browser tab. The Host opens the same address in a fresh tab of the same browser (you stay signed in; page state held only in the old tab is lost), moves the view to it and closes the old tab. It can be pressed once every couple of seconds.

The live view is interactive by default. The Host can also issue a view-only ticket (read access) that shows the page but refuses input. The console gate always asks for the interactive one.

Page dialogs {#page-dialogs}

JavaScript dialogs are not drawn in the picture, and the page stays blocked until one is answered. The console shows them as an overlay on top of the picture, as plain text (the page's text is never treated as HTML).

Limits

Everything typed in the live view, including passwords, goes to the Host's browser only. The Host never logs, stores or audits input or frames.

Docker and hosts with no screen

A human login needs a browser the operator can see. A Host with no screen shows the live view when it serves the console. A Host with no screen and no live view, such as a CLI-only run, fails the login stage before the agent starts with a clear message. Log in on a machine with a screen first, or use a headless check only.

Host config

Block sites for every browser stage in $STAGEFLOW_HOME/config.yaml:

browser:
  blocked_sites:
    - example.com

A stage whose allow_domains, check URLs or login_url touch a blocked site (or a subdomain) fails when the stage starts, before the browser opens. sf browser check and sf browser login refuse a blocked --url the same way (exit code 1, JSON code: "browser_site_blocked"). YAML cannot override the list.

Permission requests are denied by default

Every browser the Host launches (the shared browser of a profile, profile-less stages, sf browser login) starts with Chrome's --deny-permission-prompts switch. A page that asks for location, notifications, camera, microphone or clipboard read gets "denied" at once instead of a prompt nobody can see (a headed browser on a Host with no screen, or the live view, does not draw native prompts, so the page would wait forever). This applies to every browser stage, not only login stages.

Browser sign-in and password-save prompts are not drawn in the live view either. Stageflow does not add a switch for them: Chrome has no launch switch for its password manager (it is a profile preference), and agent-browser already starts Chrome with --password-store=basic and --disable-sync. See the help text on a live view gate for what operators should do.

Host launch options

Optional keys tune how the Host starts Chrome and handles live view dialogs. launch_args and executable_path are off by default.

browser:
  launch_args:
    - --no-sandbox
    - --use-gl=angle
  executable_path: /usr/bin/chromium
  dialog_timeout_seconds: 60

launch_args come after the Host's own switches (--deny-permission-prompts; listing it again has no effect). They apply to every browser the Host launches (the shared browser of a profile, profile-less stages, sf browser login) and are computed once per stage, so every command and resume uses the same values. They are never read from the environment and cannot be set in YAML: a stage browser: block with launch_args, args, executable_path, executable or display fails to load.

Container browser host {#container-browser-host}

Development only; there is no production orchestrator yet. With browser.host: container the shared browser of each (run, profile) runs in its own container instead of on the Host. The container holds Chromium, a virtual display and the profile volume, nothing else. It always has a virtual display, so human login stages use the live view.

browser:
  host: container
  container:
    image: stageflow-browser-sandbox:local
    shm_size: 1g
    memory: 1g
    pids_limit: 512

Linux hosts with no screen

A Linux Host with no DISPLAY or WAYLAND_DISPLAY but with Xvfb on PATH runs browsers headed on a private virtual display that agent-browser starts itself. Nothing is shown on the Host, but the browser behaves like a headed one (same user agent as a desktop). A stage that sets headed: false still runs headless. Without Xvfb the Host falls back to headless as before. For Docker, the browser image recipe installs Xvfb, Chromium and agent-browser.

Redaction and audit

Stages with a browser field get extra pattern redaction for cookie, storage, header, and token shapes in stream logs, live activity, and envelopes. Other stages are not changed. Pattern redaction is best effort. The skill also tells the agent never to print cookies or storage. Run state and envelopes hold the profile name only, never a path or a value.

The Host appends audit records to $STAGEFLOW_HOME/browser/audit.jsonl: profile created, profile used by a run and stage, and profile deleted. Records hold names and ids only. For profile stages with allow_domains it also records navigation_outside_allowlist (host only, once per host) and allowlist_unverified (stage activity log unreadable); see the soft allowlist below.

Allowlist caveat

allow_domains is enforced by agent-browser only for stages with no profile. agent-browser rejects its native allowlist together with a profile. For a stage with a profile, allow_domains is soft: it goes into the stage prompt and the agent is told to obey it. The Host does not enforce it. When the stage ends it audits explicit agent-browser open|goto|navigate|tab new <url> commands from the stage's tool activity and records hosts outside the list (navigation_outside_allowlist). Redirects, link clicks and in-page navigation are not seen. It is not a security boundary for profile stages. Use browser.blocked_sites and your own review for hard limits.

Encryption

The Chrome profile is protected by owner-only folder permissions and by Chrome's use of the OS keychain (on macOS the "Chrome Safe Storage" key). The agent-browser encryption key is not used. It only protects agent-browser's own restore and state files, and Stageflow does not write those. On Linux, Chrome's basic password store is weak. Use disk encryption for the home directory.

What is not built

Contributor detail and the full list: Browser sessions internals.

Multi-tenant readiness

Hosted use is not built. The seams are in place: the owner scope (set by the Host), the profile store (all path code), the profile lease, the browser host (starts the shared browser and returns the CDP address and session a stage needs; a remote host can supply the address instead; it reports what it can do, such as screen and live view), and the handoff kind in the gate payload. Each can be replaced without changing YAML.

Sites can forbid automation

Many sites forbid bots and scraping in their terms and can limit or close accounts that break them. For example, the LinkedIn User Agreement prohibits bots and scraping, and LinkedIn can restrict accounts. If you point a stage at such a site, keep volume low, stay read-only, and accept the risk yourself. Stageflow does not judge a site's terms. The example in this repo uses a neutral local site on purpose.

Cookies across runs

Within a run, every cookie, session-only or persistent, is shared by all stages, because the shared browser stays open. When the run ends the Host closes the browser gracefully so Chrome writes its profile. A real-Chrome test (agent-browser 0.38.2) found that a graceful close and reopen of the profile kept both the persistent and the session-only fixture cookie. Do not rely on this across runs. Chrome and agent-browser may drop session-only cookies in other versions or after a crash or hard kill, and a site can expire the login by itself. Persistent cookies are the safe choice. Check the cookie type of your site. LinkedIn's main login cookie is persistent.

For contributors

How the feature is built, its file layout, invariants, and test guide: Browser sessions internals.