Skip to content

Build1 publisher2 min readPublished

The weaker of agent-browser's two safety flags is the one that survives a restart

An end-to-end read of the browser-automation CLI found --pin-tab written to an fsynced sidecar file and re-read at startup, while --allowed-domains is seeded only from an environment variable that a reconnecting command never sets.

The Engineer · Build desk

Illustration accompanying The weaker of agent-browser's two safety flags is the one that survives a restart

What happened

  • A user report that a containment flag disappears silently when the daemon restarts prompted an end-to-end read of agent-browser, a browser-automation CLI built as a client plus a long-lived per-session daemon.
  • Inside a live daemon, --allowed-domains behaves as documented: a later command that omits the flag inherits the adopted filter, and a launch that fails puts the previous filter back.
  • The relaunched browser loses the WebRTC restriction as well, because restrict_webrtc is derived from the allowlist and the list is hashed into the launch hash.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure Reconnecting is the dangerous operation here: the session that comes back accepts any domain, and the check that refused a --state replay while a filter was active no longer fires.
  • constraint A caller cannot verify containment before it starts driving the browser, because the active allowlist is not among the fields the session-info handler returns.
  • cost Closing the gap costs one sidecar write and one load call at state construction, both already written in the same repository for the tab pin.
  • decision Anyone shipping a flag whose documented semantics are sticky has to choose where the baseline is stored, because a suite that mutates the flag in a live process will pass either way.

Run any command against a session whose daemon has exited, and the client spawns a replacement. The client exports AGENT_BROWSER_ALLOWED_DOMAINS only for the command doing the spawning, and only when that command carried the flag [5]. DaemonState::new reads the filter from that variable and nothing else, at actions.rs:696-701 [4]. A reconnect that does not repeat the flag therefore builds a daemon whose filter is None [8].

That leaves the files on disk. {session}.config is a fingerprint of other daemon options, and allowed_domains is not among the hashed fields [6]. The five remaining sidecars, .engine, .provider, .extensions, .port and .stream, are deleted at startup [7][17]. Whatever the previous filter was, the reconnect path cannot read it back [18].

The allowlist is not the only thing that goes. ensure_state_replay_supported_by_active_domain_filter refuses --state and storageState while a filter is active, and with None it permits them again [9]. restrict_webrtc is derived from the same list, and the list is hashed into the launch hash, so the relaunched browser comes back without the WebRTC restriction [10].

The repository already contains the fix, in the file for the other flag. --pin-tab is the weaker boundary, and it was made restart-proof with a sidecar file [11]. The atomic writes and the fsync went to the tab pin. The header comment on cli/src/native/tab_binding.rs says that once a session is created with --pin-tab, subsequent commands and daemon restarts keep the strict semantics without repeating the flag, and that because a lost or corrupt binding silently drops that safety boundary, writes are atomic (temp file + rename), owner-only, and fsynced, with both save and load reporting failures instead of swallowing them [12]. tab_binding::load runs when the daemon state is built, at actions.rs:683, and --allowed-domains has no equivalent load [13].

Tests do not catch this because every end-to-end test of the domain filter mutates the filter [15]. A test that changes the allowlist inside a live process exercises the half that works. The handler a client would poll reports session, engine, launch hash and the whole restore-* family, but not the active allowlist [14]. The symptom in the original report was exit code 0 and nothing printed [16].

This transfers to your own CLI if it has a per-session daemon, a flag documented as sticky until explicitly dropped, and a cold-start constructor that reads the boundary from somewhere only one command writes. Where does the state constructor get the boundary on a cold start, and can any command other than the spawning one set it? The post says it includes a five-question procedure for any daemon-shaped CLI; the published text breaks off before the questions [19].

What to watch

  • Whether agent-browser adds a tab_binding-style load for the allowlist, or extends the {session}.config hash to include allowed_domains.
  • Whether the session-info handler starts returning the active allowlist so a client can detect a dropped filter.
  • Whether the state-replay guard is changed to refuse when no filter has been recorded, instead of only when one is active.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories