Skip to content

fix(tools): reuse supervisor sessions for browser_cdp target_id - #32950

Open
LeonSGP43 wants to merge 1 commit into
NousResearch:mainfrom
LeonSGP43:fix/32685-browser-cdp-supervisor
Open

fix(tools): reuse supervisor sessions for browser_cdp target_id#32950
LeonSGP43 wants to merge 1 commit into
NousResearch:mainfrom
LeonSGP43:fix/32685-browser-cdp-supervisor

Conversation

@LeonSGP43

Copy link
Copy Markdown
Contributor

Summary

  • reuse the live browser supervisor session when browser_cdp is called with a target_id that already belongs to the connected supervisor
  • track the top-level page target id in CDPSupervisor and resolve target ids back to live session ids before falling back to the legacy stateless attach flow
  • add a regression test covering browser_cdp(target_id=...) against the supervisor-backed path

Closes #32685.

Verification

  • uv run --frozen --extra dev pytest tests/tools/test_browser_cdp_tool.py tests/tools/test_browser_supervisor.py -q
  • source /Users/leongong/Desktop/LeonProjects/worktrees/hermes-agent/.base/.venv/bin/activate && ruff check tools/browser_cdp_tool.py tools/browser_supervisor.py tests/tools/test_browser_supervisor.py
  • git diff --check

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists tool/browser Browser automation (CDP, Playwright) labels May 27, 2026
@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for tackling the persistent-CDP-session path. The reported stateless behavior still exists on current main: browser_cdp opens a per-call WebSocket at tools/browser_cdp_tool.py:206 and target attachment is performed on that same connection at :217-260.

Problems

  • The new target_id return precedes the shared private-page guard at tools/browser_cdp_tool.py:484-490. Commit 47764f19f462c0b3a99865255f3b1dfae5098e74 fixed the same early-return bypass for frame_id; the target route needs that guard too.
  • #32685's documented Target.getTargets → target-ID workflow is not covered. The new route only runs after a target ID is provided, but the new test reads private sv._page_target_id. SupervisorSnapshot has no top-level target field (tools/browser_supervisor.py:255-279), and browser_snapshot exposes only snapshot.to_dict() (tools/browser_tool.py:2998-3007).
  • The new success payload should redact its result like the stateless payload does at tools/browser_cdp_tool.py:524-527.

Suggested changes

  • Preserve validation and run _browser_cdp_private_guard before target routing; add the corresponding private-page regression test.
  • Provide and test a public supervisor-backed target-discovery path, then use its returned target ID in the E2E test.
  • Update website/docs/user-guide/features/browser.md:549, which currently says each non-frame_id call is stateless.

Automated hermes-sweeper review.

Comment thread tools/browser_cdp_tool.py
@@ -340,6 +405,16 @@ def browser_cdp(
params=params,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This early return is before current main's shared validation and _browser_cdp_private_guard (tools/browser_cdp_tool.py:448-490). A resolved supervisor target can therefore invoke a raw CDP read on a private current page, recreating the bypass fixed for frame_id in 47764f19. Run the shared guard before this route and add an equivalent target-ID private-page regression test.

@chrisyoung2005

Copy link
Copy Markdown

Verification from a production browserless deployment (Fedora 43, rootless podman, hermes v0.19.0, self-hosted browserless v2 / Chrome 149, tokenless BROWSER_CDP_URL=http://browserless:3000): the approach here fixes #32685's stateless-target failure for pure-CDP workflows — with target ids resolved against the live supervisor session, Target.getTargetsPage.navigate(target_id)Runtime.evaluate(target_id) chains become self-consistent, which they are not on main today (each call currently lands in a freshly-spawned browserless browser).

One edge case worth handling (or at least documenting) before merge: under browserless, the supervisor's websocket owns a different browser than the agent-browser CLI daemon that browser_navigate/browser_snapshot/browser_click drive — browserless spawns a private browser per connection, so the two paths can never see each other's pages. With this PR, a mixed workflow (browser_navigate, then browser_cdp(target_id=...) using a supervisor-resolved target) resolves to a live, persistent target — in a browser that never saw the navigation. Callers get coherent-looking but empty results instead of today's hard failure, which is arguably harder to debug. We hit exactly this shape via the browser_console supervisor fast path: document.title returned "" while the snapshot showed the fully-loaded page, and the agent misdiagnosed it as target-site bot detection.

A cheap guard would be: if the daemon-driven page's targetId does not appear in the supervisor's Target.getTargets, emit a warning in the tool result (split-brain browser detected) rather than silently serving supervisor-side state as canonical. Happy to test a revision against our deployment.

@chrisyoung2005

Copy link
Copy Markdown

@LeonSGP43 — this fix matters for every Browserless/BaaS deployment (we run one in production, verification above), and it'd be a shame to lose it to the conflict backlog: the branch is now ~2 months behind main (mergeable_state: dirty) and teknium1's July 13 review has three concrete asks outstanding (private-page guard on the target_id return path, public-API coverage of the Target.getTargets workflow instead of the sv._page_target_id test, redaction on the success payload).

If you're still on it — great, happy to re-verify a revision against our deployment. If you're unavailable, I'll re-file this rebased onto current main with the review feedback applied as a salvage PR (crediting this one, per house convention) early next week so the mechanism doesn't stall. Just say the word either way.

@chrisyoung2005

Copy link
Copy Markdown

Salvage PR is up as promised: #71743 — rebased onto current main with the July 13 review feedback applied (guard before target routing + regression test, public page_target_id discovery on SupervisorSnapshot instead of the private-attr test, redaction on the supervisor payload, browser.md update). Credit retained in the commit/PR. @LeonSGP43 if you come back to this, happy to hand it over or fold in anything I missed.

chrisyoung2005 added a commit to chrisyoung2005/hermes-agent that referenced this pull request Aug 20, 2026
NousResearch#32950)

browser_cdp opens a fresh CDP WebSocket per call. On Browserless-style
backends that spawn a private browser per connection, a target id from
one call is invalid by the next — multi-step Target.getTargets ->
target_id workflows always fail (NousResearch#32685).

Route target-scoped calls through the live CDPSupervisor session when
the target is already attached (top-level page, OOPIF frame, or
auto-attached child), falling back to the stateless attach otherwise.

Salvages NousResearch#32950 (credit: @LeonSGP43) rebased onto main with the review
feedback applied:

- the target route runs after the shared private-page guard, so it
  cannot become the sibling bypass fixed for frame_id in 47764f1
  (regression test included)
- public target discovery: SupervisorSnapshot now carries
  page_target_id (surfaced in browser_snapshot output via to_dict);
  the E2E test uses it instead of private supervisor attributes
- the supervisor-backed payload redacts its result via
  _redact_cdp_output like the stateless payload (regression test
  included)
- browser.md no longer claims every non-frame_id call is stateless
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/browser Browser automation (CDP, Playwright) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: browser_cdp opens stateless CDP connections, breaking Browserless/BaaS targets between tool calls

4 participants