Skip to content

refactor(browser): one page gate and one owner for a host command's hold - #30652

Merged
nwparker merged 2 commits into
mainfrom
nwparker/browser-bridge-cancel-design
Oct 19, 2026
Merged

nwparker merged 2 commits into
mainfrom
nwparker/browser-bridge-cancel-design

Conversation

@nwparker

@nwparker nwparker commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​18 $\color{#cf222e}{\Huge{\mathbf{−}}}$​13 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​5
Prod 9 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​120 $\color{#cf222e}{\Huge{\mathbf{−}}}$​165 $\color{#cf222e}{\Huge{\mathbf{−}}}$​45

ELI5

#30630 makes a cancelled or revoked SSH host's browser command stop on the desktop. To get there it grew two separate "is this host still allowed?" checks and two copies of the "what does this command still have pressed?" bookkeeping. The steps of a command's hold on a page were also split between two files. This PR folds each of those into one place. Behaviour doesn't change.

What Changed

Before:

  • Page sinks used one of two checks, or both:
    • one read the host command's own scope;
    • the other read a per-page map plus the proxy message's context.
  • The CDP debugger channel had a third copy of the check.
  • Held buttons and keys were recorded at the CDP send and again at input injection. CDP input was recorded twice, and three call sites passed an extra { method, params } argument just for that.
  • Retiring the helper on revocation, the retire memo, removing the listener and converting the error all lived in the browser queue. Registering the hold and releasing held input lived in the admission module.

After:

  • One gate. assertRemoteCliPageAccess checks the host command's scope plus the hold that a proxy message arrived under. It runs at the guest CDP send, native input and edit commands, and the proxy's intake. The debugger channel's own check is gone, because every one of its sends reaches the CDP send.
  • One recorder. trackRemoteCliInput(id, method, params, deliver) records a press before it is delivered and a release only once it lands. It is called at the CDP send, and once on the single native widget-key path, which skips CDP.
  • One owner of the hold. runWithRemoteCliPageAdmission(id, admit, run, { revoked, retire, releaseHeld }) runs the whole sequence: hold the page; retire the helper as soon as the host is revoked; when the command settles, end the hold; replay held input off the page; report the revocation. The queue only supplies the hooks.
  • One scope capture. captureRemoteCliBrowserScope() now also returns the revoked signal, which replaces captureRemoteCliBrowserRevokedSignal.

Production code is 45 lines smaller (+120 / −165). Test changes are limited to two probe call sites, which now use the renamed APIs. No wire change.

Edge cases that change:

  • Proxy work that started while no host held the page, but landed while a revoked host held it, was refused before. It is now allowed. That work is the desktop's own, so allowing it matches fix(ssh): a cancelled or revoked host's browser command stops on the desktop #30630's rule that desktop-local commands are never refused.
  • A frame-session press refused before delivery is no longer recorded as held. The cleanup therefore no longer sends a release for a press that never landed.

Why

#30630's design is the right one:

  • an optional per-request cancel event, gated on a capability;
  • one abort signal per request;
  • enforcement where page actions are sent;
  • the release list replayed by the desktop.

Its review rounds went to finding sinks and edge cases, and each fix landed where the bug was found. This PR keeps the model and removes the duplicate checks and bookkeeping that built up, so the next sink only has to call one gate and one recorder. A larger rewrite was considered: putting proxy messages into the host's own scope instead of a separate hold context. It was rejected because long-lived timers created while handling a message would then inherit the host's scope and refuse the desktop's later sends. That was the same class of leak #30630 fixed for the proxy itself.

ref-oss check: the cancel table, per-request token and "replay the input cancel list" cleanup match the standard remote-RPC and input-automation models. The approach didn't change. This PR only consolidates.

Linked Issue

Follow-up to #30630 (SSH hardening review 4, finding #6). No GitHub issue.

Visual Proof

N/A. No UI changes.

Testing

  • src/main/browser, src/main/runtime/desktop-browser-bridge and the bridge RPC/IPC tests, single-worker: 242 files, 2299 tests passed.

  • pnpm tc:node.

  • Every step of the "static analysis and typecheck" job, run locally, including the ratchets.

  • Verified in unit tests only. Not exercised live against a real SSH host.

  • I manually tested these changes locally

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

Review

  • Codex (gpt-6.1-sol, high), round 1: no findings.
  • Opus, round 1: no regressions. It confirmed every input path still reaches the gate and the recorder, that ordering is unchanged, and that the crash check still runs in the same task as the send.

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

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 changed the base branch from nwparker/browser-bridge-cancel to main October 19, 2026 08:59
The revocation gate, the input cancel list and the hold's lifecycle each lived in two places.
Sinks now call one gate, held input is recorded at the CDP send (and the one native key path),
and runWithRemoteCliPageAdmission owns retire-on-revoke, release and error conversion.
@nwparker
nwparker force-pushed the nwparker/browser-bridge-cancel-design branch from c644234 to 36957bc Compare October 19, 2026 09:15
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

The changes centralize remote CLI page admission, revocation hooks, and input tracking. The host-command queue uses the admission hooks to retire helper sessions and release held input. CDP and direct-key paths use the updated page-access checks and input-tracking APIs. The debugger channel no longer checks page admission before dispatching commands. Tests update admission checks and verify helper access during revocation.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to 9517b

If helper retirement fails, a key can remain held and later proxy commands can be denied. Handle the retirement failure without skipping page cleanup before merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 61.11% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 10 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main refactor: consolidating the page gate and host-command hold ownership.
Description check Passed The description is complete and directly matches the changes. It covers the user-facing behavior, implementation details, rationale, edge cases, testing, scope, and checklist. The Linked Issue section…
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.
Full details: Description check

Explanation

The description is complete and directly matches the changes. It covers the user-facing behavior, implementation details, rationale, edge cases, testing, scope, and checklist. The Linked Issue section states that no GitHub issue exists, which is a minor deviation from the template but does not make the otherwise detailed description incomplete.

  • Fix all pre-merge checks with AI
✨ 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.

@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: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 66c8e053-9183-410d-9395-5a4ea3a2b791
📥 Commits

Reviewing files that changed from the base of the PR and between 912cb06 and 9517bd4.

📒 Files selected for processing: 11
  • src/main/browser/agent-browser-bridge-host-revocation.test.ts
  • src/main/browser/agent-browser-bridge-queue.ts
  • src/main/browser/agent-input-injection.ts
  • src/main/browser/cdp-debugger-channel.ts
  • src/main/browser/cdp-page-input-commands.ts
  • src/main/browser/cdp-ws-proxy-host-admission.test.ts
  • src/main/browser/cdp-ws-proxy.ts
  • src/main/browser/guest-cdp-command.ts
  • src/main/browser/guest-direct-key-input.ts
  • src/main/browser/remote-cli-page-admission.ts
  • src/main/runtime/desktop-browser-bridge/remote-cli-browser-scope.ts
💤 Files with no reviewable changes: 1
  • src/main/browser/cdp-debugger-channel.ts

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

Comment on lines +47 to +51
let retiring: Promise<void> | undefined
const retire = (): Promise<void> | undefined =>
hooks.retire && (retiring ??= runOutsideRemoteCliBrowserScope(hooks.retire))
const onRevoked = (): void => void retire()
hooks.revoked?.addEventListener('abort', onRevoked, { once: true })

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '35,165p' src/main/browser/remote-cli-page-admission.ts
sed -n '85,165p' src/main/browser/agent-browser-bridge-queue.ts

Repository: stablyai/orca

Length of output: 7305


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- admission references and owners ---'
rg -n -F --glob '*.ts' -e 'admissions.delete' -e 'runWithRemoteCliPageAdmission' -e 'assertRemoteCliPageAccess' -e 'trackRemoteCliInput' src/main/browser
printf '%s\n' '--- queue processing and revocation/cleanup references ---'
rg -n -F --glob '*.ts' -e 'destroySession(' -e 'onRevoked' -e 'revoked.abort' -e 'releaseHeldPageInput' -e 'processQueue(' src/main/browser
printf '%s\n' '--- lifecycle implementation ---'
sed -n '150,235p' src/main/browser/agent-browser-bridge-lifecycle.ts
printf '%s\n' '--- queue owner and processing ---'
sed -n '1,240p' src/main/browser/agent-browser-bridge-queue.ts
printf '%s\n' '--- admission module complete ---'
sed -n '1,190p' src/main/browser/remote-cli-page-admission.ts

Repository: stablyai/orca

Length of output: 22185


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- revocation and remote scope ---'
sed -n '1,125p' src/main/browser/desktop-browser-bridge/desktop-browser-bridge-connection.ts
sed -n '1,125p' src/main/runtime/desktop-browser-bridge/remote-cli-browser-scope.ts
printf '%s\n' '--- page input delivery and release ---'
sed -n '1,215p' src/main/browser/guest-direct-key-input.ts
printf '%s\n' '--- proxy admission boundary ---'
sed -n '150,215p' src/main/browser/cdp-ws-proxy.ts
printf '%s\n' '--- guest command and direct input paths ---'
sed -n '1,75p' src/main/browser/guest-cdp-command.ts
sed -n '1,175p' src/main/browser/guest-direct-key-input.ts

Repository: stablyai/orca

Length of output: 27735


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- proxy admission caller ---'
rg -n -F --glob '*.ts' -e 'runUnderRemoteCliPageAdmission' -e 'runUnderRemoteCliPageAdmission(' src
printf '%s\n' '--- proxy dispatch context ---'
rg -n -F --glob '*.ts' -e 'runUnderRemoteCliPageAdmission' src/main/browser/cdp-ws-proxy.ts src/main/browser || test "$?" -eq 1
sed -n '1,170p' src/main/browser/cdp-ws-proxy.ts
sed -n '215,360p' src/main/browser/cdp-ws-proxy.ts
printf '%s\n' '--- all admission map writes and input tracking callers ---'
rg -n -F --glob '*.ts' -e 'admissions.set' -e 'trackRemoteCliInput(' -e 'runUnderRemoteCliPageAdmission(' src/main

Repository: stablyai/orca

Length of output: 10573


Handle retirement rejection when creating retiring.

If hooks.retire rejects, the finally block exits at await retire() before it deletes the admission or calls releaseAll. processQueue only rejects the command, so no other owner clears the stale admission or releases its held input. A delivered key can therefore remain held. A proxy message that reaches CdpWsProxy can also run under the stale admission and be denied. Direct desktop-owned page gates do not use this map, so the denial is specific to work routed through the stale proxy admission.

🛡️ Suggested fix
--- "a/src/main/browser/remote-cli-page-admission.ts"
+++ "b/src/main/browser/remote-cli-page-admission.ts"
@@ -44,9 +44,12 @@
   hooks: PageHoldHooks = {}
 ): Promise<T> {
   const admission: Admission = { admit, held: new Map() }
   let retiring: Promise<void> | undefined
   const retire = (): Promise<void> | undefined =>
-    hooks.retire && (retiring ??= runOutsideRemoteCliBrowserScope(hooks.retire))
+    hooks.retire &&
+    (retiring ??= runOutsideRemoteCliBrowserScope(hooks.retire).catch(() => {
+      // Best effort: the helper may already be gone; the revocation is what gets reported.
+    }))
   const onRevoked = (): void => void retire()
   hooks.revoked?.addEventListener('abort', onRevoked, { once: true })
   admissions.set(webContentsId, admission)
📝 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 retiring: Promise<void> | undefined
const retire = (): Promise<void> | undefined =>
hooks.retire && (retiring ??= runOutsideRemoteCliBrowserScope(hooks.retire))
const onRevoked = (): void => void retire()
hooks.revoked?.addEventListener('abort', onRevoked, { once: true })
let retiring: Promise<void> | undefined
const retire = (): Promise<void> | undefined =>
hooks.retire &&
(retiring ??= runOutsideRemoteCliBrowserScope(hooks.retire).catch(() => {
// Best effort: the helper may already be gone; the revocation is what gets reported.
}))
const onRevoked = (): void => void retire()
hooks.revoked?.addEventListener('abort', onRevoked, { once: true })

@nwparker
nwparker merged commit 90236c3 into main Oct 19, 2026
42 checks passed
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.

1 participant