Skip to content

v0.32.0

Latest

Choose a tag to compare

@github-actions github-actions released this 13 Oct 03:42
· 3 commits to main since this release

v0.32.0 — cookie integrations log themselves back in, and agent browser tabs can no longer reach loopback

Cookie-auth integrations (internal tools you log into with SSO or a password) used to die quietly. The session cookie expired, every tool call failed with "reconnect in the portal", and someone had to open the portal and log in again. This release adds auto-reconnect. A plugin declares how to tell a dead session from a live one, and the steps to log back in. When a session dies, workbench runs those steps in the user's own browser, captures fresh cookies and retries the call. It is the cookie equivalent of an OAuth refresh on 401. SSO logins usually need only one click, because the identity provider's session in the browser profile outlives the app's. Password logins are filled from the user's vault.

Hardening this surfaced an older problem. Every user's chromium listens for DevTools on a loopback port, and an agent's browser tab could reach it, along with any other loopback service. A tab could list or close another user's tabs. Agent tabs can no longer reach loopback at all, and this is enforced inside the browser on every request, not only in browser_navigate.

Ships as a release candidate first: it touches auth (vault credentials typed by the server, new portal-session-only routes), the browser and how requests reach it (a network guard on every agent tab, a new DevTools origin, a new forwarding timeout), and chromium's launch flags.

Features

  • Cookie auth: auto-reconnect. A cookie plugin's manifest can now declare session and reconnect.

    • session sets which ctx.http response means "dead" (status codes, optionally a redirect to the login page). It can also name a probe request that proves the session is alive.
    • reconnect lists declarative login steps: goto, click, fill, press, waitFor and waitUrl. It can also declare credential slots, such as username and password.

    When a dead response comes back, the server first checks whether the browser profile still holds a live session, which needs no typing. Only if it doesn't does it run the recipe in a hidden tab, then verify, store the cookies and retry the request once. The execute_tools connected-check revives a session that died overnight, so the first call of the day works. Plugins without these blocks behave exactly as before. See the plugin guide and the finding.

  • Cookie auth: the credential stays out of reach. Vault values are typed by the server, never handed to the page or the agent.

    • Isolated world. The value goes to one element through Runtime.callFunctionOn in a CDP isolated world, so page scripts cannot patch the check or intercept the call.
    • Exact host and secure page. The value is delivered only when the page's host exactly matches the app's domain or a declared allowHosts entry. The page must also be a secure context (https, or http on loopback), and the element must belong to the page that was checked.
    • Private tab. The recipe runs in a tab that browser_tabs, every browser_* tool and the portal live view cannot see or reach.
    • Never echoed back. The value never appears in errors, logs, audit rows or tool results.
    • Not saved by chromium. The password manager is turned off in spawned profiles, so chromium never saves it either.
  • Cookie auth: login attempts are limited.

    • Only one recipe runs per user and app at a time.
    • After a recipe runs, whether it succeeded or failed, another can't start for 10 minutes. That is at most six password attempts an hour, below typical lockout thresholds.
    • A run has a hard deadline. Any step still in flight when it expires is aborted before it can type anything.
    • Only a portal session can lift the cooldown: a manual reconnect, or saving new bindings. An agent's API key cannot, not even by deleting and re-importing the connection.
    • An API-key disconnect inside the window answers 409. Disconnecting from the portal always works.
  • Portal: Auto-reconnect panel. A cookie app with a recipe now shows an Auto-reconnect panel on its detail page.

    • Password logins: the user picks which vault entries fill which slots.
    • SSO logins: nothing to set up.
    • Status line: shows the last outcome, such as "Auto-reconnected 2h ago" or "Auto-reconnect failed (step 3: SELECTOR_NOT_FOUND) — reconnect manually".
    • Who can bind: PUT /api/connections/:integration/reconnect accepts a portal session only, never an API key or OAuth bearer.
  • Cluster: recipe apps run where the browser lives. Under CLUSTER_ENABLED, tool calls on apps with a recipe are routed to the replica that owns the user's chromium, the same way browser_* calls already are. The /mcp handler moved into src/mcp/route.ts; its behaviour is unchanged.

  • Browser: BROWSER_LOOPBACK_ALLOW_PORTS. A comma-separated list of loopback ports that agent tabs may open, for local development (for example 8080). A live chromium debug port, the server's own PORT and its internal host stay refused even if listed. No chromium is ever started on a listed port. Don't list a port that proxies to the workbench server, such as the portal's Vite dev server on 5173. See the browser guide and environment.

  • Server: not crawlable. /robots.txt now disallows everything, every response carries X-Robots-Tag: noindex, nofollow, noarchive, nosnippet, noimageindex, and the portal shell has a robots noindex meta tag. The workbench is a private console and API, so nothing should be indexed. Link unfurlers such as Slack still fetch pages.

Fixes

  • Browser: agent tabs cannot reach loopback, on any port. Each user's chromium listens for DevTools on its own 127.0.0.1 port. Before this release, an agent tab could load /json/list for any user's chromium and see their tabs, or call /json/close on them. That worked through browser_navigate, a script setting location, a redirect, a popup, an iframe, a worker or a plain <img>.

    • Checked inside the browser. Every agent tab's requests are now checked in the browser, and anything resolving to loopback, unspecified addresses or the server's internal host is refused. That includes redirect hops, popups, iframes, and dedicated, shared and service workers.
    • Fails closed. The guard is in place before the session is handed out. On any doubt it refuses the request: a DNS error or timeout, an unparsable URL, a target it couldn't guard, a hostname over 253 characters, an unreadable /etc/hosts.
    • At least as strict as chromium's own resolver. It checks both IPv4 and IPv6 answers, honours /etc/hosts and search domains, and refuses single-label names.
    • Each user's browser gets its own DNS budget, so one user's page cannot slow down or block another's.
    • DevTools origin. DevTools now accepts WebSocket connections only from an origin no page can have.
    • Behaviour change: agent tabs can no longer open localhost, hostnames that exist only in /etc/hosts, or Kubernetes short service names (use the FQDN). Use BROWSER_LOOPBACK_ALLOW_PORTS for local dev servers.
  • Browser: Enter now submits forms. pressKey sent Enter as a raw key-down with no text, so chromium never fired the keypress that submits a form. This affected browser_key as well as recipes. Enter now carries \r. Chords such as ctrl+Enter are unchanged.

  • Browser: no Keychain prompts on macOS. Chromium launches with --use-mock-keychain --password-store=basic. Local runs no longer ask for the login password. The Linux image already used the basic store, so nothing changes there.

  • Cluster: a slow forward no longer runs a tool twice. A forwarded call that timed out after being sent used to fall back to running locally as well. Now:

    • It answers 504 instead. Only a connection error before sending falls back.
    • Recipe apps get 150 s to cover a login. Other forwarded calls keep 30 s.
  • Browser audio: the uplink answers at once and heartbeats. The uplink POST used to send its response only when the call's body ended, so a proxy that waits for response headers killed every call at its timer. Behind Cloudflare that was a 524 about 100 s after the uplink opened, and the voice client saw the call as lost. The uplink now answers 200 (application/json) as soon as it is accepted, writes a newline every 20 s, and ends with {"played_ms"} as before; the body is still one valid JSON document. A refused uplink still gets its 409, 404 or 415 before any headers. See the finding.

Known gaps

  • Plain WebSockets. WebSockets from agent tabs to loopback services other than DevTools are not blocked. The browser's request interception does not see WebSocket handshakes.
  • DNS rebinding. A hostname whose answer flips from public to loopback between the guard's lookup and chromium's own can still get through.
  • Recipe tab is unguarded. The private recipe tab runs a trusted plugin recipe and is not covered by the loopback guard. Agents cannot reach it.

Commits

  • fix(browser-audio): answer the uplink at once and heartbeat, so a proxy waiting for headers keeps the call (f6a0d8c)
  • feat(shared): cookie auth session + reconnect recipe manifest contract (39917d1)
  • feat(cookie): per-connection reconnect state in connections.config (192ce44)
  • feat(cookie): single-flight reconnect runner with cooldown and host guard (1620a2a)
  • feat(cookie): reconnect and retry on a dead-session response (e3983f5)
  • feat(cookie): route recipe integrations to the chromium-owning replica (e44405d)
  • feat(api): auto-reconnect credential bindings and status (0a63900)
  • feat(portal): auto-reconnect credential bindings and status on app detail (20bf1fa)
  • fix(cookie): deliver fill in an isolated world with exact host match (488af7f)
  • fix(reconnect): run recipes in a private tab no browser tool can reach (ff34f13)
  • fix(reconnect): overnight expiry, retry cooldown, no duplicate run on forward timeout (1c1a1f6)
  • fix(reconnect): cooldown survives delete + import, and a recipe success holds it too (f091eb3)
  • fix(reconnect): deliver a credential only to a secure context, into the checked document (77b8d4f)
  • fix(reconnect): a fast-path success can no longer shorten the recipe window (6b1415b)
  • fix(browser): agent tabs cannot reach loopback on any port (cross-tenant) (fade34f)
  • fix(browser): network guard fails closed; workers, DNS, WebSocket origin, localhost allow-list (a761811)
  • fix(browser): per-session DNS budget for the network guard, every wait bounded (834d94d)
  • fix(browser): guard verdict at least as strict as chromium's resolver (72f5512)
  • fix(browser): no ReDoS on hostile hosts; unreadable system files fail closed (5bb5da5)
  • fix(browser): Enter key carries text so it submits forms (53d8075)
  • fix(browser): spawn chromium with mock keychain and basic password store (5c999a4)

Full diff: v0.31.0-rc.4...v0.32.0