Skip to content

Make browser tools safe for hosted sessions#166

Open
yyy900 wants to merge 10 commits into
mainfrom
browser-hosted-session-tools
Open

Make browser tools safe for hosted sessions#166
yyy900 wants to merge 10 commits into
mainfrom
browser-hosted-session-tools

Conversation

@yyy900

@yyy900 yyy900 commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • keep the current daemon/client co browser CLI mode intact
  • serialize BrowserAutomation public calls through one Playwright-owning worker thread
  • make _browser_is_usable() safe for daemon-side liveness checks from non-browser threads
  • add per-session tabs through bind_browser_session so hosted sessions do not fight over one page
  • add portable login state import/export, BROWSER_PROXY support, iframe-aware selector clicks, bounded screenshot payloads, and human_jitter
  • document the Browser tool changes

Verification

  • .venv/bin/python -m pytest tests/unit/test_browser_automation.py tests/unit/test_browser_session_pages.py tests/e2e/cli/test_browser_daemon.py -q
  • .venv/bin/python -m py_compile connectonion/useful_tools/browser_tools/browser.py connectonion/useful_plugins/human_jitter.py connectonion/useful_plugins/bind_browser_session.py connectonion/useful_plugins/init.py

Note: the daemon tests require AF_UNIX socket bind permissions and were run outside the sandbox.

yyy900 added 9 commits June 26, 2026 15:42
Playwright's sync API binds to the thread that started it. host() runs each
agent turn on an arbitrary threadpool thread, so a hosted agent's second turn
hit 'Cannot switch to a different thread', close() failed the same way while
still nulling out state, and the orphaned Chromium kept the profile locked.

Route every public BrowserAutomation method through a single worker thread:
playwright starts on it and every later call executes there, whatever thread
the server picked.
…ave_state)

seed_state=<path> injects an exported storage_state JSON's cookies into the
persistent context once, right after it is created and before any navigation,
so a deployed agent starts already signed in. launch_persistent_context() can't
take storage_state=, so the cookies are applied with add_cookies. save_state()
exports the current session to that portable JSON. Unset seed_state => no
injection, behaviour unchanged.
… params

click_element_by_selector already clicks through Playwright's input layer
(trusted, isTrusted=true) — the only thing it couldn't do was reach a
cross-origin iframe. Add frame_url_contains/frame_name to give it that, and
delete click_in_frame, which otherwise duplicated the selector-click protocol.
Drop _human_mouse_move (its only caller); pre-click motion realism lives in the
opt-in human_jitter plugin, not the click tool. Removes now-unused random/time
imports.
Each chat session drives its own tab in the shared (single-login) context so
concurrent sessions don't navigate over each other; the bind_browser_session
plugin routes calls by session_id. Tabs are reclaimed by idle-TTL and a max-tabs
backstop so a long-lived host doesn't leak. Re-encode screenshots over ~600KB as
JPEG so a single frame stays under the relay's 1MB cap.
- wait_for_manual_login returns immediately without an interactive TTY instead
  of blocking input() on the single shared browser worker thread (that thread
  serializes every session, so blocking it would freeze them all on a host)
- bound the per-session restore-URL map (LRU, _max_url_memory) so it cannot grow
  unbounded by session id on a long-lived shared host
- omit :port from BROWSER_PROXY server string when the URL has no port
- reframe iframe targeting, BROWSER_PROXY, and cursor-motion docs around
  legitimate use; drop CAPTCHA/anti-detection framing
- document save_state/seed_state output as a secret (gitignore, secret store)
- tests for the no-TTY guard and the bounded restore-URL map
@yyy900 yyy900 marked this pull request as ready for review June 26, 2026 07:15

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: abf5294099

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Context-level methods that must NOT auto-create a tab before they run:
# open_browser creates the tab itself after launching the context; close/save_state
# operate on the whole context, and a fresh tab right before teardown is wrong.
_NO_TAB_METHODS = {"open_browser", "close", "save_state"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent open_browser from closing other sessions

When a hosted BrowserAutomation already has a live tab for session A and session B calls the exposed open_browser tool first, keeping open_browser in _NO_TAB_METHODS means the wrapper does not create B's tab before the liveness check. _browser_is_usable() then evaluates self.page for B as None, so open_browser() treats the live shared context as stale and calls close(), tearing down A's tab/context. Let open_browser ensure the current session page or check context liveness before deciding the browser is stale.

Useful? React with 👍 / 👎.

Comment on lines +389 to +391
cookies = json.loads(Path(self._seed_state).read_text()).get("cookies", [])
if cookies:
self.browser.add_cookies(cookies)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore full storage state, not just cookies

For a state file produced by the new save_state(), Playwright includes origins with localStorage as well as cookies, but seed_state only reads and imports the cookies here. When a site keeps its auth token or required session data in localStorage, the hosted browser will start unauthenticated even though the exported state was supplied. Either replay the saved origins/localStorage into the context or make the export/import explicitly cookie-only.

Useful? React with 👍 / 👎.

A shared BrowserAutomation drives one tab per session. Three teardown paths could let
one session destroy every other session's tabs:
- a brand-new session's open_browser read its own (None) page as 'context unusable' and
  tore the shared context down — now it ensures its tab BEFORE judging usability
- open_browser(force=True) and close() always tore the whole context down — now a BOUND
  session recycles/closes only its OWN tab; the full teardown (extracted to _teardown())
  is reserved for an unbound caller (single-session CLI / context-manager exit)
The else-branch still does a FULL _teardown() when the context is genuinely dead.

Tests: a second session reuses the live context; force/close from a bound session leave
other sessions intact; an autouse fixture resets the session threadlocal per test.
wu-changxing pushed a commit that referenced this pull request Jul 7, 2026
…loses #168) (#186)

* browser: per-session tabs and hosted-session safety

Serialize BrowserAutomation's public calls through one Playwright-owning
worker thread so a shared instance is safe to call from any host() thread,
and give each session (chat panel) its own tab in the shared login context
so concurrent sessions stop navigating over one another. Reclaim idle tabs
by TTL and an LRU backstop, restoring a reclaimed session's last URL on next
use. Adds portable login state (save_state/seed_state), BROWSER_PROXY egress,
frame-aware selector clicks, a bounded screenshot payload, and the opt-in
human_jitter and bind_browser_session plugins.

Ported from #166 onto current main.

* browser: enable per-session isolation on the hosted co-ai agent

The per-session tab machinery only activates when bind_browser_session runs
before each tool, but no agent registered it — so on a co ai deploy every
chat panel still shared one tab, the exact bug the machinery fixes. Register
the plugin on create_coding_agent (the hosted browser agent) and assert it
stays wired.

* browser: replace Playwright with Patchright for driver-level stealth

Patchright is a stealth-patched, API-compatible drop-in for Playwright. It
removes the Runtime.enable / Console.enable CDP leaks and the navigator.webdriver
flag at the driver level — below where Chrome flags and page init scripts operate,
which is exactly what modern bot-detection (Cloudflare, DataDome, Kasada) reads.

- swap the import; the sync API is identical so only the import line changes
- drop the navigator.webdriver add_init_script shim (Patchright hides it, and
  init-script injection is itself a timing-detectable tell)
- trim browser_config.py from the 53-flag browser-use mirror to only
  run-environment flags; Patchright owns anti-detection now, and its docs warn
  that over-configuring launch args can defeat the patches. Removes the standing
  browser-use sync burden.
- rename PLAYWRIGHT_AVAILABLE -> BROWSER_AVAILABLE (it no longer gates Playwright)
- update pyproject, the browser template requirements, and the co-ai Dockerfile
  (patchright install --with-deps chrome)

Verified live: BrowserAutomation launches real Chrome via patchright, navigates,
and navigator.webdriver reads false with no init script. Standalone raw-Playwright
demos under examples/ are left as-is (not the SDK browser path).

* docs(browser): update install to patchright

The built-in browser tools ship Patchright now; the doc's pip/install
commands pointed users at Playwright.

* browser: instance-owned session binding + documented tab TTL/cap kwargs

Cleanups on the per-session tab machinery, mechanism unchanged (the
opt-in bind_browser_session plugin stays the way a hosted agent enables
isolation on a shared browser):

- The session binding moves off a module-level threading.local onto the
  BrowserAutomation instance (per instance, per thread). No shared
  module state between browser instances, and the test suites drop
  their autouse reset fixtures — a fresh instance starts unbound.
- The plugin's handler is renamed _bind_browser_session so it no longer
  shadows the browser method (_bind_session) it calls; docstrings spell
  out the handler-vs-method split and who registers the plugin.
- tab_idle_ttl / max_tabs become documented constructor kwargs so
  memory-tight deployments stop poking private attributes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cli: add hosted-browser template — production-shaped site-automation agent

The shape proven by the live LinkedIn agent, packaged as 'co init/create
--template hosted-browser': one shared Patchright browser (persistent
login profile, headed under Xvfb) served to many sessions via a
host(create_agent) factory with bind_browser_session giving each session
its own tab; site workflows as .co/skills; credentials and 2FA through
ask_user; CAPTCHA = report-and-stop; and the memory bounds that keep a
small deploy box alive (tab TTL/cap kwargs, a browser idle reaper
class). Local one-shot mode (python agent.py "task") keeps
wait_for_manual_login for first login; the hosted path removes it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* browser: replace _NO_TAB_METHODS name set with @_no_auto_tab markers

The name set was stringly-typed and far from the methods it exempted:
rename open_browser/close/save_state and the set silently stops
matching, so the method starts auto-creating tabs with no error. The
marker sits on the method itself and survives renames.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cli/docs: finish the playwright -> patchright / browser rename

Two kinds of stragglers left behind by earlier renames:

- The interactive create menu still offered a 'playwright' template that
  has no templates/playwright/ directory (renamed to 'browser' long ago),
  so picking it always died with "Template not found". The menu now
  lists the real templates (browser, hosted-browser) and the --template
  help text / module notes on init/create stop advertising 'playwright'.
- User-facing install instructions for the SDK browser path still said
  'pip install playwright && playwright install chromium' while the code
  and its error message moved to patchright + chrome. Docs now match.

Raw-Playwright tutorial examples (custom stateful tools in docs/concepts,
best-practices) and the sync_playwright/PLAYWRIGHT_BROWSERS_PATH API
names stay: patchright exports Playwright's names on purpose.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant