Skip to content

fix(ssh): agents on a managed SSH host can drive the desktop Orca browser again, behind the per-host switch - #29384

Merged
nwparker merged 15 commits into
mainfrom
nwparker/ssh-cli-reverse-bridge
Oct 16, 2026
Merged

nwparker merged 15 commits into
mainfrom
nwparker/ssh-cli-reverse-bridge

Conversation

@nwparker

@nwparker nwparker commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 18 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1833 $\color{#cf222e}{\Huge{\mathbf{−}}}$​2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1831
Prod 51 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1996 $\color{#cf222e}{\Huge{\mathbf{−}}}$​33 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1963

ELI5

Before 1.4.225, an agent working in a terminal on an SSH host could use the Orca browser on your desktop. That browser is already signed in to your accounts, so the agent did not need its own login. 1.4.225 moved SSH hosts onto a small Orca server that runs on the host. After that move, the host had no way to reach your desktop's browser, so every orca tab … / orca goto … command on the host failed with "Browser automation is unavailable on this host."

This PR brings it back, but only for hosts where you turned on "Allow this host's orca CLI to control Orca". When that switch is on, your desktop keeps a private channel open to that host's Orca server. Browser commands from the host's orca CLI travel down that channel, and your desktop runs them in browser tabs that belong to that host's workspaces. When the switch is off, the host gets a clear error that names the switch.

What Changed

Before (1.4.225+): on a managed SSH host, orca tab create, orca tab list, orca snapshot and the rest returned browser_unavailable. The per-host switch was stored and shown, but nothing read it. Its help text promised access to "other hosts, orchestration, files, and integrations", and none of that worked.

After:

  • Switch on, desktop connected to the host: the host's orca CLI can open, list, switch, close, navigate, read (snapshot, screenshot, get/is/find/wait, eval, console) and type into or click desktop browser tabs in that host's workspaces. A new tab is a normal desktop tab in that workspace. If SSH browser routing is on (the default), its traffic goes out through the host, the same as a tab opened from the tab bar.
  • Switch off, an older desktop, or no desktop connected: the CLI gets desktop_browser_bridge_unavailable with a message that names the switch.
  • A command that is not on the allowlist (see the threat model) gets desktop_browser_bridge_not_forwarded.
  • The switch's help text now says what it does: browser tabs only, only in this host's workspaces, only while the host is connected, and no files, clipboard or cookies.
  • Saved switches are reset once. A true saved while the switch did nothing (1.4.225 onward) is not consent to this new behavior. On first load it is turned off, and the host's SSH settings show a notice under the switch explaining why. Turning the switch on again stamps allowRemoteCliControlConsent: 'desktop-browser-bridge' in main, so the value is kept from then on.

Mechanism:

  • orcad (host side). orcad now serves two RPC methods, desktopBrowserBridge.attach (a long-lived stream) and desktopBrowserBridge.result. It advertises the new capability browser.desktopBridge.v1 in its status. When the host's own CLI calls an allowlisted browser.* method and a desktop is attached, the dispatcher sends the call down the stream instead of to orcad's own browser layer. Before it does, orcad turns the caller's workspace selector (path:, branch:, cwd) into a concrete workspace id. orcad does this only for calls from its local CLI, never for paired clients. The desktop must answer over the same socket that attached, or the answer is ignored. Detaching fails any waiting command at once.

  • Desktop. A new DesktopBrowserBridgeManager attaches to exactly the managed SSH hosts that have the switch on, are connected now, and whose server advertises the capability. It re-checks on every SSH state change and on every host-settings save, and it detaches at once when a host stops qualifying. Each forwarded command goes through executeDesktopBrowserBridgeRequest, which:

    • checks the method against the allowlist again;
    • asks the renderer, which owns the map from workspace to host, whether that host's own machine owns the workspace;
    • for a --page target, checks the page's workspace;
    • removes the fields a host must not choose when it creates a tab (profile, page id, tab group, focus, placement);
    • writes an audit line;
    • runs the command through an RpcDispatcher whose registry holds only the allowlisted methods.

    While the command runs, an AsyncLocalStorage scope pins every workspace selector the browser layer resolves to that one workspace. Any other selector is refused.

  • Renderer. A tab that this desktop's runtime asks for is now always created as a desktop page (browserRuntimeEnvironmentId: null), which is what the tab bar's + already does in a managed SSH workspace. Before, the renderer took the page's owner from the workspace, so a page created in a host's workspace was treated as one the host's server streams, and it never loaded.

Unchanged: local browser automation, paired-server client-hosted pages, headless hosts that use ORCA_BROWSER_EXECUTABLE while no desktop is attached, and the desktop's own CLI.

Threat model

A remote host that you may not fully trust (for example a sandbox VM that runs agents) gets to drive a browser that is signed in to your accounts. This is what each layer prevents:

Threat Mitigation
A host drives the desktop browser without consent Off by default. It is enabled per host by "Allow this host's orca CLI to control Orca", and an absent value counts as off. The desktop attaches only to hosts with the switch on. Turning the switch off, an SSH disconnect, removing the host, disconnecting the server in Orca, or retiring its transport (for example, a re-pair) each detach at once. On detach, any command that has not run yet is cancelled, including one queued behind another browser command, and orcad fails any command still waiting for an answer. Each forwarded command also carries a deadline that ends 10s before orcad stops waiting, which leaves room for transit. A command still queued on the desktop past that deadline never starts.
A host reaches the desktop's own worktrees or another host's tabs Every command must resolve to one workspace that the desktop's renderer shows as owned by that host's own machine. A --page in any other workspace is reported as not found. While the command runs, every workspace selector is pinned to that workspace, and anything else is refused. A command that names no workspace is refused rather than widened to all tabs.
A host reads or writes desktop files, or takes secrets The allowlist is narrower than the 1.4.223 relay, which ran any CLI command on the desktop. It leaves out upload, download, pdf (desktop file paths), the cookie jar (HttpOnly cookies), the network log (request headers), clipboard, exec (raw agent-browser commands), browser profiles and identity, credentials and headers, interception, certificate bypass, screencast and client-host methods. Orchestration, files and the rest of the old CLI surface are not forwarded. orcad serves those on the host itself now. The desktop re-checks the allowlist, and its dispatcher registry contains only the allowlisted methods.
A host reaches the desktop's own loopback or LAN The bridge runs only while this host's tabs are SSH-routed: SSH browser routing must be on, and the host must not be set to "browse from this device". Otherwise every command is refused with desktop_browser_bridge_unrouted. Routed tabs use a SOCKS5 proxy with no loopback bypass (<-loopback>), so all page traffic leaves through the host: subresources, fetch from eval, WebSockets and DNS (resolved by the proxy). The existing WebRTC, QUIC/H3 and DNS-prefetch guards and their tests cover the remaining paths. While the route is being prepared or has failed, the gate mounts no page at all. Turning routing off for a host detaches its bridge at once and cancels any command still waiting. A queued command checks, just before it runs, that its page's session is this host's prepared route partition.
A host's script reads another browsing context's cookies or storage Host-driven tabs live in that host's route partition, persist:orca-browser-v<N>-<digest(orca-profile, browser-profile, local SSH provider identity, ssh:<targetId>)> (deriveBrowserRoutePartition). It is separate from local browsing and from every other host. It is shared only with the user's own tabs in that same host's workspaces, which is the signed-in context this feature exists to reach.
A host's page script hangs or outlives revocation A bridged eval is bounded at 30s. It also stops at once when the bridge detaches. Either way the page's script is terminated with Runtime.terminateExecution, and the page's command queue is freed.
A host writes files onto the desktop download is not forwarded. A page in a workspace a host has driven also has every native download cancelled, so a navigation to a response that forces a download (Content-Disposition: attachment) cannot drop a file into the desktop's Downloads folder.
A host reads desktop files through the browser goto and tab create accept only http://, https:// or about:blank. A bare path would otherwise become file://, and loadURL skips the guest navigation guard. A command also refuses to run on a page that currently shows a local document, such as a file:// the user opened in that tab. Chromium's own error page still allows navigating away.
A host reads the desktop clipboard through a page it scripts Every page in a workspace a host has driven, and every popup those pages open, loses the auto-granted clipboard-read permission for the rest of the run, in both the request handler and the check handler. Once a host has driven any page, a guest whose owning page is unknown is also refused. Paste chords (Ctrl/Cmd+V, Shift+Insert) are refused in keypress. Typed and filled text still work.
A host repoints a command at another page or browser agent-browser reads global options such as --cdp anywhere in its argv. Any host value that reaches that argv and starts with - is refused, including every chunk of typed text. Eval runs over CDP and fill writes through stdin, so those two are exempt.
A host picks the desktop's browser profile or steals focus tab create drops the profile, page id, tab group, activation and placement fields. New tabs open in the background on the default profile.
Network attacker or a different peer The channel runs only inside the existing paired, end-to-end encrypted runtime connection through the managed SSH tunnel. Attach requires an authenticated paired-runtime socket (a device id and a connection id) that declared the capability. Phones are refused by the mobile method allowlist. Answers are accepted only from the socket and device that attached.
A desktop is bounced into running its own commands orcad forwards only calls from its own local CLI, never from a paired client. A desktop runtime never serves the bridge, because only orcad turns it on.
Unnoticed use The desktop logs one [desktop-browser-bridge] audit line per forwarded command: host, environment, method, workspace, page, URL origin (never the path or query, which can hold tokens) and outcome. Params are not logged, because typed text can be a password.

Residual risk: with the switch on, the host can do in that workspace's desktop tabs what a person at the keyboard could do on those pages, including running JavaScript on the page (eval). That is the point of the feature, and it is why the switch is per host and off by default.

Why

  • Option (a), a reverse bridge, restores the 1.4.223 behavior that users depend on: the signed-in desktop browser. A headless browser on the host is not signed in and leaves through the host's network.
  • Option (b), client-hosted pages from orcad, would move managed SSH tabs off the SSH-routed desktop tabs that Keep new browser tabs on SSH routing after managed conversion #26570 and fix(browser): managed SSH terminal links open in the Orca Browser again #29215 just settled on. It would also need a browser command layer in orcad that does not use Electron.
  • Mixed versions: the stream is a new optional method, and both sides negotiate it with a new capability.
    • A new orcad with an older desktop: nothing attaches, so the CLI gets the clear error.
    • A new desktop with an older orcad: the capability is missing from status, so the desktop never attaches and the old behavior stays.
    • No existing opcode or published payload changes.
  • I checked the 1.4.223 relay (ssh-relay-session.ts and ssh-remote-orca-cli.ts at v1.4.223). It forwarded the whole CLI through orca.cli. This PR forwards a curated browser-only subset on purpose.

Linked Issue

Fixes #29145

Visual Proof

N/A for a UI change: the only UI change is the help text under an existing switch. The Docker e2e saves a screenshot (desktop-browser-bridge-on.png) of the desktop after the host opened and navigated a tab.

Testing

  • Unit tests:

    • src/main/runtime/desktop-browser-bridge/desktop-browser-bridge-dispatch.test.ts runs a real RpcDispatcher for orcad. It covers forwarding and the answer, rejected spoofed answers from another socket, non-allowlisted methods, the error that names the switch when no desktop is attached (an old desktop or the switch off), a refused attach without the capability, refused unauthenticated or phone sockets, a desktop runtime refusing to serve, failing waiting commands on detach, keeping a local headless provider, and never forwarding a paired client's call.
    • desktop-browser-bridge-allowlist.test.ts checks that every allowlisted method is a real one-shot workspace-tier browser method, and that the risky methods are excluded.
    • src/main/browser/desktop-browser-bridge/desktop-browser-bridge-executor.test.ts covers scope enforcement: a foreign workspace, a foreign page, no workspace, a mismatched selector, the scope pinning other selectors during dispatch, tab create dropping fields, 16 refused methods, and audit entries that never contain the URL query.
    • desktop-browser-bridge-manager.test.ts covers attach and answer over the same socket, an old orcad never being attached, detaching when the switch turns off, a request before readiness, a non-allowlisted request at the protocol layer, a transient status failure retrying soon, and a removed host never retrying.
    • browser-request-ipc-bridge.owner.test.ts covers the renderer's owner check: the owning environment, another environment, a local workspace, an unknown workspace, a page resolved to its workspace, a page in another workspace, and a store that throws. It also checks that a requested page in a host's workspace is created as a desktop page.
  • Docker SSH e2e, tests/e2e/ssh-orcad-desktop-browser-bridge.spec.ts, run locally with ORCA_BACKGROUND_LAUNCH=1 and hidden windows against a real managed orcad on a Docker host:

    • With the switch off, orca tab list on the host is refused with the switch's name.
    • With it on, orca tab create --url opens a desktop tab in the host's workspace. A host-only hostname loads through the host.
    • orca goto --page navigates the tab, and orca tab list shows it.
    • With the switch off again, the host is refused.
  • pnpm tc, pnpm run check:code-quality:changed and the localization checks pass.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Security: see the threat model.
  • Platforms: cross-platform, with no paths or child processes added.
  • SSH: this PR is SSH-only by design.
  • Folder workspaces: they go through the same owner check, by workspace key.
  • Mobile: unaffected, because phones cannot call the new methods.
  • Performance: with the switch off (the default), the only extra work is a scan of the in-memory host list on SSH state changes. With it on, there is one idle stream per host.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@nwparker
nwparker force-pushed the nwparker/ssh-cli-reverse-bridge branch 4 times, most recently from aa7986d to 8f9939b Compare October 16, 2026 05:07
@nwparker
nwparker marked this pull request as ready for review October 16, 2026 05:25
@nwparker
nwparker force-pushed the nwparker/ssh-cli-reverse-bridge branch from 1727f96 to 912a0ce Compare October 16, 2026 05:32
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 1 minute.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ebcbea26-da84-43fa-95f5-eb8f3e6e4646
📥 Commits

Reviewing files that changed from the base of the PR and between bc67127 and ec97dea.

⛔ Files ignored due to path filters: 1
  • src/shared/rpc-contract/rpc-params-catalog.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing: 13
  • src/main/orcad/orcad-lifecycle.ts
  • src/main/runtime/orca-runtime-get-status.ts
  • src/main/runtime/terminal/driver/terminal-driver-resources.ts
  • src/preload/api/ui-command-event-api.ts
  • src/renderer/src/hooks/ipc-events-agent-status-window-test-fixtures.ts
  • src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts
  • src/renderer/src/hooks/ipc-events-terminal-create-window-test-fixtures.ts
  • src/renderer/src/hooks/useIpcEvents-browser-tab-create.test.ts
  • src/renderer/src/hooks/useIpcEvents-cli-worktree-activation.test.ts
  • src/renderer/src/hooks/useIpcEvents-close-routing-browser-pages.test.ts
  • src/renderer/src/hooks/useIpcEvents-ssh-disconnect-cleanup.test.ts
  • src/renderer/src/hooks/useIpcEvents-updater-status.test.ts
  • src/renderer/src/i18n/locales/en.json
📝 Walkthrough

This change adds browser RPC forwarding from eligible managed SSH host CLIs to the desktop browser. It adds bridge protocols, attachment management, workspace scoping, request validation, revocation handling, and clipboard-read and download restrictions for remote-CLI-driven guests. SSH settings now record explicit bridge consent and display updated help text and reset notices. The change also adds routing, unit, and end-to-end tests.

Priority: ⬆️ High

Severity of issue fixed: Medium

Merge Risk: 🟡 Moderate · up to bc671

The browser bridge mostly works as intended. However, tab creation from the desktop's own CLI in paired-server workspaces now opens a local page instead of the server's page, which should be fixed before merge. There are also two smaller issues: the "setting was turned off" notice can disappear after editing a host, and command timeouts are reported as the desktop having stopped sharing its browser.

Pre-merge checks: Passed 4 · Inconclusive 1

Failed Failed checks: 1 inconclusive

Check name Status Explanation Resolution
Docstring Coverage Inconclusive Docstring coverage is 32.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 70 functions across 50 files. (18 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed Issue #29145 requests a supported way for a managed SSH host CLI to use the desktop browser when the per-host switch is enabled. The PR adds a desktop bridge for allowlisted browser RPCs, attaches onl…
Out of Scope Changes check Passed The changes stay within issue #29145. The bridge protocol, consent reset, routing checks, workspace ownership checks, security restrictions, cancellation, localization, lifecycle wiring, and tests dir…
Title check Passed The title clearly identifies the SSH-related fix and the per-host switch that gates restored desktop browser control.
Description check Passed The description is complete and closely follows the template. It explains the user impact, mechanism, security controls, linked issue, testing, visual proof, and checklist status.
Full details: Docstring Coverage

Explanation

Docstring coverage is 32.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 70 functions across 50 files. (18 skipped: 7 unsupported, 11 over the file limit.)

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@nwparker
nwparker force-pushed the nwparker/ssh-cli-reverse-bridge branch from 912a0ce to bc67127 Compare October 16, 2026 06:13

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 35bd06d0-60e5-400e-8dda-a465ea83548d
📥 Commits

Reviewing files that changed from the base of the PR and between 8259633 and bc67127.

⛔ Files ignored due to path filters: 1
  • src/shared/rpc-contract/rpc-params-catalog.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing: 68
  • config/scripts/ci-e2e-job-selection.test.mjs
  • config/scripts/locale-ko-key-overrides.json
  • config/scripts/pr-e2e-source-routing.mjs
  • src/main/browser/agent-browser-bridge-capture-commands.ts
  • src/main/browser/agent-browser-bridge-queue.ts
  • src/main/browser/browser-manager-queries.ts
  • src/main/browser/browser-session-partition-policies.test.ts
  • src/main/browser/browser-session-partition-policies.ts
  • src/main/browser/desktop-browser-bridge/desktop-browser-bridge-connection.ts
  • src/main/browser/desktop-browser-bridge/desktop-browser-bridge-executor.test.ts
  • src/main/browser/desktop-browser-bridge/desktop-browser-bridge-executor.ts
  • src/main/browser/desktop-browser-bridge/desktop-browser-bridge-manager.test.ts
  • src/main/browser/desktop-browser-bridge/desktop-browser-bridge-manager.ts
  • src/main/browser/desktop-browser-bridge/desktop-browser-bridge-renderer-scope.ts
  • src/main/browser/desktop-browser-bridge/remote-cli-driven-workspaces.test.ts
  • src/main/browser/desktop-browser-bridge/remote-cli-driven-workspaces.ts
  • src/main/browser/local-ssh-browser-partitions.ts
  • src/main/ipc/desktop-browser-bridge-reconcile.ts
  • src/main/ipc/desktop-browser-bridge-wiring.ts
  • src/main/ipc/runtime-environment-manual-disconnect.ts
  • src/main/ipc/runtime-environments.ts
  • src/main/ipc/ssh-renderer-broadcast.ts
  • src/main/ipc/ssh-target-crud-handlers.ts
  • src/main/ipc/ssh.ts
  • src/main/orcad/orcad-lifecycle.ts
  • src/main/persistence/leasing-ssh-ptys/ssh-normalization-remote-cli-consent.test.ts
  • src/main/persistence/leasing-ssh-ptys/ssh-normalization.ts
  • src/main/runtime/desktop-browser-bridge/desktop-browser-bridge-allowlist.test.ts
  • src/main/runtime/desktop-browser-bridge/desktop-browser-bridge-dispatch.test.ts
  • src/main/runtime/desktop-browser-bridge/desktop-browser-bridge-registry.test.ts
  • src/main/runtime/desktop-browser-bridge/desktop-browser-bridge-registry.ts
  • src/main/runtime/desktop-browser-bridge/desktop-browser-bridge-route.ts
  • src/main/runtime/desktop-browser-bridge/remote-cli-browser-scope.ts
  • src/main/runtime/orca-runtime-get-status.ts
  • src/main/runtime/rpc/dispatcher.ts
  • src/main/runtime/rpc/methods/desktop-browser-bridge.ts
  • src/main/runtime/rpc/methods/index.ts
  • src/main/runtime/session-graph/session-graph-authority.ts
  • src/main/runtime/terminal/driver/terminal-driver-resources.ts
  • src/main/ssh/ssh-connection-store.test.ts
  • src/main/ssh/ssh-connection-store.ts
  • src/main/startup/main-process-quit.ts
  • src/preload/api/ui-bridge-state-and-menu-commands.ts
  • src/preload/api/ui-command-event-api.ts
  • src/renderer/src/components/settings/SshHostAdvancedFields.tsx
  • src/renderer/src/components/settings/ssh-target-draft.ts
  • src/renderer/src/hooks/ipc-events-agent-status-window-test-fixtures.ts
  • src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts
  • src/renderer/src/hooks/ipc-events-terminal-create-window-test-fixtures.ts
  • src/renderer/src/hooks/ipc-events/browser-request-ipc-bridge.owner.test.ts
  • src/renderer/src/hooks/ipc-events/browser-request-ipc-bridge.profile-switch.test.ts
  • src/renderer/src/hooks/ipc-events/browser-request-ipc-bridge.ts
  • src/renderer/src/hooks/ipc-events/browser-request-workspace-owner.ts
  • src/renderer/src/hooks/useIpcEvents-browser-tab-create.test.ts
  • src/renderer/src/hooks/useIpcEvents-cli-worktree-activation.test.ts
  • src/renderer/src/hooks/useIpcEvents-close-routing-browser-pages.test.ts
  • src/renderer/src/hooks/useIpcEvents-ssh-disconnect-cleanup.test.ts
  • src/renderer/src/hooks/useIpcEvents-updater-status.test.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/web/preload-api/web-ui-api.ts
  • src/shared/desktop-browser-bridge-protocol.ts
  • src/shared/ssh-types.ts
  • tests/e2e/ssh-orcad-desktop-browser-bridge.spec.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

Comment on lines +42 to +47
if (active?.revoked?.aborted) {
throw new BrowserError(
DESKTOP_BROWSER_BRIDGE_UNAVAILABLE_CODE,
'The Orca desktop stopped sharing its browser before this command ran.'
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

A missed desktop deadline is reported as "the desktop stopped sharing its browser".

In src/main/browser/desktop-browser-bridge/desktop-browser-bridge-connection.ts (lines 110-112), answer() merges this.revoked.signal and AbortSignal.timeout(request.timeoutMs) into one signal. That merged signal reaches this scope as revoked. Several checks then treat any abort as a detach:

  • captureRemoteCliBrowserRevocation (this range).
  • onRevoked in boundRemoteCliBrowserWork (lines 99-105).
  • The pre-dispatch check in desktop-browser-bridge-executor.ts (lines 96-101).

Each one returns DESKTOP_BROWSER_BRIDGE_UNAVAILABLE_CODE with a "stopped sharing" message.

Trigger: a command waits in the per-page queue behind a slow command until its deadline passes. The bridge is still attached, so orcad accepts the result and the CLI reports the bridge as unavailable. The agent or user then looks at the setting or the connection, but the real cause was a timeout.

Fix: check signal.reason. AbortSignal.timeout aborts with a DOMException whose name is 'TimeoutError'. Map that reason to browser_timeout.

Proposed fix
--- "a/src/main/runtime/desktop-browser-bridge/remote-cli-browser-scope.ts"
+++ "b/src/main/runtime/desktop-browser-bridge/remote-cli-browser-scope.ts"
@@ -38,13 +38,16 @@
  */
 export function captureRemoteCliBrowserRevocation(): (webContentsId?: number) => void {
   const active = scope.getStore()
   return (webContentsId) => {
     if (active?.revoked?.aborted) {
+      if ((active.revoked.reason as { name?: string } | undefined)?.name === 'TimeoutError') {
+        throw new BrowserError('browser_timeout', 'The browser command passed its deadline before it ran.')
+      }
       throw new BrowserError(
         DESKTOP_BROWSER_BRIDGE_UNAVAILABLE_CODE,
         'The Orca desktop stopped sharing its browser before this command ran.'
       )
     }
     // Why at execution: routing can be turned off while the command waits, remounting the page unrouted.
     if (webContentsId !== undefined && active?.admitGuest && !active.admitGuest(webContentsId)) {
       throw new BrowserError(

Apply the same check in onRevoked (lines 99-105) and in executeDesktopBrowserBridgeRequest (lines 96-101).

📝 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
if (active?.revoked?.aborted) {
throw new BrowserError(
DESKTOP_BROWSER_BRIDGE_UNAVAILABLE_CODE,
'The Orca desktop stopped sharing its browser before this command ran.'
)
}
if (active?.revoked?.aborted) {
if ((active.revoked.reason as { name?: string } | undefined)?.name === 'TimeoutError') {
throw new BrowserError('browser_timeout', 'The browser command passed its deadline before it ran.')
}
throw new BrowserError(
DESKTOP_BROWSER_BRIDGE_UNAVAILABLE_CODE,
'The Orca desktop stopped sharing its browser before this command ran.'
)
}

...updates,
allowRemoteCliControlConsent:
updates.allowRemoteCliControl === true ? 'desktop-browser-bridge' : undefined,
allowRemoteCliControlReset: undefined

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 5 'buildSshTargetSavePayload\s*\(|payload\.updates|updateTarget\s*\(' \
  src/renderer/src/components/settings src/main/ipc

Repository: stablyai/orca

Length of output: 12629


🏁 Script executed:

set -euo pipefail
printf '%s\n' '--- payload helper ---'
nl -ba src/renderer/src/components/settings/ssh-target-save-payload.ts | sed -n '1,180p'
printf '%s\n' '--- store consent helper and callers ---'
nl -ba src/main/ssh/ssh-connection-store.ts | sed -n '240,335p'
printf '%s\n' '--- relevant tests ---'
rg -n -C 8 -F -- 'allowRemoteCliControlReset' src/main src/renderer/src/components/settings

Repository: stablyai/orca

Length of output: 21023


🏁 Script executed:

set -euo pipefail
nl -ba src/main/ssh/ssh-connection-store.ts | sed -n '1,145p'

Repository: stablyai/orca

Length of output: 7214


Preserve the reset notice when the switch value is unchanged.

buildSshTargetSavePayload always includes allowRemoteCliControl: undefined in updates when the switch is off. The normal settings-save path sends those updates to SshConnectionStore.updateTarget, and withRemoteCliControlConsent clears allowRemoteCliControlReset based on property presence. An unrelated host edit can therefore remove the migration notice.

Compare the effective switch value with the existing target before clearing the notice. Preserve consent stamping when the update includes the switch.

Suggested fix
-function withRemoteCliControlConsent<T extends Partial<SshTarget>>(updates: T): T {
+function withRemoteCliControlConsent<T extends Partial<SshTarget>>(
+  updates: T,
+  clearResetNotice = true
+): T {
   if (!('allowRemoteCliControl' in updates)) {
     return updates
   }
   return {
     ...updates,
     allowRemoteCliControlConsent:
       updates.allowRemoteCliControl === true ? 'desktop-browser-bridge' : undefined,
-    allowRemoteCliControlReset: undefined
+    ...(clearResetNotice ? { allowRemoteCliControlReset: undefined } : {})
   }
 }
-    const consented = withRemoteCliControlConsent(updates)
+    const remoteCliControlChanged =
+      'allowRemoteCliControl' in updates &&
+      (existing?.allowRemoteCliControl === true) !==
+        (updates.allowRemoteCliControl === true)
+    const consented = withRemoteCliControlConsent(updates, remoteCliControlChanged)

Comment on lines +77 to +79
// Why null, not inferred: this desktop's runtime only ever asks for a page this desktop
// renders, including one in a managed SSH host's workspace (as the tab bar's + does).
browserRuntimeEnvironmentId: null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
rg -nP -C6 "browser:requestTabCreate" src/main
rg -nP -C4 "browserRuntimeEnvironmentId" src/renderer/src/store/slices/browser | head -80

Repository: stablyai/orca

Length of output: 16423


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- diff ---'
git diff --no-ext-diff --unified=30 0acb4933c098489a7a670f71f9619243a0244a24 bc67127cad8a44863a6e4789c046d90be286b076 -- src/renderer/src/hooks/ipc-events/browser-request-ipc-bridge.ts src/main/runtime/browser/browser-tab-renderer-create.ts src/main/runtime/browser/browser-commands.ts src/main/runtime/orca-runtime-browser.ts
printf '%s\n' '--- renderer bridge ---'
nl -ba src/renderer/src/hooks/ipc-events/browser-request-ipc-bridge.ts | sed -n '1,180p'
printf '%s\n' '--- main create and callers ---'
rg -n -F --glob '*.ts' -e 'createBrowserTab' -e 'requestTabCreate' -e 'resolveRemoteCliBrowserScopeSelector' src/main src/renderer/src | head -240
printf '%s\n' '--- owner test ---'
nl -ba src/renderer/src/hooks/ipc-events/browser-request-ipc-bridge.owner.test.ts | sed -n '1,230p'

Repository: stablyai/orca

Length of output: 41754


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- main browser creation ---'
nl -ba src/main/runtime/browser/browser-tab-creation.ts | sed -n '1,240p'
nl -ba src/main/runtime/browser/browser-tab-renderer-create.ts | sed -n '1,120p'
printf '%s\n' '--- renderer tab/page creation ---'
nl -ba src/renderer/src/store/slices/browser/browser-tab-actions.ts | sed -n '1,120p'
nl -ba src/renderer/src/store/slices/browser/browser-page-create-actions.ts | sed -n '1,100p'
printf '%s\n' '--- runtime id and ownership helpers ---'
rg -n -F --glob '*.ts' -e 'browserRuntimeEnvironmentId' -e 'isBrowserWorkspaceOwnedByPairedServer' -e 'resolveRemoteCliBrowserWorkspace' src/renderer/src src/main | head -240

Repository: stablyai/orca

Length of output: 42681


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- exact runtime-owner semantics ---'
nl -ba src/renderer/src/store/slices/browser/browser-host-state.ts | sed -n '1,145p'
nl -ba src/renderer/src/store/slices/browser-page-records.ts | sed -n '45,95p'
nl -ba src/renderer/src/hooks/ipc-events/browser-request-workspace-owner.ts | sed -n '1,130p'
printf '%s\n' '--- inference consumers ---'
rg -n -F --glob '*.ts' -e 'getRuntimeEnvironmentIdForWorktree' -e 'browserRuntimeEnvironmentId ??' -e 'browserRuntimeEnvironmentId === undefined' -e 'browserRuntimeEnvironmentId === null' src/renderer/src | head -180
printf '%s\n' '--- resolveBrowserWorkspace binding ---'
rg -n -F --glob '*.ts' -e 'resolveBrowserWorkspace:' -e 'resolveBrowserWorkspace(' -e 'resolveBrowserWorkspace =' src/main | head -180

Repository: stablyai/orca

Length of output: 41734


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- bound main resolver ---'
nl -ba src/main/runtime/session-graph/session-graph-authority.ts | sed -n '145,245p'
printf '%s\n' '--- page owner routing ---'
rg -n -F --glob '*.ts' -e 'page.browserRuntimeEnvironmentId' -e 'browserRuntimeEnvironmentId)' -e 'browserRuntimeEnvironmentId,' -e 'browserRuntimeEnvironmentId ?' src/renderer/src/components src/renderer/src/hooks src/renderer/src/lib src/renderer/src/store | head -180

Repository: stablyai/orca

Length of output: 7409


Preserve runtime-owner inference for the desktop CLI.

browser:requestTabCreate now forces every request to use local rendering. For an explicit paired-server worktree, the desktop CLI still resolves and sends the request, but the renderer stores browserRuntimeEnvironmentId: null instead of inferring the worktree owner. This creates a desktop-rendered page instead of the paired server's page.

Pass a bridge-only marker through IPC and set browserRuntimeEnvironmentId: null only for bridged creates. Keep the field omitted for ordinary desktop CLI requests, or reject or reroute explicit paired-server worktrees before sending browser:requestTabCreate.

… at run time, and cancels host-driven downloads
@SahilZ0810

Copy link
Copy Markdown
Contributor

since #27214 profile commands from an ssh host don't work anymore, earlier the relay used to forward them

orca tab profile create --label x --scope isolated now gives desktop_browser_bridge_not_forwarded and tab create strips profileId so no way to open a tab in another profile

can we allow a host to list/create/use/delete only its own isolated profiles ? import, cookies etc can stay blocked

our agents on remote seats need a separate login for testing and right now there is no way to do it

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.

[Bug]: Agents on SSH hosts can no longer drive the desktop Orca browser in 1.4.225 (worked via relay in 1.4.223)

2 participants