fix(browser): real-profile follow-ups — reap launched Chrome, headless display-less Linux, register real_profile_pin default + docs

- _terminate_real_profile_chrome(): directly-launched real browsers are ours
  to reap (agent-browser only attaches); wired into the atexit emergency
  cleanup and both launch-failure paths so orphaned Chrome processes can't
  accumulate.
- Display-less Linux gate: append --headless=new (shares the profile's normal
  cookie store, unlike legacy headless) so the direct-launch path doesn't
  regress servers without DISPLAY/WAYLAND_DISPLAY.
- Register browser.real_profile_pin in config_defaults.py and document the
  new launch model + pin in website/docs/user-guide/features/browser.md.
- Drop unused tempfile import from the cherry-picked commit.
This commit is contained in:
Teknium
2026-08-29 18:18:05 -07:00
parent a50b41f843
commit f8546c2eac
4 changed files with 67 additions and 3 deletions
@@ -0,0 +1 @@
runninwithitmarketing
+8
View File
@@ -611,6 +611,14 @@ DEFAULT_CONFIG = {
# retry. Still locked afterward → stays blocked, no loop, no auto-kill.
# OFF by default. No effect on macOS/Linux (copy-while-running works).
"real_profile_autoclose": False,
# Pin WHICH source browser profile directory gets snapshotted for
# real-profile browsing (e.g. "Profile 2"). Unset/empty: follows the
# browser's last-used profile (Local State → profile.last_used). On a
# machine with several profiles (work + personal), last-used roulette
# can silently hand the agent the wrong identity; a pin locks it. A pin
# naming a directory that doesn't exist FAILS CLOSED with a fixable
# message rather than falling back to last-used.
"real_profile_pin": "",
"allow_unsafe_evaluate": False, # Legacy override: when true, browser_console(expression=...) bypasses the restrict_evaluate denylist entirely
"restrict_evaluate": False, # Opt-in denylist blocking sensitive JS primitives (cookies/storage/clipboard/network/form values) in browser_console(expression=...)
# CDP supervisor — dialog + frame detection via a persistent WebSocket.
+37 -1
View File
@@ -1457,6 +1457,27 @@ _real_profile_cdp_cache: dict = {}
_real_profile_chrome_procs: list = [] # Popen handles of directly-launched real browsers
def _terminate_real_profile_chrome() -> None:
"""Terminate real-browser processes launched for real-profile sessions.
The real-profile path launches the user's actual browser binary on the
profile COPY (bypassing agent-browser's mock-keychain launch). Those
processes are ours to reap: agent-browser only ATTACHED to them, so its
own session cleanup never kills them. Idempotent; safe from atexit.
"""
while _real_profile_chrome_procs:
proc = _real_profile_chrome_procs.pop()
try:
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except Exception:
proc.kill()
except Exception as e:
logger.debug("real-profile chrome terminate failed: %s", e)
def _agent_browser_argv(browser_cmd: str) -> list:
"""Command prefix to invoke agent-browser (binary or npx sentinel)."""
if _is_npx_agent_browser_sentinel(browser_cmd):
@@ -1671,7 +1692,6 @@ def _real_profile_cdp() -> tuple:
"browser.use_real_profile is on, but the real browser binary for "
f"'{browser}' could not be found. Reinstall it or turn the toggle off."
)
import tempfile
port_file = os.path.join(copy_dir, "DevToolsActivePort")
try:
@@ -1694,6 +1714,14 @@ def _real_profile_cdp() -> tuple:
"--disable-features=Translate",
"--no-startup-window",
]
if sys.platform.startswith("linux") and not (
os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")
):
# Display-less Linux (servers, CI): the real binary cannot open a
# window, so it exits at startup. Chrome's NEW headless mode shares
# the profile's normal cookie store (unlike legacy --headless with
# its separate store), so real-profile auth still loads.
chrome_argv.append("--headless=new")
try:
chrome_proc = subprocess.Popen(
chrome_argv,
@@ -1722,12 +1750,14 @@ def _real_profile_cdp() -> tuple:
except OSError:
pass
if chrome_proc.poll() is not None:
_terminate_real_profile_chrome()
return None, (
"browser.use_real_profile is on, but Chrome exited during "
"startup (another instance may hold the profile copy)."
)
_time.sleep(0.25)
if port is None:
_terminate_real_profile_chrome()
return None, (
"browser.use_real_profile is on, but the real-profile browser "
"did not expose a debug port in time. Retry, or turn the toggle off."
@@ -2197,6 +2227,12 @@ def _emergency_cleanup_all_sessions():
# Clean up this process's own sessions first, so their owner_pid files
# are removed before the reaper scans.
# Real-profile Chrome processes are launched directly (not by
# agent-browser), so the session cleanup below never reaps them.
try:
_terminate_real_profile_chrome()
except Exception as e:
logger.debug("Real-profile chrome cleanup on exit failed: %s", e)
if _active_sessions:
logger.info("Emergency cleanup: closing %s active session(s)...",
len(_active_sessions))
+21 -2
View File
@@ -173,8 +173,13 @@ browser:
When enabled, Hermes copies your default browser's **active** profile — the one
you actually browse (`Local State → profile.last_used`), with its cookies, saved
logins, and preferences — into a managed snapshot under
`~/.hermes/browser-profile/<browser>/`, then drives that snapshot with its
packaged Chromium. Your live browser profile is **never opened directly**: the
`~/.hermes/browser-profile/<browser>/`, then launches your **real browser
binary** on that snapshot and attaches its browsing engine to it. Launching the
real binary (instead of a bundled Chromium with mock-keychain switches) is what
keeps OS-encrypted cookies decryptable — on macOS, Chrome cookies are encrypted
through the Keychain, and a mock-keychain launch would silently drop every one
of them, opening signed out. Your live browser profile is **never opened
directly**: the
snapshot is a separate directory, so it doesn't fight your running browser for
the profile lock and it sidesteps Chrome 136+'s block on remote-debugging the
default profile directory. The auth files (cookies/logins/preferences) are
@@ -182,6 +187,20 @@ re-synced from your real profile whenever a fresh session is launched, so logins
you do in your own browser show up in the agent's session. Only the active
profile is copied — other Chrome profiles are never snapshotted.
If your browser has several profiles (say a work profile and a personal one)
and you don't want "whichever profile you touched last" deciding the agent's
identity, pin the snapshot source explicitly:
```yaml
# ~/.hermes/config.yaml
browser:
use_real_profile: true
real_profile_pin: "Profile 2" # directory name under the browser's user-data dir
```
A pin naming a profile directory that doesn't exist fails closed with a
fixable message — it never silently falls back to the last-used profile.
When you turn the toggle back off, Hermes deletes the snapshot store
(`~/.hermes/browser-profile/`) on the next browser use, so the copied
credentials don't linger after you revoke consent.