Skip to content

feat(browser): consent-gated real Chromium profile for local browsing + local_browser arg - #95520

Closed
teknium1 wants to merge 1 commit into
mainfrom
browser-real-profile
Closed

teknium1 wants to merge 1 commit into
mainfrom
browser-real-profile

Conversation

@teknium1

Copy link
Copy Markdown
Collaborator

Adds a backend-independent, consent-gated way to drive the user's real default-Chromium profile (their logins/cookies) for local browser use.

What the user asked for

  1. A GUI/config toggle (backend-independent, in the Browser settings section). When on, local Chromium browsing always uses the default Chromium browser's real profile. Fails if the default browser is non-Chromium (e.g. Firefox).
  2. A consent-gated local_browser: bool tool arg so the agent can force a local real-profile session even under a cloud backend — same spirit as the existing localhost/LAN→local fallback.
  3. Chromium-family only: Chrome, Edge, Brave, Chromium.

How it works

The local backend (agent-browser) natively supports --profile <user-data-dir> (persists real cookies/logins) and --executable-path. So this needs no CDP chrome://inspect toggle and no cua-driver dependency — a local launch just points at the real profile dir + browser binary.

  • browser.use_real_profile (new config, default false). Consent gate. When on, every local Chromium launch — built-in tools, or a local sidecar spawned under a cloud backend — appends --profile <real-default-profile> + --executable-path <binary>.
  • Default-browser + profile resolution (hermes_cli/browser_connect.py): detect_default_chromium() (Windows UserChoice ProgId, macOS LaunchServices, Linux xdg-settings), real_profile_data_dir(), chromium_executable() for all four browsers, per-OS. Non-Chromium default → resolves to None → fail closed with a message naming the browser and the toggle.
  • local_browser arg on browser_navigate: routes through the existing ::local sidecar mechanism (same one used for private-URL→local today), but only when consent is granted. Requested without consent → ignored, with a note in the result (never silently routes the real profile). A CDP override or Camofox mode still owns the session.
  • Desktop GUI: new backend-independent Browser settings section (Globe icon) with the real-profile toggle plus the two existing browser routing keys, moved out of Safety for cohesion. Renders as a Switch from the boolean config value; label + risk-describing description added to FIELD_LABELS/FIELD_DESCRIPTIONS.

Scope / honest boundaries

  • Full real-profile support lands for the built-in browser tools (both requirements). Browser Use CLI mode (browser_exec) attaches only via CDP, and real-profile-over-CDP hits Chrome ≥136's default-user-data-dir remote-debugging block — which requires the chrome://inspect-toggle route. That's deliberately deferred (documented follow-up); this PR does not claim to cover browser_exec.
  • Security framing (agreed): a consent-gated local convenience, not a hardened isolation boundary. The description says so.
  • Profile-lock note (risk): Chromium refuses a second process on a user-data-dir already open. If the user's browser is running, --profile on the real dir may fail; agent-browser surfaces that. We do not silently fork the profile (avoids the token-fork class from feat(gateway): share_auth on profiles.create + MCP servers in profiles.describe/configure #85963).

Files

  • hermes_cli/config_defaults.pybrowser.use_real_profile (default false).
  • hermes_cli/browser_connect.py — resolvers + default-browser detection (per-OS, POSIX/NT path-correct via posixpath/ntpath so an explicit system arg resolves right on any host).
  • tools/browser_tool.py_use_real_profile(), _real_profile_launch_args(), local-launch injection (fail-closed), local_browser on _navigation_session_key + browser_navigate + schema + dispatch, consent-miss note.
  • apps/desktop/src/app/settings/constants.ts — Browser section + labels/description.
  • tests/tools/test_browser_real_profile.py — 12 tests.

Verification

  • tests/tools/test_browser_real_profile.py: 12 passed (resolvers per-OS, consent on/off, non-Chromium fail-closed, missing-dir fail-closed, local_browser routing with/without consent, CDP-override precedence).
  • tests/tools/test_browser_hybrid_routing.py: 10 passed — no regression from the _navigation_session_key signature change (new param defaulted).
  • Desktop field-copy transform validated (browser.use_real_profilebrowser.useRealProfile, matches the added label/description entries). Full desktop vitest not run here (node_modules not installed in the worktree); CI runs it.
  • Caught + fixed a real cross-platform path bug during testing: the resolver now builds target-OS-correct separators instead of the host's.

Not run: broad suite (per maintainer preference — targeted checks only).

@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on cf7d308 — feat(browser): consent-gated real default-Chromium profile f

❌ Job failures

Python lints / Windows footguns (blocking) · View job

Job Python lints / Windows footguns (blocking) failed.


Python tests / Run tests · View job

Job Python tests / Run tests failed.


⚠️ Warnings

OSV vulnerability scan · View job

7 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


debug info

CI timings

CI timings · View report · View job

Wall time 4m21s vs 3m53s (+12.0%). 8 job(s) slower, 5 faster,

  • Python tests / Run tests: +25.0s
  • OS-specific tests / macOS-only tests: +25.0s
  • OS-specific tests / Windows-only tests: +15.0s
  • Python tests / e2e: +7.0s
  • OSV scan / Emit review status: -5.0s

@alt-glitch alt-glitch added type/feature New feature or request comp/desktop Electron desktop app (apps/desktop/*) comp/cli CLI entry point, hermes_cli/, setup wizard tool/browser Browser automation (CDP, Playwright) area/config Config system, migrations, profiles P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 26, 2026
@unsupportedpastels

unsupportedpastels commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Review notes — real Chromium profile browsing

Update: see my follow-up comment with empirical testing results — item 1 (Chrome ≥136) is now confirmed on branded Chrome 152 stable (silent hang), items 2 and 5 reproduced, and the happy path verified end to end on Chromium.

Really like the direction here — default-off, honest risk description, reusing the existing sidecar routing. A few things we noticed that might save a follow-up fix cycle, roughly in order of impact:

Worth verifying before relying on it

  1. Chrome ≥136 may block the whole launch path. Running the real Chrome binary on the real default user-data-dir under automation is the exact configuration Chrome 136+ refuses (the same block the PR defers for the CDP route). One end-to-end launch on a current Chrome would settle whether the built-in path actually escapes it.
  2. The browser-already-open case. Chromium's profile lock means the most common state for someone who just flipped the toggle — browser running — fails on first navigation. A pre-flight lock check with a "close your browser first" message would make the experience much friendlier. (Two concurrent agent tasks will also contend for the same lock.)
  3. Path-mode --profile writes the live directory directly (agent-browser only snapshots in profile-name mode), so successful launches write straight into the user's primary profile. Might be worth a note in the description if that's intended.

Bugs we can reproduce

  1. macOS detection can pick the wrong browser. _detect_default_darwin matches a bundle-id and "https" anywhere in the whole LSHandlers dump, not in the same entry — a fixture with Safari owning https and Chrome registered only for ftp returns "chrome". The "first installed Chromium wins" fallback then drives a non-default browser's profile, which is a bit beyond what the user consented to. Parsing per-handler and dropping the fallback would restore the fail-closed behavior the docstring promises. (Windows and Linux detection both look correct.)
  2. Missing executable falls through open. When chromium_executable() finds nothing, _real_profile_launch_args continues with --profile alone, so the bundled Chromium opens the real profile — risking a one-way profile migration, and on Windows the cookies won't decrypt anyway (app-bound encryption). Failing closed here, like the other two branches, seems safer.
  3. Toggle changes need a restart to take effect. _use_real_profile() caches for the process lifetime and the desktop save doesn't invalidate it (reproduced: one disk read ever). Off doesn't revoke, on may silently no-op. Since it's a consent switch, re-reading the boolean per launch feels worth the tiny cost.
  4. Detection runs per command. _real_profile_launch_args() sits in _run_browser_command, so every click/snapshot shells out to xdg-settings/defaults (5s timeout), and a fail-closed error also blocks close/cleanup. Caching per session would fix both.

Security thoughts

The agreed framing — consent-gated convenience, not a hardened isolation boundary — is stated clearly in the PR, and the inherent real-profile risks are squarely inside that accepted tradeoff. Nothing in the change adds injection, an external bypass, or an isolation escape; argv handling is clean, CDP/Camofox precedence holds, and no-consent local_browser is ignored with a note rather than silently routed. 👍

Three things touch the consent mechanism itself, which is the one property the PR does claim:

  1. Revocation not taking effect (item 6 above).
  2. macOS handing over a non-default profile (item 4 above).
  3. The gate lives in a file the agent can write. Since browser.use_real_profile is ordinary config, a prompt-injected agent could flip it and then pass local_browser=true. Maybe protect config writes, or acknowledge it in the description so the boundary is explicit.

And two friendly hardening suggestions:

  1. Couple real-profile mode to restrict_evaluate — it defaults to off, so browser_console(document.cookie) is one-call cookie exfiltration for a prompt-injecting page. Auto-enabling it when the real profile is active is a cheap win.
  2. Consider a per-use confirmation, behind its own toggle. Once on, every local launch (including the automatic private-URL sidecar) uses the real profile with nothing in the output saying so. A confirmation prompt is less convenient but more secure — a companion option like browser.confirm_real_profile_use (default on) lets users pick their own tradeoff. Either way, stamping "used_real_profile": true into navigation results costs nothing and makes usage auditable.

Small stuff

  1. Snap/Flatpak Chromium keeps its profile outside ~/.config/chromium, so Ubuntu-default users hit a confusing "profile directory not found."
  2. The consent-off note only attaches to successful navigations — on failure the agent never learns why local_browser was ignored.

Summary

Toggle off, this is essentially inert and low-risk. Toggle on, the highest-value improvements are items 4–6 plus a real-launch check for item 1 — those cover the cases most likely to come back as bug reports. Items 10 and 11 are the best bang-for-buck hardening. Happy to help with any of these.

@GottZ

GottZ commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Review: the resolver/consent plumbing is clean, but the launch it feeds doesn't deliver a usable real profile on the main platforms

Read the PR head (cf7d308) against main, plus the agent-browser driver that actually consumes --profile (vercel-labs/agent-browser 8934fdb, v0.35.0; hermes pins agent-browser@^0.26.0, and v0.26.0's chrome.rs:109-165 already carries the same --remote-debugging-port=0 / keychain-switch logic) and the Chromium sources for the two behaviours the PR depends on. Every finding below cites the line it was verified at. I did not launch a real Chrome — findings 1–3 are source-level, so treat them as "needs a real-launch check" rather than "measured".

Blockers (the feature as described does not work end to end)

  1. Google Chrome ≥136 refuses the launch — the remote-debugging block applies to the built-in path too, not only to browser_exec. agent-browser's native driver starts every session with --remote-debugging-port=0 (cli/src/native/cdp/chrome.rs:453) and passes the --profile path through as --user-data-dir=<path> (chrome.rs:522). Chromium decides "default user data dir" by comparing paths, not by switch presence: IsUsingDefaultDataDirectory() returns DIR_USER_DATA == GetDefaultUserDataDirectory() (chrome/common/chrome_paths.cc:523-538), and IsRemoteDebuggingAllowed rejects when that is true under GOOGLE_CHROME_BRANDING (chrome/browser/devtools/remote_debugging_server.cc:168-180). So with the user's real Chrome dir Chrome never writes DevToolsActivePort, and agent-browser fails with Timeout waiting for DevToolsActivePort (chrome.rs:938) after its 3 attempts. The PR body attributes this block to the CDP/browser_exec route only; agent-browser is a remote-debugging client, so the "no CDP toggle needed" premise doesn't hold for Chrome. Brave/Chromium builds skip the check (Chromium branding, gated behind a testing flag at remote_debugging_server.cc:171-173); Edge I did not verify.

  2. The profile's logins/cookies are not readable on macOS or on keyring-backed Linux Chrome. agent-browser adds --password-store=basic and --use-mock-keychain unless use_real_keychain is set (chrome.rs:486-489), and that flag is only set on the profile-name path, which copies the profile (chrome.rs:717-736). A path argument never takes it: is_chrome_profile_name is false for anything containing / or \ (chrome.rs:1261-1263). --use-mock-keychain is Chromium's testing switch (components/os_crypt/common/os_crypt_switches.h:16-18); the basic store is the fixed v10 key derived from "peanuts" (components/os_crypt/async/browser/posix_key_provider.cc:17-22). Cookies the user's real browser wrote under the Keychain / libsecret / kwallet key (v11) therefore don't decrypt — the session starts logged out — and anything the automation writes goes under the mock key. Windows (DPAPI / app-bound) is unaffected by these two switches. Together with 1, the "real profile with live logins" claim holds at most on Windows and on Linux installs that already use the basic store; neither was exercised — the PR's 12 tests patch the resolvers and never launch (AGENTS.md:84-87 asks for the real path on config propagation / security boundaries).

  3. One user-data-dir, N sessions. Every local session gets its own agent-browser --session h_<uuid> (tools/browser_tool.py:2424), and with consent on every one of them now points at the same --user-data-dir. Chromium's process singleton hands a second launch to the running instance (PROCESS_NOTIFIED, chrome/browser/process_singleton_posix.cc:780-930) and the new process exits, which agent-browser reports as Chrome exited early … without writing DevToolsActivePort (chrome.rs:925). That is hit not just by the user's own browser (acknowledged in the PR), but by two concurrent tasks/subagents — and by the PR's own local_browser arg in plain local mode: _navigation_session_key returns ::local before the _get_cloud_provider() is None check (browser_tool.py:1587-1589), so a consented task that already has a bare local session (real profile) spawns a second local session on the same dir. feat(browser): parallel browser execution across multiple agents and profiles #49691 (parallel browsers across agents/profiles) is designing around exactly this axis. agent-browser's own answer is the named-profile copy (README "Chrome Profile Reuse"; it also flips use_real_keychain), which this PR rejects on purpose — without the copy the design needs a single shared session for the real profile plus a pre-flight SingletonLock check, otherwise the failure mode is a 3×-retried timeout.

Should fix before merge

  1. local_browser overrides the user's private-URL opt-outs. The flag returns the ::local key ahead of the auto_local_for_private_urls and cloud-provider checks (browser_tool.py:1587), and every private-URL gate in browser_navigate is keyed off auto_local_this_nav: the credential-query block (:3460), the _is_safe_url block (:3484-3489) and the post-nav redirect check (:3563-3567). So with a cloud provider configured and browser.auto_local_for_private_urls: false / browser.allow_private_urls: false set explicitly, a model-supplied browser_navigate("http://192.168.1.1/admin", local_browser=True) still opens the LAN page in a host-side Chromium — with the real profile's cookies. Consent to "use my profile" is not consent to "ignore my LAN opt-out"; the flag should only override the backend, not the private-URL policy (auto_local_this_nav needs to stay false for a local_browser-forced key, or the flag must respect _auto_local_for_private_urls()).

  2. macOS detection contradicts the fail-closed requirement. _detect_default_darwin falls back to "first installed Chromium app wins" (hermes_cli/browser_connect.py:259-262): Safari/Firefox default + Chrome installed → drives Chrome's real profile, which requirement 1 says must fail. The LSHandlers match is also unscoped — frag in out and "https" in out over the whole plist dump (browser_connect.py:257) returns the first bundle that appears anywhere (e.g. registered for .html files), not the https handler; map order makes Edge-default + Chrome-installed resolve to chrome.

  3. CI is red for three concrete reasons (all reproduced locally):

    • subprocess.run(..., text=True) in _detect_default_darwin / _detect_default_linux (browser_connect.py:253, :268) lacks encoding="utf-8", errors="replace"tests/scripts/test_footgun_subprocess_encoding.py::TestFullRepoScan::test_new_rule_find_only_known_violations and the blocking "Windows footguns" lint job.
    • tests/tools/test_browser_extension_router_wiring.py:39 replaces browser_navigate with lambda url="", task_id=None — the registry handler now passes local_browser=TypeError in two tests.
    • tests/tools/test_browser_open_timeout.py:99 monkeypatches _navigation_session_key with lambda task_id, urlbrowser_navigate now calls it with local_browser=TypeError (1 failed / 7 passed in that file).
      The PR's "no regression from the signature change" rests on test_browser_hybrid_routing.py alone.
  4. Consent is cached process-wide without the multiplex bypass its sibling has. _allow_private_urls re-resolves whenever get_hermes_home_override() is set so one profile's opt-out never leaks into another (browser_tool.py:1655-1668); _use_real_profile (:1433-1454) has no such branch and isn't in the cache-reset block (:5176-5195). In a multiplexed gateway the first profile to touch the browser decides real-profile behaviour for all of them, and flipping the desktop toggle off has no effect until restart.

  5. Profile resolution runs on every command, after the session is already registered, and regardless of engine. _real_profile_launch_args() sits inside _run_browser_command (browser_tool.py:2999) — 23 call sites including close from _cleanup_single_browser_session (:5113) — and detect_default_chromium() shells out (xdg-settings / defaults read, 5 s timeout) plus stats the dir each time. Consequences: on a host without xdg-settings (Docker/SSH/WSL) every command returns the "not a supported Chromium" error while _get_session_info (:2451) has already registered a phantom session; close returns the same error before reaching agent-browser, so an already-launched daemon (and its profile lock) outlives the Python-side entry; and with browser.engine: lightpanda agent-browser rejects the launch outright — Profiles are not supported with Lightpanda (cli/src/native/browser.rs:86) — after which the Chrome fallback (:1225) runs in a throwaway session without the profile. Resolve once (cache next to the consent bool), inject only on the session-creating command, and skip or reject when the effective engine is lightpanda.

  6. Tests cover the mocks, not the mechanism. All 12 tests patch _use_real_profile and the resolvers; nothing exercises the config read (read_raw_configbrowser.use_real_profile), the fail-closed return from _run_browser_command, the browser_navigate note, or any detector's parsing (test_detect_default_windows_progid_maps asserts the _WINDOWS_PROGID_MAP literal against itself). A _detect_default_linux test fed firefox.desktop / google-chrome.desktop output, and one _run_browser_command test asserting --profile/--executable-path land in argv (and the error return when they don't), would cover the actual contract.

  7. Docs. website/docs/user-guide/features/browser.md documents the browser: keys (:145-148, headed :715-722) and tools-reference.md the tool args; neither mentions use_real_profile or local_browser.

Minor

  1. Snap/Flatpak Linux: the data-dir is hard-wired to $XDG_CONFIG_HOME/<name> (browser_connect.py:168), so snap Chromium (chromium_chromium.desktopchromium~/.config/chromium, which is absent or a stale deb-era dir) and Flatpak Brave (~/.var/app/…) get "launch that browser at least once"; Flatpak Chrome (com.google.Chrome.desktop) matches neither google-chrome nor chromium and gets "not a supported Chromium browser". All three messages mislead about what's actually installed.
  2. Windows executable lookup uses os.path.join (browser_connect.py:204) while the data-dir uses ntpath — inconsistent with the "target-OS-correct separators" note; only visible with an explicit system on a foreign host, which is exactly what the tests do.
  3. The extension-controller lane of routed_browser_handler doesn't read local_browser (tools/browser_extension_router.py), so when that route is active the flag is dropped silently — opt-in path, but the schema promises a note when the flag isn't honoured.
  4. apps/desktop/src/i18n/{ar,ja,zh,zh-hant}.ts carry the existing browser labels; the new section label, field label and description aren't added there.
  5. Adding another Chromium derivative (Vivaldi, Opera, Arc, Yandex) currently means editing five tables (_real_profile_relparts, the three chromium_executable dicts, and the three detection maps). One record per browser would make that a one-liner — optional, but worth it before more browsers land.
  6. Framing: with consent on, local_browser is model-facing, so page content can steer the agent into any site inside the real profile, and browser_console (restrict_evaluate default false) can read document.cookie there. The PR's "convenience, not an isolation boundary" wording is honest — I'd put that sentence into the toggle description too.

Related open work

Suggested path

Either adopt agent-browser's copy semantics (profile name → --profile <name> gets you the copy and the real keychain, and sidesteps 1 and 3) and describe it as "snapshot of your logins", or keep the in-place drive and (a) exclude Google Chrome ≥136 explicitly with a message pointing at the chrome://inspect route, (b) drop the mock-keychain switches (needs an agent-browser change or AGENT_BROWSER_ARGS support for removing them), (c) serialize real-profile use behind one shared session. Fix 4–8 either way; 9–10 are small. Happy to send the test/footgun/engine-guard fixes as a follow-up.

@unsupportedpastels

Copy link
Copy Markdown
Contributor

Following up on my review above with empirical testing results — I ran the launch path end to end on Linux (agent-browser 0.26.0, isolated HERMES_HOME, unmocked code path from this PR's head).

Item 1 confirmed: real Google Chrome ≥136 blocks the launch path — silent hang

Tested with branded Google Chrome 152.0.7977.64 stable (extracted current .deb) as --executable-path, profile at the real default location ~/.config/google-chrome:

  • Via agent-browser (exactly what this PR launches): Chrome starts, but the session hangs until timeout — no error surfaced. agent-browser's daemon then kept respawning Chrome in a retry loop.
  • Chrome directly with --user-data-dir=$HOME/.config/google-chrome --remote-debugging-port=...:
    DevTools remote debugging requires a non-default data directory. Specify this using --user-data-dir.
    
    and the CDP endpoint never opens. Note: this fires even with --user-data-dir explicitly passed — Chrome compares the path against its default location, not flag presence. So pointing at the real default profile dir is blocked no matter how the flag is spelled.
  • Same binary, same flags, neutral directory: DevTools listening on ws://127.0.0.1:.../... immediately. The directory is the only variable.

So the Chrome ≥136 restriction this PR defers for the CDP/browser_exec route applies equally to the built-in launch path — it's the same mechanism. For the dominant case (default browser = Google Chrome), the feature can't work as designed on any Chrome from 136 onward, and it fails as a silent timeout rather than a clear error. Edge/Brave likely inherit the same upstream block (untested). Possible directions: snapshot-copy to a non-default dir (re-opens the token-fork tradeoff), the deferred chrome://inspect route, or scoping the feature to builds that permit it.

The good news: the mechanism works where Chrome doesn't block it

Full unmocked end-to-end on Chromium 151 (Chrome for Testing build, which doesn't enforce the block): config read → default-browser detection → resolver → agent-browser launch → browser_navigate succeeded, and the page snapshot showed a cookie persisted in the real-location profile from an earlier session (~/.config/chromium/Default/Cookies on disk confirmed it). So for a Chromium-default user, the design delivers exactly what it promises.

Other reproductions

  • Item 2 (profile lock): second concurrent session on the same profile dies with raw Chrome stderr — Failed to create .../SingletonLock: File exists / "Aborting now to avoid profile corruption." This is what a user with their browser open will see; a pre-flight lock check with a friendly message would help a lot.
  • Item 5 (missing executable fails open): with no Chromium binary findable, _real_profile_launch_args() returned (['--profile', '<real dir>'], None) — no error, no --executable-path — unmocked, exactly as flagged.
  • Fail-closed path: on a headless box, real xdg-settings returns empty → the clean "not a supported Chromium" error, as designed. (Side note: this means any headless/SSH Linux session fails closed even with a Chromium default configured, since detection needs a working xdg environment.)
  • Item 11 nuance (in the PR's favor): agent-browser 0.26 itself refuses eval access to document.cookie / localStorage (SecurityError: Access is denied), so the cookie-exfiltration-via-evaluate surface is narrower than my original comment implied — worth confirming Hermes's browser_console path inherits that same refusal.

Happy to share exact commands/fixtures for any of these.

@Adolanium

Adolanium commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Two gaps none of the reviews have covered yet, plus one caveat on the Chromium 151 result and two small corrections.

1. The bundled-Chromium gate blocks or bloats the real-profile path on Windows and macOS

_run_browser_command fails fast when _chromium_installed() is false (around line 2949), before _real_profile_launch_args() ever runs. That check probes only three things (the function is defined around line 5234): the AGENT_BROWSER_EXECUTABLE_PATH env var, Chrome/Chromium names in PATH, and the Playwright cache. A real Chrome, Edge, or Brave on Windows or macOS lives in Program Files or /Applications and is in none of those. Those are exactly the machines this feature targets.

Two outcomes, both wrong for a consented user whose real browser exists:

  • With security.allow_lazy_installs on (the default), _maybe_autoinstall_chromium() saves the gate by downloading ~170MB of bundled Chromium the launch never uses, because --executable-path already names the real binary.
  • With lazy installs off, offline, or after a failed download, the tool hard-blocks and tells the user to install a Chromium they do not need.

This is the desktop flip side of the headless-Linux note above. On headless Linux the resolver fails closed because xdg has no answer. On a Windows or macOS desktop the detection succeeds and this separate gate is what misfires.

Fix: when consent is on and chromium_executable() resolves a real binary, skip the gate. Or teach _chromium_installed() about real-browser install locations.

2. Non-stable browser channels resolve to the wrong profile

The detection maps treat every channel of a browser as the same browser. They are not. I did not test this on real Beta/Canary installs, so the registry details are worth a quick live check, but the mapping logic is right there in the diff.

  • Windows: Chrome Beta, Dev, and Canary all register ProgId ChromeHTML (with a hash suffix for secondary installs). The prefix match maps them all to "chrome", and the tables only know stable paths (Google\Chrome\User Data). Canary's profile is in Google\Chrome SxS\User Data, Beta's in Google\Chrome Beta\User Data. Neither is probed. With stable Chrome co-installed, Hermes silently drives stable's profile, not the default browser's. Without stable, the user gets "launch that browser at least once", which is wrong, since their default browser runs daily.
  • Windows: Edge Beta registers MSEdgeBHTML, which matches no prefix. An Edge-family default gets "not a supported Chromium browser", which is also a wrong message.
  • macOS: com.google.chrome.canary matches the com.google.chrome substring and maps to stable Google Chrome.app paths.
  • Linux: google-chrome-beta.desktop matches the google-chrome fragment and resolves to ~/.config/google-chrome, but Beta's profile is ~/.config/google-chrome-beta. The exe lookup only tries stable names too.

The consent story is "we drive your default browser's real profile". Driving a different profile is the kind of silent wrong turn the fail-closed design exists to prevent. Fix direction: on Windows, resolve the real exe from HKCU\Software\Classes\<ProgId>\shell\open\command and derive the data dir from the exe's install dir. Or probe channel dirs before falling back to stable, then fail closed with "Chrome Canary is not supported" instead of guessing. A test feeding ChromeHTML.HASH and MSEdgeBHTML would pin it.

3. Caveat on the Chromium 151 "good news" result

The cookie that decrypted in that test proves less than it seems. Per the agent-browser source, path-mode profiles always get --password-store=basic --use-mock-keychain, so cookies in that session use the basic key. On a headless box with no desktop keyring, the user's real Chromium also falls back to the basic store, so everything decrypts. That is the exact exception GottZ's keychain blocker names. It does not test the case that matters: a desktop with gnome-keyring, kwallet, or the macOS Keychain, where the user's real cookies use the keyring key and should not decrypt under the mock keychain. Also, if the earlier session that wrote the cookie was an agent-browser run, it wrote with the basic key regardless.

Small

  • The consent-miss note and the local_browser schema description say "Settings → Browser". That surface exists only in the desktop app. CLI, TUI, and gateway users see this text too. Naming the config.yaml key serves everyone.

Two corrections

  • "Edge/Brave likely inherit the same upstream block": per the Chromium source cited earlier, the remote-debugging block is gated on GOOGLE_CHROME_BRANDING. Brave and Chromium builds skip it, and Edge is Microsoft-branded, so it most likely skips it too. If that holds, the feature's working set is Edge, Brave, and Chromium defaults, and only Google-branded Chrome is blocked. Worth a live check, since it changes the picture from "dominant case broken" to "one browser broken".
  • Item 4 of the first review says "Windows and Linux detection both look correct". The channel mapping in point 2 above says otherwise for Windows, and Linux shares the same class.

@GottZ

GottZ commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Follow-up PR with fixes for the points above, stacked on this branch: #95549.

Twelve commits, one topic each with its own tests, so they can be cherry-picked individually: the three CI failures; fail-closed on a missing binary; launch-flag resolution once per process with close kept reachable; the Lightpanda conflict; per-entry macOS https-handler parsing without the installed-browser fallback; local_browser kept inside the private-URL policy and the existing local session; consent re-read per use (revocation without restart, multiplex-safe); snap/Flatpak profile dirs; a SingletonLock pre-flight (names the pid); a Google Chrome ≥ 136 gate that fails with the reason instead of hanging; docs; used_real_profile in navigation results plus the consent note on failures.

Thanks @unsupportedpastels — the Chrome 152 and lock reproductions went straight into the commit messages and test fixtures. Not touched, since they're design calls: in-place drive vs. agent-browser's profile copy, the mock-keychain flags (agent-browser side), a shared session for concurrent tasks, restrict_evaluate coupling, per-use confirmation, and the desktop locale entries — happy to add any of those.

@GottZ

GottZ commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

@Adolanium — both gaps are real and both are now in #95549 (launch-path and detection commits):

  1. Bundled-Chromium gate — confirmed: _chromium_installed() probes AGENT_BROWSER_EXECUTABLE_PATH, four PATH names and Playwright's cache, nothing under /Applications or Program Files, and it runs before the resolver. The gate is now skipped when consent resolved the user's own binary, so no 170 MB download and no block on a desktop whose real browser is the one being launched. (It only ever applied in pure-local mode — the cloud sidecar bypasses it — so the headless-Linux and desktop cases really are the two sides you describe.)

  2. Channels — macOS and Linux were exactly as you say: com.google.chrome.canary starts with com.google.chrome, google-chrome-beta.desktop contains google-chrome, both resolved to stable's profile silently. One correction on the Windows half: Chrome's channels do not share ChromeHTML. Beta, Dev and Canary register ChromeBHTML, ChromeDHTML and ChromeSSHTM (chrome/install_static/google_chrome_install_modes.h:52,117,190,262), and Edge Beta/Dev MSEdgeBHTML / MSEdgeDHTML per Microsoft's default-browser docs. With the PR's prefix map those fell through to None, i.e. a wrong message ("not a supported Chromium browser"), not a wrong profile. The fix classifies the raw identifier first and names the channel ("your default browser is Google Chrome Beta …"), fail-closed on all three OSes; tests feed ChromeBHTML.HASH and MSEdgeBHTML. Full channel support (per-channel profile dirs and binaries) is left as a maintainer call.

  3. Agreed on the Chromium 151 caveat — a headless box without a keyring puts the user's own Chromium on the basic store too, so that run sits inside the exception, not the desktop case.

The consent-off note now names browser.use_real_profile in config.yaml and mentions the desktop page only in parentheses. On Edge and the ≥ 136 block: plausible for the same branding reason, but I have not verified it, so #95549 keeps the check on Google Chrome only and the docs say "Edge untested".

@kshitijk4poor

Copy link
Copy Markdown
Contributor

E2E test: Chrome 136+ blocks the --profile path — fix inside

The problem

Tested your PR end-to-end on Chrome 152 / macOS. The --profile injection works in code, but Chrome 136+ blocks --remote-debugging-port on the default user-data-dir. agent-browser launches Chrome with --remote-debugging-port=0 --user-data-dir=, so Chrome starts but never opens a CDP port -> 120s timeout.

Chrome stderr:

DevTools remote debugging requires a non-default data directory.
Specify this using --user-data-dir.

Your PR body already documents this as a browser_exec-only limitation, but it also breaks the built-in browser tools path — agent-browser uses CDP internally (--remote-debugging-port=0) even for --session/--profile launches.

The fix (3 changes)

1. hermes_cli/browser_connect.py — add ensure_remote_debugging_policy() after detect_default_chromium():

The enterprise policy RemoteDebuggingAllowed overrides Chrome 136+ default-profile block. Per-OS:

  • macOS: defaults write com.google.Chrome RemoteDebuggingAllowed -bool true
  • Windows: HKCU\Software\Policies\Google\Chrome -> RemoteDebuggingAllowed DWORD=1
  • Linux: drop {"RemoteDebuggingAllowed": true} into /etc/opt/chrome/policies/managed/

Full function with all 4 browsers per-OS:
https://github.com/kshitijk4poor/hermes-agent/blob/browser-real-profile-fix/hermes_cli/browser_connect.py#L294-L370

2. tools/browser_tool.py — rewrite _real_profile_launch_args() to return --cdp instead of --profile when the policy succeeds:

def _real_profile_launch_args() -> tuple:
    if not _use_real_profile():
        return [], None
    from hermes_cli.browser_connect import (
        chromium_executable, detect_default_chromium,
        ensure_remote_debugging_policy, real_profile_data_dir,
    )
    browser = detect_default_chromium()
    if browser is None:
        return [], ("...not a supported Chromium...")
    data_dir = real_profile_data_dir(browser)
    if not data_dir or not os.path.isdir(data_dir):
        return [], ("...profile directory was not found...")
    exe = chromium_executable(browser)
    policy_ok = ensure_remote_debugging_policy(browser)
    if policy_ok and exe:
        _launch_real_profile_chrome(exe, data_dir, _REAL_PROFILE_CDP_PORT)
        return ["--cdp", f"http://127.0.0.1:{_REAL_PROFILE_CDP_PORT}"], None
    # Fallback: old --profile path (Chrome <136)
    args = ["--profile", data_dir]
    if exe:
        args += ["--executable-path", exe]
    return args, None

Plus a _launch_real_profile_chrome() helper that spawns Chrome with --remote-debugging-port= --remote-allow-origins=* (detached, skips if SingletonLock exists):
https://github.com/kshitijk4poor/hermes-agent/blob/browser-real-profile-fix/tools/browser_tool.py#L1521-L1545

3. tools/browser_tool.py caller (_run_browser_command) — when _real_profile_launch_args() returns --cdp, it must REPLACE --session, not append:

if _profile_args:
    if _profile_args[0] == "--cdp":
        backend_args = _profile_args  # replace --session with --cdp
    else:
        backend_args += _profile_args  # old --profile path

This is critical — agent-browser silently ignores --cdp when --session is present (you already noted this in a comment for the cloud path).

Port note

Chrome chrome://inspect toggle (devtools.remote_debugging.user-enabled) forces port 9222 and overrides --remote-debugging-port=. Use 9222 if the toggle is on, or any port if it is off (the policy allows it either way). I hardcoded 9222 in the test; a DevToolsActivePort read would be more robust.

E2E verification

  • browser_navigate("https://www.linkedin.com/feed/", local_browser=True) -> success: true, title: "Feed | LinkedIn"
  • LinkedIn feed loaded with real session cookies (logged in as the user)
  • All 23 tests pass (13 real-profile + 10 hybrid routing)
  • New test test_chromium_default_uses_cdp_with_policy verifies the --cdp path

Full diff: https://github.com/kshitijk4poor/hermes-agent/compare/browser-real-profile-fix

@teknium1

Copy link
Copy Markdown
Collaborator Author

Closed in favor of newer: #95620

@teknium1 teknium1 closed this Aug 26, 2026
@teknium1

Copy link
Copy Markdown
Collaborator Author

Superseded by #95620 (merged, 824f7e08), which carries this work forward.

Thanks @teknium1 — the real-profile consent + resolvers from this PR are the foundation of the merged change. The redo re-based it onto the current lane and fixed the issues surfaced in review:

  • Chrome 136+ / lock: copy the active profile into a Hermes-managed snapshot (non-default dir) and drive it, instead of pointing at the live default dir.
  • Active profile: follows Local State → profile.last_used (fixes the "logged out" case when the session lives in a non-Default profile).
  • Windows: a running browser locks its cookie DB deny-all (proven on windows-latest), so it fails fast with guidance — and browser.real_profile_autoclose + hermes browser close-profile let Hermes close it after your approval.
  • Credential-store lifecycle (backup/read-guard/perms/consent-off cleanup), channel-identity safety, sidecar cookie isolation, and the cua_browser route removal all landed too.

Your authorship is preserved in the merged history. Closing this in favor of #95620.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/cli CLI entry point, hermes_cli/, setup wizard comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades tool/browser Browser automation (CDP, Playwright) type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants