fix(browser): close remaining CDP-URL leak paths in supervisor (review)
Review of the salvage found the timeout-message redaction left the more common failure mode unguarded: when the first websockets.connect(cdp_url) fails (bad URI / refused / TLS), the raw websockets exception -- which embeds the full cdp_url incl. ?token= and user:pass@ -- is stashed as _start_error and re-raised verbatim by start(), and two reconnect logger.warning sites log the same raw exception. Add a module-level _redact_cdp_error_text() chokepoint (delegating to agent.redact.redact_cdp_url) and route all four supervisor egress points through it: - start() TimeoutError message (already covered; kept) - start() _start_error re-raise -> now raises a redacted RuntimeError with 'from None' so no secret leaks via message OR traceback cause chain - connect-failed and session-dropped reconnect warnings Guard tests assert the re-raised message is redacted for both token and userinfo, the raw cause is suppressed, and the helper preserves non-secret context (host/reason). Verified with a mutation check: reverting to the raw 'raise err' fails the new tests. Correct the redact_cdp_url docstring to scope its guarantee to direct-URL redaction and point exception callers at the supervisor helper.
This commit is contained in:
@@ -34,6 +34,25 @@ from websockets.asyncio.client import ClientConnection
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _redact_cdp_error_text(exc: object) -> str:
|
||||
"""Redact any CDP endpoint credentials from an error's string form.
|
||||
|
||||
``websockets`` bakes the raw target URL into its exception messages
|
||||
(``InvalidURI``, connection errors, TLS failures all embed the full
|
||||
``self.cdp_url`` — including a ``?token=`` query credential or
|
||||
``user:pass@`` userinfo). Every supervisor egress point that turns such an
|
||||
exception into log text or a re-raised message MUST route through here so
|
||||
those credentials never reach Hermes logs or tracebacks. Falls back to a
|
||||
fixed sentinel if redaction itself raises, erring toward masking.
|
||||
"""
|
||||
try:
|
||||
from agent.redact import redact_cdp_url
|
||||
|
||||
return redact_cdp_url(str(exc))
|
||||
except Exception:
|
||||
return "<error redacted>"
|
||||
|
||||
|
||||
# ── Config defaults ───────────────────────────────────────────────────────────
|
||||
|
||||
DIALOG_POLICY_MUST_RESPOND = "must_respond"
|
||||
@@ -353,7 +372,14 @@ class CDPSupervisor:
|
||||
if self._start_error is not None:
|
||||
err = self._start_error
|
||||
self.stop()
|
||||
raise err
|
||||
# ``err`` is a raw ``websockets`` exception whose message embeds the
|
||||
# full cdp_url (token / userinfo). Re-raise a redacted RuntimeError
|
||||
# and suppress the raw cause (``from None``) so no credential leaks
|
||||
# via the message OR the traceback chain. Type is not load-bearing:
|
||||
# the sole caller (_ensure_cdp_supervisor) only logs it.
|
||||
raise RuntimeError(
|
||||
f"CDP supervisor failed to start: {_redact_cdp_error_text(err)}"
|
||||
) from None
|
||||
|
||||
def stop(self, timeout: float = 5.0) -> None:
|
||||
"""Cancel the supervisor task and join the thread."""
|
||||
@@ -631,7 +657,7 @@ class CDPSupervisor:
|
||||
return
|
||||
logger.warning(
|
||||
"CDP supervisor %s: connect failed (attempt %s): %s",
|
||||
self.task_id, attempt, e,
|
||||
self.task_id, attempt, _redact_cdp_error_text(e),
|
||||
)
|
||||
await asyncio.sleep(min(backoff, 10.0))
|
||||
backoff = min(backoff * 2, 10.0)
|
||||
@@ -668,7 +694,7 @@ class CDPSupervisor:
|
||||
"CDP supervisor %s: session dropped after %.1fs: %s",
|
||||
self.task_id,
|
||||
time.time() - last_success_at,
|
||||
e,
|
||||
_redact_cdp_error_text(e),
|
||||
)
|
||||
finally:
|
||||
with self._state_lock:
|
||||
|
||||
Reference in New Issue
Block a user