Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/browser_harness/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,10 @@ def _ws_from_devtools_active_port(http_url: str) -> str | None:
for base in PROFILES:
try:
active = (base / "DevToolsActivePort").read_text(encoding="utf-8", errors="replace").splitlines()
except (FileNotFoundError, NotADirectoryError):
# PermissionError: macOS TCC guards ~/Library/Application Support/Google/Chrome,
# so a terminal without Full Disk Access gets EPERM here. Skip the profile and
# keep looking rather than killing discovery outright.
except (FileNotFoundError, NotADirectoryError, PermissionError):

@cubic-dev-ai cubic-dev-ai Bot Aug 16, 2026

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.

P2: In the get_ws_url discovery loop, when every profile is unreadable (the PR's FDA-less scenario) the new PermissionError catch skips each profile, but the immediately-following liveness gate then misreports the browser as not running. supported_browser_running()browser_running_for_profile() reads SingletonLock and also gets EPERM/OSError under the same TCC protection, returning False, so get_ws_url raises "chrome-not-running" before ever reaching the 9222/9223 probe or the final "enable chrome://inspect" error the PR claims the fix unblocks. So a user whose browser is genuinely running on another port gets a misleading "not running" error instead of the probe. The added comment "Skipping lets the 9222/9223 probe below still run" is therefore inaccurate for the all-profiles-blocked case. Consider gating the liveness check on actually scanning at least one readable profile, or falling through to the probe when every profile is unreadable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/browser_harness/daemon.py, line 205:

<comment>In the get_ws_url discovery loop, when every profile is unreadable (the PR's FDA-less scenario) the new PermissionError catch skips each profile, but the immediately-following liveness gate then misreports the browser as not running. `supported_browser_running()` → `browser_running_for_profile()` reads `SingletonLock` and also gets EPERM/OSError under the same TCC protection, returning False, so get_ws_url raises "chrome-not-running" before ever reaching the 9222/9223 probe or the final "enable chrome://inspect" error the PR claims the fix unblocks. So a user whose browser is genuinely running on another port gets a misleading "not running" error instead of the probe. The added comment "Skipping lets the 9222/9223 probe below still run" is therefore inaccurate for the all-profiles-blocked case. Consider gating the liveness check on actually scanning at least one readable profile, or falling through to the probe when every profile is unreadable.</comment>

<file context>
@@ -199,7 +199,10 @@ def _ws_from_devtools_active_port(http_url: str) -> str | None:
+        # PermissionError: macOS TCC guards ~/Library/Application Support/Google/Chrome,
+        # so a terminal without Full Disk Access gets EPERM here. Skip the profile and
+        # keep looking rather than killing discovery outright.
+        except (FileNotFoundError, NotADirectoryError, PermissionError):
             continue
         port = active[0].strip() if active else ""
</file context>
Fix with cubic

continue
port = active[0].strip() if active else ""
ws_path = active[1].strip() if len(active) > 1 else ""
Expand Down Expand Up @@ -241,7 +244,9 @@ def get_ws_url():
for base in PROFILES:
try:
active = (base / "DevToolsActivePort").read_text(encoding="utf-8", errors="replace").splitlines()
except (FileNotFoundError, NotADirectoryError):
# PermissionError: see _ws_from_devtools_active_port — an FDA-less terminal
# gets EPERM from TCC. Skipping lets the 9222/9223 probe below still run.
except (FileNotFoundError, NotADirectoryError, PermissionError):
continue
port = active[0].strip() if active else ""
ws_path = active[1].strip() if len(active) > 1 else ""
Expand Down
43 changes: 43 additions & 0 deletions tests/unit/test_daemon.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import asyncio

import pytest

from browser_harness import daemon


Expand Down Expand Up @@ -293,3 +295,44 @@ def test_current_tab_meta_returns_not_attached_when_no_target_id():
assert result == {"error": "not_attached"}
# No CDP call should have been issued.
assert d.cdp.calls == []


class _DeniedProfile:
"""A profile dir whose DevToolsActivePort read is refused by the OS.

macOS TCC guards ~/Library/Application Support/Google/Chrome, so a terminal
without Full Disk Access gets EPERM -- not ENOENT -- when reading the file.
"""

def __truediv__(self, _name):
return self

def read_text(self, *_args, **_kwargs):
raise PermissionError(1, "Operation not permitted")

def __str__(self):
return "<denied-profile>"


def test_ws_from_devtools_active_port_skips_profiles_it_cannot_read(monkeypatch):
monkeypatch.setattr(daemon, "PROFILES", [_DeniedProfile()])

# Must return None (no match) rather than propagating PermissionError.
assert daemon._ws_from_devtools_active_port("http://127.0.0.1:9222") is None


def test_get_ws_url_reports_chrome_not_running_when_profiles_are_unreadable(monkeypatch):
"""An unreadable profile must not abort discovery.

Before this was handled, a PermissionError escaped get_ws_url() as a fatal
traceback, so the daemon never reached its own fallbacks or the actionable
"start Chrome" error below.
"""
# An explicit endpoint short-circuits discovery, so clear both overrides.
monkeypatch.delenv("BU_CDP_WS", raising=False)
monkeypatch.delenv("BU_CDP_URL", raising=False)
monkeypatch.setattr(daemon, "PROFILES", [_DeniedProfile()])
monkeypatch.setattr(daemon, "supported_browser_running", lambda: False)

with pytest.raises(RuntimeError, match="chrome-not-running"):
daemon.get_ws_url()