Skip to content

pr 14#15

Open
skulidropek wants to merge 17 commits into
mainfrom
pr-14
Open

pr 14#15
skulidropek wants to merge 17 commits into
mainfrom
pr-14

Conversation

@skulidropek

Copy link
Copy Markdown
Member
  • Initial commit with task details
  • feat: add personal browser targets
  • Revert "Initial commit with task details"
  • Add browser share relay and Edge extension artifact

konard and others added 4 commits June 24, 2026 07:13
Adding .gitkeep for PR creation (default mode).
This file will be removed when the task is complete.

Issue: #13
@skulidropek

Copy link
Copy Markdown
Member Author

Agent Plan Update

Branch: pr-14 at 25fac54.

1. Plan

Source: codex - Captured: 2026-06-24T09:46:40.255Z

Personal Browser via MCP + noVNC

Summary

  • Keep managed as the Docker Chromium/noVNC browser.
  • Add first-class personal display support: MCP controls the host browser through CDP, and noVNC shows the host desktop/browser through a VNC-to-WebSocket proxy.
  • The current branch already supports MCP target switching, but /usr/local/bin/browser-connection is still old 0.3.0, so the binary must be rebuilt/reinstalled after changes.

Key Changes

  • Extend browser targets from NAME=CDP_ENDPOINT to include optional display data:
    • --browser NAME=CDP_ENDPOINT
    • --browser-vnc NAME=HOST:PORT
    • --browser-novnc NAME=URL
    • shortcuts: --personal-browser CDP_URL, --personal-vnc HOST:PORT
    • env equivalents: BROWSER_CONNECTION_BROWSERS, BROWSER_CONNECTION_BROWSER_VNCS, BROWSER_CONNECTION_PERSONAL_CDP_ENDPOINT, BROWSER_CONNECTION_PERSONAL_VNC_ENDPOINT, BROWSER_CONNECTION_ACTIVE_BROWSER
  • Add a noVNC proxy container for VNC-backed external targets:
    • container name: <project-browser-container>-novnc-<target>
    • deterministic host port from project + target, separate from managed noVNC ports
    • connects to host.docker.internal:5900 or configured VNC endpoint
    • returns URL like http://127.0.0.1:<port>/vnc.html?...
  • Update MCP behavior:
    • browser_list includes cdpEndpoint, vncEndpoint, novncUrl, active.
    • browser_select(name) switches active CDP target and ensures noVNC proxy for that target if VNC is configured.
    • managed still uses the Docker browser’s own noVNC/CDP.
  • Fix current PR validation issues while touching this code:
    • invalid --active-browser fails at startup
    • malformed --cdp-endpoint is reported, not silently treated as absent

Host Setup

  • Start personal Chrome/Edge with CDP and non-default profile:
google-chrome \
  --remote-debugging-address=0.0.0.0 \
  --remote-debugging-port=9222 \
  --user-data-dir="$HOME/.browser-connection-personal" \
  --no-first-run
  • Start/enable a VNC server for the host desktop, for example on Linux/X11:
x11vnc -display :0 -rfbport 5900 -listen 0.0.0.0 -nopw -forever -shared
  • Configure Codex MCP after reinstalling the new binary:
[mcp_servers.playwright]
command = "browser-connection"
args = [
  "--project", "dg-rust-browser-connection-pr-14",
  "--network", "container:dg-rust-browser-connection-pr-14",
  "--browser", "personal=http://host.docker.internal:9222",
  "--browser-vnc", "personal=host.docker.internal:5900",
  "--active-browser", "managed",
]

Test Plan

  • Unit tests for target parsing, VNC/noVNC display fields, reserved names, endpoint normalization, invalid active target rejection.
  • MCP stdio tests for browser_list and browser_select personal returning novncUrl without starting the managed browser.
  • Docker-shell tests around proxy container naming, port calculation, labels, and host.docker.internal:host-gateway.
  • Manual validation:
    • curl http://host.docker.internal:9222/json/version from container succeeds.
    • browser_select personal then browser_navigate controls the host browser.
    • returned noVNC URL displays the host desktop/browser.
    • browser_select managed returns to Docker Chromium/noVNC.

Assumptions

2. Plan

Source: codex - Captured: 2026-06-24T09:48:16.361Z

Codex Plan

Steps

  • in_progress: Inspect current target/MCP/browser modules and test shape
  • pending: Add target display metadata parsing and config wiring
  • pending: Add noVNC proxy orchestration for external VNC targets
  • pending: Update MCP inventory/select behavior and validation
  • pending: Update docs/tests and run verification

3. Plan

Source: codex - Captured: 2026-06-24T10:13:46.893Z

Codex Plan

Steps

  • in_progress: Add shared runtime state and HTTP control panel server
  • pending: Expose control panel URL/port through CLI/config/inventory
  • pending: Update README/tests for UI-driven target switching
  • pending: Run verification and reinstall binaries

4. Plan

Source: codex - Captured: 2026-06-24T10:21:03.251Z

Codex Plan

Steps

  • completed: Add shared runtime state and HTTP control panel server
  • completed: Expose control panel URL/port through CLI/config/inventory
  • completed: Update README/tests for UI-driven target switching
  • completed: Run verification and reinstall binaries

5. Plan

Source: codex - Captured: 2026-06-24T10:38:17.658Z

Link-Based Remote Edge Sharing

Summary

  • Добавить новый target type: shared-extension, чтобы удаленный Edge подключался к агенту через расширение и relay-ссылку, без SSH/VPN/CDP-порта.
  • Пользовательский flow: установить Edge extension → нажать Share browser → получить одноразовую ссылку → вставить ссылку в browser-connection control panel → выбрать edge target.
  • Для v1 используем Extension only, Hosted relay, Session approve: это самый простой UX. Доступ ограничен возможностями extension API, не равен полному desktop/VNC.

Key Changes

  • Edge extension:
    • Manifest V3 extension с permissions debugger, tabs, activeTab, scripting, storage.
    • Popup UI: Share browser, Copy link, Stop sharing, статус подключения.
    • Service worker подключается наружу к relay по wss://...; входящие порты на машине пользователя не нужны.
    • Управление tab’ом через chrome.debugger/extension APIs: navigate, evaluate, click/type by selector, screenshot, snapshot.
  • Relay service:
    • Новый Rust binary/service browser-connection-relay.
    • Link format: https://relay.example.com/share/<session_id>#agent=<agent_token>.
    • WebSocket endpoints:
      • browser side: /ws/browser/<session_id>?token=[REDACTED]
      • agent side: /ws/agent/<session_id>?token=[REDACTED]
    • Relay только маршрутизирует JSON messages между extension и browser-connection; session TTL по умолчанию 30 минут.
  • browser-connection:
    • Добавить --browser-share NAME=SHARE_URL и UI поле Add shared browser link в control panel.
    • Inventory target shape: kind: "shared-extension", shareUrl, active, без cdpEndpoint.
    • MCP dispatch получает driver abstraction: existing CDP targets идут через CDP driver, shared extension targets идут через relay driver.
    • Existing noVNC iframe для CDP/VNC остается; для shared extension target control panel показывает live screenshot preview и active tab metadata.

Test Plan

  • Unit tests:
    • Parse/validate share links, token/session extraction, expired/invalid session errors.
    • Relay message routing: browser connects, agent connects, request/response correlation works.
    • MCP driver selection: CDP targets keep old behavior, shared targets route through extension driver.
  • Integration tests:
    • Start local relay, simulated extension client, browser-connection; add share link through API; select target; browser_list shows active shared Edge.
    • MCP tools over shared target: navigate, evaluate, click/type, screenshot.
    • Revocation: extension stops sharing; agent target becomes disconnected with clear error.
  • Browser/manual tests:
    • Sideload extension in Edge, click Share, paste link in control panel, verify agent actions affect the remote Edge tab.
    • Verify no public 9222 port, no SSH/VPN, no local inbound firewall rule.

Assumptions

  • First version is extension-only; native helper is deferred.
  • Hosted relay is the default architecture; self-host relay can reuse the same binary later.
  • Access is per-session, user-approved, revocable, and time-limited.
  • Full CDP/VNC parity is out of scope for v1 because chrome.debugger does not expose every CDP domain; this is documented by Chrome, and Edge extension APIs follow Chromium compatibility with Edge-specific support lists.
  • References: Edge extension API support, Chrome chrome.debugger, Edge native messaging.

@skulidropek

skulidropek commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 25fac54
Status: failure
Error: failed to create blob for skulidropek/docker-git-sessions:ProverCoderAI/rust-browser-connection/pr-15/current/.codex/sessions/2026/06/24/rollout-2026-06-24T09-49-34-019ef908-f57b-7ce3-99e5-7a4a2235b09e.jsonl: gh: Bad credentials (HTTP 401)

git status

On branch pr-14
Your branch is up to date with 'origin/pr-14'.

nothing to commit, working tree clean

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added browser target selection, including personal, managed, shared-link, and direct browser connections.
    • Introduced a local control panel for switching active browsers and approving share connections.
    • Added the standalone rbc browser automation CLI and a relay utility for shared browser sessions.
    • Expanded browser tools with tab listing, tab switching, screenshots, and shared-session Playwright support.
  • Bug Fixes

    • Improved browser connection handling, retries, and target resolution for more reliable automation.
    • Added support for loading and sharing Edge browser sessions through the extension.

Walkthrough

Adds named browser targets with runtime switching, a browser-connection-relay WebSocket relay binary, an Edge extension for shared browser sessions, per-target noVNC proxy containers, and MCP/CLI control-panel updates for selecting active browsers.

Changes

Named browser targets, shared relay, and control panel

Layer / File(s) Summary
Named browser target contracts
src/browser_target.rs, src/lib.rs
Defines NamedBrowserEndpoint, reserved browser names, target display structs, deterministic port helpers, endpoint parsing and normalization, env merging, and tests for target contracts.
Per-target noVNC proxy lifecycle
src/browser.rs, src/browser/assets.rs, src/browser/tests.rs
Adds noVNC proxy container/image setup, reuse checks, readiness probing, proxy build/start assets, and browser-side tests for proxy and Docker helper behavior.
Shared browser relay and client
src/shared_browser.rs, src/bin/browser-connection-relay.rs, Cargo.toml, tests/shared_browser_relay.rs
Adds the relay config/startup path, share URL parsing, relay endpoint/session handling, WebSocket forwarding, the shared-browser client API, the relay binary target, and relay integration coverage.
Edge share extension
extension/edge-share/*
Adds the extension manifest, popup, connect page, content bridge, provider API, and service worker for share/session state, relay connection, and command dispatch.
MCP runtime, tools, and activity
src/mcp.rs, src/mcp/activity.rs, src/mcp/tools.rs, src/mcp/tests.rs
Expands MCP config/runtime state, adds browser inventory and selection tools, records activity, and covers protocol, inventory, and browser-selection behavior.
CLI wiring, target resolution, and docs
src/bin/browser-connection.rs, src/bin/playwright/mod.rs, src/bin/rbc.rs, src/bin/target_resolution/mod.rs, tests/mcp_stdio.rs, tests/cli_status.rs, README.md, artifacts/README.md, extension/edge-share/README.md, skills/browser-cli/*, changelog.d/*
Updates the CLI flags and routing, adds target resolution and Playwright bridging, extends stdio/control-panel/relay integration tests, and documents the new browser target, relay, and extension flows.

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient as MCP Client
  participant MCPServer as browser-connection
  participant ControlPanel as Local Control Panel
  participant RelayServer as browser-connection-relay
  participant EdgeExtension as Edge Share Extension
  participant SharedClient as SharedBrowserClient

  MCPClient->>MCPServer: tools/call browser_select
  MCPServer->>MCPServer: register active browser
  ControlPanel->>MCPServer: POST /api/select
  MCPServer->>MCPServer: update active browser

  EdgeExtension->>RelayServer: connect browser session
  SharedClient->>RelayServer: connect agent session
  MCPClient->>MCPServer: tools/call browser_navigate
  MCPServer->>SharedClient: dispatch browser command
  SharedClient->>RelayServer: JSON command
  RelayServer->>EdgeExtension: forward command
  EdgeExtension-->>RelayServer: JSON response
  RelayServer-->>MCPServer: forward response
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐇 I hop through relays, swift and bright,
With browser names to choose just right.
A panel, proxy, and share-link glow,
While MCP tools help targets flow.
No restart needed, hop-hop hooray,
The browsers dance the rabbit way. 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is too vague to describe the changeset and does not convey the main change. Replace it with a concise, specific title naming the primary feature, such as browser share relay, personal browser targets, or Edge extension support.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description is related to the browser-target and share-relay work in the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-14

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 OpenGrep (1.23.0)
src/bin/target_resolution/mod.rs

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

[00.19][ERROR]: unable to find a config; path .coderabbit-opengrep-fallback.yml does not exist

src/bin/rbc.rs

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

[00.18][ERROR]: unable to find a config; path .coderabbit-opengrep-fallback.yml does not exist

extension/edge-share/service_worker.js

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

[00.28][ERROR]: unable to find a config; path .coderabbit-opengrep-fallback.yml does not exist

  • 9 others
🔧 markdownlint-cli2 (0.22.1)
README.md

markdownlint-cli2 v0.22.1 (markdownlint v0.40.0)
Error: Unable to use configuration file '/coderabbit-0.markdownlint-cli2.jsonc'; ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at throwForConfigurationFile (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:48:9)
at readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:169:5)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
[cause]: Error: ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at async open (node:internal/fs/promises:640:25)
at async Object.readFile (node:internal/fs/promises:1287:14)
at async readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:141:17)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/coderabbit-0.markdownlint-cli2.jsonc'
}
}

changelog.d/20260625_000000_rbc_cli.md

markdownlint-cli2 v0.22.1 (markdownlint v0.40.0)
Error: Unable to use configuration file '/coderabbit-0.markdownlint-cli2.jsonc'; ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at throwForConfigurationFile (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:48:9)
at readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:169:5)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
[cause]: Error: ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at async open (node:internal/fs/promises:640:25)
at async Object.readFile (node:internal/fs/promises:1287:14)
at async readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:141:17)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/coderabbit-0.markdownlint-cli2.jsonc'
}
}

extension/edge-share/README.md

markdownlint-cli2 v0.22.1 (markdownlint v0.40.0)
Error: Unable to use configuration file '/coderabbit-0.markdownlint-cli2.jsonc'; ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at throwForConfigurationFile (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:48:9)
at readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:169:5)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
[cause]: Error: ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at async open (node:internal/fs/promises:640:25)
at async Object.readFile (node:internal/fs/promises:1287:14)
at async readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:141:17)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/coderabbit-0.markdownlint-cli2.jsonc'
}
}

  • 1 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@extension/edge-share/README.md`:
- Around line 100-102: Clarify the manifest permissions guidance in the Edge
share README: the relay connection is not controlled by <all_urls>, so update
the wording around the extension’s permissions to distinguish page-level access
from relay WebSocket access. In the section describing production restrictions
for the edge-share extension, reference the relay connectivity behavior
explicitly and instruct readers to narrow
content_security_policy.extension_pages.connect-src to the known relay origins
rather than implying that <all_urls> governs relay access.

In `@extension/edge-share/service_worker.js`:
- Around line 437-466: The screenshot fallback in the `screenshot` handler
returns a different payload shape than the CDP path, so make the `catch` branch
match the success branch used in `sendCdp`/`Page.captureScreenshot`. In
`service_worker.js`, update the fallback that uses
`chrome.tabs.captureVisibleTab` to normalize the returned Data URL into the same
base64 field used by the main path (and keep the existing `mimeType`, `tab`,
`fallback`, and `warning` fields consistent), so callers always receive the
documented screenshot result shape regardless of which capture path succeeds.
- Around line 820-836: The normalizeRelayUrl helper currently accepts any http
URL, which should be restricted to loopback-only development use. Update
normalizeRelayUrl so it still allows https everywhere, but rejects non-loopback
http: relay URLs unless the hostname is localhost, 127.0.0.1, or ::1; keep the
existing validation and cleanup behavior in this function while enforcing the
scheme/host check before returning the normalized href.
- Around line 482-510: getTargetTabId() only updates the in-memory
state.activeTabId, so the selected tab is lost when the MV3 service worker
suspends. Update the selection paths in getTargetTabId() to persist the chosen
tab ID whenever it changes (for example, alongside the state.activeTabId
assignment), and restore it before using the fallback active-tab query so the
“last selected tab” behavior survives worker restarts.
- Around line 558-565: The attach error handling in chrome.debugger.attach is
incorrectly caching tabs even when attach failed, which causes sendCdp() to skip
retrying later. Update the try/catch around chrome.debugger.attach so
attachedTabs.add(tabId) only happens after a successful attach, and in the
“Another debugger is already attached” branch do not add the tab to
attachedTabs; instead leave it uncached so the next sendCdp() call can attempt
attach again.

In `@src/bin/browser-connection-relay.rs`:
- Around line 13-15: The `bind` argument in `browser-connection-relay` currently
shows a public plaintext address as a normal example, which can imply unsafe
defaults; update the `bind` field documentation and/or validation so that
non-loopback binds like public addresses are clearly marked as requiring TLS or
authenticated proxying, or require an explicit opt-in for insecure public binds.
Use the `bind` CLI argument and its surrounding `#[arg(...)]` metadata in
`browser-connection-relay` to add the warning or guard without presenting
`0.0.0.0:8765` as a safe default example.

In `@src/browser_target.rs`:
- Around line 345-361: The upsert_browser_share_url helper currently adds a
share_url onto an existing NamedBrowserEndpoint even when that endpoint already
has a cdp_endpoint, which allows a browser target to end up with both transport
modes at once. Update upsert_browser_share_url in src/browser_target.rs so
share-based and CDP-based routing are mutually exclusive: either prevent
attaching share_url to an existing CDP endpoint, or explicitly clear the
conflicting transport fields when setting share_url. Make sure the behavior
aligns with the dispatch logic in src/mcp.rs where share_url takes precedence.
- Around line 266-280: The normalize_share_url function currently only trims and
checks the scheme, which lets invalid relay share links slip through until
SharedBrowserClient is used. Update normalize_share_url to fully parse and
validate the share URL against the relay contract so malformed CLI/env/config
values fail early during startup or selection. Use the existing
normalize_share_url helper in src/browser_target.rs to enforce the expected
share-link structure, not just http/https/ws/wss, and return an error with the
parsed validation result when the link does not match.
- Around line 239-250: Update the endpoint normalization logic in the browser
target parsing path so IPv6 literals keep their brackets: the current host
cleanup in the endpoint handling code strips brackets from any host, which turns
inputs like [::1]:5900 into an ambiguous result. Adjust the parsing/formatting
in the endpoint normalization flow so host/port extraction still validates the
port, but the final normalized value preserves brackets when the host is an IPv6
address, while leaving regular hostnames and IPv4 unchanged.

In `@src/browser.rs`:
- Around line 128-136: The proxy reuse logic in BrowserTargetDisplayRuntime
currently only checks the VNC endpoint label, so it can incorrectly reuse a
running container even when the effective network mode has changed. Update the
reuse condition in novnc_proxy_matches and the related
BrowserTargetDisplayRuntime path so it also compares the target’s network
configuration before returning the existing container, ensuring a recreated
container is used whenever the network mode differs.

In `@src/mcp.rs`:
- Around line 242-252: Define and enforce a clear precedence for browser targets
that have both cdp_endpoint and share_url, since active_share_url() currently
wins implicitly and can override CDP routing. Update handle_tool_call() and
browser_endpoint_kind() so the dispatch path and reported kind match the chosen
rule, either by rejecting mixed targets during config handling or by making one
field explicitly take priority and reflecting that consistently across these
helpers.

In `@src/mcp/control_panel.rs`:
- Around line 148-158: The localhost control API is too permissive because
route_request currently allows GET for the state-changing /api/share and
/api/select endpoints, and the control panel exposes wildcard CORS. Restrict
these handlers to POST only in route_request, and update the response
headers/CORS behavior used by the embedded panel so it does not advertise
Access-Control-Allow-Origin: *; keep same-origin access for the
control_panel_html flow while preventing cross-site requests from mutating
McpRuntime via register_share_response and select_browser_response.

In `@src/shared_browser.rs`:
- Around line 193-236: The shared browser client in `SharedBrowser::call` is
swallowing relay registration failures by treating a closed socket as a generic
“relay closed” error. Update the relay handshake/registration path so failures
from `RelayEndpoint::parse` and `state.register` are surfaced back to the caller
as a structured error frame, or fail the connection before entering the read
loop. Make sure the `call` method reports the actual registration error instead
of only returning the close message, and apply the same fix to the other
affected relay path referenced in the review.
- Around line 451-453: The relay path in shared_browser’s connection handling is
busy-waiting: the short read timeout plus the timeout-retry loop in the
read/forward logic keeps idle browser/agent sessions spinning. Update the stream
handling around the setup and the retry loop in shared_browser so it blocks or
uses an evented wait instead of immediately retrying on read timeout, and make
sure the forwarding flow no longer treats idle timeouts as a fast poll. Focus on
the connection setup and the retry logic in the same relay functions that
configure the stream and process reads.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 94d71daa-27db-4ae0-abaa-7b1d4e78656e

📥 Commits

Reviewing files that changed from the base of the PR and between 8f0aa06 and 25fac54.

⛔ Files ignored due to path filters (1)
  • artifacts/edge-share-extension.tar.gz is excluded by !**/*.gz
📒 Files selected for processing (22)
  • Cargo.toml
  • README.md
  • artifacts/README.md
  • changelog.d/20260624_071400_personal_browser_targets.md
  • extension/edge-share/README.md
  • extension/edge-share/manifest.json
  • extension/edge-share/popup.html
  • extension/edge-share/popup.js
  • extension/edge-share/service_worker.js
  • src/bin/browser-connection-relay.rs
  • src/bin/browser-connection.rs
  • src/browser.rs
  • src/browser/assets.rs
  • src/browser/tests.rs
  • src/browser_target.rs
  • src/lib.rs
  • src/mcp.rs
  • src/mcp/control_panel.rs
  • src/mcp/tests.rs
  • src/shared_browser.rs
  • tests/mcp_stdio.rs
  • tests/shared_browser_relay.rs

Comment on lines +100 to +102
- The extension uses broad host permissions so it can connect to arbitrary relay
hosts during development. A production build should narrow this to known relay
origins.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Inspect the README section and related extension manifest/settings
git ls-files extension/edge-share
printf '\n--- README excerpt ---\n'
sed -n '80,130p' extension/edge-share/README.md
printf '\n--- manifest files containing connect-src / host_permissions / all_urls ---\n'
rg -n '"host_permissions"|"connect-src"|<all_urls>|websocket|relay' extension/edge-share -S

Repository: ProverCoderAI/rust-browser-connection

Length of output: 6001


🏁 Script executed:

# Read the relevant manifest and any docs that define extension network permissions
fd -a 'manifest.json|manifest.*.json|README.md' extension/edge-share

Repository: ProverCoderAI/rust-browser-connection

Length of output: 279


🏁 Script executed:

# Map the extension files if the previous searches are inconclusive
ast-grep outline extension/edge-share/README.md --view expanded

Repository: ProverCoderAI/rust-browser-connection

Length of output: 220


🏁 Script executed:

sed -n '1,120p' extension/edge-share/manifest.json

Repository: ProverCoderAI/rust-browser-connection

Length of output: 810


🏁 Script executed:

rg -n '"content_security_policy"|connect-src|host_permissions|permissions' extension/edge-share/manifest.json extension/edge-share/service_worker.js extension/edge-share/README.md -n -S

Repository: ProverCoderAI/rust-browser-connection

Length of output: 633


🏁 Script executed:

rg -n 'chrome\.scripting|executeScript|fetch\(|XMLHttpRequest|WebSocket|tabs\.|debugger|host_permissions' extension/edge-share/service_worker.js extension/edge-share/manifest.json -S

Repository: ProverCoderAI/rust-browser-connection

Length of output: 2668


🏁 Script executed:

sed -n '1,120p' extension/edge-share/manifest.json
printf '\n--- service_worker relay / page-access calls ---\n'
sed -n '1,260p' extension/edge-share/service_worker.js

Repository: ProverCoderAI/rust-browser-connection

Length of output: 6856


🏁 Script executed:

sed -n '1,120p' extension/edge-share/manifest.json
printf '\n--- service_worker relay / page-access calls ---\n'
sed -n '1,260p' extension/edge-share/service_worker.js

Repository: ProverCoderAI/rust-browser-connection

Length of output: 6856


Clarify <all_urls> vs relay access. <all_urls> is for page-level access (debugger, scripting, tabs), not the relay WebSocket. If the goal is to limit relay connectivity, narrow content_security_policy.extension_pages.connect-src to the known relay origins instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extension/edge-share/README.md` around lines 100 - 102, Clarify the manifest
permissions guidance in the Edge share README: the relay connection is not
controlled by <all_urls>, so update the wording around the extension’s
permissions to distinguish page-level access from relay WebSocket access. In the
section describing production restrictions for the edge-share extension,
reference the relay connectivity behavior explicitly and instruct readers to
narrow content_security_policy.extension_pages.connect-src to the known relay
origins rather than implying that <all_urls> governs relay access.

Comment on lines +437 to +466
try {
await sendCdp(tabId, "Page.enable", {});
const result = await sendCdp(tabId, "Page.captureScreenshot", {
format,
quality,
fromSurface: true,
captureBeyondViewport: params.fullPage === true
});

return {
tab: await currentTabSummary(tabId),
mimeType: `image/${format}`,
data: result.data
};
} catch (error) {
const tab = await chromeCall(chrome.tabs.get, chrome.tabs, tabId);
await chromeCall(chrome.tabs.update, chrome.tabs, tabId, { active: true });
const dataUrl = await chromeCall(chrome.tabs.captureVisibleTab, chrome.tabs, tab.windowId, {
format,
quality
});

return {
tab: tabSummary(tab),
mimeType: `image/${format}`,
dataUrl,
fallback: "tabs.captureVisibleTab",
warning: errorMessage(error)
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep the screenshot payload shape consistent on fallback.

Line 449 returns raw base64 in data, but Line 462 switches to a full Data URL in dataUrl. That makes the protocol branch-dependent for pages where the CDP capture path is rejected, even though the README documents a base64 screenshot result.

Proposed fix
   } catch (error) {
     const tab = await chromeCall(chrome.tabs.get, chrome.tabs, tabId);
     await chromeCall(chrome.tabs.update, chrome.tabs, tabId, { active: true });
     const dataUrl = await chromeCall(chrome.tabs.captureVisibleTab, chrome.tabs, tab.windowId, {
       format,
       quality
     });
+    const match = /^data:([^;]+);base64,(.*)$/.exec(dataUrl);
+    if (!match) {
+      throw new Error("Unexpected screenshot payload from captureVisibleTab");
+    }

     return {
       tab: tabSummary(tab),
-      mimeType: `image/${format}`,
-      dataUrl,
+      mimeType: match[1],
+      data: match[2],
       fallback: "tabs.captureVisibleTab",
       warning: errorMessage(error)
     };
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
await sendCdp(tabId, "Page.enable", {});
const result = await sendCdp(tabId, "Page.captureScreenshot", {
format,
quality,
fromSurface: true,
captureBeyondViewport: params.fullPage === true
});
return {
tab: await currentTabSummary(tabId),
mimeType: `image/${format}`,
data: result.data
};
} catch (error) {
const tab = await chromeCall(chrome.tabs.get, chrome.tabs, tabId);
await chromeCall(chrome.tabs.update, chrome.tabs, tabId, { active: true });
const dataUrl = await chromeCall(chrome.tabs.captureVisibleTab, chrome.tabs, tab.windowId, {
format,
quality
});
return {
tab: tabSummary(tab),
mimeType: `image/${format}`,
dataUrl,
fallback: "tabs.captureVisibleTab",
warning: errorMessage(error)
};
}
try {
await sendCdp(tabId, "Page.enable", {});
const result = await sendCdp(tabId, "Page.captureScreenshot", {
format,
quality,
fromSurface: true,
captureBeyondViewport: params.fullPage === true
});
return {
tab: await currentTabSummary(tabId),
mimeType: `image/${format}`,
data: result.data
};
} catch (error) {
const tab = await chromeCall(chrome.tabs.get, chrome.tabs, tabId);
await chromeCall(chrome.tabs.update, chrome.tabs, tabId, { active: true });
const dataUrl = await chromeCall(chrome.tabs.captureVisibleTab, chrome.tabs, tab.windowId, {
format,
quality
});
const match = /^data:([^;]+);base64,(.*)$/.exec(dataUrl);
if (!match) {
throw new Error("Unexpected screenshot payload from captureVisibleTab");
}
return {
tab: tabSummary(tab),
mimeType: match[1],
data: match[2],
fallback: "tabs.captureVisibleTab",
warning: errorMessage(error)
};
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extension/edge-share/service_worker.js` around lines 437 - 466, The
screenshot fallback in the `screenshot` handler returns a different payload
shape than the CDP path, so make the `catch` branch match the success branch
used in `sendCdp`/`Page.captureScreenshot`. In `service_worker.js`, update the
fallback that uses `chrome.tabs.captureVisibleTab` to normalize the returned
Data URL into the same base64 field used by the main path (and keep the existing
`mimeType`, `tab`, `fallback`, and `warning` fields consistent), so callers
always receive the documented screenshot result shape regardless of which
capture path succeeds.

Comment on lines +482 to +510
async function getTargetTabId(params) {
if (Number.isInteger(params.tabId)) {
state.activeTabId = params.tabId;
return params.tabId;
}

if (Number.isInteger(state.activeTabId)) {
try {
await chromeCall(chrome.tabs.get, chrome.tabs, state.activeTabId);
return state.activeTabId;
} catch (_error) {
state.activeTabId = null;
}
}

const activeTabs = await chromeCall(chrome.tabs.query, chrome.tabs, {
active: true,
lastFocusedWindow: true
});

if (activeTabs.length > 0 && Number.isInteger(activeTabs[0].id)) {
state.activeTabId = activeTabs[0].id;
return activeTabs[0].id;
}

const tabs = await chromeCall(chrome.tabs.query, chrome.tabs, { active: true });
if (tabs.length > 0 && Number.isInteger(tabs[0].id)) {
state.activeTabId = tabs[0].id;
return tabs[0].id;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Persist activeTabId when selection changes.

getTargetTabId() only mutates in-memory state. In an MV3 service worker that means the documented "last selected tab" behavior is lost after suspension, and the next command can silently target whatever tab is currently active instead.

Proposed fix
 async function getTargetTabId(params) {
   if (Number.isInteger(params.tabId)) {
-    state.activeTabId = params.tabId;
+    await persistState({ activeTabId: params.tabId });
     return params.tabId;
   }
@@
   if (activeTabs.length > 0 && Number.isInteger(activeTabs[0].id)) {
-    state.activeTabId = activeTabs[0].id;
+    await persistState({ activeTabId: activeTabs[0].id });
     return activeTabs[0].id;
   }
@@
   if (tabs.length > 0 && Number.isInteger(tabs[0].id)) {
-    state.activeTabId = tabs[0].id;
+    await persistState({ activeTabId: tabs[0].id });
     return tabs[0].id;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function getTargetTabId(params) {
if (Number.isInteger(params.tabId)) {
state.activeTabId = params.tabId;
return params.tabId;
}
if (Number.isInteger(state.activeTabId)) {
try {
await chromeCall(chrome.tabs.get, chrome.tabs, state.activeTabId);
return state.activeTabId;
} catch (_error) {
state.activeTabId = null;
}
}
const activeTabs = await chromeCall(chrome.tabs.query, chrome.tabs, {
active: true,
lastFocusedWindow: true
});
if (activeTabs.length > 0 && Number.isInteger(activeTabs[0].id)) {
state.activeTabId = activeTabs[0].id;
return activeTabs[0].id;
}
const tabs = await chromeCall(chrome.tabs.query, chrome.tabs, { active: true });
if (tabs.length > 0 && Number.isInteger(tabs[0].id)) {
state.activeTabId = tabs[0].id;
return tabs[0].id;
async function getTargetTabId(params) {
if (Number.isInteger(params.tabId)) {
await persistState({ activeTabId: params.tabId });
return params.tabId;
}
if (Number.isInteger(state.activeTabId)) {
try {
await chromeCall(chrome.tabs.get, chrome.tabs, state.activeTabId);
return state.activeTabId;
} catch (_error) {
state.activeTabId = null;
}
}
const activeTabs = await chromeCall(chrome.tabs.query, chrome.tabs, {
active: true,
lastFocusedWindow: true
});
if (activeTabs.length > 0 && Number.isInteger(activeTabs[0].id)) {
await persistState({ activeTabId: activeTabs[0].id });
return activeTabs[0].id;
}
const tabs = await chromeCall(chrome.tabs.query, chrome.tabs, { active: true });
if (tabs.length > 0 && Number.isInteger(tabs[0].id)) {
await persistState({ activeTabId: tabs[0].id });
return tabs[0].id;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extension/edge-share/service_worker.js` around lines 482 - 510,
getTargetTabId() only updates the in-memory state.activeTabId, so the selected
tab is lost when the MV3 service worker suspends. Update the selection paths in
getTargetTabId() to persist the chosen tab ID whenever it changes (for example,
alongside the state.activeTabId assignment), and restore it before using the
fallback active-tab query so the “last selected tab” behavior survives worker
restarts.

Comment on lines +558 to +565
try {
await chromeCall(chrome.debugger.attach, chrome.debugger, { tabId }, "1.3");
attachedTabs.add(tabId);
} catch (error) {
if (String(errorMessage(error)).includes("Another debugger is already attached")) {
attachedTabs.add(tabId);
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant file and surrounding logic.
git ls-files 'extension/edge-share/service_worker.js'
ast-grep outline extension/edge-share/service_worker.js --view expanded

# Show the relevant lines and nearby helpers.
sed -n '520,640p' extension/edge-share/service_worker.js

# Find all references to attachedTabs and debugger attach/send paths.
rg -n 'attachedTabs|chrome\.debugger\.attach|chrome\.debugger\.sendCommand|sendCdp\(' extension/edge-share/service_worker.js

Repository: ProverCoderAI/rust-browser-connection

Length of output: 6525


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the initialization and any debugger detach event handling.
sed -n '1,80p' extension/edge-share/service_worker.js

# Search for onDetach / detachment-related cleanup.
rg -n 'onDetach|detach|attachedTabs\.delete|attachedTabs\.clear|debugger' extension/edge-share/service_worker.js

Repository: ProverCoderAI/rust-browser-connection

Length of output: 2839


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect initialization and debugger detach handling.
sed -n '1,80p' extension/edge-share/service_worker.js

# Find all cleanup paths that may clear attachedTabs.
rg -n 'onDetach|attachedTabs\.delete|attachedTabs\.clear|chrome\.debugger|detachAllDebuggers' extension/edge-share/service_worker.js

Repository: ProverCoderAI/rust-browser-connection

Length of output: 2687


🌐 Web query:

chrome.debugger.onDetach event semantics another debugger already attached attach failure attachedTabs cache chrome extension debugger

💡 Result:

The chrome.debugger.onDetach event is triggered when the Chrome browser terminates the debugging session for a specific target (such as a tab) [1]. This termination typically occurs due to one of two main scenarios: the target (tab) is being closed, or the user has manually invoked the Chrome DevTools for the already-attached tab [1]. Regarding attachment conflicts: 1. Exclusivity: Chrome supports only one active debugger attachment per target at any given time [2]. If your extension attempts to call chrome.debugger.attach on a target that is already being debugged by another process or another instance of the developer tools, the operation will fail [2]. 2. Error Handling: When an attachment attempt fails—including when another debugger is already attached—the promise returned by chrome.debugger.attach will be rejected [3][1]. If using the older callback-based syntax, chrome.extension.lastError will be set to the corresponding error message [4][3]. 3. Handling Detachment: Because onDetach signals the end of your debugging session, it is standard practice to use this event to clean up any internal state, stop associated monitoring, or update the user interface to reflect that the extension is no longer attached to that specific tab [1]. To avoid issues, always ensure your extension handles the onDetach event gracefully and checks the current attachment status if necessary before attempting to attach. Note that there is no public API to "force" the detachment of an existing debugger from another process; you must wait for the existing session to terminate or handle the error resulting from the conflict [2][3].

Citations:


🌐 Web query:

chrome.debugger.onDetach event semantics another debugger already attached attach failure attachedTabs cache chrome extension debugger

💡 Result:

In the Chrome Debugger API, the chrome.debugger.onDetach event is fired when a debugging session for a specific tab or target is terminated [1]. Understanding the semantics of this event and the behavior of attachment failures is essential for maintaining a stable extension [2][3]. Key Semantics and Failure Modes: 1. Detachment Behavior: The onDetach event provides a reason for the termination [1]. Common reasons include the tab being closed, the user invoking Chrome DevTools on the same tab (as Chrome only allows one primary debugger connection at a time), or internal browser events [1][3]. When a session is detached, the extension must handle cleanup to prevent errors in subsequent commands, such as those attempting to use a stale connection [2][3]. 2. "Another Debugger Already Attached": Chrome generally permits only one debugger to be attached to a given target at a time [1]. If an extension attempts to attach to a tab that is already being debugged—either by another extension, an internal process, or the user via the F12 DevTools—the chrome.debugger.attach method will fail [1][4]. 3. Handling Attachment Failures: - Race Conditions: Extensions often encounter race conditions when attempting to attach to a new tab immediately after creation [5]. It is recommended to wait for the tab to commit (or navigate to a stable state) before calling attach [5]. - Persistent Monitoring: To handle unexpected detachments (e.g., when a user opens DevTools or a page navigates to a privileged chrome:// URL), many developers implement a retry mechanism [3]. By listening to onDetach, an extension can log the event and, if appropriate, attempt to re-establish the connection after a short delay [3]. - Troubleshooting: If "Cannot attach to this target" errors persist, ensure no other extensions are interfering and check that your extension has the necessary manifest permissions [4]. In some cases, the debugger may detach automatically due to internal browser navigation constraints [2][3]. For robust implementation, always check chrome.runtime.lastError when calling attach or sendCommand to gracefully handle and log connection failures [5][2][3].

Citations:


🌐 Web query:

chrome.debugger.onDetach event semantics another debugger already attached attach failure attachedTabs cache chrome extension debugger

💡 Result:

The chrome.debugger.onDetach event is fired when a debugging session is terminated by the browser [1]. This occurs primarily in two scenarios: when the target tab or extension is closed, or when the user opens the Chrome DevTools window for the same tab, which force-detaches any existing debugger [1]. Regarding attachment failures and the error message Another debugger is already attached to the tab with id, note the following: 1. Exclusivity: Chrome only allows a single debugger instance to be attached to a specific target (e.g., a tab) at any given time [1]. If your extension attempts to call chrome.debugger.attach on a tab that is already being debugged—either by another extension or by a user-opened DevTools window—the attachment operation will fail [2][3]. 2. Error Handling: When attach fails due to an existing debugger or other reasons, the promise returned by the method will be rejected (or the callback will receive an error if not using Promises), and chrome.runtime.lastError will contain the specific error message [1]. 3. Handling Detachment: Developers should listen for the onDetach event to clean up local state or inform the user that debugging has ended [1]. Because the browser may detach the extension's debugger without notice (e.g., when a user opens the DevTools UI), relying on onDetach is essential for maintaining a synchronized state [1]. 4. Prevention and Recovery: If your extension encounters this error, it is standard practice to verify if the tab is already being debugged or to implement logic to gracefully handle the rejection [3]. You cannot force-attach if another entity (like the built-in DevTools) is already holding the debugging lock [1]. Always ensure your background script is robust enough to handle unexpected detachment and subsequent re-attachment attempts [3].

Citations:


Do not cache tabs after a failed attach
If chrome.debugger.attach returns Another debugger is already attached, this tab still isn’t attached. Adding it to attachedTabs makes sendCdp() skip the next attach attempt, so the tab stays unusable until this cache is cleared.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extension/edge-share/service_worker.js` around lines 558 - 565, The attach
error handling in chrome.debugger.attach is incorrectly caching tabs even when
attach failed, which causes sendCdp() to skip retrying later. Update the
try/catch around chrome.debugger.attach so attachedTabs.add(tabId) only happens
after a successful attach, and in the “Another debugger is already attached”
branch do not add the tab to attachedTabs; instead leave it uncached so the next
sendCdp() call can attempt attach again.

Comment on lines +820 to +836
function normalizeRelayUrl(input) {
const raw = String(input || "").trim();
if (!raw) {
throw new Error("Relay URL is required");
}

const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `https://${raw}`;
const url = new URL(withScheme);

if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error("Relay URL must use http or https");
}

url.search = "";
url.hash = "";
url.pathname = url.pathname.replace(/\/+$/, "");
return url.href.replace(/\/$/, "");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject cleartext relay URLs outside local development.

Accepting arbitrary http: relays means the browser token, agent token, and live control traffic can all be observed or tampered with on the network. This should be limited to loopback development URLs and require https: everywhere else.

Proposed fix
 function normalizeRelayUrl(input) {
@@
   const url = new URL(withScheme);

-  if (url.protocol !== "http:" && url.protocol !== "https:") {
+  if (url.protocol !== "http:" && url.protocol !== "https:") {
     throw new Error("Relay URL must use http or https");
   }
+  const isLoopback =
+    url.hostname === "127.0.0.1" ||
+    url.hostname === "localhost" ||
+    url.hostname === "::1" ||
+    url.hostname === "[::1]";
+  if (url.protocol === "http:" && !isLoopback) {
+    throw new Error("Remote relay URLs must use https");
+  }

   url.search = "";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function normalizeRelayUrl(input) {
const raw = String(input || "").trim();
if (!raw) {
throw new Error("Relay URL is required");
}
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `https://${raw}`;
const url = new URL(withScheme);
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error("Relay URL must use http or https");
}
url.search = "";
url.hash = "";
url.pathname = url.pathname.replace(/\/+$/, "");
return url.href.replace(/\/$/, "");
function normalizeRelayUrl(input) {
const raw = String(input || "").trim();
if (!raw) {
throw new Error("Relay URL is required");
}
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `https://${raw}`;
const url = new URL(withScheme);
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error("Relay URL must use http or https");
}
const isLoopback =
url.hostname === "127.0.0.1" ||
url.hostname === "localhost" ||
url.hostname === "::1" ||
url.hostname === "[::1]";
if (url.protocol === "http:" && !isLoopback) {
throw new Error("Remote relay URLs must use https");
}
url.search = "";
url.hash = "";
url.pathname = url.pathname.replace(/\/+$/, "");
return url.href.replace(/\/$/, "");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extension/edge-share/service_worker.js` around lines 820 - 836, The
normalizeRelayUrl helper currently accepts any http URL, which should be
restricted to loopback-only development use. Update normalizeRelayUrl so it
still allows https everywhere, but rejects non-loopback http: relay URLs unless
the hostname is localhost, 127.0.0.1, or ::1; keep the existing validation and
cleanup behavior in this function while enforcing the scheme/host check before
returning the normalized href.

Comment thread src/browser.rs
Comment on lines +128 to +136
let state = inspect_container_state(&spec.container_name)?;
match state.as_deref() {
Some("running") if novnc_proxy_matches(spec)? => {
let novnc_url = wait_for_novnc_proxy(spec)?;
return Ok(BrowserTargetDisplayRuntime {
container_name: spec.container_name.clone(),
vnc_endpoint: spec.vnc_endpoint.clone(),
novnc_url,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Include network configuration in the proxy reuse check.

Reuse currently keys only on the VNC endpoint label. If the target's network mode changes between runs, this path will keep the old container even though it is attached to the wrong network, and the readiness probe just times out against that stale proxy. Compare the effective network mode too before deciding not to recreate.

Also applies to: 342-347

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/browser.rs` around lines 128 - 136, The proxy reuse logic in
BrowserTargetDisplayRuntime currently only checks the VNC endpoint label, so it
can incorrectly reuse a running container even when the effective network mode
has changed. Update the reuse condition in novnc_proxy_matches and the related
BrowserTargetDisplayRuntime path so it also compares the target’s network
configuration before returning the existing container, ensuring a recreated
container is used whenever the network mode differs.

Comment thread src/mcp.rs
Comment thread src/mcp/control_panel.rs Outdated
Comment thread src/shared_browser.rs
Comment on lines +193 to +236
fn call(&self, command: &str, params: Value) -> Result<Value> {
let websocket_url = agent_ws_url_from_share_url(&self.share_url)?;
let (mut socket, _) = connect(&websocket_url).with_context(|| {
format!("failed to connect to shared browser relay {websocket_url}")
})?;
let request = json!({ "id": 1, "command": command, "params": params });
socket
.send(Message::Text(request.to_string()))
.with_context(|| format!("failed to send shared browser command {command}"))?;

loop {
match socket
.read()
.with_context(|| format!("failed to read shared browser response for {command}"))?
{
Message::Text(text) => {
let value: Value = serde_json::from_str(&text).with_context(|| {
format!("shared browser response for {command} was not JSON")
})?;
if value.get("id").and_then(Value::as_i64) != Some(1) {
continue;
}
if value.get("ok").and_then(Value::as_bool) == Some(false) {
let error = value
.get("error")
.and_then(Value::as_str)
.unwrap_or("shared browser command failed");
return Err(anyhow!("{error}"));
}
return Ok(value.get("result").cloned().unwrap_or(Value::Null));
}
Message::Ping(payload) => {
socket
.send(Message::Pong(payload))
.context("failed to answer shared browser relay ping")?;
}
Message::Close(_) => {
return Err(anyhow!(
"shared browser relay closed before {command} response"
))
}
_ => {}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Relay registration failures are hidden from the agent.

Here the socket handshake succeeds before RelayEndpoint::parse/state.register, and any failure is then dropped by the client thread. The caller only gets shared browser relay closed before ... response, so bad share links and expired sessions become almost impossible to diagnose through the MCP path. Please return a structured error frame (or reject earlier) instead of silently closing.

Also applies to: 458-476

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared_browser.rs` around lines 193 - 236, The shared browser client in
`SharedBrowser::call` is swallowing relay registration failures by treating a
closed socket as a generic “relay closed” error. Update the relay
handshake/registration path so failures from `RelayEndpoint::parse` and
`state.register` are surfaced back to the caller as a structured error frame, or
fail the connection before entering the read loop. Make sure the `call` method
reports the actual registration error instead of only returning the close
message, and apply the same fix to the other affected relay path referenced in
the review.

Comment thread src/shared_browser.rs
Comment on lines +451 to +453
.set_read_timeout(Some(Duration::from_millis(100)))
.ok();
stream.set_write_timeout(Some(Duration::from_secs(5))).ok();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Idle relay connections spin forever.

Line 451 turns every client thread into a 100ms polling loop, and Lines 520-521 immediately retry on timeout. With MAX_SESSIONS at 1024, idle browser/agent pairs can create thousands of runnable threads and tens of thousands of wakeups per second. Please switch this path to a blocking/evented design so idle connections do not burn CPU.

Also applies to: 497-525

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared_browser.rs` around lines 451 - 453, The relay path in
shared_browser’s connection handling is busy-waiting: the short read timeout
plus the timeout-retry loop in the read/forward logic keeps idle browser/agent
sessions spinning. Update the stream handling around the setup and the retry
loop in shared_browser so it blocks or uses an evented wait instead of
immediately retrying on read timeout, and make sure the forwarding flow no
longer treats idle timeouts as a fast poll. Focus on the connection setup and
the retry logic in the same relay functions that configure the stream and
process reads.

@skulidropek

skulidropek commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 3b4f970
Status: success
Files: 14 (9.04 MB)
Links: README | Manifest

git status

On branch pr-14
Your branch is up to date with 'origin/pr-14'.

nothing to commit, working tree clean

@skulidropek

Copy link
Copy Markdown
Member Author

Agent Plan Update

Branch: pr-14 at 3b4f970.

1. Planning Decision

Source: codex - Captured: 2026-06-25T06:00:16Z

Questions:

  • | page asks installed extension: connect this origin/workspace/browser pool?

Answer:
Давай вариант B

@skulidropek

Copy link
Copy Markdown
Member Author

Agent Plan Update

Branch: pr-14 at 79fe825.

1. Plan

Source: codex - Captured: 2026-06-25T06:29:16.269Z

noVNC-Control Panel Edge Connect

Summary

Да, это можно сделать на уровне текущего noVNC/control panel без обязательного обновления docker-git: пользователь открывает wrapper-панель noVNC в своём Edge, панель автоматически вызывает расширение, расширение показывает approve, после approve Edge подключается к relay, а панель регистрирует shareUrl в текущем browser-connection runtime через /api/share.

Key Changes

  • Сделать control panel главным entrypoint: при открытии страницы она детектит window.browserConnection, один раз auto-триггерит request({ method: "connect" }), показывает статус и оставляет кнопку Connect Edge как fallback.
  • Встроить relay WebSocket endpoints в тот же control panel origin: /ws/browser/<session> и /ws/agent/<session>. Тогда расширение может использовать relayUrl = location.origin, без хардкода и без отдельной Cloudflare/relay ссылки для local v1.
  • Довести Edge extension flow: content script/provider, connect.html/connect.js approve window, service worker connect request, возврат { shareUrl, sessionId, browserName }.
  • После approve панель делает POST /api/share с именем edge и полученным shareUrl, затем обновляет inventory и выбирает этот browser target.
  • Добавить CSRF/control token для mutating API (/api/share, /api/select), потому что панель потенциально может быть открыта/проксирована наружу.
  • Оставить browser-connection-relay как отдельный binary для старого/manual режима, но новый noVNC flow использует same-origin relay панели.

Test Plan

  • cargo fmt --check
  • cargo check --locked --bins
  • cargo test --locked
  • cargo clippy --locked --all-targets -- -D warnings
  • node --check для extension JS файлов и parse manifest.
  • Smoke: открыть control panel в Edge с расширением, получить approve, подтвердить, увидеть edge в browser list, выполнить MCP snapshot/evaluate.
  • Smoke через публичный/proxied URL с WebSocket upgrade: расширение должно подключаться к origin открытой панели.

Assumptions

  • v1 регистрирует Edge в текущий browser-connection runtime, не в глобальный platform pool.
  • Пользователь открывает именно wrapper control panel/noVNC URL в своём Edge; raw upstream noVNC URL сам расширение не триггерит.
  • Для другой машины URL панели должен быть доступен с этой машины по HTTPS/WSS или через reverse proxy/tunnel; расширение само туннель не создаёт.

2. Plan

Source: codex - Captured: 2026-06-25T06:39:25.841Z

Codex Plan

Steps

  • in_progress: Inspect current partial changes and relay/control-panel internals
  • pending: Implement integrated same-origin relay and control-panel connect UI
  • pending: Finish Edge extension approval flow and artifact packaging
  • pending: Run full checks and smoke validation
  • pending: Commit/push PR updates if checks pass

3. Plan

Source: codex - Captured: 2026-06-25T06:46:20.879Z

Codex Plan

Steps

  • completed: Inspect current partial changes and relay/control-panel internals
  • completed: Implement integrated same-origin relay and control-panel connect UI
  • in_progress: Finish Edge extension approval flow and artifact packaging
  • pending: Run full checks and smoke validation
  • pending: Commit/push PR updates if checks pass

4. Plan

Source: codex - Captured: 2026-06-25T06:59:49.744Z

Codex Plan

Steps

  • completed: Inspect current partial changes and relay/control-panel internals
  • completed: Implement integrated same-origin relay and control-panel connect UI
  • completed: Finish Edge extension approval flow and artifact packaging
  • completed: Run full checks and smoke validation
  • in_progress: Commit/push PR updates if checks pass

@skulidropek

skulidropek commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 79fe825
Status: success
Files: 17 (12.52 MB)
Links: README | Manifest

git status

On branch pr-14
Your branch is up to date with 'origin/pr-14'.

nothing to commit, working tree clean

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@extension/edge-share/connect.js`:
- Around line 57-67: The reject() flow in connect.js closes the popup in a
finally block, which can hide failures and close the UI even if sendMessage({
type: "reject_platform_connect", requestId }) fails. Update reject() so
window.close() runs only after sendMessage succeeds, and let any error from
sendMessage surface instead of being swallowed; keep the busy state handling
around the async call.

In `@extension/edge-share/service_worker.js`:
- Around line 117-122: The shared session is losing the requesting tab identity
because `handlePlatformRequest()` captures `sender.tab.id` but
`approvePlatformConnect()` and `startShare()` do not carry it through and reset
`activeTabId` to null. Update the session payload flow so the tab id captured in
`handlePlatformRequest()` is passed into `approvePlatformConnect()` and
preserved by `startShare()` when initializing the shared state, using the
`activeTabId` field instead of clearing it.

In `@src/mcp/control_panel.rs`:
- Around line 203-206: Reject non-loopback Host values before serving the
token-bearing panel or API routes. In the request handling flow around match on
request.method and request_path, add a Host validation step that only allows
loopback/local hosts before calling control_panel_response or
browser_inventory_response, and return a rejection for any other host to prevent
DNS-rebinding/origin-confusion exposure.
- Around line 56-62: The 3-second read/write timeouts are being applied before
the relay WebSocket path in control_panel.rs, which can prematurely disconnect
idle BrowserShareRelay sessions. Update the logic around
is_relay_websocket_request and BrowserShareRelay::handle_stream so the timeout
is either cleared/reset before handing off the stream or only applied on the
HTTP request path, keeping the relay WebSocket connection free of the short
timeout.
- Around line 420-425: The control token generator in generate_control_token
currently reads from /dev/urandom, which is Linux-only and breaks startup on
Windows. Replace the File::open("/dev/urandom").read_exact(...) path with an
OS-agnostic entropy source such as getrandom::getrandom, keeping the existing
context error handling and token hex-encoding logic in generate_control_token.
Also add the getrandom dependency to Cargo.toml so the control panel works
across the full CI matrix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d391d060-0a7f-4744-8ca7-53dc4f955964

📥 Commits

Reviewing files that changed from the base of the PR and between 25fac54 and 79fe825.

⛔ Files ignored due to path filters (2)
  • artifacts/edge-share-extension.tar.gz is excluded by !**/*.gz
  • artifacts/edge-share-extension.zip is excluded by !**/*.zip
📒 Files selected for processing (13)
  • README.md
  • artifacts/README.md
  • extension/edge-share/README.md
  • extension/edge-share/connect.html
  • extension/edge-share/connect.js
  • extension/edge-share/content_script.js
  • extension/edge-share/manifest.json
  • extension/edge-share/popup.js
  • extension/edge-share/provider.js
  • extension/edge-share/service_worker.js
  • src/mcp/control_panel.rs
  • src/shared_browser.rs
  • tests/mcp_stdio.rs
✅ Files skipped from review due to trivial changes (4)
  • artifacts/README.md
  • extension/edge-share/connect.html
  • extension/edge-share/README.md
  • README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • extension/edge-share/manifest.json
  • extension/edge-share/popup.js
  • src/shared_browser.rs

Comment on lines +57 to +67
async function reject() {
setBusy(true);

try {
await sendMessage({
type: "reject_platform_connect",
requestId
});
} finally {
window.close();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep the popup open until rejection succeeds.

finally { window.close(); } hides any rejection failure and can leave the connect request pending even though the UI disappears. Only close after a successful reject_platform_connect, and surface the error if the message fails.

Suggested fix
 async function reject() {
   setBusy(true);
 
   try {
     await sendMessage({
       type: "reject_platform_connect",
       requestId
     });
-  } finally {
     window.close();
+  } catch (error) {
+    showError(error);
+    setBusy(false);
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function reject() {
setBusy(true);
try {
await sendMessage({
type: "reject_platform_connect",
requestId
});
} finally {
window.close();
}
async function reject() {
setBusy(true);
try {
await sendMessage({
type: "reject_platform_connect",
requestId
});
window.close();
} catch (error) {
showError(error);
setBusy(false);
}
}
🧰 Tools
🪛 ast-grep (0.44.0)

[error] 57-57: React's useState should not be directly called
Context: setBusy(true)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extension/edge-share/connect.js` around lines 57 - 67, The reject() flow in
connect.js closes the popup in a finally block, which can hide failures and
close the UI even if sendMessage({ type: "reject_platform_connect", requestId })
fails. Update reject() so window.close() runs only after sendMessage succeeds,
and let any error from sendMessage surface instead of being swallowed; keep the
busy state handling around the async call.

Comment thread extension/edge-share/service_worker.js
Comment thread src/mcp/control_panel.rs
Comment on lines +56 to +62
stream.set_read_timeout(Some(Duration::from_secs(3))).ok();
stream.set_write_timeout(Some(Duration::from_secs(3))).ok();

if is_relay_websocket_request(&stream)? {
return relay
.handle_stream(stream)
.context("failed to handle embedded browser share relay client");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Don’t carry the 3-second HTTP timeout into relay WebSockets.

Line 56 sets timeouts before Line 59 hands relay requests to BrowserShareRelay; an idle shared-browser session can fail after 3 seconds. Clear/reset the timeouts before handle_stream, or only apply them to the HTTP request path.

Suggested fix
     if is_relay_websocket_request(&stream)? {
+        stream.set_read_timeout(None).ok();
+        stream.set_write_timeout(None).ok();
         return relay
             .handle_stream(stream)
             .context("failed to handle embedded browser share relay client");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
stream.set_read_timeout(Some(Duration::from_secs(3))).ok();
stream.set_write_timeout(Some(Duration::from_secs(3))).ok();
if is_relay_websocket_request(&stream)? {
return relay
.handle_stream(stream)
.context("failed to handle embedded browser share relay client");
stream.set_read_timeout(Some(Duration::from_secs(3))).ok();
stream.set_write_timeout(Some(Duration::from_secs(3))).ok();
if is_relay_websocket_request(&stream)? {
stream.set_read_timeout(None).ok();
stream.set_write_timeout(None).ok();
return relay
.handle_stream(stream)
.context("failed to handle embedded browser share relay client");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/control_panel.rs` around lines 56 - 62, The 3-second read/write
timeouts are being applied before the relay WebSocket path in control_panel.rs,
which can prematurely disconnect idle BrowserShareRelay sessions. Update the
logic around is_relay_websocket_request and BrowserShareRelay::handle_stream so
the timeout is either cleared/reset before handing off the stream or only
applied on the HTTP request path, keeping the relay WebSocket connection free of
the short timeout.

Comment thread src/mcp/control_panel.rs
Comment on lines +203 to +206
match (request.method.as_str(), request_path(&request.target)) {
("OPTIONS", _) => empty_response(204, "No Content"),
("GET", "/") | ("GET", "/index.html") => control_panel_response(runtime, control_token),
("GET", "/api/browsers") => browser_inventory_response(runtime),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Validate Host before serving the token-bearing panel.

GET / returns HTML containing controlToken, but routing currently accepts any Host value. Reject non-loopback hosts before serving the panel or APIs to reduce DNS-rebinding/origin-confusion exposure.

Suggested fix
 fn route_request(
     runtime: Arc<Mutex<McpRuntime>>,
     control_token: &str,
     request: &HttpRequest,
 ) -> HttpResponse {
+    if !has_valid_control_host(request) {
+        return json_response(403, "Forbidden", json!({ "error": "invalid host" }));
+    }
+
     match (request.method.as_str(), request_path(&request.target)) {
+fn has_valid_control_host(request: &HttpRequest) -> bool {
+    let Some(host) = request.header("host") else {
+        return false;
+    };
+    let host = host
+        .trim()
+        .rsplit_once(':')
+        .map(|(name, _)| name)
+        .unwrap_or(host.trim());
+    matches!(host, "127.0.0.1" | "localhost")
+}
+
 impl HttpRequest {

Also applies to: 238-243

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/control_panel.rs` around lines 203 - 206, Reject non-loopback Host
values before serving the token-bearing panel or API routes. In the request
handling flow around match on request.method and request_path, add a Host
validation step that only allows loopback/local hosts before calling
control_panel_response or browser_inventory_response, and return a rejection for
any other host to prevent DNS-rebinding/origin-confusion exposure.

Comment thread src/mcp/control_panel.rs
Comment on lines +420 to +425
fn generate_control_token() -> Result<String> {
let mut bytes = [0_u8; 32];
File::open("/dev/urandom")
.context("failed to open /dev/urandom for control panel token")?
.read_exact(&mut bytes)
.context("failed to read control panel token bytes")?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check declared platform expectations and whether a portable RNG dependency already exists.
fd -i 'Cargo.toml|README.md|.*\.ya?ml|rust-toolchain.*' -t f . \
  | xargs -r rg -n -i 'windows|msvc|edge|getrandom|rand|/dev/urandom'

Repository: ProverCoderAI/rust-browser-connection

Length of output: 3519


🏁 Script executed:

cat Cargo.toml

Repository: ProverCoderAI/rust-browser-connection

Length of output: 1116


🏁 Script executed:

head -n 50 src/mcp/control_panel.rs

Repository: ProverCoderAI/rust-browser-connection

Length of output: 1892


🏁 Script executed:

sed -n '410,435p' src/mcp/control_panel.rs

Repository: ProverCoderAI/rust-browser-connection

Length of output: 959


🏁

Length of output: 25


🏁 Script executed:

fn generate_control_token() -> Result<String> {
    let mut bytes = [0_u8; 32];
    // getrandom::getrandom(&mut bytes)?;  // Requires adding getrandom to Cargo.toml
    std::fs::File::open("/dev/urandom") // Fails on Windows
        .context("failed to open /dev/urandom for control panel token")?
        .read_exact(&mut bytes)
        .context("failed to read control panel token bytes")?;
    // ...
}

Repository: src/mcp/control_panel.rs

Length of output: 2056


Use a cross-platform entropy source for the control token

The current implementation uses File::open("/dev/urandom"), which does not exist on Windows and will cause the control panel to fail at startup. Since your CI matrix includes windows-latest, you must replace this with an OS-agnostic RNG, such as the getrandom crate.

Add getrandom = "0.2" to your Cargo.toml dependencies and update the function as follows:

// src/mcp/control_panel.rs
fn generate_control_token() -> Result<String> {
    let mut bytes = [0_u8; 32];
    getrandom::getrandom(&mut bytes)
        .context("failed to read control panel token bytes")?;
    
    let mut token = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        write!(&mut token, "{byte:02x}").expect("writing to a String cannot fail");
    }
    Ok(token)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/control_panel.rs` around lines 420 - 425, The control token generator
in generate_control_token currently reads from /dev/urandom, which is Linux-only
and breaks startup on Windows. Replace the
File::open("/dev/urandom").read_exact(...) path with an OS-agnostic entropy
source such as getrandom::getrandom, keeping the existing context error handling
and token hex-encoding logic in generate_control_token. Also add the getrandom
dependency to Cargo.toml so the control panel works across the full CI matrix.

@skulidropek

Copy link
Copy Markdown
Member Author

Agent Plan Update

Branch: pr-14 at 08cc457.

1. Plan

Source: codex - Captured: 2026-06-25T07:00:59.727Z

Codex Plan

Steps

  • completed: Inspect current partial changes and relay/control-panel internals
  • completed: Implement integrated same-origin relay and control-panel connect UI
  • completed: Finish Edge extension approval flow and artifact packaging
  • completed: Run full checks and smoke validation
  • completed: Commit/push PR updates if checks pass

@skulidropek

skulidropek commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 08cc457
Status: success
Files: 17 (12.97 MB)
Links: README | Manifest

git status

On branch pr-14
Your branch is up to date with 'origin/pr-14'.

nothing to commit, working tree clean

@skulidropek

skulidropek commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 6883148
Status: success
Files: 18 (14.70 MB)
Links: README | Manifest

git status

On branch pr-14
Your branch is up to date with 'origin/pr-14'.

nothing to commit, working tree clean

@skulidropek

skulidropek commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 4abd3b2
Status: success
Files: 19 (15.14 MB)
Links: README | Manifest

git status

On branch pr-14
Your branch is up to date with 'origin/pr-14'.

nothing to commit, working tree clean

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
extension/edge-share/service_worker.js (1)

271-283: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard close handlers against stale sockets.

connectRelay() can close an old socket and immediately assign a new one; if the old socket’s close event fires after the replacement opens, Line 272 can clear the new connection’s keepalive timer and Line 273 can null the active socket. Capture the socket instance and ignore lifecycle events from stale sockets.

Proposed fix
-  socket = new WebSocket(wsUrl);
+  const currentSocket = new WebSocket(wsUrl);
+  socket = currentSocket;
@@
-  socket.addEventListener("open", () => {
+  currentSocket.addEventListener("open", () => {
+    if (socket !== currentSocket) return;
@@
-  socket.addEventListener("message", (event) => {
+  currentSocket.addEventListener("message", (event) => {
+    if (socket !== currentSocket) return;
@@
-  socket.addEventListener("close", (event) => {
+  currentSocket.addEventListener("close", (event) => {
+    if (socket !== currentSocket) return;
     clearKeepAliveTimer();
     socket = null;
@@
-  socket.addEventListener("error", () => {
+  currentSocket.addEventListener("error", () => {
+    if (socket !== currentSocket) return;
     clearKeepAliveTimer();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extension/edge-share/service_worker.js` around lines 271 - 283, The socket
close handler in connectRelay() needs to ignore stale close events from
previously replaced sockets. Capture the current socket instance when
registering the close listener, then in the handler verify it still matches the
active socket before calling clearKeepAliveTimer(), setting socket to null, or
scheduling reconnect/persisting disconnected state. Use the connectRelay()
socket setup and its close event callback to apply the guard so only the live
connection can affect lifecycle state.
src/mcp/control_panel.rs (1)

203-207: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Require the control token for browser inventory.

/api/browsers is unauthenticated, but inventory includes shareUrl and endpoint metadata. Gate it like the other panel APIs and call it through apiFetch().

Proposed fix
-        ("GET", "/api/browsers") => browser_inventory_response(runtime),
+        ("GET", "/api/browsers") => {
+            if !has_valid_control_token(request, control_token) {
+                return json_response(
+                    403,
+                    "Forbidden",
+                    json!({ "error": "invalid control token" }),
+                );
+            }
+            browser_inventory_response(runtime)
+        },
@@
-  const response = await fetch("/api/browsers", { cache: "no-store" });
+  const response = await apiFetch("/api/browsers", { cache: "no-store" });

Also applies to: 738-740

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/control_panel.rs` around lines 203 - 207, The browser inventory
endpoint is currently exposed without authentication, which leaks `shareUrl` and
endpoint metadata. Update the request routing in `control_panel_response` so the
"/api/browsers" branch is gated by the control token like the other panel APIs,
and ensure the client side calls it through `apiFetch()` instead of a raw fetch.
Use the existing `apiFetch` and `browser_inventory_response` symbols to locate
the affected flow and keep the auth handling consistent with the other control
panel endpoints.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/mcp.rs`:
- Around line 273-290: The shared-browser I/O is being performed while the
runtime mutex is held, which can block MCP and control panel requests. Refactor
browser_activity and refresh_active_shared_tabs, and also the activate_tab flow
referenced in the review, so they only read the share URL or tab state under
runtime lock, then drop the lock before calling
SharedBrowserClient::new(...).list_tabs() or activate_tab(), and finally re-lock
only to record the resulting activity/error state.

In `@src/mcp/activity.rs`:
- Around line 78-85: Activity events are still recording user-entered text from
browser_type, so redact typed input before it reaches the event payload. Update
the activity event construction in the event-writing logic around
push_front/json! and the related code path that also mirrors this payload to
ensure arguments are sanitized for browser_type, not just token/share URL keys.
Use the existing sanitize_value flow as the central place to strip or mask typed
text before storing anything exposed through /api/activity.
- Around line 30-38: The activity snapshot is returning every retained
screenshot with full data URLs, which makes each poll unnecessarily large.
Update snapshot() in ActivityState to stop cloning the full screenshots history
and instead return only the latest full image plus lightweight metadata or
thumbnails for prior entries. Use the existing latestScreenshot and screenshots
fields as the place to reshape the payload so /api/activity stays small while
preserving recent image access.

In `@src/mcp/tools.rs`:
- Around line 141-144: The browser_type branch in the tool dispatch is trimming
the typed text by using required_str for the text argument, which strips
intentional whitespace. Update the browser_type handling in src/mcp/tools.rs so
selector/URL-style inputs still use required_str, but the text parameter is read
with a raw non-empty helper that preserves leading and trailing spaces; apply
the same change anywhere else the browser_type text path is handled in the
referenced dispatch cases.

---

Outside diff comments:
In `@extension/edge-share/service_worker.js`:
- Around line 271-283: The socket close handler in connectRelay() needs to
ignore stale close events from previously replaced sockets. Capture the current
socket instance when registering the close listener, then in the handler verify
it still matches the active socket before calling clearKeepAliveTimer(), setting
socket to null, or scheduling reconnect/persisting disconnected state. Use the
connectRelay() socket setup and its close event callback to apply the guard so
only the live connection can affect lifecycle state.

In `@src/mcp/control_panel.rs`:
- Around line 203-207: The browser inventory endpoint is currently exposed
without authentication, which leaks `shareUrl` and endpoint metadata. Update the
request routing in `control_panel_response` so the "/api/browsers" branch is
gated by the control token like the other panel APIs, and ensure the client side
calls it through `apiFetch()` instead of a raw fetch. Use the existing
`apiFetch` and `browser_inventory_response` symbols to locate the affected flow
and keep the auth handling consistent with the other control panel endpoints.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 463d038e-8e43-43a7-9848-664483deeb9b

📥 Commits

Reviewing files that changed from the base of the PR and between 79fe825 and 6883148.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock
  • artifacts/edge-share-extension.tar.gz is excluded by !**/*.gz
  • artifacts/edge-share-extension.zip is excluded by !**/*.zip
📒 Files selected for processing (7)
  • Cargo.toml
  • extension/edge-share/service_worker.js
  • src/mcp.rs
  • src/mcp/activity.rs
  • src/mcp/control_panel.rs
  • src/mcp/tools.rs
  • src/shared_browser.rs

Comment thread src/mcp.rs
Comment on lines +273 to +290
fn browser_activity(&mut self, refresh: bool) -> Value {
if refresh {
self.refresh_active_shared_tabs();
}
self.activity
.snapshot(&self.active_browser, &self.active_browser_mode())
}

fn refresh_active_shared_tabs(&mut self) {
let Some(share_url) = self.active_share_url() else {
self.activity.clear_tabs();
return;
};
match SharedBrowserClient::new(share_url).list_tabs() {
Ok(tabs) => self.activity.record_tabs(tabs),
Err(error) => self.activity.record_tab_error(error),
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Don’t run shared-browser network calls while holding the runtime mutex.

list_tabs() and activate_tab() can block on relay/browser I/O, and the control panel invokes these through runtime.lock(), stalling MCP tool calls and panel requests behind a slow or missing extension. Snapshot the share URL under lock, drop the lock for relay I/O, then re-lock only to record activity.

Also applies to: 319-331

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp.rs` around lines 273 - 290, The shared-browser I/O is being performed
while the runtime mutex is held, which can block MCP and control panel requests.
Refactor browser_activity and refresh_active_shared_tabs, and also the
activate_tab flow referenced in the review, so they only read the share URL or
tab state under runtime lock, then drop the lock before calling
SharedBrowserClient::new(...).list_tabs() or activate_tab(), and finally re-lock
only to record the resulting activity/error state.

Comment thread src/mcp/activity.rs
Comment on lines +30 to +38
pub(super) fn snapshot(&self, active_browser: &str, mode: &str) -> Value {
json!({
"activeBrowser": active_browser,
"mode": mode,
"tabs": self.latest_tabs.clone().unwrap_or(Value::Null),
"tabsError": self.latest_tab_error,
"screenshots": self.screenshots.iter().cloned().collect::<Vec<_>>(),
"latestScreenshot": self.screenshots.front().cloned().unwrap_or(Value::Null),
"events": self.events.iter().cloned().collect::<Vec<_>>(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Avoid returning every full screenshot in each activity poll.

snapshot() clones all retained screenshots including full dataUrls; with 24 screenshots and 8 MiB relay messages, /api/activity can serialize hundreds of MiB repeatedly. Return only the latest full image plus metadata/thumbnails for history.

Also applies to: 94-112

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/activity.rs` around lines 30 - 38, The activity snapshot is returning
every retained screenshot with full data URLs, which makes each poll
unnecessarily large. Update snapshot() in ActivityState to stop cloning the full
screenshots history and instead return only the latest full image plus
lightweight metadata or thumbnails for prior entries. Use the existing
latestScreenshot and screenshots fields as the place to reshape the payload so
/api/activity stays small while preserving recent image access.

Comment thread src/mcp/activity.rs
Comment on lines +78 to +85
self.events.push_front(json!({
"id": event_id,
"at": started_at_ms,
"browser": browser,
"mode": mode,
"tool": tool,
"arguments": sanitize_value(arguments),
"ok": ok,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Redact typed text before writing activity events.

browser_type arguments include user-entered text, but only token/share URL keys are redacted. This can expose passwords or other PII through /api/activity.

Proposed fix
-            "arguments": sanitize_value(arguments),
+            "arguments": sanitize_arguments(tool, arguments),
@@
+fn sanitize_arguments(tool: &str, arguments: &Value) -> Value {
+    if tool == "browser_type" {
+        let mut sanitized = sanitize_value(arguments);
+        if let Value::Object(object) = &mut sanitized {
+            object.insert("text".to_string(), json!("[redacted]"));
+        }
+        return sanitized;
+    }
+    sanitize_value(arguments)
+}
+
 fn sanitize_value(value: &Value) -> Value {

Also applies to: 130-139

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/activity.rs` around lines 78 - 85, Activity events are still
recording user-entered text from browser_type, so redact typed input before it
reaches the event payload. Update the activity event construction in the
event-writing logic around push_front/json! and the related code path that also
mirrors this payload to ensure arguments are sanitized for browser_type, not
just token/share URL keys. Use the existing sanitize_value flow as the central
place to strip or mask typed text before storing anything exposed through
/api/activity.

Comment thread src/mcp/tools.rs Outdated
Comment on lines +141 to +144
"browser_type" => client.type_text(
required_str(arguments, "selector")?,
required_str(arguments, "text")?,
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve whitespace for browser_type text.

required_str() trims every string argument, so typing " value " sends "value". Keep trimming for selectors/URLs, but use a raw non-empty helper for the text argument.

Proposed fix
-            required_str(arguments, "text")?,
+            required_raw_str(arguments, "text")?,
@@
-            required_str(arguments, "text")?,
+            required_raw_str(arguments, "text")?,
@@
 fn required_str<'a>(arguments: &'a Value, name: &str) -> Result<&'a str> {
@@
 }
+
+fn required_raw_str<'a>(arguments: &'a Value, name: &str) -> Result<&'a str> {
+    arguments
+        .get(name)
+        .and_then(Value::as_str)
+        .filter(|value| !value.is_empty())
+        .ok_or_else(|| anyhow!("argument `{name}` is required"))
+}

Also applies to: 168-170, 188-194

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/tools.rs` around lines 141 - 144, The browser_type branch in the tool
dispatch is trimming the typed text by using required_str for the text argument,
which strips intentional whitespace. Update the browser_type handling in
src/mcp/tools.rs so selector/URL-style inputs still use required_str, but the
text parameter is read with a raw non-empty helper that preserves leading and
trailing spaces; apply the same change anywhere else the browser_type text path
is handled in the referenced dispatch cases.

@skulidropek

skulidropek commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 2251cc4
Status: success
Files: 20 (15.91 MB)
Links: README | Manifest

git status

On branch pr-14
Your branch is up to date with 'origin/pr-14'.

nothing to commit, working tree clean

@skulidropek

skulidropek commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: f1828b3
Status: success
Files: 20 (16.84 MB)
Links: README | Manifest

git status

On branch pr-14
Your branch is up to date with 'origin/pr-14'.

nothing to commit, working tree clean

@skulidropek

Copy link
Copy Markdown
Member Author

Agent Plan Update

Branch: pr-14 at 2ddfdf0.

1. Plan

Source: codex - Captured: 2026-06-25T09:03:55.634Z

CLI-First Browser Control Without MCP

Summary

Сделать отдельный CLI-слой для управления браузером без MCP: агент будет вызывать обычные команды и JS-скрипты, а MCP останется только совместимым wrapper поверх того же action dispatcher. Основной новый binary: browserctl.

Key Changes

  • Добавить browserctl как новый Rust binary:
    • Direct mode: browserctl --share-url <url> ... для Edge extension relay.
    • Direct mode: browserctl --cdp-url <url> ... для docker/noVNC Chromium через CDP.
    • Panel mode: browserctl --project <id> ... читает active browser из http://127.0.0.1:<control-port>/api/browsers; если panel недоступна, fallback только на managed CDP URL проекта.
  • Поддержать команды:
    • navigate <url>
    • snapshot
    • eval --expression <js> и eval --file <path>
    • click <selector>
    • type <selector> <text>
    • key <key>
    • screenshot [--full-page] [--output <png-path>]
    • tabs
    • activate-tab <tab-id>
  • Вынести общий dispatcher из src/mcp/tools.rs в библиотечный модуль, чтобы MCP tools и CLI использовали один код и не расходились по поведению.
  • Добавить --json для машинно-читаемого stdout и --trace-dir <dir> для JSONL-логов действий агента, включая command, target, duration, status и screenshot path/data summary.
  • MCP публичный API не менять: существующие tool names остаются совместимыми.

Skill

  • Добавить repo skill skills/browser-cli/SKILL.md.
  • Skill должен учить агента:
    • предпочитать browserctl вместо MCP для браузерных задач;
    • использовать snapshot перед действиями;
    • писать многошаговые операции как JS-файлы и запускать через browserctl eval --file;
    • использовать tabs и activate-tab для разделения окон/вкладок;
    • включать --trace-dir для прозрачного аудита.
  • Не добавлять лишние README/quick reference файлы; только skill и минимальные bundled resources, если реально нужны.

Test Plan

  • Unit/integration tests:
    • browserctl --help показывает все commands.
    • target resolution выбирает shareUrl прежде cdpEndpoint для active shared browser.
    • --share-url и --cdp-url bypass panel полностью.
    • eval --file читает JS из файла и передаёт его в dispatcher.
    • screenshot --output создаёт PNG из data URL.
    • MCP tool regression: schemas and existing tool behavior unchanged.
  • Existing checks:
    • cargo fmt -- --check
    • cargo test --locked
    • cargo clippy --locked --all-targets -- -D warnings
    • file-size guard script.
  • Manual smoke:
    • browserctl --project edge-share-test tabs
    • browserctl --project edge-share-test snapshot
    • browserctl --share-url "$EDGE_SHARE_URL" eval --file /tmp/check.js
    • verify trace JSONL appears when --trace-dir is set.

Assumptions

  • Default choice: new browserctl binary, not overloading browser-connection MCP stdio binary.
  • Default skill location: inside this repo, so it can be reviewed and shipped with the PR.
  • No hardcoded platform URL. CLI uses explicit URL flags or discovers active browser from the local control panel.
  • CLI does not replace noVNC UI; it makes browser automation usable without MCP and with lower token overhead.

@skulidropek

skulidropek commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 2ddfdf0
Status: success
Files: 21 (17.92 MB)
Links: README | Manifest

git status

On branch pr-14
Your branch is up to date with 'origin/pr-14'.

nothing to commit, working tree clean

@skulidropek

skulidropek commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 3b4b0dd
Status: success
Files: 21 (18.15 MB)
Links: README | Manifest

git status

On branch pr-14
Your branch is up to date with 'origin/pr-14'.

nothing to commit, working tree clean

@skulidropek

skulidropek commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 9bee3f0
Status: success
Files: 21 (18.24 MB)
Links: README | Manifest

git status

On branch pr-14
Your branch is up to date with 'origin/pr-14'.

nothing to commit, working tree clean

@skulidropek

Copy link
Copy Markdown
Member Author

Agent Plan Update

Branch: pr-14 at 626d36f.

1. Plan

Source: codex - Captured: 2026-06-25T09:43:45.866Z

Add Real Playwright Runner To rbc

Summary

Add rbc <project> pw ... for running real Playwright code against CDP-backed browsers. This will work for managed Chromium and any personal/external Chromium/Edge browser with a reachable CDP endpoint. It will not run arbitrary Playwright against extension-only Edge share links, because those expose only our relay commands, not CDP/Playwright protocol.

Official constraint: Playwright supports attaching to existing Chromium over chromium.connectOverCDP(...), but documents CDP mode as Chromium-only and lower fidelity than native Playwright protocol: https://playwright.dev/docs/api/class-browsertype#browser-type-connect-over-cdp.

Key Changes

  • Add CLI command:
    • rbc <project> pw <script.js>
    • rbc <project> pw --code 'await page.title()'
    • rbc <project> pw --json <script.js>
    • rbc <project> pw --cdp-url http://127.0.0.1:9223 <script.js>
  • Script execution model:
    • Runner connects with playwright.chromium.connectOverCDP(cdpUrl).
    • Script body runs inside an async function with playwright, browser, context, page, and pages in scope.
    • page defaults to the first page in the default context; if no page exists, create one.
    • Script return value is printed; JSON values use pretty JSON with --json.
  • Target behavior:
    • For pw, resolve a CDP endpoint only.
    • If active target is extension-only shareUrl, return an explicit error: real Playwright requires CDP; use rbc eval/click/... or expose Edge remote-debugging/CDP.
    • If a target has both shareUrl and cdpEndpoint, pw uses CDP.
  • Dependency behavior:
    • Require Node.
    • Use playwright-core if resolvable locally.
    • If missing, auto-run through npm exec --yes --package playwright-core@1.61.1 -- node ....
    • Add clear error if Node/npm is unavailable.
  • Safety:
    • Guard browser.close() and context.close() by default to avoid killing the shared session.
    • Add --allow-close to opt out.
    • Existing --trace-dir writes rbc.jsonl for pw runs too.

Examples

rbc edge-share-test pw --code 'return await page.title()'

rbc edge-share-test pw /tmp/task.js

/tmp/task.js:

await page.goto('https://example.com');
await page.getByRole('link', { name: /more/i }).click();
return { title: await page.title(), url: page.url() };

For a CDP-enabled personal Edge:

rbc my-project pw --cdp-url http://host.docker.internal:9222 /tmp/task.js

Test Plan

  • CLI tests:
    • rbc PROJECT pw --help shows file and --code.
    • pw rejects --share-url with a real-Playwright-requires-CDP error.
    • pw prefers cdpEndpoint over shareUrl when both exist.
    • pw --code and pw <file> parse correctly.
  • Runner tests:
    • Unit test generated wrapper script includes connectOverCDP, guarded close methods, and exposes page/context/browser.
    • Fake Node/playwright-core fixture confirms script result is printed and JSON mode works.
  • Regression checks:
    • cargo test --locked
    • cargo clippy --locked --all-targets -- -D warnings
    • cargo fmt -- --check
    • file-size guard
    • skill validator

Assumptions

  • V1 supports real Playwright only for CDP-backed Chromium/Edge targets.
  • Extension-only browser sharing remains on rbc eval, click, type, screenshot, tabs, and activate-tab.
  • We do not add a full Playwright protocol server; we use Playwright’s official CDP attach path.
  • We keep rbc <project> eval ... for in-page JavaScript and add rbc <project> pw ... for Playwright-level page APIs.

@skulidropek

skulidropek commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 626d36f
Status: success
Files: 24 (20.02 MB)
Links: README | Manifest

git status

On branch pr-14
Your branch is up to date with 'origin/pr-14'.

nothing to commit, working tree clean

@skulidropek

skulidropek commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: c4bcf13
Status: success
Files: 25 (20.85 MB)
Links: README | Manifest

git status

On branch pr-14
Your branch is up to date with 'origin/pr-14'.

nothing to commit, working tree clean

@skulidropek

Copy link
Copy Markdown
Member Author

Agent Plan Update

Branch: pr-14 at 79dde14.

1. Plan

Source: codex - Captured: 2026-06-25T10:20:18.297Z

Browser Pool Defaults For rbc

Summary

Сделать так, чтобы агенту не нужно было писать --share-url: браузер выбирается из уже настроенного browser pool, который пользователь подключает через платформу/noVNC UI. Основной UX:

rbc edge pw --code 'return await page.title()'
rbc chromium snapshot
rbc active pw /tmp/task.js

Явные --share-url и --cdp-url остаются override-режимом для отладки.

Key Changes

  • Изменить смысл первого positional аргумента rbc с “только project id” на “browser target or legacy project id”.
  • Добавить browser-pool discovery:
    • --control-url https://... как ручной override.
    • BROWSER_CONNECTION_CONTROL_URL как env, который платформа выставляет агенту автоматически.
    • fallback на локальную control panel по DOCKER_GIT_PROJECT_ID / PROJECT_ID.
    • fallback на старое поведение: считать positional аргумент project id и строить deterministic local CDP URL.
  • Резолв браузера:
    • rbc edge ... ищет browser с name=edge в /api/browsers.
    • rbc chromium ... сначала ищет chromium, затем alias managed.
    • rbc active ... берёт browser, выбранный в UI панели прямо сейчас.
    • если у browser есть shareUrl, использовать shared-extension backend; иначе cdpEndpoint.
  • --share-url / --cdp-url имеют самый высокий приоритет и работают как сейчас.

Platform Behavior

  • Когда пользователь открывает https://executive-fri-sea-lets.trycloudflare.com/, расширение Edge уже автоподключается и регистрирует edge в pool через существующий /api/share.
  • Agent environment получает только BROWSER_CONNECTION_CONTROL_URL=https://executive-fri-sea-lets.trycloudflare.com.
  • Агент работает командами по имени browser-а, без знания bearer share URL.
  • По твоему выбору /api/browsers остаётся public в этом плане; отдельный control token для чтения inventory не добавляем.

Tests

  • CLI target resolution:
    • rbc edge ... берёт shareUrl из fake /api/browsers.
    • rbc chromium ... резолвится в managed/CDP browser.
    • rbc active ... берёт active browser из inventory.
    • legacy rbc dg-project ... продолжает падать назад на deterministic CDP URL.
  • Integration:
    • fake control panel + fake relay: rbc edge pw --code ... работает без --share-url.
    • fake control panel с CDP browser: rbc chromium pw --code ... использует real CDP Playwright path.
  • Docs/skill:
    • обновить README и browser-cli skill: агент должен предпочитать rbc <browser-name> ..., а не передавать --share-url.

Assumptions

  • Основной browser name для удалённого Edge: edge.
  • Container Chromium можно вызывать как chromium, но внутри это alias на существующий managed browser, если отдельного browser с именем chromium нет.
  • Платформа сама выставляет BROWSER_CONNECTION_CONTROL_URL; агент не вводит Cloudflare URL руками.

@skulidropek

skulidropek commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

AI Session Backup

Commit: 79dde14
Status: success
Files: 26 (21.74 MB)
Links: README | Manifest

git status

On branch pr-14
Your branch is up to date with 'origin/pr-14'.

nothing to commit, working tree clean

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
tests/cli_status.rs (1)

67-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the validation-before-resolution path.

Because this test passes --cdp-url, it bypasses control-panel resolution and would not catch eval doing network work before argument validation. Add a variant with BROWSER_CONNECTION_CONTROL_URL set and no --cdp-url.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/cli_status.rs` around lines 67 - 75, The current eval validation test
only covers the --cdp-url path, so it misses the validation-before-resolution
behavior. Update rbc_eval_requires_expression_or_file_before_network in
cli_status.rs to add a case that sets BROWSER_CONNECTION_CONTROL_URL without
passing --cdp-url and still asserts the same eval error, so the eval command is
verified before any control-panel/network resolution happens.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bin/playwright/mod.rs`:
- Around line 286-292: The temporary wrapper file creation in
write_temp_playwright_wrapper is vulnerable because fs::write can follow
existing paths and does not enforce private permissions. Update this function to
create the file atomically with create_new, set restrictive 0600 permissions
before writing the Playwright source, and keep the existing temp name
generation/context handling so the wrapper file is only created if it does not
already exist.
- Around line 97-171: The stderr pipe in run_node_shared_playwright_wrapper is
only read after the stdout loop finishes, which can deadlock if the Node process
writes enough error output; change the bridge handling so stderr is drained
concurrently while reading stdout. Update the logic around Command::new("node"),
the stdout loop, and read_stderr so Node’s stderr is consumed in parallel (for
example with a separate reader thread or nonblocking drain) and any stderr text
is still collected for the final error/status handling.

In `@src/bin/rbc.rs`:
- Around line 167-170: Validate the tool command before any target resolution in
run_tool_cli. Move the tool parsing/validation using tool_command(&cli.command)
and browser_command(&tool)? ahead of resolve_target(cli)? so invalid local input
is rejected first and does not get masked by control panel or network failures.

In `@src/bin/target_resolution/mod.rs`:
- Around line 75-78: The control-panel fetch in target resolution can hang
indefinitely because the curl invocation has no timeout. Update the request path
in the target resolution logic around the curl Command::new("curl") call to
enforce a bounded timeout, and handle timeout failures with a clear error so a
stale BROWSER_CONNECTION_CONTROL_URL or stalled local port does not block rbc
forever.

In `@src/browser_actions.rs`:
- Around line 87-90: The `browser_type` handling in `BrowserCommand::TypeText`
is trimming user text via `required_str()`, which changes the exact content and
rejects whitespace-only input; switch this command to read the raw string
argument without trimming so the text is preserved exactly. Apply the same fix
anywhere else in the browser action parsing path that uses the same text-input
logic, including the other referenced `browser_type` cases.
- Around line 32-40: `safe_label()` is still leaking sensitive URL components
for both `Self::Cdp` and `Self::Shared`, so update it to return only sanitized
labels. In `browser_actions::BrowserTarget::safe_label`, parse the
endpoint/share URL and redact userinfo, query strings, and any token-bearing
fragment or relay-auth params (including `agent`) before formatting the label.
Preserve the existing `cdp:` and `shared:` prefixes, but ensure the returned
string never includes credentials, raw tokens, or full URLs.

---

Nitpick comments:
In `@tests/cli_status.rs`:
- Around line 67-75: The current eval validation test only covers the --cdp-url
path, so it misses the validation-before-resolution behavior. Update
rbc_eval_requires_expression_or_file_before_network in cli_status.rs to add a
case that sets BROWSER_CONNECTION_CONTROL_URL without passing --cdp-url and
still asserts the same eval error, so the eval command is verified before any
control-panel/network resolution happens.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7b39b9c2-c291-495b-a54e-ee4a07b6e20a

📥 Commits

Reviewing files that changed from the base of the PR and between 6883148 and 79dde14.

⛔ Files ignored due to path filters (2)
  • artifacts/edge-share-extension.tar.gz is excluded by !**/*.gz
  • artifacts/edge-share-extension.zip is excluded by !**/*.zip
📒 Files selected for processing (17)
  • README.md
  • changelog.d/20260625_000000_rbc_cli.md
  • extension/edge-share/README.md
  • extension/edge-share/service_worker.js
  • skills/browser-cli/SKILL.md
  • skills/browser-cli/agents/openai.yaml
  • src/bin/playwright/mod.rs
  • src/bin/rbc.rs
  • src/bin/target_resolution/mod.rs
  • src/browser_actions.rs
  • src/lib.rs
  • src/mcp/activity.rs
  • src/mcp/control_panel.rs
  • src/mcp/tools.rs
  • src/shared_browser.rs
  • tests/cli_status.rs
  • tests/shared_browser_relay.rs
✅ Files skipped from review due to trivial changes (3)
  • changelog.d/20260625_000000_rbc_cli.md
  • skills/browser-cli/agents/openai.yaml
  • skills/browser-cli/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • extension/edge-share/README.md
  • src/mcp/activity.rs
  • src/mcp/control_panel.rs
  • src/lib.rs
  • extension/edge-share/service_worker.js
  • src/shared_browser.rs

Comment thread src/bin/playwright/mod.rs
Comment on lines +97 to +171
fn run_node_shared_playwright_wrapper(share_url: &str, wrapper_path: &Path) -> Result<String> {
let mut child = Command::new("node")
.arg(wrapper_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("failed to run node for shared-extension rbc pw")?;
let mut stdin = child
.stdin
.take()
.ok_or_else(|| anyhow!("failed to open node stdin for shared-extension rbc pw"))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| anyhow!("failed to open node stdout for shared-extension rbc pw"))?;
let mut stderr = child
.stderr
.take()
.ok_or_else(|| anyhow!("failed to open node stderr for shared-extension rbc pw"))?;
let client = SharedBrowserClient::new(share_url);
let mut reader = BufReader::new(stdout);
let mut line = String::new();
let mut final_result = None;
let mut final_error = None;
let mut plain_output = String::new();

loop {
line.clear();
let bytes = reader
.read_line(&mut line)
.context("failed to read shared-extension rbc pw bridge output")?;
if bytes == 0 {
break;
}
let text = line.trim_end_matches(['\r', '\n']);
if text.is_empty() {
continue;
}

let Ok(message) = serde_json::from_str::<Value>(text) else {
plain_output.push_str(text);
plain_output.push('\n');
continue;
};
match message.get("type").and_then(Value::as_str) {
Some("rbc-command") => {
let response = shared_bridge_response(&client, &message);
writeln!(stdin, "{}", serde_json::to_string(&response)?)
.context("failed to write shared-extension rbc pw bridge response")?;
}
Some("rbc-result") => {
final_result = Some(message.get("result").cloned().unwrap_or(Value::Null));
break;
}
Some("rbc-error") => {
let error = message
.get("error")
.and_then(Value::as_str)
.unwrap_or("shared-extension rbc pw failed");
final_error = Some(error.to_string());
break;
}
_ => {
plain_output.push_str(text);
plain_output.push('\n');
}
}
}

drop(stdin);
let status = child
.wait()
.context("failed to wait for shared-extension rbc pw")?;
let stderr_text = read_stderr(&mut stderr)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Drain Node stderr while reading bridge stdout.

This reads stdout line-by-line while stderr remains piped until after wait(). If the user script writes enough stderr, Node can block on the full stderr pipe while Rust waits forever for the next stdout line.

Suggested direction
-    let mut stderr = child
+    let mut stderr = child
         .stderr
         .take()
         .ok_or_else(|| anyhow!("failed to open node stderr for shared-extension rbc pw"))?;
+    let stderr_reader = std::thread::spawn(move || {
+        let mut text = String::new();
+        stderr.read_to_string(&mut text).map(|_| text)
+    });
@@
-    let stderr_text = read_stderr(&mut stderr)?;
+    let stderr_text = stderr_reader
+        .join()
+        .map_err(|_| anyhow!("shared-extension rbc pw stderr reader panicked"))?
+        .context("failed to read shared-extension rbc pw stderr")?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn run_node_shared_playwright_wrapper(share_url: &str, wrapper_path: &Path) -> Result<String> {
let mut child = Command::new("node")
.arg(wrapper_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("failed to run node for shared-extension rbc pw")?;
let mut stdin = child
.stdin
.take()
.ok_or_else(|| anyhow!("failed to open node stdin for shared-extension rbc pw"))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| anyhow!("failed to open node stdout for shared-extension rbc pw"))?;
let mut stderr = child
.stderr
.take()
.ok_or_else(|| anyhow!("failed to open node stderr for shared-extension rbc pw"))?;
let client = SharedBrowserClient::new(share_url);
let mut reader = BufReader::new(stdout);
let mut line = String::new();
let mut final_result = None;
let mut final_error = None;
let mut plain_output = String::new();
loop {
line.clear();
let bytes = reader
.read_line(&mut line)
.context("failed to read shared-extension rbc pw bridge output")?;
if bytes == 0 {
break;
}
let text = line.trim_end_matches(['\r', '\n']);
if text.is_empty() {
continue;
}
let Ok(message) = serde_json::from_str::<Value>(text) else {
plain_output.push_str(text);
plain_output.push('\n');
continue;
};
match message.get("type").and_then(Value::as_str) {
Some("rbc-command") => {
let response = shared_bridge_response(&client, &message);
writeln!(stdin, "{}", serde_json::to_string(&response)?)
.context("failed to write shared-extension rbc pw bridge response")?;
}
Some("rbc-result") => {
final_result = Some(message.get("result").cloned().unwrap_or(Value::Null));
break;
}
Some("rbc-error") => {
let error = message
.get("error")
.and_then(Value::as_str)
.unwrap_or("shared-extension rbc pw failed");
final_error = Some(error.to_string());
break;
}
_ => {
plain_output.push_str(text);
plain_output.push('\n');
}
}
}
drop(stdin);
let status = child
.wait()
.context("failed to wait for shared-extension rbc pw")?;
let stderr_text = read_stderr(&mut stderr)?;
fn run_node_shared_playwright_wrapper(share_url: &str, wrapper_path: &Path) -> Result<String> {
let mut child = Command::new("node")
.arg(wrapper_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("failed to run node for shared-extension rbc pw")?;
let mut stdin = child
.stdin
.take()
.ok_or_else(|| anyhow!("failed to open node stdin for shared-extension rbc pw"))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| anyhow!("failed to open node stdout for shared-extension rbc pw"))?;
let mut stderr = child
.stderr
.take()
.ok_or_else(|| anyhow!("failed to open node stderr for shared-extension rbc pw"))?;
let stderr_reader = std::thread::spawn(move || {
let mut text = String::new();
stderr.read_to_string(&mut text).map(|_| text)
});
let client = SharedBrowserClient::new(share_url);
let mut reader = BufReader::new(stdout);
let mut line = String::new();
let mut final_result = None;
let mut final_error = None;
let mut plain_output = String::new();
loop {
line.clear();
let bytes = reader
.read_line(&mut line)
.context("failed to read shared-extension rbc pw bridge output")?;
if bytes == 0 {
break;
}
let text = line.trim_end_matches(['\r', '\n']);
if text.is_empty() {
continue;
}
let Ok(message) = serde_json::from_str::<Value>(text) else {
plain_output.push_str(text);
plain_output.push('\n');
continue;
};
match message.get("type").and_then(Value::as_str) {
Some("rbc-command") => {
let response = shared_bridge_response(&client, &message);
writeln!(stdin, "{}", serde_json::to_string(&response)?)
.context("failed to write shared-extension rbc pw bridge response")?;
}
Some("rbc-result") => {
final_result = Some(message.get("result").cloned().unwrap_or(Value::Null));
break;
}
Some("rbc-error") => {
let error = message
.get("error")
.and_then(Value::as_str)
.unwrap_or("shared-extension rbc pw failed");
final_error = Some(error.to_string());
break;
}
_ => {
plain_output.push_str(text);
plain_output.push('\n');
}
}
}
drop(stdin);
let status = child
.wait()
.context("failed to wait for shared-extension rbc pw")?;
let stderr_text = stderr_reader
.join()
.map_err(|_| anyhow!("shared-extension rbc pw stderr reader panicked"))?
.context("failed to read shared-extension rbc pw stderr")?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bin/playwright/mod.rs` around lines 97 - 171, The stderr pipe in
run_node_shared_playwright_wrapper is only read after the stdout loop finishes,
which can deadlock if the Node process writes enough error output; change the
bridge handling so stderr is drained concurrently while reading stdout. Update
the logic around Command::new("node"), the stdout loop, and read_stderr so
Node’s stderr is consumed in parallel (for example with a separate reader thread
or nonblocking drain) and any stderr text is still collected for the final
error/status handling.

Comment thread src/bin/playwright/mod.rs
Comment on lines +286 to +292
fn write_temp_playwright_wrapper(source: &str) -> Result<PathBuf> {
let path = std::env::temp_dir().join(format!(
"rbc-playwright-{}-{}.js",
std::process::id(),
unix_ms()
));
fs::write(&path, source).with_context(|| format!("failed to write {}", path.display()))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Create wrapper files atomically with private permissions.

fs::write creates a predictable temp-path file and follows pre-existing paths. Since the wrapper contains user Playwright code, use create_new and 0600 permissions to avoid temp-dir disclosure/symlink risks.

Suggested fix
 fn write_temp_playwright_wrapper(source: &str) -> Result<PathBuf> {
     let path = std::env::temp_dir().join(format!(
         "rbc-playwright-{}-{}.js",
         std::process::id(),
         unix_ms()
     ));
-    fs::write(&path, source).with_context(|| format!("failed to write {}", path.display()))?;
+    let mut options = fs::OpenOptions::new();
+    options.write(true).create_new(true);
+    #[cfg(unix)]
+    {
+        use std::os::unix::fs::OpenOptionsExt;
+        options.mode(0o600);
+    }
+    let mut file = options
+        .open(&path)
+        .with_context(|| format!("failed to create {}", path.display()))?;
+    file.write_all(source.as_bytes())
+        .with_context(|| format!("failed to write {}", path.display()))?;
     Ok(path)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn write_temp_playwright_wrapper(source: &str) -> Result<PathBuf> {
let path = std::env::temp_dir().join(format!(
"rbc-playwright-{}-{}.js",
std::process::id(),
unix_ms()
));
fs::write(&path, source).with_context(|| format!("failed to write {}", path.display()))?;
fn write_temp_playwright_wrapper(source: &str) -> Result<PathBuf> {
let path = std::env::temp_dir().join(format!(
"rbc-playwright-{}-{}.js",
std::process::id(),
unix_ms()
));
let mut options = fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options
.open(&path)
.with_context(|| format!("failed to create {}", path.display()))?;
file.write_all(source.as_bytes())
.with_context(|| format!("failed to write {}", path.display()))?;
Ok(path)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bin/playwright/mod.rs` around lines 286 - 292, The temporary wrapper file
creation in write_temp_playwright_wrapper is vulnerable because fs::write can
follow existing paths and does not enforce private permissions. Update this
function to create the file atomically with create_new, set restrictive 0600
permissions before writing the Playwright source, and keep the existing temp
name generation/context handling so the wrapper file is only created if it does
not already exist.

Comment thread src/bin/rbc.rs
Comment on lines +167 to +170
fn run_tool_cli(cli: &Cli) -> Result<()> {
let target = resolve_target(cli)?;
let tool = tool_command(&cli.command);
let command = browser_command(&tool)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate tool input before resolving a browser target.

resolve_target(cli)? can hit the control panel before browser_command(&tool)? rejects invalid input like bare eval, so local validation can be masked by network/control-panel failures.

Suggested fix
 fn run_tool_cli(cli: &Cli) -> Result<()> {
-    let target = resolve_target(cli)?;
     let tool = tool_command(&cli.command);
     let command = browser_command(&tool)?;
+    let target = resolve_target(cli)?;
     let started = Instant::now();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn run_tool_cli(cli: &Cli) -> Result<()> {
let target = resolve_target(cli)?;
let tool = tool_command(&cli.command);
let command = browser_command(&tool)?;
fn run_tool_cli(cli: &Cli) -> Result<()> {
let tool = tool_command(&cli.command);
let command = browser_command(&tool)?;
let target = resolve_target(cli)?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bin/rbc.rs` around lines 167 - 170, Validate the tool command before any
target resolution in run_tool_cli. Move the tool parsing/validation using
tool_command(&cli.command) and browser_command(&tool)? ahead of
resolve_target(cli)? so invalid local input is rejected first and does not get
masked by control panel or network failures.

Comment on lines +75 to +78
let output = Command::new("curl")
.args(["-fsS", url.as_str()])
.output()
.with_context(|| format!("failed to run curl for {url}"))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add timeouts to control-panel inventory requests.

curl has no timeout here; a stale BROWSER_CONNECTION_CONTROL_URL or stalled local control-panel port can hang rbc indefinitely during target resolution.

Suggested fix
     let output = Command::new("curl")
-        .args(["-fsS", url.as_str()])
+        .args(["-fsS", "--connect-timeout", "2", "--max-time", "5", url.as_str()])
         .output()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let output = Command::new("curl")
.args(["-fsS", url.as_str()])
.output()
.with_context(|| format!("failed to run curl for {url}"))?;
let output = Command::new("curl")
.args(["-fsS", "--connect-timeout", "2", "--max-time", "5", url.as_str()])
.output()
.with_context(|| format!("failed to run curl for {url}"))?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bin/target_resolution/mod.rs` around lines 75 - 78, The control-panel
fetch in target resolution can hang indefinitely because the curl invocation has
no timeout. Update the request path in the target resolution logic around the
curl Command::new("curl") call to enforce a bounded timeout, and handle timeout
failures with a clear error so a stale BROWSER_CONNECTION_CONTROL_URL or stalled
local port does not block rbc forever.

Comment thread src/browser_actions.rs
Comment on lines +32 to +40
pub fn safe_label(&self) -> String {
match self {
Self::Cdp { endpoint } => format!("cdp:{endpoint}"),
Self::Shared { share_url } => {
let redacted = share_url
.split_once("#agent=")
.map(|(base, _)| format!("{base}#agent=<redacted>"))
.unwrap_or_else(|| "<shared-browser-url>".to_string());
format!("shared:{redacted}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Redact credentials and token-bearing URL parts in safe_label.

safe_label() still emits full CDP endpoints and the shared URL base; URLs like https://user:pass@host/..., ...?token=..., or other relay-auth query params would be printed by rbc JSON output via target.safe_label().

🛡️ Suggested direction
-            Self::Cdp { endpoint } => format!("cdp:{endpoint}"),
-            Self::Shared { share_url } => {
-                let redacted = share_url
-                    .split_once("`#agent`=")
-                    .map(|(base, _)| format!("{base}`#agent`=<redacted>"))
-                    .unwrap_or_else(|| "<shared-browser-url>".to_string());
-                format!("shared:{redacted}")
-            }
+            Self::Cdp { endpoint } => format!("cdp:{}", redact_url_label(endpoint)),
+            Self::Shared { share_url } => format!("shared:{}", redact_url_label(share_url)),

Redact URL userinfo, query strings, and known sensitive fragment params such as agent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/browser_actions.rs` around lines 32 - 40, `safe_label()` is still leaking
sensitive URL components for both `Self::Cdp` and `Self::Shared`, so update it
to return only sanitized labels. In
`browser_actions::BrowserTarget::safe_label`, parse the endpoint/share URL and
redact userinfo, query strings, and any token-bearing fragment or relay-auth
params (including `agent`) before formatting the label. Preserve the existing
`cdp:` and `shared:` prefixes, but ensure the returned string never includes
credentials, raw tokens, or full URLs.

Comment thread src/browser_actions.rs
Comment on lines +87 to +90
"browser_type" => Ok(BrowserCommand::TypeText {
selector: required_str(arguments, "selector")?.to_string(),
text: required_str(arguments, "text")?.to_string(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve browser_type text exactly.

required_str() trims every string argument, so browser_type silently changes text like " hello " to "hello" and rejects whitespace-only input.

🐛 Proposed fix
         "browser_type" => Ok(BrowserCommand::TypeText {
             selector: required_str(arguments, "selector")?.to_string(),
-            text: required_str(arguments, "text")?.to_string(),
+            text: required_raw_str(arguments, "text")?.to_string(),
         }),
 fn required_str<'a>(arguments: &'a Value, name: &str) -> Result<&'a str> {
     arguments
         .get(name)
         .and_then(Value::as_str)
         .map(str::trim)
         .filter(|value| !value.is_empty())
         .ok_or_else(|| anyhow!("argument `{name}` is required"))
 }
+
+fn required_raw_str<'a>(arguments: &'a Value, name: &str) -> Result<&'a str> {
+    arguments
+        .get(name)
+        .and_then(Value::as_str)
+        .filter(|value| !value.is_empty())
+        .ok_or_else(|| anyhow!("argument `{name}` is required"))
+}

Also applies to: 152-158

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/browser_actions.rs` around lines 87 - 90, The `browser_type` handling in
`BrowserCommand::TypeText` is trimming user text via `required_str()`, which
changes the exact content and rejects whitespace-only input; switch this command
to read the raw string argument without trimming so the text is preserved
exactly. Apply the same fix anywhere else in the browser action parsing path
that uses the same text-input logic, including the other referenced
`browser_type` cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants