Conversation
… rejected Connect with a shared token cleared the stored bootstrap token and SSH tunnel before the new token was checked, whenever no device token existed. Validate first, and restore the previous record if the operator connect fails. - Run the existing pre-replacement check when a bootstrap token or tunnel is stored - Roll the registry back when that connect fails and the rollback save succeeds Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review completeClawSweeper finished reviewing this revision. The review result is being finalized. |
|
Codex review: needs real behavior proof before merge. Reviewed September 26, 2026, 2:14 PM ET / 18:14 UTC (Revision 11). ClawSweeper reviewWhat this changesThe branch restores the previous gateway record, tray settings, SSH tunnel configuration, and live operator connection after a shared-token connection fails. Merge readiness⛔ Blocked before merge - 5 items remain Current main still leaves a rejected shared token committed when authentication fails after the connect call returns, so this PR remains useful. The latest commit addresses the prior settings-capture finding. Merge readiness still depends on current-head recovery proof and a decision about the new 15-second handshake cutoff. Priority: P1 Review scores
Verification
How this fits togetherThe connection manager takes gateway credentials from the tray and stores the active gateway record before opening the operator connection. The tray service then saves matching settings and reconciles the runtime SSH tunnel. flowchart LR
A[Shared token request] --> B[Gateway record]
B --> C[Tray settings and SSH tunnel]
C --> D[Operator handshake]
D --> E{Connected or failed?}
E -->|Connected| F[New gateway active]
E -->|Failed| G[Restore prior state]
Decision needed
Why: The cutoff changes an existing asynchronous connection path, and source inspection cannot determine the acceptable timeout for users with slow gateways. Before merge
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Keep transactional rollback, then demonstrate the complete recovery path on this head and establish an accepted handshake timeout behavior for existing slow connections. Do we have a high-confidence way to reproduce the issue? Yes at the source level: current main commits the candidate before connecting and has no rollback for an authentication failure arriving after the call returns. This review did not execute the gateway path. Is this the best way to solve the issue? Unclear pending the timeout decision and current-head tunnel proof. Restoring the previous record through the connection owner is a maintainable direction. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning medium; reviewed against 5a59535216ee. LabelsLabel changes: No label changes. Label justifications:
EvidenceWhat I checked:
Likely related people:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (10 earlier review cycles; latest 8 shown)
|
A live pre-check on every stored bootstrap token rejected a normal shared-token save before the connection manager could connect. Keep that save path, and roll the registry back only when the operator connect fails. Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
|
Global triage: HOLD_FOR_AUTHOR. Take confidence 2%; recommendation confidence 99%; effort medium; risk high. Two verified rollback gaps block this exact head:
Please make registry, settings, runtime-tunnel state, and prior live connection recovery one transaction, with focused success and rollback-save-failure tests. Required proof is The three failed E2E lanes fail identically on the exact base during setup restart preparation; CI Gate is derivative. They are not attributed to this patch, but required recovery proof remains unavailable. |
ConnectWithSharedTokenAsync treated a still-connecting operator as success, so a gateway auth failure that arrived later left the rejected token committed. When a bootstrap or SSH setup credential is at risk, wait for that failure, then restore saved settings and reconnect a connection that was already live. Connection tests: 804 passed, 1 skipped. Shared tests: 4107 passed, 32 skipped. Tray tests: 3067 passed, 5 failed on the LF source-contract mismatch tracked in openclaw#1518. Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
… late After the deferred auth wait, roll the registry back only when this attempt still owns the connection generation. When another gateway was active, the settings callback receives that gateway's record so saved settings match the restored active id. Connection tests: 806 passed, 1 skipped. Shared tests: 4106 passed and 32 skipped, then the one dispose failure passed alone. Tray tests: 3067 passed, 5 failed on the LF source-contract mismatch tracked in openclaw#1518. Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
A shared-token replacement that is still connecting after the wait rolls the rejected token back instead of returning success. If settings or the runtime tunnel fail while applying the restored gateway, the callback retries that gateway and reports an out-of-sync error instead of writing the rejected snapshot back. Connection tests: 807 passed, 1 skipped. Shared tests: 4107 passed, 32 skipped. Tray tests: 3068 passed, 5 failed on the LF source-contract mismatch tracked in openclaw#1518. Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
When the handshake wait ends while the operator is still connecting, disconnect that attempt so its generation is cancelled and the operator is Idle before the previous gateway is opened again. The restore then waits for a terminal result. When rollback clears the active gateway, the settings callback restores the snapshot taken before the rejected record was applied. Connection tests: 808 passed, 1 skipped. Shared tests: 4107 passed, 32 skipped. Tray tests: 3069 passed, 5 failed on the LF source-contract mismatch tracked in openclaw#1518. Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
ApplySettings saves the candidate gateway before runtime tunnel reconciliation. If that reconciliation fails on the retry as well, write the snapshot taken before the apply back to settings and still throw, so the connection manager can roll the registry back without leaving the rejected gateway saved. Tray tests: 3069 passed, 5 failed on the LF source-contract mismatch tracked in openclaw#1518. Shared tests: 4107 passed, 32 skipped. Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
The rollback callback was capturing settings again after the candidate had been saved, so a second tunnel failure wrote the rejected gateway back. Capture the snapshot once per shared-token attempt and reuse it when the rollback callback fails. Tray tests: 3070 passed, 5 failed on the LF source-contract mismatch tracked in openclaw#1518. Shared tests: 4107 passed, 32 skipped. Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
BeginSharedTokenSettingsAttempt captures the snapshot, so the candidate callback must not clear it on success. The rollback callback still uses that snapshot when tunnel reconciliation fails twice. Tray tests: 3071 passed, 5 failed on the LF source-contract mismatch tracked in openclaw#1518. Shared tests: 4107 passed, 32 skipped. Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
The settings snapshot lived on the tray service and was captured before the connection manager took its lock, so a second connect could replace it. Each connect now captures its own attempt and passes that object into the settings callback. Shared tests: 4107 passed, 32 skipped. Tray suite: 3071 passed, 6 failed. Five failures are the LF source-contract mismatch tracked in openclaw#1518. CredentialReplacementFlows_DoNotBlindlyClearDeviceTokens then passed after its expected call was updated. Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
…lock The tray captured the settings snapshot before ConnectWithSharedTokenAsync took the transition lock, so a second request could snapshot a baseline from before the first transaction. The capture now runs inside that lock, and the second request cannot start until the first has entered it. Connection tests: 809 passed, 1 skipped. Shared tests: 4107 passed, 32 skipped. Tray tests: 3072 passed, 5 failed on the LF source-contract mismatch tracked in openclaw#1518. Signed-off-by: Sebastien Tardif <SebTardif@ncf.ca>
What Problem This Solves
Fixes: connecting with a rejected shared token removes the saved bootstrap token and SSH tunnel before the new token is checked, when no device token is stored yet. A gateway that rejects the token after the connect call has already returned also left that rejected token saved. A newer connection that starts during that wait could be overwritten, and a settings callback could target the edited gateway instead of the gateway that was active before the attempt.
User Impact
User impact: a failed shared-token connect leaves the setup-code bootstrap credential and the saved SSH tunnel in place. Saved tray settings are restored from the gateway that was active before the attempt. If the operator was already connected, that previous connection is opened again. A newer connection that takes over during the wait keeps its own record.
Why This Change Was Made
A stored device token still uses the existing pre-replacement check. When the record only has a bootstrap token or an SSH tunnel, the shared token is saved and connected on the normal path. If that operator connect fails, including an auth failure that arrives while the operator is still connecting, the previous record and active id are restored only when this attempt still owns the connection generation. If the wait ends and the operator is still connecting, that attempt is disconnected and the record is rolled back. The previous connection is opened only after the operator is Idle, and the restore waits for a terminal result. When rollback leaves no active gateway, the settings callback restores the snapshot taken before the rejected record was applied. That capture runs after ConnectWithSharedTokenAsync takes the transition lock, so a second request cannot snapshot settings before the first transaction has started. Each connectSharedToken call captures its own settings attempt and passes that object into the callback. A second call cannot replace the first call's snapshot. The app captures the snapshot before the candidate callback. That callback keeps it after a successful sync. The rollback callback reuses it. If runtime tunnel reconciliation fails on both attempts during that rollback, the pre-attempt settings are written back before the callback throws. The commit callback then runs with the record for the prior active gateway. If saving those settings or the runtime tunnel fails, the callback retries the restored gateway and reports an out-of-sync error instead of writing the rejected snapshot back. A connection that was already live is started again. A successful connect still clears a stale bootstrap token.
Evidence
Head
8c191b3938aac911dc22fa513d4e38c94d3f4602.Connection tests on that head: Passed 808, Failed 0, Skipped 1, Total 809.
The live gateway trace below was recorded on parent
41834892e8f186b0951600e2a0066532115486e7. This head adds the unfinished-handshake rollback and the settings recovery.The live check used an isolated registry and the gateway already listening at
ws://127.0.0.1:18789. The previous shared token was the installed gateway token. Its value is omitted. The replacement token wasrejected-shared-token-not-real(30 characters). The device token stored by the first connect was cleared before the replacement so the pre-replacement check did not skip the rollback.The stored shared-token length after the failure was 48 again, which is the previous token. The operator was Connecting when the call returned, and Connected again three seconds later.
Change Type
Scope
winnodeRequired proof pools
windows-wsl-gateway-e2e: saved settings, SSH tunnel, and prior live operator recovery after a rejected shared token.Validation
Head
8c191b3938aac911dc22fa513d4e38c94d3f4602../build.ps1: exit 0.dotnet test ./tests/OpenClaw.Shared.Tests/OpenClaw.Shared.Tests.csproj: Passed 4107, Failed 0, Skipped 32, Total 4139.dotnet test ./tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj: Passed 3071, Failed 5, Total 3076. The five failures are the LF source-contract mismatch tracked in fix(tests): tray source checks fail when the checkout is LF #1518.dotnet test ./tests/OpenClaw.Connection.Tests/OpenClaw.Connection.Tests.csproj: Passed 808, Failed 0, Skipped 1, Total 809.Real Behavior Proof
41834892e8f186b0951600e2a0066532115486e7, gatewayws://127.0.0.1:18789(HTTP 200 onhttp://127.0.0.1:18789/). The registry directory was a new temp directory, not the installed tray config.GatewayConnectionManager.ConnectAsyncwith the installed gateway token, device pairing approved, operator reached Connected. The stored device token was cleared.ConnectWithSharedTokenAsyncthen usedrejected-shared-token-not-realand a commit callback. The process read the operator state again three seconds later.cleared-bootstrap,has-bootstrap. The bootstrap token wasproof-bootstrap-not-realagain. The stored shared-token length was 48, matching the previous token. The operator was Connecting at return and Connected three seconds later.8c191b3938aac911dc22fa513d4e38c94d3f4602.ConnectWithSharedTokenAsync_RejectedTokenRestoresPriorLiveConnectioncovers the saved tunnel.ConnectWithSharedTokenAsync_UnfinishedHandshakeRollsBackcovers a handshake that stays connecting.SynchronizeSettings_TunnelFailure_KeepsCommittedGatewaySettingscovers a tunnel reconcile failure.ConnectWithSharedTokenAsync_NewerGenerationSkipsRollbackandConnectWithSharedTokenAsync_RejectedTokenRestoresPriorActiveGatewaySettingscover the generation check and the prior active gateway callback.Security Impact
NoYesNoNoNoYes, explain the risk and mitigation: A new shared token is validated before it replaces a bootstrap token or SSH tunnel when a device token is already stored. When no device token is stored, a failed connect, including an auth failure that arrives after connect returns, rolls the registry record back and runs the commit callback with the prior active gateway. The rollback is skipped when a newer connection owns the generation. Device tokens are not cleared by this path.Compatibility and Migration
YesNoNoReview Conversations