mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
4fbbca6a99dfd80cca293edd9762c6206b1d93a5
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4fbbca6a99 |
refactor(go): adopt modern stdlib idioms and gate them (#7831)
* refactor(go): adopt modern stdlib idioms across kernel and desktop Mechanical sweep with golangci-lint's modernize/intrange/copyloopvar/ usestdlibvars fixers plus hand-fixes for what they could not rewrite: slices.Contains, min/max, slices.Backward, WaitGroup.Go, reflect.TypeFor, maps.Copy, strings.Cut/CutPrefix/SplitSeq, strings.Builder, range-over-int. Net -549 lines with no behavior change. Three loops in session.go stay index-based: slices.Backward yields element copies, so the fixer's rewrite silently dropped writes to DecisionReceipts and ToolCalls (caught by TestDelivery* and TestDependentSameBatch*). All 49 slices.Backward sites were audited for that shape; only these three write through the index, and they carry a nolint with the reason. CaptureAfter's scan loop had an empty body and never ran any logic, so the fixer could not express it. Drop it and the accessor it was the only caller of. omitzero is deliberately untouched: turning omitempty into omitzero changes the JSON written for nested structs, which is a wire-format change to session files and the extension protocol. * build(lint): gate the modern-idiom linters Nothing stopped the codebase from drifting back to pre-1.22 idioms. Both modules are now clean for modernize, intrange, copyloopvar, and usestdlibvars, so enable them rather than leaving the sweep as a one-off. omitzero stays disabled with the reason inline; usetesting, perfsprint, and errorlint still have residue and land separately. |
||
|
|
fb41167048 |
Merge main-v2 and lock Remote Web SSH recovery
Problem: PR #7368 targeted the Remote Workbench and rpcwire stacks, but main-v2 removed both in favor of SSH Serve plus per-host Remote Web windows. Keeping the old implementation would restore a superseded architecture and leave the replacement recovery path without one end-to-end regression. Root cause: The original reconnect state machine and control/data scheduler were coupled to the deleted Workbench protocol. The current architecture recovers at the supervised SSH, persistent forward, and native window layers instead. Fix: Merge the latest main-v2 deletion and Remote Web architecture, drop every obsolete Workbench/rpcwire change, add an in-process real SSH drop integration test that proves reconnect, forward continuity, and window reload, and document transient versus terminal recovery behavior in both guides. Verification: - cd desktop && go test -run '^TestRemoteWindowRecoversAcrossRealSSHDrop$' -count=10 . - cd desktop && go test -race -run '^TestRemoteWindowRecoversAcrossRealSSHDrop$' -count=3 . - go test ./internal/remote/... -count=1 - go test -race ./internal/remote/... -count=1 - cd desktop && go test -run 'Remote|SSH|Serve' -count=1 . - cd desktop && go test -race -run 'Remote|SSH|Serve' -count=1 . - cd desktop/frontend && pnpm test:typecheck && pnpm test:remote && pnpm build - ./scripts/cache-guard.sh - git diff --check |
||
|
|
039590e93a | fix: keep remote web window alive after navigation | ||
|
|
89cdd023ea | Fix remote window identity test lint | ||
|
|
4938e8379b |
Fix remote setup and window ownership recovery
Problem: - Provider activation failures saved the key but made the advertised retry path unusable. - A remote web child could outlive a crashed Desktop and be adopted without a process registration. - One global frontend generation let launches for different SSH hosts cancel each other. Root cause: - The setup state retained the pre-save credential revision after a failed controller rebuild. - Remote child single-instance identity was host-only and children did not observe owner death. - Remote launch freshness was tracked globally instead of per host. Fix: - Track activation-pending setup state and retry controller activation without rewriting the secret. - Scope child identities to a random Desktop owner and close children when the owner process exits. - Track launch generations independently for each remote host. Verification: - go test -count=1 ./... - go test -race -count=1 ./internal/config ./internal/serve - desktop go test -count=1 -timeout 300s ./... - desktop focused race tests and Windows amd64 cross-compile - frontend test:all, test:remote, and build - cache-guard.sh and git diff --check |
||
|
|
4e30f5c3da |
fix(remote): harden web window and provider setup lifecycle
Problem: Concurrent disconnects could miss a window process registered after teardown, the Serve port file became visible before a slow balance diagnostic completed, a stale Provider setup page could overwrite a newer credential, and the removed OpenSSH transport left an unused resolver that failed Desktop lint. Root cause: Remote window open/close entry points had no shared per-host ordering boundary; runServe performed balance synchronously before starting the HTTP accept loop; Provider setup cached missing-key state without a credential-store compare-and-apply token; and the transport deletion retained its private path helper and tests. Fix: - Serialize and generation-fence window lifecycle operations per host, including reconnect and terminal status callbacks, with a deterministic late-registration regression. - Run balance diagnostics asynchronously and prove the setup page responds while a balance endpoint is blocked. - Add credential revision compare-and-apply under the existing cross-process lock and reject stale setup submissions without exposing the revision. - Delete the orphaned OpenSSH resolver and its obsolete tests. Verification: - go test -count=1 ./... - cd desktop && go test -count=1 -timeout 300s ./... - focused root/Desktop race tests - go vet and golangci-lint in both Go modules - Windows amd64 Desktop test-binary cross-compile - pnpm test:typecheck - scripts/cache-guard.sh - git diff --check |
||
|
|
a9c99b4e0f |
fix(remote): keep the live window registered across handoff and enforce ticket TTL
Problem: Re-opening a host spawned a short-lived handoff process whose Wait deleted the host's window registration, so the still-running window became invisible to disconnect/stop/quit and to reconnect re-pointing. Tickets were also only cleaned by a parent-process AfterFunc, so a crashed parent could leave an expired token file on disk. Root cause: The registry stored a single child per host and replaced it on every spawn, and consumeRemoteWindowLaunch never checked the ticket's age. Fix: - The registry now tracks a per-host process set with a per-entry generation: each spawn appends its own entry, and Wait clears only that entry. A handoff process exiting at the single-instance gate no longer clears the live window, so explicit disconnect/stop, real quit, and reconnect refresh still reach the window. Add the handoff regression test (live window + exited handoff) at both the registry and App level. - consumeRemoteWindowLaunch rejects tickets older than the TTL (mtime check) and deletes them, bounding leftovers even when the spawning process died. - OpenRemoteWorkspace documents the two-phase contract precisely and gains a regression test for the window-open-failure path (serve stays ready for the new workspace; the recorded last workspace matches the running serve). - Drop the stale compactRatioRemote field from the frontend SettingsView. Verification: - go test ./desktop: registry handoff/generation, TTL expiry, disconnect-after- handoff, and window-open-failure suites green - cd desktop && go test ./... green Co-authored-by: SivanCola <[email protected]> |
||
|
|
d7f988da13 |
feat(remote): restore SSH Serve web windows via per-host tickets
Problem:
Opening a remote SSH workspace projected the session into the main window
through the Remote Workbench RPC stack, and the pre-Workbench ticket-based
Serve HTML child window had been deleted, leaving no web-mode entry.
Root cause:
|
||
|
|
93c82a948a |
Remote Workbench with Local Provider Broker / 使用本机模型的远程工作台 (#6728)
* feat(remote): add rpcwire, RuntimeAPI registry, and protocol codegen Problem: Remote Workbench needs a bidirectional JSON-RPC transport and a single Go registry that generates schema, Schema Hash, and TypeScript DTOs so Desktop and Host negotiate strictly by schema, not by full Build ID hard-fail. Root cause: main-v2 lacked rpcwire and a generative Remote protocol surface. Fix: - Port transport-neutral NDJSON JSON-RPC (rpcwire) with strict frames, concurrent requests, cancel, and size limits (adapted from #6725). - Port RuntimeAPI protocol registry + schema generator; regenerate committed artifacts for main-v2 eventwire. - Mark eventwire large fields externalizable for content-ref offload without changing provider-visible semantics. - Extend directions for Provider Broker (host_to_client_request / client_to_host_notification) and register broker methods in the frozen surface. Verification: - go test ./internal/rpcwire ./internal/remote/protocol ./internal/remote/protocolgen ./internal/eventwire - go run ./cmd/remote-protocol-gen -check -root . Co-authored-by: SivanCola <[email protected]> Co-authored-by: taibai233 <[email protected]> * feat(remote): add Desktop Provider Broker and TargetManager Problem: Workbench Hosts must consume models without holding API keys, and the main window needs Local + one Remote adapters with a single active projection. Root cause: No in-process Broker over rpcwire and no target fencing manager on main-v2. Fix: - Desktop Broker endpoint: catalog, stream open/cancel, chunk/end notifications, concurrency limits, DeepSeek ToolCallReasoning catalog flags, connection-wide stream cancel on close (direction from #6722; fencing ideas from #6725). - TargetManager: permanent Local, optional Remote, last-click-wins fencing, busy remote host-swap rejection, restart reconnect hint only (no auto SSH). Verification: - go test ./internal/remote/broker ./internal/remote/workbench/target Co-authored-by: SivanCola <[email protected]> Co-authored-by: taibai233 <[email protected]> * feat(remote): workbench SSH, attach-workspace, adapters, mirror, trust Problem: Remote Workbench needed end-to-end transport, lifecycle, main-window adapters, workspace-safe files, mirrors, and Provider trust — not Serve HTML or HTTP Gateway tunnels. Root cause: Prior foundation only had protocol/Broker/TargetManager; no SSH stdio path, attach proxy, runtime grace, or UI wiring. Fix: - Windows: system OpenSSH + AskPass + Job Object (adapted from #6725); remote command is `reasonix remote attach-workspace --stdio`. - macOS/Linux: Go SSH session stdio to the same attach entry. - attach-workspace + per-workspace runtime on Unix socket; 5-minute grace after detach; Schema Hash gate on initialize. - Desktop Workbench adapters (connect/disconnect/switch Local, remote RPC, Provider trust prompt); OpenRemoteWorkspace uses workbench path. - File containment (relative refs, no symlink leaves, atomic writes). - Local mirror store with digest validation; restore requires session.jsonl. - Durable Provider trust store (host+fingerprint → allowed refs only). - Frontend workbenchTarget helpers + tests. Verification: - go test ./internal/remote/workbench/... ./internal/remote/broker ./internal/rpcwire ./internal/remote/protocol ./internal/cli - cd desktop && go test -run 'TestRemote|TestWorkbench|TestAskPass|TestOpenRemote' - pnpm exec tsx --test desktop/frontend/src/__tests__/workbench-target.test.ts Co-authored-by: SivanCola <[email protected]> Co-authored-by: taibai233 <[email protected]> * docs(remote): record source PR authors as repository co-contributors Problem: Repository co-contribution attribution requires explicit documentation and commit-level Co-authored-by trailers for the source PR authors (#6722, #6725). Root cause: PR body credits alone do not enter the GitHub contribution graph. Fix: Document both source PR authors, what was adopted, and that GitHub co-author attribution uses Co-authored-by trailers with public noreply emails in REMOTE_WORKBENCH docs (EN + zh-CN). Verification: - docs/REMOTE_WORKBENCH.md contains Repository co-contributors table - every commit on this branch includes both Co-authored-by trailers Co-authored-by: SivanCola <[email protected]> Co-authored-by: taibai233 <[email protected]> * fix(remote): close the workbench broker runtime loop Problem: Remote Workbench still mixed Remote state with the Local controller, left parts of the runtime API empty, and could lose reconnect, operation, history, Git, and provider semantics in real main-window use. Root cause: The first integration established the transport and broker direction but did not fully fence target projections, rebuild controller profiles, persist remote execution state, or cover strict protocol and process lifecycle parity. Fix: - route the existing main window through the Host-owned controller, files, Git, history, catalogs, operations, goals, checkpoints, and slash completion - keep provider credentials on Desktop while forwarding strict provider requests, effort, DeepSeek tool-call reasoning, and reconnect-safe stream generations - harden Build ID/schema negotiation, SSH/AskPass process cleanup, runtime socket ownership, workspace containment, mirror pulls, disconnect fencing, and background target isolation - add generated protocol contracts and regression coverage for no-Host-key broker use, paging, reconnect, overflow, lifecycle, and projection behavior Verification: - env -u DEEPSEEK_API_KEY REASONIX_RELEASE_CACHE_GUARD=1 go test -race ./... - cd desktop && go test ./... && go vet ./... && go build ./... - pnpm --dir desktop/frontend test:all && pnpm --dir desktop/frontend build - go run ./cmd/remote-protocol-gen -check -root . - Windows amd64 cross-compilation for root Remote packages and desktop module - scripts/check-cache-impact.sh Co-authored-by: SivanCola <[email protected]> Co-authored-by: taibai233 <[email protected]> * fix(remote): bind attach to its workspace target Problem:\nCI lint found an overwritten normalized workspace, and initialize could replace the SSH attach target before runtime startup.\n\nRoot cause:\nThe attach bootstrap normalized Options.Workspace but then assigned the client-provided initialize workspace to the same variable without comparing the two.\n\nFix:\nKeep the normalized attach target, normalize the requested workspace separately, and reject mismatches before schema negotiation or runtime startup.\n\nVerification:\n- go test ./internal/remote/workbench/attach ./internal/remote/workbench/runtime\n- go vet ./internal/remote/workbench/attach\n- golangci-lint v2.12.2: 0 issues * fix(desktop): remove the legacy remote child window Problem:\nThe same-window Remote Workbench still carried an unreachable child-window launcher and special shell lifecycle, causing Desktop CI unused-code failures and contradicting the selected product model.\n\nRoot cause:\nThe App bindings were migrated to TargetManager, but the old ticket-based Serve HTML launch path and its window-specific startup branches were left behind.\n\nFix:\nRemove remote-window tickets, spawning, middleware, lifecycle branches, and obsolete helpers so every SSH workbench runs in the existing main window.\n\nVerification:\n- cd desktop && go test ./...\n- desktop and root golangci-lint v2.12.2: 0 issues\n- Windows amd64 Desktop cross-compilation\n- wails generate module * fix(remote): preserve workspace diff parity Problem: After rebasing onto #6733, WorkspaceChangeDetail still executed against the Desktop filesystem while a Remote target was active. Root cause: The frozen RuntimeAPI had no method for the newly added per-file workspace diff query. Fix: Add a strict workspace/changeDetail contract, route Desktop through the Host runtime, and generate bounded Git/checkpoint patches inside the selected remote workspace. Verification: go test ./...; go test -race ./...; cd desktop && go test ./...; pnpm test:all; protocol codegen check; vet/build/lint; Windows cross-compile. Co-authored-by: SivanCola <[email protected]> * Harden Remote provider broker lifecycle Problem: the local Provider Broker became active before the live SSH peer was rebound to its trusted fingerprint, and slow provider consumers could block detach while the Host mutex was held. Root cause: peer validation happened after client.Connect returned, while Host stream delivery performed blocking channel sends inside the global lock. Fix: gate Broker activation on the authenticated live peer identity and move ordered Host delivery into bounded per-stream queues with a sole output pump. Verification: go test -race ./internal/remote/broker; go test ./internal/remote/workbench/client; cd desktop && go test -race . -run 'Test(AuthorizeWorkbenchPeerFailsClosed|Workbench|Remote)' -count=1. * Restore Remote sessions across runtime restarts Problem: Remote sessions existed only in the runtime process map, so a normal detach followed by runtime exit made persisted transcripts invisible on reconnect. Root cause: the Host had no durable mapping between protocol session identities, controller profiles, topics, and transcript paths. Fix: add a per-workspace atomic session registry, lazily rebuild controllers after Broker activation, retain temporarily unavailable records, and persist lifecycle/profile changes without holding the runtime mutex across file I/O. Verification: go test -race ./internal/remote/workbench/runtime -count=1; go test -race ./internal/remote/workbench/runtime -run 'TestRuntime(RestoresSessionRegistryAfterProcessRestart|EarlyShutdownDoesNotOverwriteUnreadRegistry)' -count=20. * Keep Remote projections and provider state current Problem: Remote callbacks stayed bound to a stale local tab, provider metadata and authorization were captured at connect time, and the bounded-handler regression test released its blockers before overload dispatch was observable. Root cause: Local navigation was not serialized with Remote activation, Broker descriptors omitted compaction/pricing fields, provider closures captured immutable config, and the rpcwire test synchronized only handler startup. Fix: Fence visible local navigation and rebind callbacks on Remote activation, propagate complete non-secret provider metadata, refresh config and trust-scoped access for live Broker connections, fail closed on empty authorization, and wait for all overload responses before releasing handlers. Verification: go test -race ./internal/boot ./internal/remote/protocol ./internal/remote/broker ./internal/remote/workbench/client ./internal/remote/workbench/runtime ./internal/rpcwire (cd desktop && go test -race ./...) go test -race ./internal/rpcwire -run TestInboundHandlerConcurrencyIsBoundedWithoutBlockingResponses -count=100 go run ./cmd/remote-protocol-gen -check -root . ./scripts/cache-guard.sh * Constrain restored Remote transcript paths Problem: An edited session registry could name an opaque topic that the wire contract permits but restoration rejected, while a nested transcript path could traverse a directory symlink outside SessionDir. Root cause: Registry validation derived a topic_ prefix from an opaque protocol ID and only performed lexical containment plus a final-component symlink check. Fix: Accept any non-empty opaque topic ID, require runtime transcripts to be direct children of SessionDir, close partially built controllers on restore errors, and isolate fallback socket paths per test run. Verification: go test -race ./internal/remote/workbench/runtime -count=3 * Fence late Remote projection callbacks Problem: A callback or snapshot refresh captured for one local tab could complete after Remote was rebound to another tab and still emit into the stale surface. Root cause: Reactivation marked Remote visible before rebinding callbacks, and callback/refresh completion trusted its captured tab ID instead of the current projection binding. Fix: Bind callbacks before activation, route late events through the current Remote tab under the transition fence, redirect snapshot completion to the live binding, and drop completion while Local is active. Verification: cd desktop && go test -race -run 'TestWorkbenchLateCallbackUsesCurrentProjectionTab|TestSetActiveTabSwitchesRemoteProjectionOnlyForDifferentTab|TestConcurrentActivateTopicSerializesSingleSurfacePruning' -count=20 * Synchronize Remote host-key prompt regression Problem: The full desktop race suite detected an unsynchronized map write in the host-key prompt serialization test. Root cause: The test started the first prompt goroutine before registering the second managed host, allowing the goroutine to read the manager map while the test wrote it. Fix: Register every host before launching concurrent prompts and wait for both prompt goroutines during test cleanup. Verification: cd desktop && go test -race -run '^TestHostKeyPromptsAreSerializedForGlobalDialog$' -count=100 .; cd desktop && go test -race ./... * Wait for queued background task teardown Problem: Windows CI could finish TestBackgroundTaskReturnsBeforeSlotFrees while its background job was still persisting transcript artifacts, causing TempDir cleanup to fail with a non-empty directory. Root cause: The test waited only for provider startup and then slept for 50 milliseconds instead of synchronizing with the job's terminal lifecycle. Fix: Scope the job to the test session, capture its returned job ID, wait for jobs.Done, and close the manager on every exit path before temporary-directory cleanup. Verification: go test -race ./internal/agent -run '^TestBackgroundTaskReturnsBeforeSlotFrees$' -count=100; REASONIX_RELEASE_CACHE_GUARD=1 go test -race ./internal/agent; REASONIX_RELEASE_CACHE_GUARD=1 go test ./...; GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go test -c ./internal/agent; go vet ./internal/agent; golangci-lint run ./internal/agent/... * fix(remote): fall back to server root workspace * fix(remote): close review blockers for workbench v1 Problem: Remote Workbench could silently expose the Host root, strand a session after registry persistence failure, leave local provider streams running after cancellation, and fail the Ubuntu/Windows attach tests. Root cause: One-click workspace resolution used an implicit root fallback; profile changes closed or mutated live controllers before durable state succeeded; Broker delivery lacked an abort path and open cancellation did not reach Desktop; attach readiness and tests depended on connection and timing assumptions that were not portable. Fix: Require explicit workspace selection, roll profile changes back while preserving the old controller, propagate best-effort Broker cancellation with abortable delivery, remove the readiness probe generation race, and use deterministic platform-aware attach tests. Verification: Remote, Broker, attach, runtime, rpcwire, race, Desktop, frontend Remote, TypeScript, production build, protocol generation, cache guard, go vet, and repeated cancellation/attach regressions passed. * fix(remote): make attach startup portable Problem: The Windows CI attach integration test could not observe the in-process AF_UNIX listener and reported that the runtime never became ready. Root cause: Startup readiness was inferred with os.Stat on the socket path, but Windows AF_UNIX does not expose a portable socket file. A separate readiness connection is also unsafe because runtime connections own generations. Fix: Let the real attach connection retry bounded socket dials until the runtime listener accepts it, and return immediately after starting the in-process test runtime. Verification: - attach integration test repeated 50 times - attach race test repeated 30 times - Windows amd64 test binary cross-compiled - go test ./internal/remote/... - remote broker, attach, and runtime race tests * fix(remote): keep runtime sockets portable Problem: Windows CI still could not connect to the in-process workbench runtime even with bounded dial retries. Root cause: The generated AF_UNIX address exceeded the platform path limit when the remote home itself was long, as on Windows hosted runners. Fix: Compact the runtime socket hierarchy while keeping it scoped under the Reasonix home, and add a regression test against the portable AF_UNIX length bound. Verification: - attach integration test repeated 50 times - runtime socket path tests repeated 20 times - attach and runtime race tests - Windows amd64 runtime test binary cross-compiled - go test ./internal/remote/... - protocol generation check and cache guard * fix(desktop): preserve native macOS app ownership Problem: Packaged macOS builds used reasonix-guard as CFBundleExecutable, so the Dock and LaunchServices owned a non-UI parent while the Wails child owned the window. The app could stay running without reopening its window from the Dock. Root cause: The packaging step replaced Wails' native bundle entry point to route startup through Guard. macOS application identity cannot be transferred reliably across that parent-child process boundary. Fix: Keep reasonix-desktop as the macOS bundle executable, retain Guard and the CLI as recovery sidecars, and run the equivalent failed-install, crash-loop rollback, repair, and Safe Mode preflight in Desktop before Wails creates WebView. Update packaging contracts and recovery documentation for the platform-specific launch model. Verification: - env -u DEEPSEEK_API_KEY go test ./... - go test ./... (desktop module) - go test -race -run 'TestDesktopStartupRecovery|TestDesktopPackagesPreserveNativePlatformLaunchers' -count=1 . (desktop module) - go vet ./... (desktop module) - DESKTOP_BUILD_SKIP_DMG=1 scripts/desktop-build.sh darwin/arm64 v0.0.0 - Signed packaged App launched with reasonix-desktop as PID 1 child, one Desktop process, no Guard process, and an onscreen CoreGraphics window * fix(desktop): satisfy startup recovery lint Problem: The Desktop CI job rejected the macOS startup recovery helper because continueLaunch was initialized before every return path overwrote it. Root cause: The named return value carried an unnecessary default assignment that go vet did not flag but golangci-lint's ineffassign check did. Fix: Initialize only the Safe Mode result and let each branch set the continue decision explicitly. Verification: - go test -run 'TestDesktopStartupRecovery|TestDesktopPackagesPreserveNativePlatformLaunchers' -count=1 . - go vet ./... * fix(remote): close workbench runtime safety gaps Problem: Remote Workbench could reject typed client requests before session hydration, execute duplicate mutations more than once, lose the committed target during a failed replacement, race controller lifecycle changes, and expose Local saved-session bindings while Remote was active. Root cause: Client authority hydration preserved serialized zero values, the advertised requestId contract had no runtime ledger or request sequencer, TargetManager stored candidates as committed state, and unsupported Remote history actions silently fell through to Local bindings. Fix: Authoritatively hydrate client-owned fields while preserving retry IDs, serialize runtime requests with a bounded 24-hour mutation replay ledger, fence controller profile transitions, promote replacement targets only after successful connection, and fail closed for unsupported Remote saved-session operations. Verification: - env -u DEEPSEEK_API_KEY go test ./... - env -u DEEPSEEK_API_KEY REASONIX_RELEASE_CACHE_GUARD=1 go test -race ./... - cd desktop && go test ./... && go vet ./... && go build ./... - pnpm --dir desktop/frontend test:remote - go run ./cmd/remote-protocol-gen -check -root . - scripts/check-cache-impact.sh with the live PR body Co-authored-by: taibai233 <[email protected]> * fix(remote): keep session rotations authoritative Problem: Completed turns could retain stale approval or ask prompts, while committed new/clear session rotations could return an error after a registry write failure and leave clients on the old runtime epoch. Root cause: Turn completion only cleared replay events when no prompt remained, session rotation treated the fallible registry write as part of the already-committed controller mutation, and the client did not apply new/clear result epochs. Fix: Clear terminal prompt state, return and adopt the committed rotation epoch while logging registry degradation, reset stale runtime state, and invalidate the previous snapshot. Verification: go test ./...; go test -race ./internal/remote/workbench/runtime ./internal/remote/workbench/client; cd desktop && go test ./...; go vet ./...; go run ./cmd/remote-protocol-gen -check -root . * fix(remote): fence snapshot refreshes and fork cleanup Problem: Remote rewind could report failure after destructive state changes committed, late subscribe responses could restore stale runtime authority, and failed forks left transcript artifacts on disk. Root cause: Post-commit registry errors were treated as rollbackable, subscribe responses had no request-authority fence, and fork rollback removed only the in-memory child session. Fix: Keep committed rewind results authoritative, serialize and revision-fence subscription refreshes with automatic stale retry, validate projection freshness, and remove all artifacts for uncommitted forks. Verification: - go test ./... - go test -race ./internal/remote/workbench/client ./internal/remote/workbench/runtime ./internal/control - go vet ./... - cd desktop && go test ./... - go run ./cmd/remote-protocol-gen -check * fix(remote): serialize projections and capability boundaries Problem: Remote subscribe snapshots could overwrite newer events, unsupported Remote Memory surfaces could reach Local state, and the broker test assumed concurrent notification arrival order. Root cause: Snapshot publication, event callbacks, and authority-changing results had no shared projection boundary; Desktop bindings did not fail closed for frozen capabilities; the test ignored stream sequence numbers. Fix: Linearize subscribe commits with session delivery and mutation results, install the initial Remote projection within that boundary, reject unsupported Remote Memory and Auto Research access, and assert broker chunks by sequence. Verification: go test ./...; go test -race ./internal/remote/...; desktop go test ./...; desktop go vet ./...; broker round-trip with coverage at count 500. * fix(remote): preserve active workbench transport Problem: Uninitialized socket probes could evict the live Desktop connection, target transition events exposed candidates as active, and Remote notification overload silently dropped ordered events. Root cause: Connection ownership was transferred before initialize response commit, candidate identity and active identity shared one event shape, and rpcwire used best-effort concurrent notification dispatch. Fix: Add provisional connection gates with post-write activation and exact daemon build checks; keep transition events on the committed target and fence submits; add ordered bounded notification queues with explicit overflow failure and generation-aware Broker delivery. Verification: go test ./... go test -race ./internal/rpcwire ./internal/remote/broker ./internal/remote/protocol ./internal/remote/workbench/... (cd desktop && go test ./...) (cd desktop/frontend && pnpm test:all) * fix(remote): serialize connection teardown Problem: A client closing immediately after initialize could race the response-commit callback, leaving a rejected gate paired with newly activated connection ownership. Root cause: Provisional connection teardown and post-write activation checked and mutated the wire registry under separate critical sections. Fix: Serialize teardown with connection activation, keep the production notification queue memory bound at 256 entries, and lock the initialize post-write contract with a router regression test. Verification: go test ./internal/remote/protocol ./internal/remote/workbench/runtime go test -race ./internal/remote/workbench/runtime ./internal/remote/protocol ./internal/remote/broker ./internal/rpcwire * test(remote): cover broker-backed tool loop Problem: The no-Host-key integration test only proved a single text response. It did not exercise the critical model-to-Host-tool-to-model loop or DeepSeek reasoning replay across the Desktop Broker. Root cause: The stub Provider always returned final text on its first request, so Host tool execution and the second Broker request were outside the end-to-end coverage. Fix: Make the stub request read_file first, assert the Host result and reasoning/tool-call replay in the second request, then complete the turn and verify projected tool events. Verification: - go test ./internal/remote/workbench/runtime -run '^TestRuntimeControllerUsesDesktopBrokerWithoutHostKey$' -count=1 -v - go test -race ./internal/remote/workbench/runtime -run '^TestRuntimeControllerUsesDesktopBrokerWithoutHostKey$' -count=20 - go test ./internal/remote/workbench/... ./internal/remote/broker ./internal/rpcwire - env -u DEEPSEEK_API_KEY go test ./... * test(remote): add physical Windows Linux acceptance Context: The Remote Workbench release gate needs reproducible evidence from a real Windows OpenSSH client to a Linux Host, including Broker-backed tool execution and reconnect cleanup. Change: Add an opt-in Windows ARM64-compatible acceptance test that exercises the production SSH transport, Linux workspace queries, Desktop Broker tool replay, session authority after reconnect, and OpenSSH process reaping. The fixture is environment-gated and keeps Provider execution on the Desktop side. Verification: - GOOS=windows GOARCH=arm64 CGO_ENABLED=0 go test -c -tags reasonix_remote_integration ./desktop - go test ./... - go test -race ./internal/remote/... * fix(remote): align Desktop and Host build identity Context: The Desktop is built from a nested Go module, so Go does not embed vcs.revision there while the bundled Host CLI built from the root module does. Strict Remote Workbench negotiation consequently rejects matching release artifacts as different builds. Fix: Allow release builds to link a validated shared source revision into the protocol package. Capture the clean repository revision before packaging mutations and inject it into both the Wails Desktop and bundled CLI sidecar, preserving the dirty suffix for local builds. Verification: - go test ./... - go test ./... (desktop module) - go test -race ./internal/remote/... - Windows ARM64 acceptance cross-compile with linked source revision - bash -n scripts/desktop-build.sh * fix(remote): preserve direct SSH credentials Context: The Windows Workbench direct-host branch reduced a saved Host to username, address, and port before starting OpenSSH. Explicit identity_file and proxy_jump settings were silently dropped, so valid direct connections could fail authentication or bypass the required jump path. Fix: Add an injection-safe direct configured transport that preserves identity and ProxyJump as distinct argv values. Route the Windows Workbench direct branch through it while retaining normalized IPv4 and IPv6 target parsing. Verification: - go test ./... (desktop module) - go test -race focused Windows Workbench transport tests - Windows ARM64 acceptance cross-compile * fix(remote): share runtime build identity Context: The detached workspace runtime duplicated Build ID discovery instead of using the shared protocol implementation. Linked source revisions used by nested Desktop release builds were therefore ignored, causing one installed CLI to reject its own runtime as a different build. Fix: Use protocol.CurrentBuildID for production runtime identity while retaining the explicit SourceRevision test seam. This keeps Desktop, attach, and detached runtime negotiation on one validated source of truth. Verification: - go test ./internal/remote/workbench/runtime - go test -race ./internal/remote/workbench/runtime - linked-revision focused regression test * fix(remote): allow initial history page Context: The Desktop history client intentionally requests the latest page without a cursor, and the Host runtime supports that state. The wire DTO nevertheless marked cursor as required, so strict validation rejected every initial session/history request after a successful Remote turn. Fix: Make the history cursor optional, regenerate the frozen protocol artifacts, and cover omission plus strict decoding. Strengthen the physical acceptance fixture to verify both product version and exact source revision before connecting. Verification: - go test ./... - go test ./... (desktop module) - go run ./cmd/remote-protocol-gen -check -root . - Windows ARM64 acceptance cross-compile * fix(remote): validate session mirror snapshots Context: After a completed turn the reconnect snapshot includes the persisted session mirror. That complete object incorrectly populated originalBytes while marked non-truncated, violating the frozen externalized-field contract and turning session/subscribe into an internal error. Fix: Emit complete session mirrors without truncation-only metadata and cover the artifact contract directly. Verification: - go test ./... - go test -race ./internal/remote/workbench/runtime - focused session mirror and runtime integration tests * fix(remote): model session mirror payload Context: Remote reconnect snapshots advertised /mirror/session.jsonl as an externalized content pointer, but SessionSnapshot had no schema field at that path. Strict response validation therefore rejected every post-turn reconnect snapshot before Desktop could fetch the mirror. Fix: Add the mirror placeholder to the frozen snapshot DTO, replace its concrete body with null during marshaling, and keep the body behind the existing bounded contentRef transfer. Regenerate Go/TypeScript protocol artifacts and prove that no session body is inlined on the wire. Verification: - go test ./... - go test ./... (desktop module) - go run ./cmd/remote-protocol-gen -check -root . - Windows ARM64 acceptance cross-compile * test(remote): add live Desktop Broker acceptance * test(remote): require live Broker tool loop --------- Co-authored-by: taibai233 <[email protected]> |
||
|
|
65fcd46525 |
Add a Remote SSH module (remote serve + tunnel) / 新增远程 SSH 模块(远端 serve + 隧道) (#6673)
* feat(serve): support --port-file/--token-file/--pid-file for supervised headless serve Extract RunGracefulListener so callers can bind first (--addr 127.0.0.1:0), record the real port, and keep the auth token out of argv. Groundwork for the remote-SSH module's detached remote serve bootstrap. * feat(config): add user-global-only [remote] section for SSH hosts [[remote.hosts]] entries carry connection/forward/workspace settings and name credential env vars (passphrase_env/password_env) instead of secrets, mirroring the provider api_key_env idiom. Like [secrets], loadForRoot pins the section back to the user-global value after the project merge — with a deep copy, because TOML decoding mutates slice backing arrays in place and a shallow pin would alias the project-injected hosts. Remote credential env names flow into CredentialEnvNames so they are filtered from tool subprocess environments. Adds RemoteStateDir/RemoteKnownHostsPath and the internal/store name helpers for the remote-side serve state layout. * feat(remote): SSH transport kernel — dial, auth, host keys, forwards, SFTP New internal/remote package group provides the connection layer for the remote module: - client.go: supervised connection with a single-goroutine state machine, keepalive ([email protected]), full-jitter exponential-backoff reconnect, shared SFTP (generation-checked), and forward re-attach. - auth.go: OpenSSH-like method order (agent, identity file, default identities, password, keyboard-interactive); interactively-obtained secrets are cached in-process so reconnects never re-prompt. - knownhosts.go: system known_hosts read-only + a Reasonix-managed TOFU file; key mismatch is a non-promptable hard error. - dial.go: ProxyJump chains (netclient proxy applies to the first hop only). - forward/: -L (persistent local listener across reconnects) and -R (re-registered on each attach) with typed ErrBindBusy/ErrDuplicateForward. - sftpfs/: list/stat/capped-read-with-text-detection/atomic-write/mkdir/ rename/remove, quarantining the new github.com/pkg/sftp dependency. - netclient/dialer.go: StreamDialer honoring the user's proxy policy (SOCKS5 via x/net/proxy, http/https via CONNECT). - sshconfig.go: ~/.ssh/config import via github.com/kevinburke/ssh_config. - sshtest/: in-process SSH+SFTP server for tests. New deps: github.com/pkg/sftp, github.com/kevinburke/ssh_config (both pure Go). x/* versions kept at main-v2's pins. Cache-impact: none — no prompt/tool/provider path touched. Cache-guard: n/a (new leaf subsystem, not in the system-prompt prefix). * feat(remote): remote serve bootstrap over SSH internal/remote/bootstrap ensures a detached 'reasonix serve' runs for a (host, workspace) pair: reuse a live process (the reconnect path), else detect the remote OS/arch (uname), locate/install/upload reasonix, write a 32-byte token 0600 via SFTP, launch serve bound to 127.0.0.1:0 with --token-file/ --port-file (token never in argv), poll the port file, and persist ServeState JSON. Status/Stop/Logs manage the recorded process. Every remote command operand is POSIX single-quote-escaped (golden test covers '; rm -rf ~'). V1 targets Linux/macOS remotes; cross-platform upload without a matching local binary is a documented limitation directing to serve_install = npm. Cache-impact: none — new leaf subsystem, no prompt/tool/provider path. Cache-guard: n/a. * feat(cli): reasonix remote command group + /remote slash Adds the remote subcommand tree (add/list/remove/import/test/connect/open/ status/forward/serve/fs), mirroring the mcp command shape. Host CRUD writes to the user-global config only (remote hosts are pinned there); connect runs a foreground supervisor that dials, bootstraps remote serve, forwards the serve port with the token in a loopback URL, applies configured -L/-R forwards, and holds the tunnel until Ctrl-C while the remote serve keeps running. Terminal prompts handle key passphrases, passwords, and TOFU host-key confirmation; dialing honors the user's proxy settings via netclient. The /remote slash command shows a read-only host summary. New i18n fields added to all three catalogs. Cache-impact: none — CLI surface + i18n only, no prompt/tool/provider path. Cache-guard: n/a. * feat(desktop): remote SSH Go bindings (remote_app.go) 25 App bindings behind a narrow remoteKernel interface (fake-injected in tests): host CRUD (user-global config), Connect/Disconnect/status, status-driven TOFU host-key confirmation (ConfirmRemoteHostKey resumes a parked dial), SFTP list/read/write-with-mtime-conflict/mkdir/rename/delete, port-forward CRUD, and remote serve EnsureServer/Open/Stop/Status/Logs. desktopRemoteManager wraps internal/remote per host; status/forward/server transitions bridge to remote:status / remote:forwards / remote:server through the asyncRuntimeEmitter (never blocking the kernel). The manager is created lazily on first use and closed in App.shutdown, rejecting any pending host-key prompt. OpenRemoteWorkspace forwards the serve port and opens the tunneled URL via Wails BrowserOpenURL; last workspace/host persist to desktop-remote.json (all-optional fields, backward-compatible). desktop/go.mod gains pkg/sftp + kevinburke/ssh_config via go mod tidy. Cache-impact: none — desktop bindings only, no prompt/tool/provider path. Cache-guard: n/a. * feat(desktop): remote SSH frontend (host manager, explorer, ports, server) - lib/types.ts: Remote* TS mirrors + SettingsTab "remote". - lib/bridge.ts: 25 Remote* AppBindings + browser-dev mock + onRemoteStatus/ onRemoteForwards/onRemoteServer subscribers with a mock fan-out for tests. - store/remote.ts: zustand store; pending_hostkey status drives the global fingerprint dialog and resolves on the next non-pending status. - RemoteHostsPage (Settings > Remote SSH tab): host list with status chip, connect/disconnect/open, add/edit form, ssh_config import picker. - RemotePanel: overlay drawer with Files (lazy tree + textarea editor with mtime-conflict Reload/Overwrite), Ports (live forward rows + add form), and Server (bootstrap phases + open-in-browser + logs) tabs. - RemoteHostKeyDialog: global TOFU modal off the store's pendingFingerprint. - StatusBar remote chip (worst live state, click opens explorer); App.tsx subscribes remote:* app-wide and renders the panel + dialog. - i18n keys added to en / zh / zh-TW (key sets stay aligned). - Editor is a monospace textarea (no CodeMirror/Monaco) per lean-deps. - Tests: src/__tests__/remote-store.test.ts (added to the test script). Cache-impact: none — desktop frontend only. Cache-guard: n/a. * docs: document the Remote SSH module (SPEC, GUIDE, CONFIG_PATHS, CHANGELOG) Adds the internal/remote layout + acyclic dependency note to SPEC 2, a bilingual Remote SSH section to GUIDE.md / GUIDE.zh-CN.md (config, CLI, reconnect/persistence semantics, host-key TOFU, remote-side state layout, desktop UI), the managed known_hosts path to CONFIG_PATHS(.zh-CN), and a CHANGELOG entry. Cache-impact: none — docs only. Cache-guard: n/a. * chore: ignore stray .reasonix state under package dirs A bootstrap test whose RealPath resolved to CWD once wrote a .reasonix/remote/ token under the package dir and it was swept into a commit. The offending file has been scrubbed from history; this guards against recurrence. * fix(remote): CI green — errcheck + cross-platform bind-busy and Windows test skips - client.go: check cl.Wait() return (errcheck lint). - forward: classify WSAEADDRINUSE as bind-busy on Windows (build-tagged addrinuse_windows.go), so ErrBindBusy is reported cross-platform. - sftpfs / bootstrap integration tests skip on Windows: their in-process SFTP server serves the LOCAL filesystem, so on Windows it serves drive paths and the POSIX-remote path translation breaks — a harness limitation, not a product one (V1 remotes are Linux/macOS). Linux/macOS CI covers the flows. Cache-impact: none — test/lint-only + a Windows errno check. Cache-guard: n/a. * test(remote): skip EnsureServe reuse/relaunch/stop tests on Windows at setup TestEnsureServeReusesLiveProcess and siblings pre-write state via pathsFor() (POSIX path.Join) before newFakeConn runs, so on Windows the temp-dir remote home yields a malformed drive path and the setup os.WriteFile fails. Move the Windows skip to the top of each such test (shared skipOnWindows helper) so it fires before any pathsFor/os setup, not just inside the harness ctor. * fix(remote): address review — P1 security / data-integrity / lifecycle Resolves the 14 findings from code review (all verified against the code): Security - Serve token no longer leaks to the remote log: runServe suppresses the token share line under --port-file (supervised), and the launch script creates the log 0600 (umask 077 + chmod). (F1) - ProxyJump no longer sends the target's password to jump hosts: each hop gets its own credential-free AuthOptions with a per-hop cache (persisted on the Client so reconnects don't re-prompt). (F2) - CLI surfaces a misconfigured proxy instead of silently dialing direct; the https-proxy path now TLS-wraps the connection before CONNECT; desktop passes the proxy Dialer it previously omitted. (F14) Data integrity - `remote fs get` streams the full file via sftpfs.Download instead of the 4 MiB-capped preview reader that silently truncated. (F3) - Workspace slug carries an FNV hash of the exact path, so /srv/a-b and /srv/a/b no longer share serve state files. (F10) - Desktop host edit merges: passphrase_env/password_env/forwards (absent from the desktop input) are preserved instead of wiped. (F7) Lifecycle / correctness - Bootstrap gates on `serve --help` advertising --port-file, not a version number (the flags ship in this change, so no released version qualifies). (F4) - Launch uses setsid only when present, so stock macOS (no setsid) works. (F5) - Desktop no longer uploads the GUI binary as the remote CLI (LocalBinary="" makes the upload path fail clearly toward npm). (F6) - Serve tunnel re-binds to the current serve address on every EnsureServer and is torn down on Stop; per-host serveMu serializes concurrent ensures. (F8) - Reuse/Stop verify the pid is actually a reasonix serve (ps args), not just kill -0, so a recycled PID is never mistaken or signalled. (F9) - Desktop connect retries after failure (failed Start drops the managed host) and builds a fully-populated host under the lock so a concurrent Disconnect can always close it. (F11) - SSH handshake is bounded by a conn deadline (ClientConfig.Timeout does not cover NewClientConn), so a banner-less host can't hang the dial or Close. (F12) - ScanSSHConfig returns a non-nil slice so an empty result encodes as [] not null. (F13) Tests: per-hop auth isolation, full-file download vs preview cap, slug collision (/srv/a-b vs /srv/a/b), serve-alive/launch/locate command shape, desktop edit-merge and non-nil scan. go test ./... green in both modules; gofmt/vet clean; Windows cross-build green; cache-guard pass. Cache-impact: none — no prompt/tool/provider/memory path touched. Cache-guard: ./scripts/cache-guard.sh — all cases pass. System-prompt-review: not required — no system-prompt-feeding config field changed. * Harden Remote SSH lifecycle and packaging Problem: Remote SSH could misapply ProxyJump credentials, race connection and service lifecycles, interrupt live forwarding during replacement, and ship Desktop builds without the CLI sidecar required by upload installation. Several UI failures were also silent or left stale state behind. Root cause: Jump aliases shared endpoint-keyed authentication, bootstrap ownership relied on weak state checks without cross-process serialization, and Desktop operations lacked generation-scoped cancellation. Forward replacement stopped the working listener before the replacement was ready, while packaging and updater contracts did not include the CLI binary. Fix: Resolve and authenticate every SSH hop independently, make dialing and Desktop operations context-aware, serialize bootstrap with an atomic remote lock, verify exact process ownership, and make forward replacement failure-atomic. Harden token, SFTP, agent, config, and ssh_config handling; package and update the CLI sidecar on macOS, Windows, and Linux; and surface actionable validation and lifecycle errors in the Desktop UI. Verification: - go test ./... - cd desktop && go test ./... - go test -race for Remote, CLI, store, and Desktop modules - go vet ./... for root and Desktop modules - pnpm test:all and pnpm build - Windows amd64 cross-compilation for Remote, bootstrap, forwarding, SFTP, and CLI packages - clean merge snapshot against latest origin/main-v2 with root, Desktop, and frontend checks passing * Add native Remote SSH workspace windows Problem: Remote SSH was buried in settings, and opening a remote workspace handed the authenticated loopback URL to the system browser instead of preserving a desktop workspace experience. Root cause: The desktop had no window-level Remote SSH control and no isolated Wails launch mode for a second native workspace window. Fix: Add a VS Code-style status entry, host switcher, command actions, and Remote dock. Launch remote workspaces in a lightweight Reasonix process that skips local session ownership, single-instance focus, tray services, and Wails bindings. Pass the loopback URL through a one-shot permission-restricted ticket so the token never appears in process arguments. Verification: - go test ./... - pnpm test:all - pnpm build - wails build -clean - native macOS multi-window smoke test - git diff --check * Tidy Remote SSH module metadata Record the SSH config and SFTP packages as direct dependencies after rebasing onto the latest main-v2 baseline. Remove stale module checksums so the root module remains tidy in CI. * Fix Remote SSH Windows test assumptions Load the fixture SSH config by its explicit path while setting both platform home variables, and use the Windows .exe suffix for the desktop CLI fixture. This keeps the Remote SSH coverage portable across the CI matrix. |