Skip to content

fix(connection): Open Dashboard can send the token before the SSH forward owns the port - #1514

Open
SebTardif wants to merge 29 commits into
openclaw:mainfrom
SebTardif:fix/f104-dashboard-owned-ssh-forward
Open

SebTardif wants to merge 29 commits into
openclaw:mainfrom
SebTardif:fix/f104-dashboard-owned-ssh-forward

Conversation

@SebTardif

@SebTardif SebTardif commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

What Problem This Solves

Open Dashboard could send a gateway token through an SSH forward that belonged to a different gateway, and it could drop that forward before the browser connected.

User Impact

The browser opens only when the gateway identity and SSH destination still match the ones checked before the wait. If a stop is already waiting before the browser is opened, the dashboard does not open and the stop finishes.

A saved gateway URL that names a remote host is left in settings. GatewayDashboardLauncher builds the browser URL. That URL keeps the saved mount path, query, and fragment, and uses the verified local forward for the host and port.

Before that URL is handed to the browser, this app binds both 127.0.0.1 and ::1 on the public port. SSH listens on a backend port. Guarded gateway clients use 127.0.0.1. If the tunnel already belongs to the connection manager, the replacement stays on that owner. Stop and Reset after the navigation is submitted end the SSH process and leave the public port occupied. Recovery drops the launch lease and can start SSH again on a backend port for the same user, host, remote port, and SSH port. A guard created for a start that fails before the browser opens is released.

Browser traffic is copied only when the accepted loopback socket belongs to that SSH process and the process start time still matches. Browser-proxy forwarding uses the saved local port. Local proxy ports 65534 and 65535 remain valid. The remote gateway port still has to leave room for the proxy's + 2.

A failed browser open shows the existing Connection notification and retry dialog. Retry keeps the requested path. A successful open dismisses that notification. The launch error is not written to the log, because the message can contain the token URL.

Switching to another saved SSH gateway that uses the same local port, while an older dashboard navigation is still protected, is still a maintainer decision.

Why This Change Was Made

The ownership wait returns the generation, local port, and SSH process id that passed listener verification.

The public port has to be owned on every loopback address the client can select, before SSH exits and before the browser receives the URL. Manager-owned tunnels keep that owner. A failed start does not leave the new bind behind.

Dashboard URL construction and launch failure handling stay in GatewayDashboardLauncher.

Evidence

Current head 2a20af4a0d672d61fec4ba95d88d49a7c5bc119e. SshTunnelService.EnsureSettingsOwnedForwardReadyAsync started real ssh.exe to root@127.0.0.1 port 22, with BatchMode and no password. The guest HTTP server listened on 127.0.0.1:19048. The owned forward was local port 19049, ssh pid 7580. GET /ocwn-1514?token=not-a-gateway-token returned HTTP 200. A browser opened that same URL. The guest log recorded token=true for those requests and did not store the query value. One later guest line was token=false, from a browser fetch that did not include the query.

A second call for remote port 19047 on the same local port returned owned=false. It did not replace the forward. Binding another listener on 19049 failed while the guard held it. The next request then failed with HttpRequestException. Calling ensure again for the original destination returned owned=true with a new ssh pid 43132, and the same GET returned HTTP 200.

A later run on the same head kept ssh pid 45824 as the recorded owner and pointed the backend port at a different process listening on 127.0.0.1:19046. The public GET failed. That other process accepted the TCP connection and read 0 bytes. The guest log did not record that request. Recovery then used ssh pid 36624 and the GET returned HTTP 200. The guest log for the whole run was two token=true lines, the allowed call and the recovered call.

The same head then covered a delayed navigation across explicit Stop and Reset. EnsureSettingsOwnedForwardReadyAsync used real ssh.exe to root@127.0.0.1 port 22, local port 19149, remote port 19148. The first owned forward was ssh pid 48196, backend port 61404. GET /ocwn-1514?token=not-a-gateway-token returned HTTP 200. The guest log was one token=true line. Navigation was submitted on that handoff, then Stop ran. Status was Stopped, the public port stayed held, and a second bind of 19149 threw SocketException. The delayed GET failed with HttpRequestException. A foreign listener on backend port 61404 accepted 0 connections and read 0 bytes. The guest log did not gain a line. Ensure for the same destination then used ssh pid 39768, backend port 61409, and the GET returned HTTP 200. The guest log gained a second token=true line.

ResetNotConfigured used the same sequence. Navigation was submitted on ssh pid 39768. After reset, status was NotConfigured, port 19149 stayed held, and a second bind threw SocketException. The delayed GET failed. A foreign listener on backend port 61409 accepted 0 connections and read 0 bytes. The guest log stayed at two lines. Ensure for the same destination then used ssh pid 35880, backend port 61414, and the GET returned HTTP 200. The guest log gained a third token=true line.

The retained-port behavior is unchanged. A local port Dashboard has reserved stays tied to the original user, host, remote port, and SSH port. A different destination is not accepted on that port. Whether saved gateways must use distinct local ports is still a maintainer decision.

Unit coverage on this head, from the earlier closeout:

Unit coverage on this head:

  • A manager-owned tunnel that is already running on the requested port stays manager-owned. A settings-owned tunnel stays settings-owned.
  • Holding the dashboard port rejects a second listener on 127.0.0.1 and on ::1. The guarded WebSocket host is 127.0.0.1.
  • Startup with SSH port 0 does not leave the public port bound.
  • Stop after a submitted navigation ends the planted SSH process and keeps the port bound.
  • A submitted navigation whose planted SSH process has exited drops the launch lease and keeps the port after Stop.
  • A different SSH port cannot reuse that hold.
  • Local port 65533 with browser-proxy forwarding emits proxy port 65535.

Change Type

  • Bug fix
  • Feature
  • Refactor
  • Docs or instructions
  • Tests or validation
  • Security hardening
  • Chore or infrastructure

Scope

  • Tray or WinUI UX
  • Windows node capability
  • Local MCP or winnode
  • Gateway, connection, or pairing
  • Setup or onboarding
  • Permissions, privacy, or security
  • Tests, CI, or docs

Required proof pools

  • windows-winui-interactive: isolated Debug tray built from this head. Settings had EnableMcpServer true, EnableNodeMode false, and UseSshTunnel true with an empty user and host, so setup did not block and the tunnel settings were incomplete. openclaw://dashboard was forwarded on the isolated pipe. The log recorded SSH tunnel is enabled but settings are incomplete. The Connection page showed a dialog titled Dashboard, with the text "Dashboard could not open. Check the verified gateway connection, then try again." Retry and Close are both fully inside the window. Invoking Retry removed that button, and the same dialog returned about 200 ms later. The incomplete-tunnel warning was logged again.

Dashboard retry on the isolated tray

Validation

Head 2a20af4a0d672d61fec4ba95d88d49a7c5bc119e.

  • ./build.ps1: exit 0
  • Shared: 4226 passed, 33 skipped, 0 failed
  • Tray: 3808 passed, 0 failed
  • Focused connection tests: 54 passed, 0 failed

Real behavior proof

  • Behavior or issue addressed: Claiming the dashboard port stopped a manager-owned tunnel and restarted it as a settings tunnel. The guard bound only 127.0.0.1, so ::1 on the same port stayed available. A failed start could also leave the new guard bound.
  • Real environment tested: Windows 11, worktree C:\Users\sebta\wt-wn-1514, head 2a20af4a0d672d61fec4ba95d88d49a7c5bc119e. Guest SSH is Ubuntu-24.04 on 127.0.0.1:22. The HTTP proof server was 127.0.0.1:19048 inside that guest.
  • Exact steps or command run after this patch: SshTunnelService.EnsureSettingsOwnedForwardReadyAsync for root@127.0.0.1, local port 19049, remote port 19048. Then an HTTP GET and a browser open of http://127.0.0.1:19049/ocwn-1514?token=not-a-gateway-token. Then the same call with remote port 19047. Then ensure again for remote port 19048. On this same head, the delayed case used local port 19149 and remote port 19148: submit the navigation, call Stop, send the GET, then ensure the original destination. Repeat that with ResetNotConfigured. The isolated tray then received openclaw://dashboard.
  • Evidence after fix: owned forward ssh pid 7580 returned HTTP 200. The changed destination returned owned=false. A second bind of 19049 was denied. Recovery used ssh pid 43132 and returned HTTP 200. On the next run, ssh pid 45824 stayed the recorded owner while a different process listened on the backend port. That process read 0 bytes and the guest did not see the request. Recovery used ssh pid 36624 and returned HTTP 200. After Stop, ssh pid 48196 was gone, the delayed GET failed, and the foreign listener on backend port 61404 read 0 bytes. Recovery ssh pid 39768 returned HTTP 200. After Reset, the foreign listener on backend port 61409 read 0 bytes. Recovery ssh pid 35880 returned HTTP 200. The isolated tray dialog is the screenshot above. Retry left and then returned.
  • Observed result after fix: Credential-bearing bytes were forwarded only through the owned ssh process. A different SSH destination did not take over that local port. A backend socket owned by another process received no request bytes. A GET that arrived after explicit Stop or Reset was not delivered, and the same destination worked again after recovery. The tray shows the failure dialog and opens it again when Retry is invoked.
  • What was not tested: Two saved SSH gateways were not switched in the UI. The maintainer has not accepted the distinct-port restriction.

Security Impact

  • New permissions or capabilities? No
  • Secrets or tokens handling changed? Yes
  • New or changed network calls? No
  • Command or tool execution surface changed? No
  • Data access scope changed? No
  • If any answer is Yes, explain the risk and mitigation: The shared token is appended only when the gateway identity and SSH destination still match the snapshot checked before the wait. This app binds 127.0.0.1 and ::1 before the URL is issued and keeps that binding after SSH stops. Guarded clients use 127.0.0.1. Forwarding happens only when the accepted loopback socket belongs to the SSH process for the original user, host, remote port, and SSH port. Launch failures are not logged, because the exception text can contain the token URL.

Compatibility and Migration

  • Backward compatible? Yes
  • Config or environment changes? No
  • Migration needed? No

Review Conversations

  • I replied to or resolved every bot review conversation addressed by this PR.
  • I left unresolved only conversations that still need maintainer judgment.

Open Dashboard marked the tunnel up as soon as ssh.exe started, then put
the shared token in the browser URL. Wait for the same listener ownership
check StartOwnedAsync uses, and do not build that URL when the wait fails.

- Add EnsureSettingsOwnedForwardReadyAsync for the settings-owned forward
- Stop the forward and show the existing tunnel toast when ownership fails
- Leave direct dashboard opens unchanged when SSH is off

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
@clawsweeper

clawsweeper Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@clawsweeper clawsweeper Bot added P1 Urgent regression or broken agent/channel workflow affecting real users now. merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Sep 24, 2026
@clawsweeper

clawsweeper Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codex review: blocked before merge. Reviewed October 6, 2026, 4:21 AM ET / 08:21 UTC (Revision 32).

ClawSweeper review

What this changes

The PR binds Dashboard browser launches to a verified SSH destination, protects the local port across tunnel teardown, and adds launch-failure handling and regression coverage.

Merge readiness

⛔ Blocked before merge - 3 items remain

The updated current-head evidence resolves the prior real-behavior-proof blocker. This work remains necessary on main, but its restriction on switching saved SSH gateways that share a local port still needs an explicit compatibility decision.

Priority: P1
Reviewed head: 2a20af4a0d672d61fec4ba95d88d49a7c5bc119e
Owner decision: Required. See Decision needed.

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) The patch has useful production-boundary and visible UI evidence with no supported blocking code finding; compatibility approval remains separate from its quality rating.
Proof confidence 🐚 platinum hermit (4/6) Sufficient (live_output): Current-head Windows evidence exercises the production SSH service and relay with real ssh.exe: allowed traffic succeeds, foreign backends receive zero bytes, Stop/Reset reject delayed requests, and same-destination recovery succeeds. The inspected screenshot and copied isolated-app observations now demonstrate the changed failure and explicit-retry path. Existing path-preservation and dismissal callbacks have supplemental regression coverage. Shared-profile compatibility remains a separate unresolved merge risk; no stored-data contract changes were found.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (live_output): Current-head Windows evidence exercises the production SSH service and relay with real ssh.exe: allowed traffic succeeds, foreign backends receive zero bytes, Stop/Reset reject delayed requests, and same-destination recovery succeeds. The inspected screenshot and copied isolated-app observations now demonstrate the changed failure and explicit-retry path. Existing path-preservation and dismissal callbacks have supplemental regression coverage. Shared-profile compatibility remains a separate unresolved merge risk; no stored-data contract changes were found.
Evidence reviewed 8 items Pinned introduced changes: Reviewed the merge-base-to-head production and test changes. The original head is 2a20af4; the supplied test merge is stale relative to the pinned base and was not used to attribute removals of main behavior.
Still necessary on main and latest release: Fetched main still uses the synchronous tunnel-start delegate before resolving credentials and launching Dashboard. Main and v2026.9.4 lack the Dashboard handoff and retained-port guard implementation, so existing WebSocket listener hardening does not establish this browser fix.
Destination restriction affects existing profiles: The retained guard pins user, host, remote gateway port, and SSH server port. StartProcess rejects a different destination on that public port. The PR body explicitly says saved-gateway switching was not exercised and the distinct-port restriction has not been accepted by a maintainer.
Findings None None.
Security None None.

How this fits together

Dashboard opening turns saved gateway credentials and SSH settings into a browser URL. The SSH service and loopback relay determine which remote gateway can receive subsequent browser traffic.

flowchart TD
  A[Saved gateway and credentials] --> B[Dashboard launch preparation]
  B --> C[Verify SSH destination and process]
  C --> D[Protected loopback relay]
  D --> E[Owned SSH forward]
  E --> F[Remote gateway]
  C --> G[Browser launch]
  G --> D
  B --> H[Visible failure and retry]
Loading

Decision needed

Question Recommendation
May opening Dashboard prevent switching to another saved SSH gateway that shares its local port, or must the implementation preserve that existing workflow? Preserve shared-port profile switching: Revise Dashboard endpoint allocation so old navigations remain destination-bound while existing saved profiles can still switch safely.

Why: Retaining the port prevents delayed credential disclosure, but the resulting profile restriction is intentional and explicitly unapproved; code review cannot choose that user-facing tradeoff.

Before merge

  • Resolve merge risk (P1) - After a Dashboard handoff, another saved SSH gateway using the same local port can no longer connect through that port during the app lifetime. Maintainers must approve or avoid this restriction, and verify the chosen behavior with fresh and existing saved profiles.
  • Complete next step (P2) - Resolve the shared-local-port compatibility choice and verify the selected contract with fresh and existing saved SSH gateway profiles before merge.
  • Resolve maintainer decision - Resolve the maintainer decision shown above before merge.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Net production and test growth production +1839 lines, tests +1011 lines The stated justification is a retained-port relay with process attribution and browser handoff protection, making the lifecycle surface substantial.

Merge-risk options

Maintainer options:

  1. Preserve existing profile switching (recommended)
    Keep old Dashboard endpoints protected while allowing another saved destination to connect without rewriting its stored preferences.
  2. Accept distinct-port requirements
    Approve the restriction explicitly and require visible remediation plus switching proof for existing saved profiles before landing.

Technical review

Best possible solution:

Keep destination-bound browser protection while preserving saved-profile switching, or provide an explicitly approved distinct-port contract with visible recovery for existing profiles.

Do we have a high-confidence way to reproduce the issue?

Yes, source provides a high-confidence race path: main starts SSH synchronously and launches the credential URL without carrying verified listener ownership through browser handoff. This read-only review did not execute a current-main failure.

Is this the best way to solve the issue?

Unclear overall: the ownership-bound relay addresses the credential race, but permanently restricting reuse of a configured local port requires an explicit upgrade-safety choice.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against 4895ed50e39e.

Labels

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient. Current-head Windows evidence exercises the production SSH service and relay with real ssh.exe: allowed traffic succeeds, foreign backends receive zero bytes, Stop/Reset reject delayed requests, and same-destination recovery succeeds. The inspected screenshot and copied isolated-app observations now demonstrate the changed failure and explicit-retry path. Existing path-preservation and dismissal callbacks have supplemental regression coverage. Shared-profile compatibility remains a separate unresolved merge risk; no stored-data contract changes were found.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): Current-head Windows evidence exercises the production SSH service and relay with real ssh.exe: allowed traffic succeeds, foreign backends receive zero bytes, Stop/Reset reject delayed requests, and same-destination recovery succeeds. The inspected screenshot and copied isolated-app observations now demonstrate the changed failure and explicit-retry path. Existing path-preservation and dismissal callbacks have supplemental regression coverage. Shared-profile compatibility remains a separate unresolved merge risk; no stored-data contract changes were found.
  • remove status: 📣 needs proof: Current PR status label is status: 👀 ready for maintainer look.
  • remove rating: 🦐 gold shrimp: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.

Label justifications:

  • P1: The PR addresses credential-bearing Dashboard traffic reaching an unowned SSH endpoint.
  • merge-risk: 🚨 compatibility: Retained destination-bound ports can prevent existing saved SSH profiles from reusing their configured local port.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): Current-head Windows evidence exercises the production SSH service and relay with real ssh.exe: allowed traffic succeeds, foreign backends receive zero bytes, Stop/Reset reject delayed requests, and same-destination recovery succeeds. The inspected screenshot and copied isolated-app observations now demonstrate the changed failure and explicit-retry path. Existing path-preservation and dismissal callbacks have supplemental regression coverage. Shared-profile compatibility remains a separate unresolved merge risk; no stored-data contract changes were found.
  • proof: sufficient: Contributor real behavior proof is sufficient. Current-head Windows evidence exercises the production SSH service and relay with real ssh.exe: allowed traffic succeeds, foreign backends receive zero bytes, Stop/Reset reject delayed requests, and same-destination recovery succeeds. The inspected screenshot and copied isolated-app observations now demonstrate the changed failure and explicit-retry path. Existing path-preservation and dismissal callbacks have supplemental regression coverage. Shared-profile compatibility remains a separate unresolved merge risk; no stored-data contract changes were found.

Evidence

What I checked:

  • Pinned introduced changes: Reviewed the merge-base-to-head production and test changes. The original head is 2a20af4; the supplied test merge is stale relative to the pinned base and was not used to attribute removals of main behavior. (2a20af4a0d67)
  • Still necessary on main and latest release: Fetched main still uses the synchronous tunnel-start delegate before resolving credentials and launching Dashboard. Main and v2026.9.4 lack the Dashboard handoff and retained-port guard implementation, so existing WebSocket listener hardening does not establish this browser fix. (src/OpenClaw.Tray.WinUI/Services/GatewayDashboardLauncher.cs:23, 4895ed50e39e)
  • Destination restriction affects existing profiles: The retained guard pins user, host, remote gateway port, and SSH server port. StartProcess rejects a different destination on that public port. The PR body explicitly says saved-gateway switching was not exercised and the distinct-port restriction has not been accepted by a maintainer. (src/OpenClaw.Connection/SshTunnelService.cs:442, 2a20af4a0d67)
  • Current-head final-effect security evidence: The captured PR body reports Windows 11 production SshTunnelService runs with real ssh.exe: allowed HTTP/browser requests returned 200; a changed destination was rejected; a foreign backend read zero request bytes; explicit Stop and Reset blocked delayed requests while retaining the public port; same-destination recovery returned 200. These observations directly exercise the relay's allowed and forbidden backend cases rather than relying on unit tests. (src/OpenClaw.Connection/DashboardForwardPortGuard.cs:176, 2a20af4a0d67)
  • Updated isolated WinUI proof: Inspected the prepared local screenshot from https://github.com/user-attachments/assets/b9b5b441-9038-4528-80a9-dff6b6bb9fcf. It visibly shows the Dashboard failure notification and dialog with Retry and Close inside the window. The captured current-head body additionally reports openclaw://dashboard activation and explicit Retry causing the dialog and incomplete-settings diagnostic to return. (src/OpenClaw.Tray.WinUI/App.xaml.cs:4037, 2a20af4a0d67)
  • Re-review continuity and validation: The previous completed review has this same head and no remaining findings. Its UI failure/retry proof request is now addressed. Requested-path retry and successful notification dismissal retain the existing main callbacks, with supplemental launcher and source-contract coverage; their absence from this screenshot is not a new runtime defect. The body reports build exit 0, Shared 4226 passed/33 skipped, Tray 3808 passed, and focused Connection 54 passed. No build or tests were executed during this read-only review. (src/OpenClaw.Tray.WinUI/App.xaml.cs:4053, 2a20af4a0d67)

Likely related people:

  • shanselman: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)
  • bkudiess: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Record the chosen shared-port contract and verify switching with fresh and existing saved SSH gateway profiles.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (31 earlier review cycles; latest 8 shown)
  • reviewed 2026-10-02T05:34:38.103Z sha 36067e9 :: needs real behavior proof before merge. :: [P1] [P1] Preserve endpoint protection before stopping SSH | [P1] [P1] Bind retained port reuse to the original gateway destination | [P1] [P1] Preserve guarded restart with browser-proxy forwarding enabled | [P1] [P1] Validate the connected backend before relaying browser traffic | [P1] [P1] Retain submitted-navigation protection after unexpected SSH exit | [P2] [P2] Close relay sockets when either direction finishes
  • reviewed 2026-10-02T14:44:57.603Z sha 6f0a4cb :: needs real behavior proof before merge. :: [P1] [P1] Include the SSH server port in retained destination checks | [P1] [P1] Attribute the accepted backend socket before copying bytes | [P1] [P1] Avoid permanently pinning ports without a Dashboard handoff | [P1] [P1] Let owned recovery restart behind the retained public port | [P2] [P2] Validate the actual proxy port when an override is supplied | [P2] [P2] Keep Dashboard endpoint policy in its existing launch owner
  • reviewed 2026-10-02T15:27:18.550Z sha 36835f2 :: needs real behavior proof before merge. :: [P1] Preserve public-port ownership before terminating SSH | [P1] Retain pending-navigation protection after dropping the recovery lease | [P1] Validate the computed proxy port without reserving another two ports
  • reviewed 2026-10-02T16:09:50.221Z sha 99f5016 :: needs real behavior proof before merge. :: [P1] Preserve manager ownership when claiming the Dashboard port | [P1] Verify the public loopback route as well as the SSH backend | [P2] Release newly acquired guards when startup fails before handoff
  • reviewed 2026-10-02T16:40:23.107Z sha 2a20af4 :: needs real behavior proof before merge. :: none
  • reviewed 2026-10-06T07:06:08.147Z sha 2a20af4 :: needs real behavior proof before merge. :: none
  • reviewed 2026-10-06T07:21:48.735Z sha 2a20af4 :: needs real behavior proof before merge. :: none
  • reviewed 2026-10-06T07:49:30.823Z sha 2a20af4 :: needs real behavior proof before merge. :: none

@karkarl

karkarl commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

Global triage: HOLD_FOR_AUTHOR. Take confidence 10%; recommendation confidence 97%; effort medium; risk high.

Two handoff races block this exact head:

  1. Dashboard tunnel ownership is reduced to a point-in-time Boolean. The verified process and generation are discarded before credential resolution and browser launch, so a concurrent tunnel stop or replacement can put another localhost listener behind a page whose fragment contains the shared token.
  2. When an older waiter detects replacement, StopIfCurrent preserves the newer process, but the catch path unconditionally writes LastError and Status=Failed, exposing a running replacement tunnel as failed.

Please carry a generation-bound ownership lease through browser launch, prevent replacement during that handoff, and guard failure-state updates by the captured process/generation. Add overlapping-attempt and replacement-before-launch regressions.

Run windows-winui-interactive with the isolated current-head app and prove real ssh.exe ownership before launch, no browser/network effect on port conflict, and no launch after ownership replacement. Required build, Shared, Tray, and rubber-duck closeout are also missing from the body.

The E2E setup refusals appear shared/baseline; the UI lane timed out and remains unresolved. CI Gate is derivative, so the required current-head proof is not green.

@shanselman shanselman added the status: 🚢 actively landing A maintainer or agent is actively driving this item through implementation, validation, or merge. label Sep 25, 2026
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0f6d0b3f-48b9-489e-a9dd-a736c63d485b
Resolve the dashboard credential and endpoint from one immutable gateway snapshot, preserve newer tunnel state when an older ownership wait fails, and revalidate the exact tunnel generation at browser handoff.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0f6d0b3f-48b9-489e-a9dd-a736c63d485b
@shanselman

Copy link
Copy Markdown
Collaborator

@clawsweeper re-review

Current head 6649c8b fixes the endpoint snapshot mismatch, preserves newer tunnel state, and holds a generation-bound owned-listener lease through browser handoff. Full local build plus Shared, Tray, and Connection suites pass. The PR body now includes current-head isolated WinUI/MCP/browser proof and the explicit live-SSH blocker.

@clawsweeper

clawsweeper Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🦞👀
Exact review queued.

Re-review progress:

@shanselman shanselman removed the status: 🚢 actively landing A maintainer or agent is actively driving this item through implementation, validation, or merge. label Sep 25, 2026
Restore PR openclaw#1514 to SebTardif's original four-file patch after the maintainer repair exceeded the bounded review scope.

This reverts commit 6649c8b.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0f6d0b3f-48b9-489e-a9dd-a736c63d485b
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Sep 25, 2026
The listener check returned before the shared-token URL was handed
to the browser, and an older wait wrote Failed onto a replacement
tunnel. Dashboard launch now keeps the generation from the ready
check and refuses to start the browser if that generation is no
longer the settings-owned forward. A wait that has already lost
the process does not publish failure.

Tests: EnsureSettingsOwnedForwardReadyAsync 2 passed. Dashboard
contract tests 2 passed. ./build.ps1 exit 0. Shared 4107 passed,
32 skipped. Tray 3116 passed.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
A manager-owned tunnel was rejected even when it was the verified
forward. The failure flag was written after the ownership lock was
released, so an older wait could mark a replacement failed. Open
Dashboard now accepts a settings or manager owner, requires the
dashboard URL to use that local port, and keeps the process alive
until the browser launch returns.

Tests: EnsureSettingsOwnedForwardReadyAsync 2 passed. Dashboard
contract test passed. ./build.ps1 exit 0. Shared 4107 passed, 32
skipped. Tray 3116 passed.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
A stop during the browser handoff used to return and let a later
start publish a new process over the old listener. A different
tunnel spec now fails while that handoff is active, and the old
process stays the tracked one. Ending the handoff no longer stops
the forward, so the port stays up after the browser is launched.

Tests: SshTunnel 67 passed. ./build.ps1 exit 0. Shared 4107 passed,
32 skipped.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
…f lease

A handoff could make StopLocked return while the caller still replaced the tunnel, and StopIfOwnedAsync reported that deferred stop as complete.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. labels Sep 28, 2026
…f ends

A stop or reset during a browser handoff was dropped, so the forward stayed up after the launch and a reset could report NotConfigured while the process was still running.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
…e browser connects

Open Dashboard waited for one SSH forward, then read the active gateway again. A different gateway on the same local port could send its token through the forward that had just been checked. The launch now keeps one gateway identity, tunnel destination, and token snapshot through that wait and the browser handoff. A token rotation on that same gateway and destination still opens.

Process.Start returns before the browser connects. A stop recorded during launch used to run as soon as the handoff method finished, which freed the port while the browser still held the token URL. A stop that is already waiting skips the launch and completes. A stop that arrives during launch keeps the listener until an established loopback connection uses the forward, or until ssh exits. If that connection does not show up, the listener stays up.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Sep 29, 2026
Core and CLI failed only WaitForOperatorHandshakeAsync_ReturnsTrue_WhenStateEventFires. The wait returned false after 934 ms. That test is not in this change, and it passed locally in 14 ms. The SSH handoff tests in the same job passed.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
The dashboard snapshot read the active gateway again after resolving
the token, so a switch could pair one gateway's token with another's
identity. The browser lease and a deferred stop are now decided under
one lock. An established loopback connection releases that stop only
when it is new for this handoff and, for an accepted socket, owned by
the SSH process.

Validation: ./build.ps1 exit 0. Shared 4107 passed, 32 skipped.
Tray 3116 passed. Connection SSH and TCP snapshot tests passed 40.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
@clawsweeper clawsweeper Bot added the rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. label Oct 2, 2026
Another tab in the same browser can open a new connection to the forward.
That is not this dashboard navigation, so the handoff stays while that SSH
process is alive. The watcher starts even when Windows returns no browser
process, and it releases the lease once that SSH process has exited.

Validation: .\build.ps1 succeeded. Shared tests 4224 passed, 33 skipped.
Tray tests 3808 passed. Connection handoff tests 42 passed.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
…forward

Disconnect, Restart, and shutdown cancel an open dashboard handoff and then
stop the SSH process. Another browser connection still does not release the
hold. A saved remote gateway URL is opened through the verified local
forward, and the saved profile is left unchanged.

Validation: .\build.ps1 succeeded. Shared tests 4224 passed, 33 skipped.
Tray tests 3808 passed. Connection handoff tests 36 passed. Snapshot tests 7 passed.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. labels Oct 2, 2026
Stop before the browser is opened cancels that launch and then stops SSH.
Stop after the URL was handed to the browser leaves the forward in place
until a second stop, or until that browser process exits. The local forward
URL keeps the saved mount path, query, and fragment.

Validation: .\build.ps1 succeeded. Shared tests 4224 passed, 33 skipped.
Tray tests 3808 passed. Connection handoff and dashboard URL tests 34 passed.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
@clawsweeper clawsweeper Bot added the merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. label Oct 2, 2026
Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
…ell exit

Repeated Stop leaves a submitted dashboard hold in place. A shell process
that exits after handing the URL to an existing browser does not release it.
Dispose, StopAsync, and Restart stop SSH and keep that local port occupied
until the next SSH start binds it.

Validation: .\build.ps1 succeeded. Shared tests 4224 passed, 33 skipped.
Tray tests 3808 passed. Connection handoff tests passed, including repeated
Stop, activation-process exit, and disposal.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
A port held after dashboard teardown is our listener, so startup does not
reject it as a foreign process. The guard stays bound. Replacement SSH
listens on a backend port, and the guard forwards only to that process.

Validation: .\build.ps1 succeeded. Shared tests 4224 passed, 33 skipped.
Tray tests 3808 passed. Connection handoff and port-guard tests 38 passed.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
This app binds the browser port before SSH starts and keeps it when SSH
stops. A replacement SSH process listens on a backend port, and traffic is
forwarded only when that backend is still the SSH process for the original
gateway. A different host or remote port cannot reuse the hold. Browser-proxy
forwarding still uses the saved local port. An unexpected SSH exit keeps the
hold instead of dropping the navigation lease.

Validation: .\build.ps1 succeeded. Shared tests 4224 passed, 33 skipped.
Tray tests 3808 passed. Focused handoff and port-guard tests 40 passed.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
Hold the browser port only while a submitted or opening dashboard
navigation needs it. An unexpected SSH exit keeps that port, records
the user, host, remote port, and SSH port, and drops the lease so
recovery can listen on a backend port. Copy bytes only after the
accepted loopback socket belongs to that SSH process and its start
time still matches. Validate the browser-proxy port on the saved local
port. Project the dashboard URL in GatewayDashboardLauncher.

Validation: ./build.ps1 exit 0. Shared 4225 passed, 33 skipped. Tray
3808 passed. Focused handoff tests 46 passed.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
Claim the public port before a dashboard SSH process listens there.
Stop and Reset end that SSH process after the navigation is submitted,
and they leave the port bound. Recovery still drops the launch lease
so the same destination can restart on a backend port. Validate the
browser-proxy local port as a normal port, including 65534 and 65535.

Validation: ./build.ps1 exit 0. Shared 4226 passed, 33 skipped. Tray
3808 passed. Focused handoff tests 46 passed.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
Dashboard startup keeps a manager-owned tunnel on that owner when it
moves SSH behind the public port. The port guard binds 127.0.0.1 and
::1, and guarded clients use 127.0.0.1. A guard acquired for a start
that fails before handoff is released.

Validation: ./build.ps1 exit 0. Shared 4226 passed, 33 skipped. Tray
3808 passed. Focused connection tests 54 passed.

Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. labels Oct 2, 2026
@karkarl

karkarl commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

Global triage: HOLD_FOR_AUTHOR. Take confidence 20%; recommendation confidence 98%; effort medium; risk high.

Reviewed exact head 2a20af4a0d67. GitHub currently reports CLEAN with 35 passing, 1 failing, and 0 pending checks. proof label is sufficient. Durable automated signal: Codex review: blocked before merge..

Author: investigate the 1 failing check(s), establish whether each failure is branch-owned, and publish a green exact-head CI Gate before reconsideration.

This is a triage classification, not a merge action. No branch changes were made.

This branch has not been deployed

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

Labels

merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P1 Urgent regression or broken agent/channel workflow affecting real users now. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants