From b6d535dd88c4868d9242c80c80b36647bce87a24 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:03:29 -0700 Subject: [PATCH] feat(browser): Brave Origin works for real-profile browsing and default-browser detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the real-profile machinery (PR #95620) to Brave Origin — Brave's standalone paid build with a fully separate install identity: - new canonical key 'brave-origin' in _CHROMIUM_BROWSERS - Windows: BraveOHTML ProgId -> brave-origin; channel ProgIds BraveOBHTML/ BraveODHTML/BraveOSHTM fail closed (identifiers from brave-core install_static) - macOS: com.brave.Browser.origin bundle id (exact match); .beta/.dev/ .nightly channel bundles fail closed; /Applications/Brave Origin.app - Linux: brave-origin.desktop matched BEFORE the bare 'brave' fragment (substring scan would otherwise resolve an Origin default to stable Brave and drive the wrong profile — #95549 wrong-principal invariant); brave-origin-{beta,nightly,dev} fail closed - profile dirs: BraveSoftware/Brave-Origin on all three OSes (per brave-core kProductPathName + Homebrew cask zap paths) - /browser connect launch tables: Brave Origin split into its OWN group so a 'brave' executable lookup can never resolve to the Origin binary - user-facing strings/docs/desktop tooltip updated Tests: progid/bundle/desktop map params + data-dir resolution for all three OSes; 125 passed in the three browser test files. --- apps/desktop/src/app/settings/constants.ts | 2 +- hermes_cli/browser_connect.py | 59 ++++++++++++++++++- hermes_cli/config_defaults.py | 2 +- hermes_cli/main.py | 2 +- .../test_browser_connect_default_chromium.py | 6 ++ tests/tools/test_browser_real_profile.py | 23 ++++++++ tools/browser_tool.py | 6 +- website/docs/user-guide/features/browser.md | 2 +- 8 files changed, 93 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/app/settings/constants.ts b/apps/desktop/src/app/settings/constants.ts index c108af1036..daa1d05cc3 100644 --- a/apps/desktop/src/app/settings/constants.ts +++ b/apps/desktop/src/app/settings/constants.ts @@ -558,7 +558,7 @@ export const FIELD_DESCRIPTIONS: Record = defineFieldCopy({ timezone: 'IANA timezone identifier. Blank uses the system timezone.', browser: { useRealProfile: - "Local browsing uses your real logins. Hermes copies your default browser's profile (cookies, logins, preferences) into a managed snapshot and drives it with its packaged Chromium — your live profile is never opened directly, and the copy is refreshed from it on each run. Also lets the agent open a local real-profile session on request even when a cloud browser backend is configured. Only Chromium browsers (Chrome, Edge, Brave, Chromium) are supported; a non-Chromium default fails with a clear message. Off by default." + "Local browsing uses your real logins. Hermes copies your default browser's profile (cookies, logins, preferences) into a managed snapshot and drives it with its packaged Chromium — your live profile is never opened directly, and the copy is refreshed from it on each run. Also lets the agent open a local real-profile session on request even when a cloud browser backend is configured. Only Chromium browsers (Chrome, Edge, Brave, Brave Origin, Chromium) are supported; a non-Chromium default fails with a clear message. Off by default." }, agent: { imageInputMode: 'Controls how image attachments are sent to the model.', diff --git a/hermes_cli/browser_connect.py b/hermes_cli/browser_connect.py index c1a8733f7d..911cb4b78f 100644 --- a/hermes_cli/browser_connect.py +++ b/hermes_cli/browser_connect.py @@ -27,6 +27,7 @@ _DARWIN_APPS = ( "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium", "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", + "/Applications/Brave Origin.app/Contents/MacOS/Brave Origin", "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", ) @@ -37,6 +38,13 @@ _WINDOWS_BROWSER_GROUPS = ( (("Chromium", "Application", "chrome.exe"), ("Chromium", "Application", "chromium.exe")), ), (("brave.exe", "brave"), (("BraveSoftware", "Brave-Browser", "Application", "brave.exe"),)), + ( + ("brave-origin.exe", "brave-origin"), + ( + ("BraveSoftware", "Brave-Origin", "Application", "brave.exe"), + ("BraveSoftware", "Brave-Origin", "Application", "brave-origin.exe"), + ), + ), (("msedge.exe", "msedge"), (("Microsoft", "Edge", "Application", "msedge.exe"),)), ) @@ -53,7 +61,7 @@ _LINUX_BROWSER_GROUPS = ( ("/usr/bin/chromium-browser", "/usr/bin/chromium"), ), ( - ("brave-browser", "brave-browser-stable", "brave", "brave-origin", "brave-origin-nightly"), + ("brave-browser", "brave-browser-stable", "brave"), ( "/usr/bin/brave-browser", "/usr/bin/brave-browser-stable", @@ -61,9 +69,20 @@ _LINUX_BROWSER_GROUPS = ( "/snap/bin/brave", "/opt/brave.com/brave/brave-browser", "/opt/brave.com/brave/brave", + "/opt/brave-bin/brave", + ), + ), + # Brave Origin is a SEPARATE product identity (side-by-side installable + # with Brave), so it gets its own group: the executable fallback in + # chromium_executable() matches by group, and mixing Origin binaries into + # the brave group would let a "brave" lookup resolve to the Origin binary + # (or vice versa) — driving the wrong browser's profile. + ( + ("brave-origin", "brave-origin-nightly"), + ( + "/usr/bin/brave-origin", "/opt/brave.com/brave-origin/brave-origin", "/opt/brave.com/brave-origin-nightly/brave-origin", - "/opt/brave-bin/brave", ), ), ( @@ -93,7 +112,12 @@ _LINUX_INSTALL_PATHS = tuple(path for _, paths in _LINUX_BROWSER_GROUPS for path # --------------------------------------------------------------------------- # Canonical Chromium browser keys we support for real-profile driving. -_CHROMIUM_BROWSERS = ("chrome", "edge", "brave", "chromium") +# ``brave-origin`` is Brave's standalone paid build: same Chromium core, but a +# fully distinct install identity (BraveSoftware/Brave-Origin product path, +# ``BraveOHTML`` ProgId, ``com.brave.Browser.origin`` bundle id) so it +# side-by-side installs with regular Brave — its profile is NOT under +# Brave-Browser and must never be conflated with the ``brave`` key. +_CHROMIUM_BROWSERS = ("chrome", "edge", "brave", "chromium", "brave-origin") # Windows UserChoice ProgId prefixes → canonical browser key. Matched # case-insensitively by prefix so version suffixes (e.g. ``ChromeHTML.X``) @@ -104,6 +128,9 @@ _CHROMIUM_BROWSERS = ("chrome", "edge", "brave", "chromium") _WINDOWS_PROGID_MAP = ( ("chromehtml", "chrome"), ("msedgehtm", "edge"), + # Brave Origin stable is ``BraveOHTML`` (brave-core install_static). Listed + # before ``bravehtml`` for clarity; the prefixes don't collide either way. + ("braveohtml", "brave-origin"), ("bravehtml", "brave"), ("chromiumhtm", "chromium"), ) @@ -117,6 +144,9 @@ _WINDOWS_CHANNEL_PROGIDS = ( "chromebhtml", "chromedhtml", "chromesshtml", "chromecanaryhtml", "msedgebhtml", "msedgedhtml", "msedgechtml", "bravebetahtml", "bravenightlyhtml", + # Brave Origin channels (brave-core install_static): Beta=BraveOBHTML, + # Dev=BraveODHTML, Nightly/SxS=BraveOSHTM (no trailing L — 10-char cap). + "braveobhtml", "braveodhtml", "braveoshtm", ) # Linux xdg default-web-browser .desktop name fragments → canonical STABLE key. @@ -128,6 +158,11 @@ _LINUX_DESKTOP_MAP = ( ("google-chrome", "chrome"), ("com.google.chrome", "chrome"), ("chromium", "chromium"), + # ORDER MATTERS: ``brave-origin.desktop`` contains the bare ``brave`` + # fragment, so the substring scan must hit the Origin entry first — + # otherwise an Origin default resolves to stable Brave and real-profile + # mode drives a DIFFERENT browser's profile (wrong-principal, #95549). + ("brave-origin", "brave-origin"), ("brave", "brave"), ("microsoft-edge", "edge"), ("com.microsoft.edge", "edge"), @@ -141,6 +176,7 @@ _LINUX_CHANNEL_FRAGMENTS = ( "com.google.chrome.beta", "com.google.chrome.dev", "com.google.chrome.canary", "microsoft-edge-beta", "microsoft-edge-dev", "microsoft-edge-canary", "brave-browser-beta", "brave-browser-nightly", "brave-browser-dev", + "brave-origin-beta", "brave-origin-nightly", "brave-origin-dev", ) # Where sandboxed Linux packages keep the profile instead of $XDG_CONFIG_HOME. @@ -161,6 +197,10 @@ _DARWIN_BUNDLE_MAP = ( ("com.google.chrome", "chrome"), ("com.microsoft.edgemac", "edge"), ("com.brave.browser", "brave"), + # Brave Origin reuses the Brave bundle id with an ``.origin`` suffix + # (Homebrew cask: com.brave.Browser.origin). Exact matching keeps it from + # ever being read as plain ``com.brave.browser``. + ("com.brave.browser.origin", "brave-origin"), ("org.chromium.chromium", "chromium"), ) @@ -169,6 +209,8 @@ _DARWIN_CHANNEL_BUNDLES = ( "com.google.chrome.beta", "com.google.chrome.dev", "com.google.chrome.canary", "com.microsoft.edgemac.beta", "com.microsoft.edgemac.dev", "com.microsoft.edgemac.canary", "com.brave.browser.beta", "com.brave.browser.nightly", + "com.brave.browser.origin.beta", "com.brave.browser.origin.dev", + "com.brave.browser.origin.nightly", ) # Sentinel returned when the OS default is a recognized-but-unsupported @@ -201,6 +243,11 @@ def _real_profile_relparts(browser: str) -> tuple: ("Chromium", "User Data"), "chromium", ), + "brave-origin": ( + ("BraveSoftware", "Brave-Origin"), + ("BraveSoftware", "Brave-Origin", "User Data"), + "BraveSoftware/Brave-Origin", + ), }[browser] @@ -258,6 +305,7 @@ def chromium_executable(browser: str, system: str | None = None) -> str | None: "chrome": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "chromium": "/Applications/Chromium.app/Contents/MacOS/Chromium", "brave": "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", + "brave-origin": "/Applications/Brave Origin.app/Contents/MacOS/Brave Origin", "edge": "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", }[browser] return app if os.path.isfile(app) else None @@ -266,6 +314,10 @@ def chromium_executable(browser: str, system: str | None = None) -> str | None: "chrome": (("Google", "Chrome", "Application", "chrome.exe"),), "chromium": (("Chromium", "Application", "chrome.exe"), ("Chromium", "Application", "chromium.exe")), "brave": (("BraveSoftware", "Brave-Browser", "Application", "brave.exe"),), + "brave-origin": ( + ("BraveSoftware", "Brave-Origin", "Application", "brave.exe"), + ("BraveSoftware", "Brave-Origin", "Application", "brave-origin.exe"), + ), "edge": (("Microsoft", "Edge", "Application", "msedge.exe"),), }[browser] bases = [ @@ -280,6 +332,7 @@ def chromium_executable(browser: str, system: str | None = None) -> str | None: "chrome": ("google-chrome", "google-chrome-stable"), "chromium": ("chromium-browser", "chromium"), "brave": ("brave-browser", "brave-browser-stable", "brave"), + "brave-origin": ("brave-origin",), "edge": ("microsoft-edge", "microsoft-edge-stable"), }[browser] for name in linux: diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py index ebc3825bad..78bdda6682 100644 --- a/hermes_cli/config_defaults.py +++ b/hermes_cli/config_defaults.py @@ -595,7 +595,7 @@ DEFAULT_CONFIG = { # never contends with the user's running browser. Turning this back off # deletes the snapshot store (~/.hermes/browser-profile/) so copied # credentials don't outlive consent. Only Chromium-family default - # browsers are supported (Chrome, Edge, Brave, Chromium); a non-Chromium + # browsers are supported (Chrome, Edge, Brave, Brave Origin, Chromium); a non-Chromium # default (e.g. Firefox) fails closed with a clear message. Default # false. Also gates the browser_exec ``local`` argument, which forces a # real-profile local session even under a cloud browser backend. Toggle diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 536ec2516f..d4e6408d94 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -13298,7 +13298,7 @@ def main(): ) browser_close.add_argument( "--browser", - help="Override detected default browser (chrome/edge/brave/chromium)", + help="Override detected default browser (chrome/edge/brave/brave-origin/chromium)", ) def _dispatch_browser(_args): diff --git a/tests/hermes_cli/test_browser_connect_default_chromium.py b/tests/hermes_cli/test_browser_connect_default_chromium.py index 4a654d8e66..cdc29ad4c5 100644 --- a/tests/hermes_cli/test_browser_connect_default_chromium.py +++ b/tests/hermes_cli/test_browser_connect_default_chromium.py @@ -97,8 +97,11 @@ class TestDetectDefaultDarwin: [ ("com.google.Chrome", "chrome"), ("com.brave.Browser", "brave"), + ("com.brave.Browser.origin", "brave-origin"), ("com.microsoft.edgemac", "edge"), ("org.chromium.Chromium", "chromium"), + ("com.brave.Browser.origin.beta", bc.UNSUPPORTED_CHANNEL), + ("com.brave.Browser.origin.nightly", bc.UNSUPPORTED_CHANNEL), ], ) def test_bundle_map(self, bundle, expected): @@ -122,6 +125,9 @@ class TestDetectDefaultLinux: ("org.chromium.Chromium.desktop", "chromium"), ("brave-browser.desktop", "brave"), ("com.brave.Browser.desktop", "brave"), + ("brave-origin.desktop", "brave-origin"), + ("brave-origin-beta.desktop", bc.UNSUPPORTED_CHANNEL), + ("brave-origin-nightly.desktop", bc.UNSUPPORTED_CHANNEL), ("microsoft-edge.desktop", "edge"), ("com.microsoft.Edge.desktop", "edge"), ("firefox.desktop", None), diff --git a/tests/tools/test_browser_real_profile.py b/tests/tools/test_browser_real_profile.py index c66e409081..1635e60c47 100644 --- a/tests/tools/test_browser_real_profile.py +++ b/tests/tools/test_browser_real_profile.py @@ -42,6 +42,29 @@ class TestRealProfileResolvers: assert m["chromehtml"] == "chrome" assert m["msedgehtm"] == "edge" assert m["bravehtml"] == "brave" + assert m["braveohtml"] == "brave-origin" + + def test_brave_origin_data_dirs(self): + import hermes_cli.browser_connect as bc + with patch.dict(os.environ, {"LOCALAPPDATA": r"C:\Users\T\AppData\Local"}, clear=False): + win = bc.real_profile_data_dir("brave-origin", "Windows") + assert win and win.endswith(ntpath.join("BraveSoftware", "Brave-Origin", "User Data")) + with patch.dict(os.environ, {"XDG_CONFIG_HOME": "/home/t/.config"}, clear=False): + assert ( + bc.real_profile_data_dir("brave-origin", "Linux") + == "/home/t/.config/BraveSoftware/Brave-Origin" + ) + mac = bc.real_profile_data_dir("brave-origin", "Darwin") + assert mac and mac.endswith("Library/Application Support/BraveSoftware/Brave-Origin") + + def test_brave_origin_channel_progids_fail_closed(self): + import hermes_cli.browser_connect as bc + # Beta=BraveOBHTML, Dev=BraveODHTML, Nightly=BraveOSHTM must be caught + # by the channel list, and must be checked BEFORE the stable map — note + # none of them share the braveohtml stable prefix, but ordering is the + # invariant the detector relies on for the other families. + for chan in ("braveobhtml", "braveodhtml", "braveoshtm"): + assert chan in bc._WINDOWS_CHANNEL_PROGIDS def test_detect_default_non_chromium_is_none(self): import hermes_cli.browser_connect as bc diff --git a/tools/browser_tool.py b/tools/browser_tool.py index 8630eed403..5adc71d17d 100644 --- a/tools/browser_tool.py +++ b/tools/browser_tool.py @@ -1596,7 +1596,8 @@ def _real_profile_cdp() -> tuple: if browser is None: return None, ( "browser.use_real_profile is on, but your default browser is not a " - "supported Chromium browser (Chrome, Edge, Brave, Chromium). " + "supported Chromium browser (Chrome, Edge, Brave, Brave Origin, " + "Chromium). " "Real-profile browsing requires a Chromium default; set one or turn " "the toggle off." ) @@ -1610,7 +1611,8 @@ def _real_profile_cdp() -> tuple: "browser.use_real_profile is on, but your default browser is a " "pre-release Chromium channel (Beta / Dev / Canary), which " "real-profile browsing does not support. Set your default to a " - "stable Chrome / Edge / Brave / Chromium, or turn the toggle off." + "stable Chrome / Edge / Brave / Brave Origin / Chromium, or turn " + "the toggle off." ) # Reuse BEFORE writing anything. A shared copy-browser may already be up diff --git a/website/docs/user-guide/features/browser.md b/website/docs/user-guide/features/browser.md index 2d72ae79f1..b96e077374 100644 --- a/website/docs/user-guide/features/browser.md +++ b/website/docs/user-guide/features/browser.md @@ -206,7 +206,7 @@ losing unsaved tabs), then retries. If the profile is still locked after that to fully quit the browser — it won't loop or kill again on its own. ::: -- **Supported browsers:** Chrome, Edge, Brave, Chromium (whichever is your OS +- **Supported browsers:** Chrome, Edge, Brave, Brave Origin, Chromium (whichever is your OS default). A non-Chromium default (e.g. Firefox) fails closed with a clear message rather than guessing. - **Works on any backend.** On a local backend it's automatic once the toggle