fix(connection): require hello-ok before operator RPCs - #1425
eleqtrizit wants to merge 4 commits into
Conversation
The gateway 1008-closes (PolicyViolation) any non-connect frame sent before hello-ok (#1418). Gate operator readiness (IsConnectedToGateway) on the handshake snapshot and guard the application send paths: - SendTrackedRequestAsync: fire-and-forget requests are silently dropped pre-handshake (mirrors the socket-closed contract; the post-handshake refresh burst re-requests the state). - SendWizardRequestAsync: throws InvalidOperationException carrying the shared HandshakePendingError marker; tolerant callers (pairing, models fallback, media, payload reads) already degrade gracefully. - GetUpdateStatusAsync: null pre-handshake, keeping its no-info contract. - TryRequestPayloadAsync: handshake-pending reads return null, same shape as unknown-method. - connect itself is exempt by construction: the handshake rides SendConnectMessageAsync/SendRawAsync, never the tracked/wizard paths. Tests: mechanical wizard/media suites satisfy the gate via a CompleteHandshakeForTest flag-set (real challenge/connect/hello-ok path is exercised by the round-trip suite, which now performs the full dance against the loopback gateway); three new guard tests pin suppression, wizard rejection, and readiness lifecycle.
|
🦞👀 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 maintainer review before merge. Reviewed September 17, 2026, 11:57 AM ET / 15:57 UTC (Revision 7). ClawSweeper reviewWhat this changesThe PR withholds operator requests until the Gateway handshake completes, reports suppressed mutations as unsuccessful, and adds regression tests and captured WebSocket traces. Merge readiness✅ Ready for maintainer review This remains a useful fix: current main and v2026.9.4 lack the handshake guard. Prior findings are resolved, and the new Windows evidence resolves the previous product-path proof blocker. No introduced correctness or security defect remains identified. Priority: P1 Review scores
Verification
How this fits togetherThe Windows Companion's shared operator client carries session, model, pairing, and wizard requests to the Gateway. Its readiness signal tells consumers when the connection can accept application requests. flowchart TD
A[Companion requests] --> B[Operator client]
B --> C{Socket open and handshake complete?}
C -->|No| D[Report unavailable or not sent]
C -->|Yes| E[Send request to Gateway]
F[Gateway hello acknowledgement] --> C
G[Disconnect] --> H[Clear handshake readiness]
H --> C
Before mergeNone. Agent review detailsSecurityNone. Review metrics
Root-cause clusterRelationship: Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Technical reviewBest possible solution: Keep handshake readiness at the shared operator request boundary, preserving explicit unsuccessful submission results and requiring fresh submissions after reconnect. Do we have a high-confidence way to reproduce the issue? Yes, source provides a high-confidence path: submit an operator request while its WebSocket is open but hello-ok is pending. Current main admits that request; this review did not execute a failing current-main reproduction. Is this the best way to solve the issue? Yes. Reusing the existing handshake snapshot at the shared send boundaries addresses the reported race without adding configuration, changing persisted state, or duplicating connection ownership. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning medium; reviewed against 3c43751b2bac. LabelsLabel justifications:
EvidenceWhat I checked:
Likely related people:
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (6 earlier review cycles)
|
SendTrackedRequestAsync returns Task<bool> now: false when the socket is closed or the hello-ok handshake is still pending (#1418), true only after the frame hit SendRawAsync successfully. Boolean mutation wrappers (ResetSessionAsync et al.) propagate the not-sent result instead of reporting a false success for frames that never reached the wire; read-style fire-and-forget callers ignore the result and rely on the post-handshake refresh burst, unchanged. Adds two live round-trip tests capturing the wire-level trace: withheld handshake suppresses sessions.reset (no frame on the wire, submission=false), and a server-initiated one-way Close drives the client's real auto-reconnect path, re-running the challenge -> connect -> hello-ok dance before the gate reopens and the mutation is sent.
Redacted [trace] lines captured from the actual green dotnet test run (TRX-embedded ITestOutputHelper output) of both HandshakeGate live round-trip tests: pre-hello-ok suppression with zero frames on the wire, gate held while disconnected after a server-initiated one-way Close, and the real auto-reconnect path re-running challenge -> connect -> hello-ok before the mutation is sent. Auth tokens, device ids, and device key material redacted; all values are test-only.
scripts/validate-docs.ps1 rejects U+2014 in maintained Markdown; replace the six em dashes in the proof document with permitted punctuation. The captured trace blocks are unchanged.
|
Body restored: Required proof pools, Validation, and Real Behavior Proof sections are filled in (captured artifact linked, exact commands and pass counts, explicit blockers: windows-wsl-gateway-e2e pool maintainer-scheduled; full build via CI). @clawsweeper re-review |
|
windows-wsl-gateway-e2e pool recorded: maintainer-provided Windows interactive-desktop host (ARM64, dotnet 10.0.401 win-x64, WSL 2.7.12), manifest command at head 17cfc72 with OPENCLAW_RUN_E2E=1. Result total=20 passed=19 skipped=1 (opt-in Ollama test, expected skip) failed=0 exit=0. TRX and redacted loopback diagnostics in the evidence gist linked in the PR body. Both previously transient failures (emulated-x64 WSL gateway startup race) passed on re-run with no code changes. @clawsweeper re-review |
What Problem This Solves
Fixes an issue where users operating the Windows Companion over a Gateway connection would have operator requests sent on the wire before the Gateway handshake completed: the gateway rejects early tracked requests with a 1008 socket close, so session resets and wizard RPCs raced against handshaking or reconnecting and knocked out the connection. Fixes #1418.
Why This Change Was Made
The operator client's readiness signal admitted tracked requests on transport connectivity alone. The fix holds a handshake snapshot at the shared request boundary: tracked mutations submitted before hello-ok are withheld and reported as not-sent instead of false success; the gate reopens once a fresh hello-ok lands, and suppressed mutations are not replayed automatically (callers resubmit). Wizard RPCs surface the pending-handshake error without closing the transport. Post-handshake behavior is unchanged.
User Impact
Tray and wizard sessions no longer lose their Gateway connection by racing the handshake: early requests are withheld (zero frames on the wire) and reported as not-sent, so callers resubmit once the session is ready instead of the Gateway rejecting them on the wire. Callers observe an accurate not-sent result rather than a false success while disconnected.
Evidence
docs/gateway-handshake-gate-proof.md(redacted TRX-embedded trace from a realdotnet testrun of the production client over loopback WebSockets).Change Type
Scope
Required proof pools
windows-wsl-gateway-e2e: reconnect and operator-request gating on the Gateway connection changed, which matches this pool's requiredWhen. Run 2026-09-17 on a maintainer-provided Windows interactive-desktop host (ARM64, Windows 10.0.28000, dotnet 10.0.401 win-x64, WSL 2.7.12): the manifest commandrun-proof-tests.ps1 -Project tests\OpenClaw.E2ETests\OpenClaw.E2ETests.csproj -Filter FullyQualifiedName~OpenClaw.E2ETests.Setup.SetupAndConnectTests -ResultName gateway-setup-connect -RuntimeIdentifier win-x64withOPENCLAW_RUN_E2E=1at head17cfc72. Result: total=20, passed=19, skipped=1 (opt-in Ollama permission test, expected skip), failed=0, exit=0 in 5.6 min. Evidence: secret gist withgateway-setup-connect.trxplus redacted loopback diagnostics (gateway-port-allocation.json,service-owned-gateway-start.jsonl): https://gist.github.com/eleqtrizit/8ed9ae54e4832375221afbc6297d3de7 . The fixture provisions an isolated scratch distroOpenClawE2E-{runId}; a first attempt produced two transient failures ("managed WSL gateway is not listening yet", an emulated-x64 WSL startup race), and both tests passed on re-run with no code changes.Validation
Environment: Linux dev box, .NET SDK from
~/.dotnet(GitVersion MSBuild target requires it on PATH).5c02ab5; code unchanged on6edabb1):export PATH="$HOME/.dotnet:$PATH"; dotnet test ./tests/OpenClaw.Shared.Tests/OpenClaw.Shared.Tests.csproj --filter "FullyQualifiedName~HandshakeGate" --logger "trx;LogFileName=proof.trx"-> 2/2 passed (both live round-trip gate tests).OpenClaw.Tray.Tests: unchanged versus a clean base run (6466a04; 18 pre-existing Linux-env failures, identical on base; CI runs this suite on Windows)../build.ps1plus full Shared/Tray validation execute as required CI checks on this push; the prior head's only CI failure wasValidate documentation(em dashes in the new proof doc, removed in this push).windows-wsl-gateway-e2epool on a maintainer-provided Windows host 2026-09-17 (see Required proof pools above).Real Behavior Proof
OpenClawGatewayClientover real loopback WebSockets against an in-process test gateway server; no external gateway or private endpoint contacted.5c02ab5(captured); production and test code identical on6edabb1(docs-only delta).dotnet test --filter FullyQualifiedName~HandshakeGate --logger "trx;LogFileName=proof.trx".docs/gateway-handshake-gate-proof.md(full redacted trace, wire-level assertions from the same run).N/A(redacted captured trace committed in-repo).windows-wsl-gateway-e2epool (run 2026-09-17 on a maintainer-provided Windows host; total=20, passed=19, skipped=1, failed=0; TRX and redacted diagnostics in the evidence gist linked under Required proof pools).Security Impact
No)No)No— the guard only withholds frames the gateway would reject; strictly fewer early frames on the wire)No)No)Yes, explain the risk and mitigation: n/aCompatibility and Migration
Yes— the only behavior change is inside the handshake window, where previously-sent RPCs would get the socket 1008-closed; callers' existing degrade paths handle the new null/throw shapes)No)No)Review Conversations