Commit Graph
10 Commits
Author SHA1 Message Date
YHH 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.
2026-08-07 15:30:33 +08:00
SivanCola 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
2026-08-05 17:32:39 +08:00
SivanCola 039590e93a fix: keep remote web window alive after navigation 2026-08-05 15:13:41 +08:00
SivanCola 89cdd023ea Fix remote window identity test lint 2026-08-05 12:09:21 +08:00
SivanCola 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
2026-08-05 12:01:23 +08:00
SivanCola 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
2026-08-05 10:33:25 +08:00
SivanCola 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]>
2026-08-05 03:34:29 +08:00
SivanCola 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:
b9e036b84 removed the remote-window tickets, spawning, middleware, and
lifecycle branches when the App bindings migrated to TargetManager.

Fix:
- OpenRemoteWorkspace is the idempotent web launcher again: EnsureServer
  (install/reuse), atomic 127.0.0.1:0 tunnel Replace, token appended to the
  first-visit URL, last workspace saved, then open/re-point the host window.
  A failed switch leaves the previous window and tunnel untouched.
- Restore the mode-0600 one-shot ticket (size bound, 2-minute TTL, consume
  deletes, loopback-only URL validation, JSON-escaped navigation JS). argv
  carries only the ticket name and a non-secret per-host digest.
- Child windows derive a per-host Wails SingleInstance identity from the host
  digest: reopening a host routes the new ticket to the existing window,
  which consumes it, updates the title, navigates, restores, and focuses.
  Different hosts run independent windows in parallel.
- The main process keeps a generation-based child registry so a stale Wait
  can never clear a newer registration; real quit terminates survivors, while
  closing a window only releases its registration (Serve and SSH stay up).
- Terminal SSH failure or explicit disconnect/stop-server closes the host
  window (the reason already surfaces via remote:status); transient
  reconnects keep it and re-point it at the rebound tunnel URL.
- Child processes expose no Wails bindings, menu, or file drops and skip
  local tabs/tray/heartbeat/providers.

Verification:
- go test ./... in desktop, including new ticket, registry, host-identity,
  lifecycle, middleware, atomic-switch, and reconnect tests
- go test -race on the remote window and open-workspace suites
- gofmt/go vet clean

Co-authored-by: SivanCola <[email protected]>
2026-08-05 03:34:29 +08:00
SivanColaandtaibai233 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]>
2026-07-22 02:29:21 +08:00
SivanCola 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.
2026-07-19 21:48:20 +08:00