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:
@@ -0,0 +1 @@
|
||||
runninwithitmarketing
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user