Skip to content

feat(desktop): signed Windows NSIS updates on the main channel - #6854

Open
JasonBuildAI wants to merge 20 commits into
loopx-project:mainfrom
JasonBuildAI:codex/desktop-windows-signed-updates
Open

JasonBuildAI wants to merge 20 commits into
loopx-project:mainfrom
JasonBuildAI:codex/desktop-windows-signed-updates

Conversation

@JasonBuildAI

@JasonBuildAI JasonBuildAI commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Goal And Delivered Outcome

  • Outcome basis / optional anchor: [Task][RFC]: Complete the Desktop execution frontend journeys #5204 (Desktop execution frontend journeys). Its restart/update/rollback items require independent evidence on each operating system; Windows currently has none.
  • Goal/source and gap: on main, the signed desktop-update pipeline builds macOS only (desktop-updater.yml has a single macos-15 matrix entry; scripts/desktop_update_feed.py publishes only darwin-aarch64), and the App's update surface is gated to macOS. A Windows install therefore has no signed-update journey at all, only manual installers and a separately installed CLI.
  • Observable before -> after, with the validation row that proves it: a Windows NSIS install now checks on launch, verifies the minisign signature, installs the update passively and restarts automatically; the update panel offers manual check / install / restart. Proven by the feed and workflow tests, the panel browser smoke, and an author-side installed-NSIS receipt at 823008357 (loopback feed + test key; the channel/platform copy logic is unchanged at 7392feb15, details in the receipt comment). The receipt covers successful replacement/restart and a verified-but-unlaunchable failure with service recovery; the official-feed/key and policy-blocked-installer variants remain for the canary.
  • Issue/task and intended base: Related to [Task][RFC]: Complete the Desktop execution frontend journeys #5204 (does not close it; per-OS release evidence and the stable rollout stay with the maintainer). Intended base main: rebased onto c97f921e2f18fbd7e155495b7eb5029031c97fe3 for this revision (see Validation).

Included Fix

  • Windows webview boot surface: while running the real-machine journey, the packaged Windows App stayed on about:blank because the navigation fence accepted only the tauri:// scheme and the workspace origin, while Windows serves the bundled boot page over the WebView2 transport alias http://tauri.localhost. workspace_navigation in lib.rs now also accepts that exact boot origin, covered by the extended navigation_keeps_the_workspace_fence_and_hands_external_web_links_off_once unit test: both boot variants stay inside the fence, lookalike hosts (http://tauri.localhost.evil.example/, http://elsewhere.localhost/) are handed off to the browser exactly once, and file:// / javascript: / mailto: remain unreachable. This repairs pre-existing Windows behavior that the delivered update journey depends on; without it the update surface cannot render on Windows at all.

  • Channel-scoped panel copy (re-review finding, 823008357): desktop_update_status now exposes the host platform, and the update panel chooses its idle copy from that platform plus the selected channel. Windows stable/MSI names the manual signed-installer path, Windows main keeps the launch-time NSIS promise, macOS keeps the prepared-update wording, and a legacy payload without platform falls back to neutral channel wording. The browser smoke asserts macOS, Windows stable, Windows main and English stable, and rejects the old unconditional Windows sentence. The re-review confirmed the fix on the current production bundle.

  • Author-side installed-NSIS receipt (test key/loopback feed): installed A 0.0.0-main.1.0, updated to B 0.0.0-main.2.0 through the real panel; the old App and both owned services exited, B restarted, new owned services connected, and an independent CLI process survived. A signed invalid-PE payload stopped the owned services, Windows refused launch, and after dismissing the OS error dialog the same window returned to app_install_failed with services restored and retry available. Details, PIDs and hashes are in the receipt comment.

  • Stable release isolation (re-review finding): Windows NSIS now builds in a main-only build-windows job. build-macos remains the stable/main leg, and publish uses explicit result checks: a stable release publishes the Darwin feed when build-windows is skipped, while a main release still rejects a skipped or failed Windows build. tests/desktop/test_update_feed.py evaluates the publish condition for both channels and pins the dependency edges.

  • Stable upload consumer (re-review finding, dcbc04589): the upload step enables nullglob before enumerating .gz/.sig/.exe, so a Darwin-only stable release reaches the immutable feed and desktop-stable pointer instead of passing the unmatched glob to gh. Main still builds Windows and the feed still rejects a missing Windows artifact. The regression executes the exact production shell block for stable Darwin-only and main complete, checking the gh call log and publication order. Main with missing Windows fails in the preceding feed builder before the upload step.

  • Shared smoke coverage restoration (re-review finding): the rebase had dropped main's general/transfer-failure and boot-readback blocks from examples/desktop-update-browser-smoke.mjs. Restored those blocks from main while keeping the Windows stable/main channel cases, and kept main's "open the local version first" macOS copy with the manual-update sentence. The production-bundle smoke passes with the restored failure, no-install, check-only-retry and boot-diagnostics assertions.

  • Review-hardening round (owner review of the delivered journey): the feed now rejects a .sig that is not a decodable minisign signature instead of only checking for non-empty content, and the workflow tests pin the signed Windows build step, its embedded-version assertion and the pointer upload staying inside the advance branch, while the artifact rename becomes idempotent. Rust-side, a failed install no longer folds the macOS continuation journal into a Windows-safe restart (install_failure_state takes an explicit journal-applies input), the platform/action gate is a pure two-column table so both host columns are covered from any test process, and the navigation-fence test pins transport-alias port and scheme lookalikes, the published .sig must match the four-line minisign structure the client parses (74-byte and 64-byte payloads) with tests for comment-only, wrong-length and embedded-newline inputs, and finalize_install_failure no longer attempts the journal effect where no such journal can exist. Both update surfaces stop naming repair or rollback in states where this release disables them (boot.js unrepairedFallback, desktop-update.tsx phaseMessage), the desktop update browser smoke asserts that wording, and the smoke's static-file guard now also serves Windows paths so the smoke runs on Windows.

Author Declaration

  • Written by: model_agent (Codex CLI, OpenAI), implementing a request from the repository owner (@JasonBuildAI), who directed opening this PR so review happens on a concrete head.

Implemented against

Criterion (spec clause) Disposition Symbol / path Test or command
#5204: independent per-OS update evidence implemented (main channel) .github/workflows/desktop-updater.yml, scripts/desktop_update_feed.py python -m unittest discover -s tests/desktop -p 'test_update_feed.py'
Feed advertises a Windows NSIS updater on main implemented QUALIFIED_PLATFORMS["main"] test_immutable_source_and_qualified_platform
Feed fails closed on missing, empty, duplicate or unsigned Windows artifacts implemented build() in scripts/desktop_update_feed.py test_windows_artifact_fails_closed_on_the_main_channel
Stable channel keeps manual Windows installs implemented QUALIFIED_PLATFORMS["stable"] test_stable_channel_excludes_windows_until_that_rollout_is_approved
Windows App check / install / restart journey implemented app_update_action_supported in maintenance.rs Rust windows_keeps_the_signed_app_update_journey_without_macos_recovery_actions (CI validate, macOS)
Windows repair / align / rollback report explicit unavailable implemented update_action_platform_unavailable + boot.js / desktop-update.tsx copy same Rust test
Automatic launch-time update on Windows implemented automatic_app_update in maintenance.rs windows-desktop-tests native regression; author-side installed receipt at 823008357
Rollback / repair mechanism on Windows out_of_scope - explicit unavailable state only
Authenticode signing out_of_scope - minisign only, unchanged from macOS
Stable-channel Windows enablement deferred (maintainer) - post-merge canary and stable decision
  • Self-check before submission: ran the desktop Python tests, tests/test_ci_resource_policy.py, cargo fmt --check (reports a pre-existing services.rs diff also present on upstream/main; not claimed clean), cargo clippy --all-targets --locked, examples/desktop-release-workflow-smoke.py and scripts/check_rfc_bilingual.py locally. Deliberately left out: no MSI feed entry, no Authenticode, no Windows rollback mechanism, no stable-channel change. An author-side installed-NSIS receipt was produced on this head with a loopback feed and test key; it covers the success and launch-failure journeys. The official-feed/public-key and policy-blocked-installer variants are not claimed.

Scope And Continuation

  • Completed scope and remaining work: main-channel signed Windows NSIS updates end to end - pipeline, feed, App gate, panel copy, docs. Remaining, not in this slice: stable-channel Windows enablement, MSI signed updates, Authenticode, the Windows rollback/repair mechanism.
  • Slice boundary / successor: the slice is internally complete and independently revertible (one workflow, one script + tests, one Rust gate, two copy additions, README updates). Successor work - the post-merge main-channel canary on a real machine and the stable decision - stays with the maintainer's release process.

Validation

  • Tested revision: 7392feb15368a26175cd99a9884213b6633e5bab (merge base c7d8c3c6fad76e297da6b5e5eaef7c28b647ccee). The installed-NSIS screenshots were captured at 823008357; git diff --quiet shows service_lifecycle.rs and lib.rs byte-identical at 7392feb15; desktop-update.tsx keeps the channel/platform idleMessage and now uses main's "open the local version first" macOS copy.
  • Run state: finished (acceptance adequacy is not claimed; see rows)
  • Input classes: synthetic | public_fixture
Check kind Result Public-safe evidence / limitation
static mixed (pre-existing) cargo fmt --check reports a pre-existing services.rs formatting diff identical on upstream/main; no Rust file is changed by this round, so formatting is not claimed clean.
unit passed python -m unittest discover -s tests/desktop: 28 passed at 7392feb15, including the exact production upload-loop regression for stable Darwin-only, main with Windows complete, and main missing Windows.
unit passed cargo test --locked --lib -- service_lifecycle --nocapture at dcbc04589: 6 passed, including the Windows update handoff and failed-launch recovery paths.
unit passed cargo test --locked --lib on Windows at 823008357: 134 passed, 1 failed, 2 ignored. The single failure is the pre-existing services::tests::timed_output_large_output_does_not_deadlock in the untouched services.rs; this host resolves python3 to the Microsoft Store stub.
unit passed Dashboard npx tsc --noEmit and npm run build:chat at 823008357.
integration passed examples/desktop-update-browser-smoke.mjs at 823008357, including the macOS, Windows stable, Windows main and English stable idle-copy assertions and the repair-free wording.
workflow passed tests/desktop/test_update_feed.py evaluates the publish condition (stable with a skipped/failed Windows leg publishes Darwin; main rejects it), pins the build/publish dependencies, and executes the exact upload shell block with a fake gh log. Stable Darwin-only uploads the feed and pointer without an .exe or literal glob; main uploads its Windows installer; feed upload precedes pointer upload; main without Windows fails closed in the feed builder.
source_premerge mixed (18/19) loopx canary premerge --from-git-diff --git-diff-base upstream/main at dcbc04589: diff hygiene, changed-Python compile, all 8 risk-profile smokes, public/private boundary and 9/10 catalog canaries passed. The remaining hot-path-interface-budget-smoke fails in the pre-existing Windows extension-doctor executable-bit check; readiness.py is outside this diff.
visual passed Installed-NSIS and production-bundle screenshots at 1280×900 and 390×844 are embedded in the Frontend / Visual Evidence section.
integration mixed on the previous approved head dcbc045 Signed Desktop Updates validate and windows-desktop-tests passed at dcbc045 (including the previously flaky loopback test). The unrelated chat-bundle-browser, windows-powershell and Release Artifacts build jobs still fail outside this diff; some Python shards were still running. The new head 7392feb15 reruns the checks; release build jobs are skipped on PR as expected.
real_entrypoint passed (author-side installed NSIS) Installed A 0.0.0-main.1.0 -> panel update B 0.0.0-main.2.0: old App PID 41724 and owned services 43428/31044 exited, B PID 52560 restarted, new owned services 43608/28136 connected, independent CLI PID 37672 stayed alive (HTTP 200). Failure: signed invalid-PE ZIP stopped the owned services via the pre-exit hook, Windows refused launch, and after the OS error dialog was dismissed the same App/window returned to error/app_install_failed with services restored (40576/44664), retry available and the independent CLI alive. Bounded: loopback feed + test key, repository CLI via LOOPX_BIN, invalid-PE payload rather than a policy-blocked installer.
real_entrypoint not_run (official canary) The official feed/key and policy-blocked-installer variants are main-only; the first live production proof is the post-merge main-channel canary.

Frontend / Visual Evidence

  • UI impact: changed
  • Before: on Windows the packaged App never left the about:blank transport page (see Included Fix), so the update panel did not render at all, and desktop_update returned the generic platform_update_not_supported fallback. The "this channel has no update package for this platform" copy is the post-change behavior when a channel has no Windows entry, not the pre-PR state.
  • After: on Windows, repair / align_runtime / rollback return the explicit update_action_platform_unavailable state with dedicated Chinese and English copy, and states without a dedicated code switch to repair-free wording instead of naming the disabled repair or hidden rollback controls; the check / install / restart journey renders as on macOS. The idle copy is channel- and platform-scoped: Windows stable/MSI points at the manual signed installer, Windows main names the launch-time NSIS path, macOS keeps the prepared-update wording, and a legacy status without platform uses neutral channel wording. Buttons remain hidden or disabled through the existing bundled_repair_available / rollback_available state fields - no layout or control change.
  • First viewport and preview: the update entry is in the sidebar footer rather than the page hero; the affected panel was captured in the installed windows at 1280×900 and in the production web bundle at 1280×900 and 390×844. The installed-NSIS images are from the 823008357 run (loopback feed + test key); the browser images use synthetic native status from the same tree. The channel/platform copy and service_lifecycle.rs/lib.rs are unchanged at 7392feb15; desktop-update.tsx now also carries main's current macOS idle copy. No private data.
  • States and viewports shown: installed A with an available update, the restarted B workspace, the recovered failure panel, and the Windows-stable idle copy at 1280×900 and 390×844.

Installed A with an available update

Installed A, real panel: 0.0.0-main.2.0 is available after the signed check.

Restarted B workspace

After the passive NSIS install and automatic restart: the B workspace is interactive.

Recovered failure window

After the refused launch and dismissing the Windows error dialog: app_install_failed, services restored, check/retry still available.

Windows stable idle copy at 1280x900

Windows-stable idle copy in the production bundle at 1280×900: manual signed installer path, main/NSIS scope.

Windows stable idle copy at 390x844

The same copy at 390×844; the panel wraps without horizontal overflow.

  • Source data: none
  • Attention review: no new control or surface; the added copy replaces a generic failure with the platform's actual capability boundary while the one-step check / install / restart control stays available.
  • Craft review: N/A - no visual layout change, no new tokens, colors or components.

Type of Change

  • New feature
  • Bug fix
  • Breaking change
  • Refactoring (no functional changes)
  • Documentation update
  • Test update

LoopX Area

  • Build, packaging, installer, or CI
  • Host or runtime integration
  • Control plane (goals, todos, quota, scheduler, registry, runtime)
  • Benchmark boundary (adapters, runners, verifiers, scoring, evidence)
  • Capability or extension (providers, adapters, skills)
  • Public docs or presentation surface (README, protocols, dashboard)

Technical Direction

Shared-authority RFC fixture impact

  • Production-scale fixture schema: N/A - no shared-authority RFC progress is claimed.
  • Semantic dimensions changed, or reviewed no-impact rationale: N/A.
  • Provider conformance arms run: N/A.
  • Read-only legacy/file/PostgreSQL three-arm rehearsal: N/A.

Coordination And Merge

  • The claim comment said "no PR before direction agreement"; the repository owner directed this submission, so this PR is that concrete head for review rather than a new direction claim.
  • PR fix(desktop): open the local workspace before background updates #6835 was merged on 2026-10-19; this branch is rebased after it.
  • This PR does not self-merge. apps/** behavior changes require maintainer authorization; merge readiness is left to the maintainer after validation.
  • Boundary: this change extends the App-driven signed-update journey to Windows. The CLI-driven desktop upgrade path (loopx update / --loopx-upgrade-request) remains macOS-only, and a Windows desktop keeps using the separately installed CLI.
  • Windows NSIS now builds in a main-only build-windows job. A stable release publishes the Darwin-only feed without depending on that leg, while a main release still requires it; stable release pages keep the unsigned installers for manual installation.
  • The stable publish condition is explicit (always() plus per-job result checks), not a prose convention: the workflow regression in tests/desktop/test_update_feed.py models stable-Darwin success with a skipped/failed Windows leg and main rejection of a missing Windows leg.

Boundary Checklist

  • Neither the diff nor this PR body/comments/attachments disclose private state, credentials, raw traces or verifier output, internal links, or local machine paths.
  • I did not duplicate maintainer-owned benchmark work.
  • I kept the change scoped to the linked issue/task.
  • Existing English/Chinese RFC pairs both have a diff, and python3 scripts/check_rfc_bilingual.py --base main --head HEAD passes, or no paired RFC changed (no paired RFC changed; the check passes).
  • I completed the visual evidence section for UI changes, or marked UI impact none.
  • Every commit includes a DCO Signed-off-by trailer (git commit -s).

@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

Real-machine receipt: Windows main-channel signed auto-update

Executed on the current head e2f4c71b6cba48eb3a501014aa51d0e3c9893c77 (real NSIS install on Windows, x64). The installed App was built by this PR's pipeline settings (Windows NSIS with updater artifacts, minisign-signed) and additionally carried a local-only feed endpoint override that is not part of this diff. The feed was generated by the real scripts/desktop_update_feed.py --channel main and its URLs were pointed at the local test endpoint; signing used a temporary test keypair. Nothing from this run (keys, endpoints, logs) is committed or attached.

Positive journey (launch auto-update)

  • Installed 0.0.0-main.1.0; the update panel rendered and reported the current version.
  • With the feed advertising 0.0.0-main.2.0, the App checked on launch and moved through checking -> available -> downloading -> installing passively, with no dialog interaction.
  • Registry readback after install: DisplayVersion and file version become 0.0.0-main.2.0.
  • The new process restarted automatically and checked the feed again (up_to_date), i.e. the passive-install + auto-restart loop behaves as on macOS.

Positive journey (manual panel)

  • From a fresh 0.0.0-main.1.0 install with the feed restored, the panel's check -> install sequence reached the same states, installed passively, bumped the registry version, and restarted into 0.0.0-main.2.0.

Negative cases

  • Tampered installer (bytes altered after signing): the check still reported the update as available, and clicking install failed during download/verification. The panel showed the existing download-or-verification-failure copy ("not installed; check again"), the installed App stayed untouched and usable, and no pending-install latch was recorded.
  • Feed without the Windows platform entry: launch-time automatic check reported update_platform_unavailable; the manual check showed the existing "no update package for this machine on this channel yet" copy; the App stayed fully usable.
  • Windows platform action boundary: with the Mac-only recovery actions invoked, the panel reports the new explicit update_action_platform_unavailable state; repair stays disabled and rollback stays hidden, matching the declared capability boundary.

Fix discovered during the journey (included in this head)

The packaged Windows App initially never left about:blank: the navigation fence accepted the tauri:// scheme and the workspace origin but not the WebView2 transport alias http://tauri.localhost that Windows uses for the bundled boot page. workspace_navigation in lib.rs now also accepts that exact boot origin (lookalike hosts still go to the external browser exactly once; file:///javascript:/mailto: stay unreachable), and the unit test covers both boot variants. After the fix the panel loads and every state above reproduces. Without this, the delivered journey cannot be exercised on Windows at all.

State

  • Pre-merge real-machine evidence is now complete for the main channel on Windows; the workflow/feed/CLI/stable rows are unchanged from the PR description.
  • This PR still does not self-merge; apps/** behavior needs maintainer authorization, and the post-merge canary plus the stable-channel decision stay with the maintainer.

@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

Follow-up: launch-time auto-update re-verified directly on the fixed build.

The main receipt above was completed before the navigation-fence fix landed in the head, so I re-ran the launch-path case on a build equivalent to e2f4c71b (same Windows pipeline settings, test key, local feed):

  • Installed 0.0.0-main.2.0, feed advertising 0.0.0-main.3.0.
  • Relaunched the installed App with no UI interaction: the startup check fetched the feed, the download of the signed 3.0 NSIS installer followed within the same second, and the log moved through checking -> available -> downloading -> installing_app (observed ~0.6 s into the startup sequence).
  • The installer ran passively for ~9 s: registry DisplayVersion and file version became 0.0.0-main.3.0.
  • A new App process was launched automatically, and its startup check fetched the feed again about 2 s after process creation with no further download (up_to_date).
  • Panel readback on the restarted instance reports app_version 0.0.0-main.3.0 with the panel serving from http://tauri.localhost/.

So the full launch journey - auto-check, signature-verified download, passive install, automatic restart, post-restart re-check - is directly observed on Windows, matching the macOS semantics.

@JasonBuildAI
JasonBuildAI force-pushed the codex/desktop-windows-signed-updates branch 2 times, most recently from ff68b24 to f2653e4 Compare October 19, 2026 06:37
@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

Review hardening landed on f2653e4c2 (rebased onto current main at 4f6e20a13, so the branch is not behind).

A review pass over the delivered journey surfaced non-blocking hardening items; they are now committed and validated on the new head.

Feed / workflow (scripts/desktop_update_feed.py, .github/workflows/desktop-updater.yml, tests/desktop/test_update_feed.py)

  • A published .sig must now base64-decode to a minisign signature (untrusted comment: header) instead of only being non-empty; negative tests cover non-base64, wrong-content and broken-padding inputs for both qualified platforms.
  • The platform-key comment cites the pinned tauri-plugin-updater 2.11.0 lookup and states that the bare windows-x86_64 fallback key must not be added until MSI installs are qualified.
  • Tests pin the signed Windows build step (if: runner.os == 'Windows', createUpdaterArtifacts, no macOS signing identity), the embedded-version assertion before collection, and the pointer upload staying nested inside the advance branch.
  • The collect-step rename is idempotent for already-renamed artifacts.

Rust (maintenance.rs, lib.rs)

  • install_failure_state takes an explicit journal-applies input, so a journal filesystem error can no longer reclassify a verified safe restart as app_install_incomplete on Windows, where no staged-replacement journal can exist.
  • The platform/action gate is a pure (platform, action) table, so the macOS and Windows columns are asserted from any test process; the command boundary still calls it with the host OS.
  • The navigation-fence test adds http://tauri.localhost:8080/ and https://tauri.localhost/ as external destinations (port and scheme lookalikes), and the boot-surface test pins the repair-free fallback switch.

Copy / docs (boot.js, desktop-update.tsx, examples/desktop-update-browser-smoke.mjs, README.md)

  • States without a dedicated code (service_error, runtime_required, available, generic error) no longer point at the disabled repair or hidden rollback actions when bundled_repair_available is false; the copy names the App update only.
  • The root README names the Windows gaps: no rollback or repair, and no Authenticode certificate (Windows may show a SmartScreen prompt).
  • The desktop update browser smoke asserts the repair-free wording and the disabled control, and its static-file guard now also serves Windows paths, so the smoke runs on Windows as well.

Validation on the new head

  • cargo fmt --check clean; cargo clippy --all-targets --locked -- -D warnings reports no findings in the touched files (only the pre-existing Windows-only unused-code warnings in the untouched services.rs / workspace_handoff.rs).
  • cargo test --locked --lib on Windows: 112 passed; the single failure is the pre-existing services::tests::timed_output_large_output_does_not_deadlock in an untouched file.
  • tests/desktop: 25 tests passed; tests/test_ci_resource_policy.py: 132 passed; examples/desktop-release-workflow-smoke.py passed; examples/desktop-update-browser-smoke.mjs passed on Windows.

Still flagged for the maintainer, unchanged in intent: the signed Windows bundling path is covered pre-merge by the real-machine transcript and post-merge by the main-channel canary, not by PR CI (identity/build/publish are main-only); and a failing Windows leg withholds the darwin-only stable feed for that run. No blockers are known on this head.

@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

Follow-up on the review-hardening pass (head 3405a5cee): the independent verification of f2653e4c2 returned merge-ready with a small set of optional items, and the two with concrete failure scenarios are now landed.

  • The feed's signature gate now parses the decoded minisign structure the client needs - four lines with 74-byte and 64-byte payloads - instead of only checking the comment prefix, so a partially written .sig fails before publication; tests add the comment-only, wrong-payload-length and embedded-newline inputs, plus the collect-step rename idempotence guard.
  • finalize_install_failure no longer attempts the staged-replacement journal effect where no such journal can exist (Windows), with a test proving both the classification and the incomplete-installation latch.

Validation on this head: tests/desktop 25 passed, tests/test_ci_resource_policy.py 132 passed, cargo fmt --check clean, cargo test --locked --lib 113 passed with the single pre-existing services::tests::timed_output_large_output_does_not_deadlock failure in an untouched file, and cargo clippy --all-targets --locked -- -D warnings reports no findings in the touched files.

The remaining verified observations need no code change: app_update_action_supported_on is intentionally permissive for platform strings other than windows (the command boundary still rejects unsupported platforms first), and the workflow rename guard is still not unit-tested beyond the content assertion now added.

@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

Final head for this round: e3d54dd36.

One follow-up on the previous hardening comment: the malformed-signature loop in tests/desktop/test_update_feed.py left the Darwin signature invalid for the whole iteration, so the Windows-side assertion was satisfied by the Darwin failure and never proved the Windows validator. The qualified Darwin signature is now restored between the two builds, and a Windows-only skip of the validator makes the test fail (mutation-checked), so the "for both qualified platforms" coverage now holds.

Local state at this head: tests/desktop 25 passed; tests/test_ci_resource_policy.py 132 passed; cargo fmt --check clean; cargo test --locked --lib 113 passed with the single pre-existing services::tests::timed_output_large_output_does_not_deadlock failure in an untouched file; examples/desktop-update-browser-smoke.mjs passes on Windows.

CI note: windows-powershell fails on this head exactly as it does on main (heartbeat_prompt_brief/json emitted 10522 chars; variant ceiling is 10500, tracked by #6843), and that pre-existing failure is also what keeps the required merge-gate from going green; cancel-obsolete-pr failed once on an incomplete cancellation of the superseded run (infrastructure job, not a required check, and not re-runnable without repository admin rights). No other blockers are known.

@songoow songoow left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结论:请求修改。 在 b2e8e1bb109852ffea533e7c960a56263aa9395d 上发现两项 Windows 更新生命周期问题:手动更新成功会绕过 App 自有服务的关闭路径;安装器启动失败会把仍在运行的窗口隐藏。两项均以本仓库和锁定依赖的源码调用链确认,原生 Windows 现场复现尚未执行。

Reviewer: model_agent · GPT-6 Astra Pro · OpenAI · self_reported

[P1] Windows 更新退出前回收 App 自有服务

定位:apps/desktop/loopx-control-plane/src-tauri/src/maintenance.rs:386–390。

在 Windows 已进入工作区、App 已启动自己的 Chat/status 子进程后手动 apply 一个有效更新,新开放的分支会调用 update.install(bytes)。锁定的 updater 2.11.0 在启动 NSIS 后直接 std::process::exit(0);默认 before-exit hook 只调用 Tauri cleanup,既不产生项目依赖的 RunEvent::Exit/ExitRequested,也不会运行 Rust Drop。因此 apps/desktop/loopx-control-plane/src-tauri/src/lib.rs:308–317 中的 shutting_down、当前路由保存和 ServiceSet::stop() 均被跳过。这里的子进程通过 apps/desktop/loopx-control-plane/src-tauri/src/services.rs:339–353 的 group_spawn() 创建,锁定 command-group 5.0.1 默认 kill_on_drop=false,只有显式启用才设置 KILL_ON_JOB_CLOSE。旧 Chat/status 服务因此可在旧 App 退出后继续运行,而新 App 会分配新端点并启动另一组服务;重复更新会累积失去 owner 的进程和后台工作。

请把 Windows 更新交接接入现有服务关闭/路由保存 owner,并与 supervisor 同步,确保成功退出前仅回收本 App 的服务及其后代。补一条原生 Windows 回归:记录手动更新前的服务 PID/端点,更新后断言旧进程退出且仅有新服务组、独立 CLI 服务不受影响。此问题由 exact-head 调用链和锁定依赖源码确认,未在本 Linux 环境现场复现;base 的 Windows apply 会在平台门禁处拒绝,所以这是本次启用带来的新可达路径。e3d 审查时的五条作者评论未覆盖该所有权检查。

[P2] 安装器启动失败后恢复窗口再承诺可继续使用

定位:apps/desktop/loopx-control-plane/src-tauri/src/maintenance.rs:667–668。

当签名已经验证、安装器已解包,但 ShellExecuteW 因执行限制等原因返回 <=32 时,Windows 分支将当前 exe 仍在磁盘上视为可继续使用的证明。然而锁定 updater 2.11.0 在调用 ShellExecuteW 之前已执行默认 before-exit hook,而 Tauri 2.11.5 cleanup 会清空资源表并隐藏所有 Windows 窗口。启动失败随后返回到本函数,现有 handler 只发布 app_install_failed,没有重新显示窗口或恢复被清理的资源。于是进程仍活着,但当前启动页/工作区以及错误、检查、重试控件已经不可见;仅检查 exe 存在不能支持“可继续使用”的承诺。

请让 Windows 安装交接在启动失败时保持或恢复可用的原生窗口,避免在尚未确认交接成功时不可逆地清理 Tauri。补一条真正越过签名验证的负向测试:拒绝 NSIS 的 ShellExecuteW 启动,确认原窗口仍可见、状态和检查/重试可用、现有服务未损坏。现有 tampered-signature 测试在 before-exit hook 之前失败,不能覆盖此路径。这里是锁定依赖的源码证明,未冒称原生复现;用户另行启动第二实例可能唤回窗口,但不能代替此次操作的同窗失败恢复。base 不开放 Windows apply,现有评论也没有此启动失败场景。

动机

直接受影响的是已安装 main 通道 Windows NSIS App、希望保留当前工作现场完成升级的用户,以及维护签名发布流水线的操作者。旧版本的 Windows App 不接受内置更新;本次改动让它在启动时或更新面板中获取 main feed、验证安装器并交给 NSIS 更新,随后应继续进入工作区。该切片能减少 Windows main 用户手动下载、安装和回到工作区的操作负担,签名检查又把发布产物与受信更新绑定。但用户真正需要的是升级后继续工作,所以评估必须走到旧进程退出及失败恢复,不能仅止于安装器被启动。

已验证的增量是 main feed 输出专属 NSIS 目标、stable feed 保持 macOS 范围、坏签名结构和缺失目标被拒绝;原生安装成功后的服务回收及安装器启动失败后的窗口恢复仍存在源码可确认的缺口。剩余缺口是 Windows 更新交接必须遵守现有 App 服务所有权,并在安装器未能启动时保留可见且可重试的窗口。本次审查不要求补齐 Windows 捆绑运行时修复、回滚、MSI 自动更新、stable Windows 自动更新或 Authenticode;这些不是本 PR 承诺的交付范围。

规范依据为 docs/architecture/rfcs/desktop-execution-frontends-v0.md,固定在 4f6e20a13b090318e14efeb20b91790e33fe694b。该文没有逐条编号,以下沿用原文小节/句首作为定位标签:Native service ownership 要求原生窗口拥有自己启动的服务,退出和重开只影响其子进程,此处因 P1 未满足;A failed manual update or recovery 要求保留错误、诊断和重试入口,此处因 P2 未满足;An available signed App update 的启动阻塞可操作性仍由现有 maintenance 状态及 UI 保留,源码与模拟入口测试覆盖,不能等同于完整原生安装资格。关联任务 #5204 的重启、更新和单一执行 owner 要求与上述边界一致。

改动思路

签名产物选择属于发布脚本,安装与退出交接属于原生维护层,能力展示属于现有启动页和更新面板;沿用这些边界和现有服务 owner 即可完成修复,无需新增平行更新框架。当前 PR 应交付 main NSIS 的签名更新以及成功退出、失败留窗这两个必要生命周期不变量;Windows 运行时修复、回滚和 stable 扩展可由后续平台切片独立验收。

main 流水线增加 NSIS 签名产物并以 installer-specific target 进入 feed;原生层按平台开放动作,并排除 macOS 的安装 journal、运行时修复及回滚;两个前端根据原生能力调整恢复提示。这些机制有真实入口,适合一起交付。问题集中在复用的插件安装 API 与项目现有退出协议之间:Windows API 成功时不返回,失败时却可能已经清理窗口,两个条件都与普通维护状态机的假设不同。

具体改动

完整 diff 共 10 个文件、增加 509 行、删除 57 行。发布 workflow、feed 生成器、原生入口/维护层、启动页/工作区面板及配套测试均已阅读;也检查了未改的 service owner、workspace handoff、runtime selection、backup 和 pinned updater 调用链。

关键代码讲解

  • build(scripts/desktop_update_feed.py:93) 消费实际产物目录、版本、tag 和 channel;main 需要 darwin 与 windows-x86_64-nsis 两个目标,stable 仍只需要 darwin。_minisign_signature 检查编码与四行结构,真正的安装器签名验证仍归 Tauri updater。已用相同结构性 fixture 经 base/head 公开 CLI 执行五组对照,stable 输出一致,main 新增 NSIS,缺目标或坏结构在 head 拒绝。
  • desktop_update(maintenance.rs:399) 是启动页和工作区共用的原生入口;新动作表在 Windows 允许 check/apply/forget/restart,supervision 锁保持串行。它把 apply 交给 perform,后者在非 macOS 直接调用插件 install,所以必须把 Windows 的非返回式退出纳入既有服务 owner,详见 P1。
  • failed_install_left_previous_app_usable(maintenance.rs:667) 为 Windows 改成检查当前 exe 存在,再由 finalize_install_failure 决定错误状态。排除 macOS journal 是正确的平台边界,但磁盘完整性不包含窗口是否已被 Tauri cleanup 隐藏,详见 P2。
  • workspace_navigation(lib.rs:97) 接受实际 boot origin,同时保留 workspace 的精确 origin 限制;scheme、host 和 port 的相似值没有获得维护权限。Windows 的 http tauri.localhost 别名不会把任意外站变为 App origin。

Windows 运行时仍来自独立 CLI 的选择/发现,bundled_repair_available 是原生能力投影;沿用缺字段兼容有真实的独立 shell/CLI 部署边界。本次没有新增 Goal、Agent 协调协议或义务 flag。共享语义使用既有维护/服务 owner;两项问题需要在原生交接处修复,无需改变 Goal vocabulary。

对主干的风险

长期持续工作的主要风险是 P1 的孤儿服务:安装成功不能证明旧 owner 已关闭,后续启动可能形成新的进程组而旧后台工作仍在。用户体验的主要回归是 P2:错误快照存在也不能说明用户看得到恢复入口。签名篡改在安装前被拒绝、纯状态机通过以及正常更新 receipt 都无法排除这两个分支。

最终审查绑定 b2e8e1bb109852ffea533e7c960a56263aa9395d。它与已执行验证的 e3d54dd36eae4dba016991580ac0bf7fd20ef363 是同一完整代码树:本地 fetch 后两个 tree OID 均为 a8c4e794b76f330385cb2ebaec02c08025a1e15c,git diff --exit-code 返回 0;GitHub compare 也没有文件差异。因此两项发现及行号均保留,沿用未失效的证据;没有声称在 b2 重新运行测试。

实际在 e3d 执行了 25 个 desktop Python 单测和五组 base/head feed CLI 对照,全部通过。e3d 相对初审 3405a5ceeb6717ffd50ae9e678ca6ff6424ce63c 只增加三行 Darwin 签名 fixture 恢复,确保后续 Windows 拒绝断言单独依赖 Windows 签名;生产代码与依赖未变。初审 3405 执行的 132 个 CI policy 测试、release workflow smoke、dashboard TypeScript/Vite 构建和 desktop-update-browser-smoke 均通过,仍适用于同一生产树。workflow smoke 初次遇到 sandbox 子进程 EPERM,在获准的隔离测试环境重跑同命令后通过;browser smoke 初次缺浏览器路径,显式指定已有 headless shell 后通过。browser 测试使用 production bundle 和 synthetic native IPC,不安装软件;检查了完整 viewport,英文恢复面板清晰,当前 Linux 缺中文字体导致中文截图为 tofu,未据此声称原生字体验收。这些测试不能观察 Windows process exit、job object 或 ShellExecuteW。

完整审查期间在前一 head 3405a5ceeb6717ffd50ae9e678ca6ff6424ce63c 观察到 Windows desktop、macOS desktop、validate 均成功,另有测试分片待完成;这不冒充新 head 的 CI 结论。windows-powershell 的三个 long-path budget 用例均为 heartbeat_prompt_brief/json 10522 > 10500,相关生产路径及测试不在本 diff;没有相同 Windows 环境的 base 对照,保留 baseline 未证状态,不能冒称已经证明是旧失败。cancel-obsolete-pr 则是远端取消旧 run 后未及时进入终态,属于未改维护工作流的外部取消问题。e3d 审查时的作者评论还将同一 budget 失败关联到 #6843,并报告 main 同样失败;本审查没有重跑其同 Windows base 环境,因此只记录该说明,未把作者声明转成独立基线证明。这些 CI 状态作为独立合并就绪信息记录,两个代码 finding 不以它们为依据。

更新后的 CI 资格单独保留:在 e3d,macOS job 117938977194 的 bundle_dmg.sh 失败并返回 exit 1;目前没有充分证据归属根因。Windows job 117942236228 也报告 budget 失败,其独立 Windows base 对照仍未建立。这两项按历史未解决 CI 状态记录,不能用 3405 的通过状态代表它们。最终 b2 的原生 packet(生成于 2026-10-19T07:55:30Z)未提供 status-check rollup,所以本审查不作 b2 全部 CI 通过的结论;完整 Windows/macOS 安装及上述两条生命周期回归仍未取得本审查的原生运行资格。

我的整体评价

这是有实际价值、范围适当的 main NSIS 增量;新增签名/feed 规则、动作隔离和能力文案有必要,未发现需要拆成另一套更新框架的理由。

请先复用现有 App 服务关闭 owner 修复成功交接,并给失败交接补齐可见窗口恢复,再在真正的 Windows App 上分别验证已连接状态手动更新后的旧 PID 清理,以及已通过签名但 NSIS 启动被拒绝后的同窗重试。重审应保留当前 feed/平台反例和 macOS 路径,核对届时最新 head。作者现有正常更新及坏签名 receipt 有帮助,但没有覆盖这两条具体生命周期路径。

English verdict: REQUEST_CHANGES — at b2e8e1b, the newly reachable Windows updater path bypasses App-owned service shutdown on success and hides the current window before a launch failure can be recovered. Source-confirmed against pinned dependencies; native Windows reproduction remains unverified.

Comment on lines +386 to +390
fn app_update_action_supported_on(platform: &str, action: &str) -> bool {
platform != "windows"
|| matches!(
action,
"check" | "apply" | "forget_runtime_selection" | "restart"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Windows 更新退出前回收 App 自有服务

在 Windows 已进入工作区、App 已启动自己的 Chat/status 子进程后手动 apply 一个有效更新,新开放的分支会调用 update.install(bytes)。锁定的 updater 2.11.0 在启动 NSIS 后直接 std::process::exit(0);默认 before-exit hook 只调用 Tauri cleanup,既不产生项目依赖的 RunEvent::Exit/ExitRequested,也不会运行 Rust Drop。因此 apps/desktop/loopx-control-plane/src-tauri/src/lib.rs:308–317 中的 shutting_down、当前路由保存和 ServiceSet::stop() 均被跳过。这里的子进程通过 apps/desktop/loopx-control-plane/src-tauri/src/services.rs:339–353 的 group_spawn() 创建,锁定 command-group 5.0.1 默认 kill_on_drop=false,只有显式启用才设置 KILL_ON_JOB_CLOSE。旧 Chat/status 服务因此可在旧 App 退出后继续运行,而新 App 会分配新端点并启动另一组服务;重复更新会累积失去 owner 的进程和后台工作。

请把 Windows 更新交接接入现有服务关闭/路由保存 owner,并与 supervisor 同步,确保成功退出前仅回收本 App 的服务及其后代。补一条原生 Windows 回归:记录手动更新前的服务 PID/端点,更新后断言旧进程退出且仅有新服务组、独立 CLI 服务不受影响。此问题由 exact-head 调用链和锁定依赖源码确认,未在本 Linux 环境现场复现;base 的 Windows apply 会在平台门禁处拒绝,所以这是本次启用带来的新可达路径。现有五条作者评论未覆盖该所有权检查。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 21747c654, with the native regression in 6e2859ba2; current head 7e87c1ca0.

The Windows handoff now goes through the existing App-service shutdown and route-save owner instead of skipping it:

  • ServiceLifecycle (apps/desktop/loopx-control-plane/src-tauri/src/service_lifecycle.rs) owns the ServiceSet, the shutdown/handoff/reclaimed state and the supervisor park. begin_app_update_handoff parks the supervisor first, then runs the same route save the window exit runs (workspace_location::remember_current) and takes and stops the owned set, so the reclaim happens inside this App's own job objects and the group's descendants leave with it.
  • The window installs the shared production constructor service_lifecycle::app_update_pre_exit_hook from maintenance.rs::find_update, i.e. the same owner RunEvent::Exit/ExitRequested uses, so the hook and the normal exit path cannot drift apart.
  • Supervisor synchronization: the supervisor neither starts nor recovers services while handoff_active() (lib.rs); a start that raced the handoff re-checks the park under the same lock and is handed back to the caller that started it instead of being adopted, so ownership stays single-valued and repeated updates cannot accumulate ownerless groups.
  • Scope: only services this App owns are reclaimed. A process this App never started (a CLI-owned listener on its own port) keeps running.

Native Windows regression: src/service_lifecycle_windows_update_tests.rs::a_verified_artifact_whose_launch_fails_keeps_the_window_usable_and_returns_the_runtime plus service_lifecycle::tests::windows_reclaim::an_update_handoff_reclaims_the_owned_group_and_its_descendants. Both hold a real owned service group and a real independent process, record their PIDs, and assert the owned PID exits while the independent PID survives and the replacement set is the only new owner (exactly one restore owed). The update is driven through the pinned tauri-plugin-updater 2.11.0 with a real loopback HTTP feed, the plugin's own minisign verification of a per-run throwaway key and its own ZIP extraction; a tampered artifact announced under the same signature must fail that verification first, so the "verified" claim is not vacuous. These run on native Windows (locally and in the new windows-desktop-tests leg).

Boundary I am not over-claiming: this is a native Windows process with the locked plugin and real job objects, but the App shell is the mock runtime. It does not reproduce endpoint allocation for the real Chat/status services, and the successful path's std::process::exit(0) (which happens only once Windows accepts the launch) cannot be reproduced in-process -- the regression asserts the reclaim the hook performs on that path instead.

Comment on lines +667 to +668
fn failed_install_left_previous_app_usable(_app: &AppHandle) -> bool {
std::env::current_exe().is_ok_and(|executable| executable.is_file())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 安装器启动失败后恢复窗口再承诺可继续使用

当签名已经验证、安装器已解包,但 ShellExecuteW 因执行限制等原因返回 <=32 时,Windows 分支将当前 exe 仍在磁盘上视为可继续使用的证明。然而锁定 updater 2.11.0 在调用 ShellExecuteW 之前已执行默认 before-exit hook,而 Tauri 2.11.5 cleanup 会清空资源表并隐藏所有 Windows 窗口。启动失败随后返回到本函数,现有 handler 只发布 app_install_failed,没有重新显示窗口或恢复被清理的资源。于是进程仍活着,但当前启动页/工作区以及错误、检查、重试控件已经不可见;仅检查 exe 存在不能支持“可继续使用”的承诺。

请让 Windows 安装交接在启动失败时保持或恢复可用的原生窗口,避免在尚未确认交接成功时不可逆地清理 Tauri。补一条真正越过签名验证的负向测试:拒绝 NSIS 的 ShellExecuteW 启动,确认原窗口仍可见、状态和检查/重试可用、现有服务未损坏。现有 tampered-signature 测试在 before-exit hook 之前失败,不能覆盖此路径。这里是锁定依赖的源码证明,未冒称原生复现;用户另行启动第二实例可能唤回窗口,但不能代替此次操作的同窗失败恢复。base 不开放 Windows apply,现有评论也没有此启动失败场景。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 21747c654, with the negative regression in 6e2859ba2; current head 7e87c1ca0.

The hook this window installs is no longer the plugin's default cleanup_before_exit. service_lifecycle::app_update_pre_exit_hook reclaims this App's services and performs none of Tauri's cleanup: it does not clear the resource tables and does not hide the window, precisely because a launch that fails returns in-process. Tauri's cleanup is gone from this path, so the failure branch cannot leave an invisible-but-running App.

Recovery in the same window: install_verified_app releases the handoff (release_app_update_handoff) when the plugin reports an install error, and the live supervisor sees restore_pending() and starts the same runtime again in place (maintenance::restore_services_after_update_failure), without re-announcing a phase, so the failure snapshot the operator retries from stays on screen with its check/retry controls. failed_install_left_previous_app_usable remains the layout-level guard for the same-window retry promise, but it is no longer the only thing behind "still usable", because the window is never hidden on this path.

The negative test now crosses the signature boundary instead of asserting the cause: the real plugin verifies a real signature over an NSIS archive, unpacks it, and Windows refuses the resulting ShellExecuteW launch, so install(bytes) returns Err with the pre-exit hook already executed. It then asserts the App and window resource sentinels survive, the window is still addressable, the reclaimed runtime returns to the supervisor (exactly one restore owed), and a service this App never started is untouched. A contrast run with the plugin's default cleanup_before_exit hook shows those survival assertions detect the difference, and a mutation check (adding app.cleanup_before_exit() to the shared production hook) fails the regression at "the App resources must survive a failed installer launch" instead of passing vacuously.

Local evidence at this head, native Windows: cargo test --locked 135 passed / 0 failed / 2 ignored, cargo fmt --check clean, and the new windows-desktop-tests CI leg runs these lifecycle regressions on windows-latest.

@mergify

mergify Bot commented Oct 19, 2026

Copy link
Copy Markdown

This pull request has merge conflicts with main and cannot be merged
until they are resolved. Please rebase or merge the base branch, @JasonBuildAI.

Choose the remote for the base repository, not an out-of-date fork.
For a fork clone, first inspect git remote -v; upstream must point
to https://github.com/loopx-project/loopx.git. If it is absent, add it
with git remote add upstream https://github.com/loopx-project/loopx.git.
Then run:

git fetch upstream
git rebase upstream/main
# Resolve each conflict, git add the resolved files, then git rebase --continue.
git push --force-with-lease origin HEAD

For a same-repository clone whose origin points to
https://github.com/loopx-project/loopx.git, use origin instead of
upstream for fetch/rebase. If you prefer merging the base, use
git merge <base-remote>/main and push normally.

Keep the DCO Signed-off-by trailer on every commit when you rebase.
https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase Mergify: the pull request has merge conflicts with its base branch label Oct 19, 2026
@JasonBuildAI
JasonBuildAI force-pushed the codex/desktop-windows-signed-updates branch from b2e8e1b to 5ee7baf Compare October 19, 2026 10:10
@mergify mergify Bot removed the needs-rebase Mergify: the pull request has merge conflicts with its base branch label Oct 19, 2026
@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

Both blockers from the review are addressed on the current head, and the branch is rebased and mergeable again. Re-review requested.

P1 — Windows update handoff now reclaims this App's own services

  • New single owner apps/desktop/loopx-control-plane/src-tauri/src/service_lifecycle.rs (ServiceLifecycle): it holds the ServiceSet plus shutting_down / handoff / reclaimed / restore_pending. The window exit and the updater pre-exit hook both go through the same reclaim(): save the route through the existing workspace_location::remember_current and stop the owned set through its own job objects, so descendants leave with it and a service this App never started stays up.
  • The live supervisor in lib.rs parses ownership from that owner: it parks on handoff_active(), and a start already in flight hands its set back (ServiceLifecycle::adopt returns Err) and stops it instead of stranding a group.
  • find_update installs the production hook .on_before_exit(crate::service_lifecycle::app_update_pre_exit_hook(app)). app_update_pre_exit_hook is one constructor shared by the real window and the regression, so the hook under test cannot drift from the shipped hook.
  • The install-error path calls release_app_update_handoff(app): it records restore_pending, and the supervisor restores the reclaimed runtime through maintenance::restore_services_after_update_failure (which reuses resume_runtime and does not overwrite the failure snapshot the operator is retrying from).
  • Windows regression: service_lifecycle::tests::windows_reclaim::an_update_handoff_reclaims_the_owned_group_and_its_descendants and service_lifecycle_windows_update_tests::a_verified_artifact_whose_launch_fails_keeps_the_window_usable_and_returns_the_runtime run real child processes in real job objects, record PIDs before the handoff, assert the old PID (including a grandchild that outlives its cmd /c parent) is gone, assert a CLI-owned process on its own port is untouched, and assert a second update reclaims exactly the replacement group.

P2 — a refused launch keeps a usable window

  • The hook deliberately no longer runs Tauri's cleanup_before_exit. Replacing the plugin's default hook is what removes the "hide windows + clear resource tables" step from the pre-launch path, so the ShellExecuteW <= 32 return lands on an intact window; the install error then only publishes app_install_failed on top of a window that was never cleared. Tauri is never cleaned up before the handoff is known to have succeeded.
  • Negative regression (same test): the run first proves signature verification is real (a tampered artifact announced with a valid signature is rejected), then update.install(bytes) drives the pinned plugin's own ZIP extraction and a real ShellExecuteW that Windows refuses (the payload is not a portable executable), i.e. the exact <= 32 branch. Assertions: app and window resource tables survive, the window stays addressable, state/check/retry stay usable, and existing services are not corrupted. The test also does a contrast run that installs through the plugin's default hook and asserts the resource table is cleared there, so the survival assertions cannot pass vacuously.

Rebase, conflicts, scope

  • Rebased onto main (merge base 3a8164117) with the conflicts in the desktop README.md, the dashboard update copy and maintenance.rs resolved; mergeable is back to MERGEABLE (was CONFLICTING).
  • Main's automatic-update split is carried into the Windows journey: automatic_app_update now performs the bounded check-and-apply on ordinary launch on Windows before any App-owned service exists, while macOS keeps the prepared-archive flow.
  • Windows now reports the macOS-only recovery actions (repair, align_runtime, rollback) as explicitly unavailable instead of offering a half-built path.

Validation (head f2fdc2335)

  • cargo fmt --check, cargo clippy --locked --lib --tests, cargo check --locked --all-targets — pass, only the pre-existing dead-code warnings in services.rs / workspace_handoff.rs that main carries too.
  • cargo test --locked --lib — 134 passed, 1 failed, 2 ignored; both new Windows regressions pass. The single failure is the pre-existing services::tests::timed_output_large_output_does_not_deadlock, which requires a real python3 (this machine resolves it to the Microsoft Store stub) and fails on main as well.
  • npx tsc --noEmit in apps/presentation/dashboard — pass.
  • New CI job windows-desktop-tests (.github/workflows/desktop-updater.yml) runs those regressions on windows-latest (cargo test --locked --lib -- service_lifecycle), so the requested Windows regression is enforced by CI rather than only by author receipts.
  • The windows-powershell job failure is the pre-existing heartbeat budget failure tracked by test(ci): qualify brief JSON budgets across native path depths #6843; it is not caused by this branch.

Not claimed

  • The two scenarios still have no real-machine (installed NSIS) receipt on this head. The regression drives the pinned plugin end to end — real loopback HTTP feed, real minisign verification, real ZIP extraction, real ShellExecuteW — but with a mock window runtime, so it proves the ownership and window rules, not a full installer run. A real-machine receipt for "manual update reclaims the old service PIDs" and "refused launch keeps the window usable" is outstanding; treat this as unverified until it is attached.
  • Merge authorization for apps/** runtime behavior stays with the maintainer; this PR is left open for that decision.

@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

Follow-up: the native Windows job is green on head 7e87c1ca0.

The first run of the new windows-desktop-tests job failed before executing any test — the crate's build script embeds the signed runtime archive, so a bare checkout cannot compile the test target. 7e87c1ca0 prepares that archive exactly as the macOS validate leg does, then runs the regressions.

Job windows-desktop-tests (windows-latest, https://github.com/loopx-project/loopx/actions/runs/39237015711/job/118008434095):

Prepared exact desktop runtime: caa72676fe15 (4502 installable entries)
running 6 tests
test service_lifecycle::tests::windows_reclaim::an_update_handoff_reclaims_the_owned_group_and_its_descendants ... ok
test service_lifecycle::windows_update_tests::a_verified_artifact_whose_launch_fails_keeps_the_window_usable_and_returns_the_runtime ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 131 filtered out

The signed-desktop lane is green on this head: validate, Windows desktop, macOS desktop, windows-desktop-tests, Sign-off, dependency-review. windows-powershell remains the pre-existing heartbeat budget failure tracked by #6843.

Still unverified, as in the previous comment: this is a native Windows test job, not a full NSIS install run, so the real-machine receipt for the two scenarios stays outstanding.

@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

Thanks for the review -- both findings are addressed on head 7e87c1ca0.

Fixes

  • P1 (App-owned services across the Windows handoff): 21747c654. One owner (ServiceLifecycle) now serves the window exit, the supervisor and the App-update pre-exit hook: route save, supervisor park, owned-set stop inside this App's job objects (descendants included), and a start that raced the handoff is handed back instead of adopted. CLI-owned processes are untouched.
  • P2 (window hidden before a launch failure could be recovered): 21747c654. The installed hook is deliberately not cleanup_before_exit, so a failed launch keeps the window, its resource tables and the failure snapshot; the released handoff has the supervisor start the same runtime in place.
  • Native regressions: 6e2859ba2, with the CI leg in 5ee7baf4f and 7e87c1ca0. The pinned plugin is driven through a real loopback feed, real minisign verification and real ZIP extraction on native Windows, and windows-desktop-tests (windows-latest) runs the lifecycle regressions there. 7e87c1ca0 prepares the bundled-runtime archive that job needs before it compiles.

Rebase and integration

The branch is rebased onto current main, which now carries the prepared-update split from #6835. 824074051 carries the reviewed Windows journey through it: Windows has no staged replacement archive to consume, so an ordinary launch keeps the bounded check and applies the verified installer before any App-owned service exists (macOS keeps main's prepared-archive flow), the panel's idle copy names that difference, and main's rejected_prepared_update_keeps_diagnostics_without_false_boot_stop passes the platform journal flag. The feed/platform counterexamples, the macOS path and the signature/journal gates from the earlier rounds are unchanged.

Validation at 7e87c1ca0

  • native Windows: cargo test --locked 135 passed / 0 failed / 2 ignored; cargo fmt --check clean; cargo clippy --all-targets --locked -- -D warnings -A dead_code clean (the exemption covers only the pre-existing Windows-only dead_code set; the macOS validate leg runs -D warnings without it and passes)
  • python -m unittest discover -s tests/desktop 25 OK; pytest tests/test_ci_resource_policy.py 132 passed; examples/desktop-release-workflow-smoke.py ok; examples/desktop-update-browser-smoke.mjs passed (f2fdc2335 aligned its panel-copy assertion with the merged automatic-update wording)
  • mutation check: adding app.cleanup_before_exit() to the shared production hook fails the new regression at the App-resource survival assertion, so it is not passing vacuously
  • CI at this head: validate (macOS) pass, windows-desktop-tests pass (3m42s), Windows desktop / macOS desktop bundle compiles pass, Sign-off and dependency-review pass

Boundary

The regressions run real Windows processes with the shipped dependencies, but the App shell is the mock runtime: they do not cover an installed NSIS App being replaced, endpoint allocation for the real Chat/status services, or a launch that Windows accepts. Those stay with the canary run after merge; I am not claiming a native installed-App upgrade receipt.

The branch is currently 20 commits behind main with no overlapping files; I left it un-rebased so these results stay attached to this exact head. I can update it whenever you prefer.

Requesting re-review. I am not merging this myself.

@loopx-agent loopx-agent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer: model_agent; gpt-6.1-sol; OpenAI; runtime_reported; reasoning_effort=max

动机

REQUEST_CHANGES,针对 7e87c1ca07892eb60c625951e8d502a986dea157。当前可复现的问题是更新面板把 Windows 自动更新的承诺扩到了稳定通道;真实安装窗口的更新成功与启动失败恢复资格也仍未证明。后者是验收缺口,不是声称已复现一个新的 Windows 进程泄漏。

使用 Windows NSIS main 预览版的用户,希望从现有 App 完成签名更新,并在失败后继续使用原窗口。过去 Windows 只能手工替换安装包;本次增加启动时检查、验证、安装和重新打开的路径,同时继续让独立安装的 CLI 由原来的安装方式维护。独立验证确认了 feed 的通道分离、本机 macOS 回归和实际构建面板的部分交互,但尚不能确认真实 Windows 安装后的服务所有权和同窗口恢复。范围限于 Windows NSIS main 更新与既有 macOS 路径,不包括 Windows stable/MSI 自动更新、Windows repair/rollback、Authenticode 或完成 #5204。

做这项功能有实际价值:手动下载、确认版本和重开窗口会重复占用用户注意力。但“取消默认 cleanup”或“停止一组测试进程”仍不是更新后恢复原工作区的完整结果。原两条评审意见提出的源代码缺口已得到针对性修复;本次重新审视整个 17 文件提案,没有继承旧 head 的结论,也没有把作者的 Windows 运行声明算作本机执行。

改动思路

签名验证继续交给固定版本 updater,服务所有权集中到同一个 ServiceLifecycle,安装失败由既有 Maintenance 和 supervisor 恢复;无需再建一套安装状态机或服务发现机制。当前 PR 应交付 main/NSIS 的受限更新路径、独立 CLI 不受影响的恢复结果和真实平台证据;stable/MSI 与 Windows 修复回滚保持原边界。

链路为:发布工作流构建两平台产物,feed 仅向 main 宣告 NSIS target;原生启动检查发现可用更新,下载并验签后调用安装;插件在启动安装器前运行共享 hook,保存路由、停住 supervisor 并回收本窗口的进程组。若启动安装器失败,则释放 handoff、恢复被回收的服务,并保留失败信息供用户重试。插件成功启动后直接退出进程,因此不能依赖 RunEvent::Exit 才回收服务。

这一组织方式符合既有 owner,新增恢复债务也有实际的生产消费者。较小的修复仍应留在这些 owner 内:纠正面板的通道承诺,补齐已承诺平台的真实安装验证。无需增加授权确认、新的 Goal 任务树或通用恢复框架。这里用文字描述分支,没有把未核验的示意图当作证据。

具体改动

评审基准是 Desktop execution RFC,spec_ref 为 docs/architecture/rfcs/desktop-execution-frontends-v0.md,spec_revision 为 cd1b2b4c69caddac4cfbf5a51f8eb961730cf591,并读取了 #5204 的既有验收。本提案的实际 merge base 是 3a816411785fea545c379769a1ff98f5e881bd5d。下列标签映射既有要求,没有新增父级验收门。

既有要求映射 当前判断
NS-ownership:原生窗口只拥有自己启动的服务,退出和恢复不影响独立 CLI not_met:共享 hook 和单 owner 的源代码修复成立;当前 head 的真实 Windows 安装窗口、独立 CLI 与更新后端点读回仍缺证据
NS-failure:失败更新保留可用窗口、诊断与重试,普通重开及同窗口恢复分别验证 not_met:mock Runtime 的资源表生存与注入 ServiceSet 是支持证据,尚未执行完整安装窗口与真实 status/Chat 服务恢复
NS-offline:自动更新源不可用时,现有 App 仍可启动 implemented:既有失败继续路径、macOS 本机回归及源代码分支覆盖;Windows 的完整安装资格仍受前两行限制
NS-truthful-scope:每个平台的支持声明和可行动状态必须与实际范围一致 not_met:稳定通道的实际构建面板宣称当前通道自动更新,而 feed 排除了 Windows stable target

关键符号及完整改动覆盖:

  1. automatic_app_update 增加 Windows 启动检查与 apply,macOS 继续使用已准备 archive;action table 拒绝 Windows repair、align_runtime、rollback,journal 也保持 macOS 范围。
  2. app_update_pre_exit_hook 同时被生产 updater 和 Windows 回归使用。adopt/reclaim/release 管理同一个 ServiceSet;lib.rs 的 supervisor 在 handoff 时停住,在失败时恢复服务,关闭窗口仍走该 owner。
  3. find_update 覆盖插件默认退出 hook。对照本地固定依赖 tauri-plugin-updater 2.11.0,Windows 顺序确实是 extract、hook、ShellExecuteW、成功后 process::exit;取消默认 cleanup 能避免失败前清空窗口资源。
  4. build 从已有发布边界选择 main 的 Darwin/NSIS 或 stable 的 Darwin,拒绝缺失/歧义/空产物及不合法签名结构。工作流补 Windows 构建、版本名检查、上传和原生测试;build.rs 与目标专用 dev-dependencies 支持 Windows libtest 的 manifest。
  5. 更新面板、boot fallback、导航 origin、两处 README 和测试一起检查。Windows 的 bundled boot alias 仅按精确 origin 接受;测试覆盖相似主机、错误端口/协议和非 Web 链接。全部改动为 +1623/-98,包含大量 cfg(test) 与 Windows 专用验证代码,不能全算运行时代码。

对主干的风险

[P2] 自动更新文案必须保留 main/NSIS 限定。 在真实构建 bundle 中注入明确不可 repair/rollback 的 Windows 状态,默认稳定通道仍显示“App 会在当前通道自动更新……Windows 版在启动时检查并安装”。同一界面的检查随后只能显示该通道没有本机更新包。feed 的 stable 平台表以及 README 均明确 stable/MSI 需手动更新。最小修复是在这条文案说明 main/NSIS 的范围及 stable/MSI 的手动路径;复用既有通道信息,无需再加设置。回归应断言 stable/MSI 不承诺自动安装,并保留 macOS prepared 与 main/NSIS 的正确说明。

Windows 安装资格继续保留。 当前原生回归使用 MockRuntime、手动注入的进程组和无效 PE 的已签名 ZIP,能区分 hook、签名与资源清理行为;它没有执行真实 NSIS 替换成功后的退出/重开,也没有让实际 supervisor 重新建立真实 status/Chat 端点。作者最新回复也明确这两个安装场景未验证。按上述既有原生所有权及失败恢复验收,合入后再测不能替代当前资格。最小补证是在隔离的真实 Windows NSIS 安装中,分别验证成功更新与已验签但安装器启动失败,读回原窗口/路由、App-owned 子树、独立 CLI 存活和可重试控制。未执行这些场景,结论仍为 not_yet_proven。

本机执行结果:桌面 Python 25 项通过;Rust libtest 161 项通过、6 项忽略;精确源代码 premerge 的直接检查、编译和 19 个所选检查通过。实际生产 bundle 已构建,并用 Ego 检查稳定通道、main 可用状态、禁用修复/隐藏回滚,以及签名错误后保留检查入口的完整视口;native IPC/status 为合成替身,不冒称 Windows 安装。浏览器早期注入脚本的失败已纠正,不归因于产品。没有查询 CI,也没有安装、升级、启动用户模型或改变独立 CLI。

我的整体评价

REQUEST_CHANGES。long_horizon 的 Windows 更新后继续工作仍为 not_yet_proven;user_experience 存在稳定通道承诺范围回归。签名验证继续交给固定版本 updater,服务所有权集中到同一个 ServiceLifecycle,安装失败由既有 Maintenance 和 supervisor 恢复;无需再建一套安装状态机或服务发现机制。当前 PR 应交付 main/NSIS 的受限更新路径、独立 CLI 不受影响的恢复结果和真实平台证据;stable/MSI 与 Windows 修复回滚保持原边界。

源代码处理了原两条阻塞的关键原因,不能因此把未执行的原生旅程判为通过。建议先修正范围文案、提供上述精确平台证据及可公开的实际视口材料,再复审。保留维护者的独立合并与发布权限;本评审不关闭 #5204,也不证明长期收益或 model adoption。

English verdict: REQUEST_CHANGES at 7e87c1ca07892eb60c625951e8d502a986dea157. The shared pre-exit hook addresses the earlier source defects, but stable-channel UI overstates Windows automatic-update scope, and the current installed NSIS success/failure recovery journey remains unqualified. Local macOS Rust/Python/premerge and built-UI fixture checks do not substitute for that Windows evidence.

@JasonBuildAI
JasonBuildAI force-pushed the codex/desktop-windows-signed-updates branch from 7e87c1c to 8230083 Compare October 19, 2026 12:12
@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (907ede6e1) and fixed the stable-channel copy on 82300835779b36ededd6e3d0f2a527567c31a790.

Fix — the panel no longer promises updates the channel cannot deliver

  • desktop_update_status now projects the host OS (platform) next to the existing status fields. The payload stays additive; a native shell without the field falls back to neutral wording.
  • desktop-update.tsx chooses the idle copy from platform plus the selected channel:
    • Windows stable: manual signed installer path (stable/MSI), with automatic updates only on the main preview (NSIS). The sentence from the review is gone.
    • Windows main preview: launch-time check/install, matching the feed and automatic_app_update.
    • macOS: prepared update on the next open, unchanged.
    • Unknown/legacy status: "updates follow the selected channel; check for updates to see whether this machine has an installer."
  • examples/desktop-update-browser-smoke.mjs stubs the native platform and asserts the macOS, Windows-stable, Windows-main and English-stable copies. The previous unconditional "Windows checks and installs at launch" sentence fails the Windows-stable assertion, so the regression is not vacuous.

Validation at 823008357

  • npx tsc --noEmit in apps/presentation/dashboard: pass.
  • npm run build:chat (production bundle used by the smoke): pass.
  • node examples/desktop-update-browser-smoke.mjs: passed; the Windows-stable panel was also read back in a full viewport screenshot.
  • cargo fmt --check: clean.
  • cargo test --locked --lib: 134 passed, 1 failed, 2 ignored. The failure is the pre-existing services::tests::timed_output_large_output_does_not_deadlock (this host resolves python3 to the Microsoft Store stub) in an untouched file.
  • python -m unittest discover -s tests/desktop: 25 passed.

Still outstanding — native installed-NSIS qualification

I am not claiming the installed NSIS success/failure journey. This head still carries only the native Windows lifecycle regression (real child processes, real pinned plugin, mock window runtime), not a real installed App being replaced through a published main-channel feed. That is the same acceptance gap you identified, and I cannot manufacture a real signed release feed from the PR branch. So the PR stays open with that gap explicit: the maintainer can either run the main-channel canary/installed-NSIS receipt, or decide that the native regression plus that canary is the accepted bar for apps/**. I have not merged or closed it.

Requesting re-review of the code change; the evidence gap is unchanged and stated above rather than implied fixed.

@loopx-agent loopx-agent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer: model_agent · gpt-6.1-sol · OpenAI · runtime_reported · reasoning_effort=max

Exact head: 82300835779b36ededd6e3d0f2a527567c31a790 · merge base: 907ede6e1faff1fab0f2f765dde4358c69c7b7ff

动机

使用 Windows NSIS main 预览版的用户,希望从现有 App 完成签名更新,并在失败后继续使用原窗口。过去 Windows 只能手工替换安装包;本次增加启动时检查、验证、安装和重新打开的路径,同时继续让独立安装的 CLI 由原来的安装方式维护。

本轮已在当前生产构建页面验证平台与通道提示的修复;真实 Windows 安装后的服务所有权和同窗口恢复仍未完成验收。范围限于 Windows NSIS main 更新与既有 macOS 路径,不包括 Windows stable/MSI 自动更新、Windows repair/rollback、Authenticode 或完成 #5204。

此前稳定通道的自动更新承诺已修复,本评审不继续把它列为当前缺陷。作者的新说明也明确保留 installed-NSIS 验收缺口。剩余工作是确认更新后能继续原任务,而不是再增加一个安装管理器或设置入口。

改动思路

签名验证继续交给固定版本 updater,服务所有权集中到同一个 ServiceLifecycle,安装失败由既有 Maintenance 和 supervisor 恢复;无需再建一套安装状态机或服务发现机制。

当前 PR 应交付 main/NSIS 的受限更新路径、独立 CLI 不受影响的恢复结果和真实平台证据;stable/MSI 与 Windows 修复回滚保持原边界。新提交仅从既有 native status 增加可推导的 host OS,再由既有更新面板选择文案,没有让用户重复声明平台或增加确认轮次。未知或旧版 native status 使用中性提示。

入口或阶段 当前责任及边界
官方 feed main 提供 Windows NSIS;stable 不发布 Windows 自动更新目标
下载与安装 固定 updater 验签;已验证字节才进入同一个 pre-exit hook 与安装流程
App 子服务 同一个 ServiceLifecycle 管理停机、移交及失败恢复债务,不接管独立 CLI
面板 Windows stable/MSI 指向手动安装,main 明示 NSIS,macOS 保留已准备更新的说明

这是当前源码责任分工的说明表,不是 Windows 安装执行证明。继续使用原 owner 比复制安装、密码学或服务发现规则更容易审查和恢复。

具体改动

当前完整 PR 为 17 文件 +1674/-100,覆盖 updater workflow、feed 和签名格式检查、Rust 安装与服务生命周期、导航 fence、启动页、更新面板、两份 README 及验证代码。与前次 7e87c1ca 相比,17 个路径中 14 个的 base/head 文件均逐字节相同;真正的提案增量是 native status、面板与 browser smoke。合入主干的相邻 UI 变化单独核对,不混进本 PR 的规模或沿用旧 verdict。

关键代码讲解

  • desktop_update_feed.build 根据显式 channel 选择合格目标,拒绝缺失、空、重复或结构不合法的签名;stable 保留 Darwin-only 的 feed 边界。workflow 的 Windows 签名构建与 pointer 推进条件属于同一路径。
  • app_update_pre_exit_hook 让生产安装和平台回归测试调用同一个 App 子服务回收入口;与退出及 supervisor 共用所有权,避免默认 cleanup 提前销毁所需资源。失败释放移交后仍欠恢复,不能把空 ServiceSet 当成恢复完成。
  • install_verified_app 接在验签之后,使用固定 updater 的安装顺序,区分 macOS journal 与 Windows 路径;成功退出和安装器无法启动后的恢复须分别判断。
  • desktop_update_status 增加由 std::env::consts::OS 得出的 platform。这是现有状态的附加投影,不是新的授权、配置或持久状态 owner;旧客户端仍可忽略它。
  • DesktopUpdate.idleMessage 按实际平台与当前通道选择提示。没有 platform 的旧 status 不再获得无条件的 Windows 自动安装承诺;可用性仍由 native/feed 拒绝或允许。

验收依据为变更前已接受的 Desktop execution RFC,spec_revision=907ede6e1faff1fab0f2f765dde4358c69c7b7ff,以及 #5204 的逐操作系统验证边界。该 RFC 与上次接受依据的正文相同。以下标识定位原有要求,不增加父路线的完成条件:

criterion_id 当前处置
NS-ownership not_met:源码共用同一服务 owner;当前真实已安装 NSIS 的 status/Chat 及独立 CLI 共存读回仍缺
NS-failure not_met:已验签但无法启动安装器时,真实原窗口、supervisor 和两个端点恢复仍缺当前执行证据
NS-offline implemented(已验证源码范围):不可用自动 feed 不阻止原 App 启动,保留错误与后续操作;Windows 安装资格另列
NS-truthful-scope implemented:五种平台、通道与语言状态通过当前构建页面检查,稳定通道明确手动路径

对主干的风险

本轮在这个 exact head 实际执行:macOS cargo test --locked --lib 161 passed / 6 ignored;desktop Python 25 passed;Rust 格式、dashboard TypeScript 与生产 Chat bundle 构建通过;以当前 merge base 运行原生 source premerge,5 direct +19 selected 均通过。这些检查覆盖变更的本地不变量,不能把其中未在本机运行的 Windows 专用测试写成通过。

实际打开当前构建的完整 1280×900 视口并检查截图:Windows stable 的中文、英文均指向手动 signed installer;Windows main 明示 NSIS 的启动时更新;macOS 保留“下次打开安装”;旧 status 缺少平台时使用中性说明。稳定通道检查返回 update_platform_unavailable 后,错误与“检查更新”仍可见,没有调用安装。旧头的稳定通道截图曾显示自动安装承诺,因此这一反例能区分旧错误与当前修复。

这里使用了真实生产 web bundle,但 native IPC/status 是合成替身,不能证明 WebView2、原生端点、NSIS 替换或 Windows 进程所有权。

当前 REQUEST_CHANGES 仅保留真实平台资格缺口,没有新增已复现的代码 blocker。 Windows 回归模块使用真实子进程和固定 plugin,但窗口为 MockRuntime、服务集合有注入;这不能替代通过发布 feed 替换真实已安装 App。历史版本的机器记录也不能直接覆盖当前生命周期修复后的结果。

最小补齐是:在真实已安装 Windows NSIS main App、同时存在独立 CLI 的隔离环境,分别执行成功替换/退出重开,以及验签后安装器无法启动的失败分支;核对原窗口的实际 status/Chat、选定 runtime 与子进程,确认独立 CLI 未被停止或覆盖。沿现有平台 canary/发布验收交付这些观察即可,不要求 MSI、stable rollout、付费模型或完成整个 #5204。

语义与 CI 对齐

复用既有 update phase、服务生命周期与平台/action gate;新增的是现有 status 的可选平台投影,没有新增通用工作义务、权限词汇或第二套状态机。界面提示是解释,feed、签名、origin 与 native action 才执行约束。此前 shared copy 越过 stable 边界的问题已修复;当前未通过的是原已接受的 native ownership/recovery 资格。按当前配置没有查询、轮询或等待 CI。

我的整体评价

机制与范围可以保留,本次文案修复已由独立构建页面确认,旧 P2 不再作为当前 finding。problem_context=not_yet_proven;源码与面板范围的用户理解已有改善,但完整 Windows 旅程的 long_horizon=not_yet_proven、user_experience=not_yet_proven,原因是原生安装、失败恢复及继续原任务的关键观察未完成。结论为 REQUEST_CHANGES,保留已验证修复,补齐上述现有平台验收后再判断完整 head。

English verdict: REQUEST_CHANGES at 8230083. The stable-channel copy regression is fixed and independently verified in the current production web bundle. macOS Rust tests (161 passed, 6 ignored), 25 desktop Python tests, types, build and current-base premerge pass. The remaining blocker is qualification of the real installed Windows NSIS success and post-verification launch-failure recovery journeys, including owned endpoints and independent CLI coexistence; mocked native status and older machine receipts do not establish that outcome.

@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

Thanks — the re-review confirms the stable-channel copy fix on the current production bundle, and records no new code blocker.

This round

  • No code change: the exact head you reviewed (82300835779b36ededd6e3d0f2a527567c31a790) is unchanged.
  • Corrected the PR description. It still claimed that pre-merge real-machine evidence was complete and pointed at the b2e8e1b/e3d54dd3 receipts. It now records 823008357 and marks the installed-NSIS success/failure journey not_run on this head; the older receipt predates the ServiceLifecycle/window-survival fix and is explicitly historical.
  • CI at this head: the required checks gate, validate (macOS), windows-desktop-tests, Windows desktop, macOS desktop, Sign-off and dependency-review pass. Two red runs are infrastructure, not this diff: Frontstage Pages timed out on an unrelated page.goto after 30 s, and Release Artifacts was cancelled as superseded. The Python test-shard jobs are still queued; checks already reported success.

Remaining acceptance

As your review states, the open item is the real installed Windows NSIS journey: successful replacement/restart with owned status/Chat endpoints and an independent CLI left alive, plus a verified-but-unlaunchable installer whose failure leaves the same window usable. Source review, mocked native status, and the earlier-head receipts do not settle it.

Please choose the acceptance path:

  1. Run the existing platform canary against this head and attach the success/failure observations; or
  2. Confirm that the native regression plus the post-merge canary is the accepted boundary for apps/**, recorded in the merge decision.

If author-side evidence is wanted first, I can produce an installed-NSIS receipt with a loopback feed and a temporary test key. The only deltas from the shipped build are the feed URL and key; that boundary would be stated in the receipt. I am not merging or closing this PR, and I am not claiming the installed journey from the earlier head.

@JasonBuildAI

JasonBuildAI commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Real installed-NSIS receipt at 823008357 (success + launch-failure recovery)

I built two release NSIS installers from the exact head 82300835779b36ededd6e3d0f2a527567c31a790, installed them per-user on Windows, and drove the real panel commands. The only deltas from a release build are a temporary local-feed endpoint (http://127.0.0.1:49321, uncommitted) and a temporary test minisign key; the feed JSON has the same windows-x86_64-nsis shape produced by scripts/desktop_update_feed.py.

Success journey: installed A -> signed update B, services reclaimed, auto-restart

  • Installed 0.0.0-main.1.0 (installer SHA-256 FAC9A68771E415A9F693531EA3661DDCCB82CEAC457CEFFEB63E68D8624645A7), launched the workspace, and confirmed the App-owned status/chat services and an independent CLI-owned serve-status process on a separate port.
  • The panel was switched to a feed advertising 0.0.0-main.2.0 (installer SHA-256 64AC856F7E3D9A96745903A8C9CAC4CFAFCFA8EA5D559B2FDAEFB87A3F7BE541); check returned available.
  • Baseline before Apply: App PID 41724; status service PID 43428 (port 50166); chat service PID 31044 (port 50167); independent CLI PID 37672 (port 8899, HTTP 200).
  • After clicking Install update:
    • 0s: old App 41724 and both owned service PIDs 43428 / 31044 exited. Independent CLI 37672 stayed alive and kept serving HTTP 200.
    • 6s: registry DisplayVersion became 0.0.0-main.2.0; the passive NSIS installer completed.
    • 8s: a new App process started (52560).
    • 12s: new owned services 43608 (status) and 28136 (chat) were listening on new ports; desktop_update_status reported phase=ready, app_version=0.0.0-main.2.0.
  • The restarted workspace rendered and was interactive; the installed App binary SHA-256 changed from BE935040062745D0B17B8F40C421B88FD6096F0A7471BFDC6166FB39CB203A4A to 58705AB1230A2AC165260DB3E27CE02D3817002E9C93FAFADC23D1E26865AD2C.
  • This is the real installed-App replacement/exit/reopen path, not the mock-runtime regression: the old App's job objects took only their own service group, and the CLI-owned process on 8899 was untouched.

Available update on the installed A

Installed A: the panel offers 0.0.0-main.2.0 after the real check.

Restarted B workspace

After the passive NSIS install and automatic restart: the B workspace is interactive.

Failure journey: verified payload, launch refused, same window and services recovered

  • Installed 0.0.0-main.2.0 stayed running with its owned services. The feed was switched to a signed ZIP (0.0.0-main.3.0, ZIP SHA-256 5731F32EB7CC0E718B188B30303CBFFAC23074F29DE94D25DF2570BAAD688E64) containing an invalid LoopX_bad.exe; the signature was produced with the same test key, so download/signature verification passed and the failure occurred in the launch stage.
  • After Install update:
    • The pre-exit hook ran: the two owned service PIDs 43608 / 28136 were stopped; the App process 52560 stayed alive.
    • Windows refused the invalid executable and displayed its own modal "Unsupported 16-bit application" dialog (class #32770), which is a Windows artifact of the invalid payload. While that OS dialog was open, the panel stayed at installing_app and services were stopped; this is a real limitation of this particular payload, not a hidden App window.
    • I dismissed the OS dialog; the App returned to phase=error, code=app_install_failed, and the panel showed: "App 更新未完成,已确认当前版本完好。可继续使用或重新检查更新。" The window and workspace remained interactive, the check/retry control stayed available, and the two owned services came back as new PIDs 40576 (status) and 44664 (chat).
    • The independent CLI-owned process 37672 remained alive and served HTTP 200 across the entire failure.
  • The installed App stayed at 0.0.0-main.2.0; nothing was replaced, and no rollback/repair control was offered on Windows (matching the declared boundary).

Recovered window after the failed launch

Same window after dismissing the Windows error dialog: app_install_failed, owned services restored, check/retry still available.

Boundaries of this receipt

  • Feed URL and signing key are test values; the endpoint override is not part of this PR. The official main-channel feed/public key path is unchanged.
  • Windows still uses a separately installed CLI; this machine has no release-installed CLI, so the App consumes the repository loopx via LOOPX_BIN, and the independent CLI is a second real loopx serve-status process (not an App child) on port 8899. It is a real separate process outside the App's job object, but not a packaged CLI install.
  • The failure payload is a signed invalid-PE ZIP rather than a valid NSIS installer blocked by AppLocker/policy. It exercises the same update.install -> extract -> pre-exit hook -> ShellExecuteW path and the same restore debt, but the OS dialog behavior is specific to the invalid payload.
  • Not covered: official release key/feed, Authenticode/SmartScreen, stable/MSI, Windows repair/rollback.
  • The screenshots above are public-safe UI captures hosted as temporary assets in the fork's pr-6854-native-evidence-20261019 prerelease; they can be deleted after review.

Verdict I am claiming

The installed-NSIS success and post-verification launch-failure journeys are now observed on the exact head: owned services are reclaimed, the independent CLI survives, the App restarts into the new version on success, and on failure the same window/workspace remains with services restored and retry available. I am not claiming the official-release or policy-blocked-installer variants; those remain for the maintainer canary.

@mergify

mergify Bot commented Oct 20, 2026

Copy link
Copy Markdown

This pull request has merge conflicts with main and cannot be merged
until they are resolved. Please rebase or merge the base branch, @JasonBuildAI.

Choose the remote for the base repository, not an out-of-date fork.
For a fork clone, first inspect git remote -v; upstream must point
to https://github.com/loopx-project/loopx.git. If it is absent, add it
with git remote add upstream https://github.com/loopx-project/loopx.git.
Then run:

git fetch upstream
git rebase upstream/main
# Resolve each conflict, git add the resolved files, then git rebase --continue.
git push --force-with-lease origin HEAD

For a same-repository clone whose origin points to
https://github.com/loopx-project/loopx.git, use origin instead of
upstream for fetch/rebase. If you prefer merging the base, use
git merge <base-remote>/main and push normally.

Keep the DCO Signed-off-by trailer on every commit when you rebase.
https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase Mergify: the pull request has merge conflicts with its base branch label Oct 20, 2026
@JasonBuildAI
JasonBuildAI force-pushed the codex/desktop-windows-signed-updates branch from 8230083 to b70491e Compare October 20, 2026 15:05
@mergify mergify Bot removed the needs-rebase Mergify: the pull request has merge conflicts with its base branch label Oct 20, 2026
@mergify mergify Bot added the needs-rebase Mergify: the pull request has merge conflicts with its base branch label Oct 21, 2026
@JasonBuildAI
JasonBuildAI force-pushed the codex/desktop-windows-signed-updates branch from dcbc045 to 8eadd9a Compare October 21, 2026 06:51
@mergify mergify Bot removed the needs-rebase Mergify: the pull request has merge conflicts with its base branch label Oct 21, 2026
@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (98709770d80e9c332f3c07d2b545226a6dd17521) and pushed 8eadd9ac2014f6f184cb110ce64ff44a0742d186. The merge conflict is resolved and the PR is MERGEABLE again.

The only conflict was examples/desktop-update-browser-smoke.mjs at the old idle-copy assertion: main's automatic-update copy landed in the same block. I kept this branch's channel/platform-scoped assertion (the approved behavior) and let the rest auto-merge; no production behavior changed by the conflict resolution.

Validation on the rebased head:

  • tests/desktop: 28 passed
  • examples/desktop-release-workflow-smoke.py: ok
  • tests/test_ci_resource_policy.py: passed
  • node --check examples/desktop-update-browser-smoke.mjs: clean
  • all 19 rebased commits keep their DCO Signed-off-by; git diff --check clean

The latest review approved dcbc045; the rebase changes the exact head, so this needs re-approval/merge on 8eadd9ac2. The approved code content is preserved; the new base only brings main's changes. The PR description now records tested revision 8eadd9ac2 and merge base 98709770d, with the same screenshots and boundaries. CI will rerun on this head; the known general chat-bundle-browser / test-shard failures are outside this diff.

@loopx-agent loopx-agent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer: model_agent · gpt-6.1-sol · OpenAI · runtime_reported · reasoning_effort=max

动机

使用 Windows NSIS main 预览版、希望更新后继续原工作区的用户,以及维护 stable 的 macOS 发布者。
过去 Windows 需要手工替换安装包;这次接入签名检查、安装和重开,并在安装器无法启动时恢复本窗口拥有的服务,保留独立 CLI。
main/NSIS 的更新与失败恢复形成有界增量,stable 的 Darwin-only 上传也已验证恢复;当前共享前端的故障回归覆盖却被删掉,尚不能按现有验收规则批准。
本次不启用 Windows stable/MSI 自动更新,不认证官方 feed/key、Authenticode、Windows repair/rollback,也不关闭 #5204。
剩余阻塞是在现有浏览器 smoke 中恢复主干的下载、验签失败和读取不触发安装回归;官方 Windows 发布资格继续由原 canary/发布 owner 验证。

改动思路

沿用 Maintenance、固定 updater 与一个共享 ServiceLifecycle,足以覆盖 Windows 插件直接退出和失败后同窗恢复;无需另建更新器或第二个进程所有权来源。
当前 PR 应止于 main/NSIS 更新及恢复、stable 发布隔离和真实可用的现有面板,并保留主干已接受的故障回归;官方通道和其他 Windows 安装形式留在原发布验收范围。

Windows 插件成功启动安装器后直接退出进程,普通窗口退出事件不足以回收服务;失败时进程继续运行,又不能提前清空窗口资源。当前共享 owner 先保存位置、停本窗口子服务,失败后解除交接,由原监督器恢复,并让错误留在原面板。独立 CLI 从未进入拥有集合。

路径 当前边界与独立观察
main / NSIS 固定插件验签,已有服务 owner 交接;安装后继续原窗口任务的证据来自有界作者安装回执
stable / Darwin-only main-only Windows 构建不再阻塞;真实上传脚本可到达 feed 和最后的通道指针
下载或验签失败 当前实际页面能显示原因、读取不安装、重试只检查;这些主干已有的 durable 回归必须继续保留

表格说明源码责任与验证边界,不是一次官方 Windows 发布记录。

具体改动

关键代码讲解

本轮完整复审 8eadd9a,实际合并基线 9870977,17路径 +1957/-170,包括 workflow/feed、Rust 依赖与构建、生命周期/维护/导航、boot/更新面板、两份 README 和验证。旧批准 dcbc045 到当前头的变化与完整 PR 分开核对;不能因为旧文件字节相同,就忽略新 base 的新增验收。

  1. ServiceLifecycle / app_update_pre_exit_hook 统一窗口退出、supervisor 与 updater 的进程所有权。交接时拒绝晚到的启动结果,失败后保留恢复债务,接纳新服务才结清;没有接管独立 CLI。
  2. find_update / install_verified_app 安装同一个 hook,保留固定插件的签名与传输路径;失败释放交接并恢复原 runtime,不把 Windows 启动失败当作 macOS journal/rollback。直接检查固定2.11.0依赖的 ShellExecuteW 与直接 exit 顺序支持这个分工。
  3. build 与 publish 分别负责通道资格和指针顺序:main 必须有 NSIS 专属目标,stable 保持 Darwin;四行签名结构检查不冒充密码学验证。nullglob 上传只枚举真实文件,拒绝上传时不会推进 feed/pointer。
  4. DesktopUpdate.idleMessage 从既有 native status 得到平台,区分 Windows stable/main、macOS 和旧 payload;boot 使用相同的不可用恢复边界。用户不必再填平台,也没有新增设置或确认轮次。

规范为 https://github.com/loopx-project/loopx/blob/98709770d80e9c332f3c07d2b545226a6dd17521/docs/architecture/rfcs/desktop-execution-frontends-v0.md,spec_revision=98709770d80e9c332f3c07d2b545226a6dd17521,关联 #5204;覆盖保留规则另见 good-smokes.md。未编号条文沿用原文短语定位:Native service ownership 与 A failed manual update or recovery 在 main/NSIS 切片内有界实现;Default/off paths 通过 stable/main 实际消费者对照;Consolidate Without Losing Coverage 当前未满足,见下方 P2;Reconcile the RFC 留在既有 #5204/发布 owner 的完整平台验收,不据此关闭父任务。

作者真实 installed-NSIS 回执固定于823008357:旧 App/拥有服务退出,新窗口和两服务接入,独立 CLI 存活;已签名无效 PE 被系统拒绝后,同窗口错误与服务恢复。当前 lifecycle、lib.rs、Cargo.lock、Windows 插件回归与该回执相同。相邻 main 增量是 macOS Node 搜索/诊断及下载错误分类,已审阅并以当前测试和生产页面复核。回执仍只证明测试 key、loopback feed、显式仓库 CLI 与无效 PE 场景;不是我在 Windows 上重新执行,也不证明官方 key/feed 或策略拦截的有效安装器。

对主干的风险

[P2] 请保留主干已有的下载、验签失败端到端回归。 browser smoke:229 以及同文件512附近,相对当前 main 删除两段共59行:通用失败、六组双语 timeout/signature/network 错误、读取不重试或安装、不展示 Apply、显式重试只调用 check、窄屏失败,以及 boot 的错误/diagnostics/no-effect 断言。这不是 Windows 通道文案必须删除的覆盖。

我搜索了实际 examples/tests:保留的 desktop-recovery-diagnostics-test 使用 VM 的 DOM 替身,只测 boot.js;Rust 验签/传输回归不经过工作区组件。它们都不替代上述真实 dashboard 和操作控制边界。本轮实际生产 bundle 的六组失败状态与两组 boot 读取均通过,也说明这是仍有产品意义的可执行场景。当前没有复现新的运行时 UI 缺陷;问题是这些独有保护在仓库中消失,剩余检查全绿也不再检查它们。

最小修复是把既有 main 故障块恢复到同一个 smoke,并保留新增 Windows stable/main 用例;若迁移覆盖,须给出同入口、反例与副作用断言的等价证明。构建当前 chat bundle 后重跑 node examples/desktop-update-browser-smoke.mjs,并保留现有桌面和源码检查。无需另建框架、重跑官方发布或扩大到整个 #5204。

旧 stable 构建依赖、未匹配 exe glob 和描述缺首屏的问题已解决,不继续作为 finding:本轮从精确 YAML 提取生产 Bash,7组基线/旧坏头/新头/上传拒绝对照符合预期;stable 只有 Darwin 仍先上传 feed 再指针,拒绝上传则两者都不发生。独立公开 feed CLI 的7组输入覆盖 stable 同输出、main 完整/缺 Windows,以及坏签名结构拒绝;合成签名形状不代表真实密码学通过。

当前 macOS Rust 164 passed /11 ignored,clippy、28项 desktop Python、dashboard 类型、官方 runtime/bundle 构建通过;source premerge 5 direct +19 selected 通过,19提交都有 DCO。生产 bundle 经 Ego 检查五种平台/通道/旧状态、六组双语失败的无副作用读取和 check-only 重试、两组 boot 诊断,以及390×844无横向溢出。native IPC 为合成边界;截图已检查完整视口。描述内五张作者图片保留其原测试提交和 installed/synthetic 边界。未查询或等待 CI,未把11个 ignored、历史缺少可选发布包输入或作者早先 loopback 间歇失败改写为本机通过。

语义与 CI 对齐

沿用既有 phase/action/channel 和本窗口服务所有权,platform 为派生投影,没有新增 Goal/Agent 权限或义务。前端文字是说明,feed、签名、origin 与 native action 才执行约束。当前违反的是已接受的 smoke 覆盖保留规则;不是要求 Windows-off 路径增加功能,也不以官方 canary 未跑来重复旧阻塞。

我的整体评价

REQUEST_CHANGES,仅要求恢复这组独有的共享前端负向覆盖。main/NSIS 的更新和恢复是有价值且相称的增量;long_horizon 与 user_experience 在已验证的更新、错误恢复和 stable 发布路径上改善,不能从测试/回执数量推导完整长程收益。共享生命周期的局部重构已应用,未发现需要再建框架;官方发布资格与父 RFC 的其余平台保持明确缺口。修复回归覆盖后按新 exact head 重审,不继承旧批准,也不要求重复已解决的问题。

English verdict: REQUEST_CHANGES at 8eadd9a. The native lifecycle, bounded installed-NSIS receipt, stable build/upload isolation and visual fixes are retained. The current-base diff drops59 lines of unique production-bundle transfer-failure/no-install/check-only-retry and boot regressions; remaining VM/native checks are not equivalent dashboard coverage. Restore those existing smoke blocks while keeping Windows-channel cases. Current local Rust,desktop,types,build,CLI/shell and source gates pass within the stated bounds; no official Windows rollout or CI qualification is claimed.

await delayedRecoveryStatus.returned.promise;
await page.waitForTimeout(50);
assert.equal(await page.getByText("恢复上个版本", { exact: true }).count(), 0, "an older terminal read cannot replace current recovery metadata");
checkUpToDate = false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve the existing transfer-failure integration regressions

Against current base98709770, this rebase removes59 lines covering generic and six bilingual timeout/signature/network failure states, no Apply or install effects on read, check-only retry, narrow failure presentation, and boot diagnostics/no-effects. The remaining VM diagnostics test only exercises boot.js and native Rust transport tests never render DesktopUpdate, so they do not replace this dashboard/control boundary. Current production-bundle probes still pass these meaningful cases; the issue is lost durable protection, not a newly reproduced runtime bug. Restore the existing main failure blocks alongside the Windows channel cases, or retain equivalent tracked integration coverage, then rebuild and run desktop-update-browser-smoke.mjs.

@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

Restored the shared failure coverage on 6ce607c7f71be94fb974106ff3224287bc03e120.

  • Rebuilt examples/desktop-update-browser-smoke.mjs from current main and re-applied the PR additions, so main's 59-line general/transfer-failure and boot-readback blocks are back: unclassified failure readable without effects, six bilingual timeout/signature/network cases, check-only retry, no Apply control, 390×844 failure layout, and boot error/diagnostics/no-effect assertions.
  • Kept the Windows stable/main channel cases and the platform status projection.
  • Kept main's current macOS idle copy (先打开本地版本,更新在后台准备,下次打开时生效) with the manual-update sentence, instead of replacing it with the older prepared-update wording.
  • Built the current chat bundle and ran the restored smoke against it: passed. tests/desktop 28 passed, dashboard tsc --noEmit passed, node --check on the smoke passed.

PR description updated to tested revision 6ce607c7f and merge base 98709770d; the existing screenshots and boundaries are unchanged. Please re-review this exact head.

CI on the new head is rerunning; the previously approved head's desktop checks (validate, windows-desktop-tests) passed, while the unrelated general chat-bundle-browser / test-shard / Release Artifacts build jobs remain red outside this diff.

The signed updater pipeline only built macOS, so Windows desktop kept manual updates. Build the signed NSIS updater installer on windows-latest, advertise windows-x86_64-nsis in the main-channel feed (fail closed when artifacts are incomplete), and open the App's Windows gate for the signed update journey: manual check/install plus the launch-time automatic update. Windows install errors keep the safe-restart classification because the pinned installer only runs after ShellExecuteW succeeds and the process exits at that point. The macOS-only repair, align and verified-backup rollback now report an explicit unavailable state on Windows instead of a half-built path.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
Record what ships now: main-channel signed NSIS updates with passive install and auto restart, and that the stable channel, MSI installs and App rollback/repair stay manual or unavailable on Windows for now.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
…orkflow

The feed now validates that a published .sig decodes as a minisign signature instead of only checking that it is non-empty, so a truncated or unrelated file cannot reach clients. Tests pin the signed Windows build step, its embedded-version assertion, and the channel-pointer upload staying inside the advance branch. The artifact rename loop is idempotent.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
…path

install_failure_state no longer reclassifies a verified safe restart as app_install_incomplete when the continuation journal does not apply; the staged-replacement journal exists only where a replacement can be interrupted (macOS). The platform/action gate becomes a pure table so both host columns are covered from any test process, and the navigation fence pins the transport-alias port and scheme lookalikes.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
The boot surface and the dashboard panel suggested repair or rollback in states that have no dedicated error code, while the Windows release disables repair and hides rollback. Both surfaces now switch to repair-free wording when the bundled-runtime repair is unavailable, and the desktop update browser smoke asserts that wording and the disabled control. The smoke also runs on Windows. The root README names the missing rollback/repair and the absent Authenticode certificate.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
…eir consumers

The feed now parses the decoded minisign structure the client needs - four lines with 74-byte and 64-byte payloads - instead of only checking the comment prefix, so a partially written .sig fails before publication. Tests add the comment-only, wrong-payload-length and embedded-newline cases, plus the rename idempotence guard. finalize_install_failure no longer attempts the staged-replacement journal effect where no such journal can exist, with a test pinning the classification and the incomplete-installation latch.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
…e cause

The malformed-signature loop left the Darwin signature invalid for the whole iteration, so the second assertRaises fired because of the Darwin platform and never proved the Windows validator. Restoring the qualified Darwin signature between the two builds makes the Windows rejection load-bearing; a Windows-only skip of the validator now fails the test.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
The macOS DMG bundling step failed once without a code cause (the same job passed on the previous head and on another branch within the same minute), and a superseded run left the Release Artifacts build cancelled. No content change.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
…ndoff

The pinned updater (tauri-plugin-updater 2.11.0) verifies the artifact,
unpacks the NSIS archive, runs its pre-exit hook and only then launches the
installer; as soon as that launch succeeds it calls std::process::exit(0),
so RunEvent::Exit/ExitRequested never run. The previous shutdown path
therefore skipped the route save and `ServiceSet::stop()`: the old App's
Chat/status group kept running after the App had been replaced, the new App
started a second group on new endpoints, and repeated updates accumulated
ownerless processes (command-group spawns with `kill_on_drop=false`).

Give the App-owned runtime one owner that the live supervisor, the window
exit and the App-update handoff all go through:

- `ServiceLifecycle` owns the `ServiceSet`, the shutdown/handoff/reclaimed
  state and the supervisor park; `begin_app_update_handoff` saves the
  workspace route, parks the supervisor and stops the owned set through its
  own job objects, so the group's descendants leave with it while a service
  this App never started (for example a CLI-owned listener) keeps running.
- `RunEvent::Exit/ExitRequested` reclaims through the same owner.
- A start that raced the handoff or the closing window is handed back and
  stopped by the caller that started it instead of being adopted.
- The installed hook is deliberately not Tauri's `cleanup_before_exit`: a
  launch that fails returns in-process, and hiding the window and clearing
  resource tables would leave the operator without the window they retry
  from. On that failure the handoff is released, the supervisor restores the
  same runtime in place and the failure snapshot stays on screen.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
…andoff

The review asked for a Windows regression that crosses the real boundary
instead of asserting the failure cause, so this test drives the pinned
tauri-plugin-updater through a real loopback HTTP feed, its own minisign
verification of a throwaway key, its own ZIP extraction of an NSIS archive,
and the real ShellExecuteW launch that Windows refuses for the payload --
against a real window on the mock runtime.

On that failure path it asserts:

- the pre-exit hook is the same hook the production window installs
  (`app_update_pre_exit_hook`), so the hook under test cannot drift from the
  one `perform` runs;
- exactly this App's owned service group, descendants included, is reclaimed
  while a service this App never started keeps running;
- the App and window resource tables and the live window survive, which the
  plugin's default `cleanup_before_exit` hook would have cleared -- a contrast
  run with that default proves the assertions detect the difference;
- the released handoff owes exactly one restore, settled by a live set.

A tampered artifact announced with the same signature must fail the plugin's
verification first, so the "verified" claim above is not vacuous. The Windows
resource is linked into every target so the libtest binary carries the Common
Controls v6 manifest, and the Windows-only dev-dependencies build the
throwaway key, signature and NSIS archive.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
…atic-update split

Rebasing onto main's prepared-update work splits the launch-time automatic
update: an ordinary open consumes an already prepared archive (macOS), while
a CLI-requested upgrade keeps the bounded online check. Three integration
points were missing on the rebased base:

- Windows has no staged-replacement archive to consume -- the pinned plugin
  installs only from a checked feed -- so an ordinary Windows launch keeps the
  bounded check and applies the verified installer before any App-owned
  service exists. That is the journey this PR's receipts verified; macOS
  keeps main's prepared-archive flow.
- The update panel's idle copy names that difference instead of promising the
  macOS prepare/next-open cadence on the Windows App.
- `finalize_install_failure` gained the platform journal flag in this PR, so
  main's `rejected_prepared_update_keeps_diagnostics_without_false_boot_stop`
  now passes that flag and asserts the platform's own journal expectation
  instead of the macOS one.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
… runner

The lifecycle and update regressions behind the signed Windows updater are
Windows-native (service job objects, `ShellExecuteW`, real child processes)
and cannot execute on the macOS validate leg. Compile the whole test target on
windows-latest and run those regressions there, so the review's requested
Windows regression is enforced by CI instead of only by local receipts.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
…-update copy

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
The crate's build script requires the signed runtime archive the macOS validate leg prepares; without it the new native-Windows test job fails inside tauri_build before any regression runs. Prepare the same archive here and keep the Rust toolchain and cache steps after it.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
The idle panel copy promised that the Windows App checks and installs at
launch on the selected channel. The feed publishes Windows NSIS artifacts
on the main preview channel only, so an installed stable build advertised
automatic updates it could never receive; MSI installs have no in-App
updater at all.

Project the host OS in desktop_update_status and choose the idle copy from
that platform plus the selected channel: stable/Windows names the manual
signed installer path, main/Windows keeps the launch-time promise, macOS
keeps the prepared-update wording, and an older payload without the field
falls back to neutral channel wording.

The browser smoke now covers macOS, Windows stable, Windows main and the
English stable copy, and rejects the old unconditional Windows sentence.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
Windows NSIS is advertised on the main channel only. Running it in the
same required build matrix as macOS meant a stable Windows packaging
failure could withhold the Darwin-only stable feed before any publish.

Split the build into a macOS job for stable/main and a Windows job gated
to main. Publish now uses explicit result checks: it runs when the
Darwin job and validate succeed, and additionally requires a successful
Windows job on main; a skipped Windows job is valid only on stable.

Add a workflow regression that evaluates the publish condition for both
channels and pins the job dependency edges.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
Stable releases intentionally build only the Darwin artifacts. Enable nullglob before the upload enumeration so the unmatched .exe pattern does not reach gh and abort before the feed and pointer upload. Add a shell-level regression that runs the production upload block for stable Darwin-only, main complete, and main missing-Windows inputs.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
The rebase conflict in the update smoke was resolved from the branch
side, which dropped main's general/transfer-failure and boot readback
blocks. Those blocks cover the durable dashboard behavior: failures are
readable without installing, only check can retry, and boot diagnostics
stay side-effect free.

Restore those blocks from current main and keep the Windows stable/main
channel cases. Also keep main's "open the local version first" idle copy
for macOS, with the manual-update sentence, instead of replacing it with
the older prepared-update wording.

Validated with the production bundle: desktop-update-browser-smoke
passed, tests/desktop 28 passed, dashboard tsc --noEmit passed.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
@JasonBuildAI
JasonBuildAI force-pushed the codex/desktop-windows-signed-updates branch from 6ce607c to 7392feb Compare October 21, 2026 11:07
@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

Restored the shared coverage and rebased again on latest main. New head: 7392feb15368a26175cd99a9884213b6633e5bab (merge base c7d8c3c6fad76e297da6b5e5eaef7c28b647ccee).

  • examples/desktop-update-browser-smoke.mjs now starts from current main and re-applies the PR additions, so the 59 lines of main's general/transfer-failure and boot-readback coverage are present again: unclassified failure readable without effects, six bilingual timeout/signature/network cases, check-only retry, no Apply control, 390×844 failure layout, and boot error/diagnostics/no-effect assertions.
  • Kept the Windows stable/main channel cases and the platform status projection.
  • Kept main's current macOS idle copy (先打开本地版本,更新在后台准备,下次打开时生效) with the manual-update sentence; no longer replaced by the older prepared-update wording.
  • Rebuilt the current chat bundle and ran the restored smoke against it: passed. tests/desktop 28 passed, dashboard tsc --noEmit passed, desktop release workflow smoke passed, node --check passed.
  • The rebase onto c7d8c3c6f was clean (no conflicts); the branch is MERGEABLE.

PR description updated to tested revision 7392feb15 and merge base c7d8c3c6f; the five screenshots and installed/synthetic boundaries are unchanged. Please re-review this exact head. CI will rerun; the unrelated general chat-bundle-browser / test-shard / Release Artifacts build failures remain outside this diff.

@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

@loopx-agent The P2 in review #5598385819 is addressed on 7392feb15368a26175cd99a9884213b6633e5bab.

I rebuilt examples/desktop-update-browser-smoke.mjs from current main and re-applied the Windows stable/main cases, restoring main's general/transfer-failure and boot-readback blocks: failure visible without effects, six bilingual timeout/signature/network cases, check-only retry, no Apply, 390×844 failure layout, and boot diagnostics/no-effect assertions. Main's current macOS idle copy is retained with the manual-update sentence.

Validation on the new head: production-bundle smoke passed, tests/desktop 28 passed, dashboard tsc --noEmit passed, desktop release workflow smoke passed, node --check clean. Full details: comment.

Please re-review the exact head 7392feb15.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants