Skip to content

fix(desktop): prepare updates before closing the workspace - #8675

Open
huangruiteng wants to merge 1 commit into
mainfrom
codex/desktop-update-startup-20261113
Open

huangruiteng wants to merge 1 commit into
mainfrom
codex/desktop-update-startup-20261113

Conversation

@huangruiteng

@huangruiteng huangruiteng commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

loopx upgrade closed the usable macOS App before downloading its replacement. Ordinary launch already used background preparation, but this explicit terminal path kept the workspace behind the startup splash for the entire archive transfer.

The CLI now asks an installed App that advertises preparation support to check, download and verify without a window or service ownership, reusing a matching verified cache. It quits/reopens only after successful preparation. A request-scoped archive and frozen target keep the switch offline; cache/signature/version mismatches fail rather than download at startup or certify success. Final success still requires the expected App and qualified service identities. Older Apps and protected installation directories retain the existing interactive native authorization path.

The Rust updater remains the channel/signature/install owner; Python is only its process adapter. This follows the preparation-before-restart principle in VS Code and Zed, without combining LoopX's separately owned CLI/App installations. Ordinary startup, Windows and update authority remain unchanged. The additive native arguments and packaging marker belong to the existing CLI/App adapter, not a new control-plane capability.

Validation

  • 37 focused Python tests passed; 2 opt-in AppKit cases skipped. Tests hold preparation pending and prove no quit on an incomplete download, rejected signature, malformed reply or timeout; legacy and protected-install paths retain parity.
  • 194 native tests passed serially; 12 environment-dependent cases skipped. Real loopback HTTP verifies signed preparation, cache reuse, offline target binding, corruption and mismatch refusal. An initial parallel native run hit a private-port race and poisoned two dependent test locks; the complete serial rerun passed.
  • Actual packaged macOS helper checked the current official signed channel in 2.28 seconds for a no-update case. A separately identified QA App downloaded and verified the 93,023,684-byte signed release in 109.74 seconds while its existing workspace and real isolated Core remained usable. QA startup to service readiness was 7.03 seconds. These are individual observations, not matched latency percentiles or a claim of faster network transfer.
  • Reopened the real release cache and rejected tampered bytes with the shipped key. Rust Clippy, formatting, Python Ruff, bilingual diff gate and public-boundary checks passed.
  • The locally built repair also passed installed App/CLI/service source readback through the normal prepared terminal-upgrade path; this is a local repair build, not a newly published signed release.
  • Packaged QA used synthetic empty Goal state and an immutable Core at 09886ba7afec07dba709229cc9dfb7fabf15d03a; no model Turn was invoked. Full signed replacement into a production installation and Windows execution are not established by this evidence. The existing installer/rollback tests retain that separate boundary.

Visual evidence

The workspace composition is unchanged; the changed journey leaves it usable during preparation. Native QA captures exercise the implementation committed as 185e1e34cc54 with a separately identified debug-packaged shell. Only the machine model-settings label is masked in the QA image; no Goal or conversation content is present. The desktop shell retains its existing minimum viewport, so this change adds no separate mobile layout.

Before: the explicit upgrade blocks the startup viewport during download.

Before: download blocks startup

After: the existing QA workspace stays available while the native helper prepares the signed release.

After: usable workspace during preparation

Roll back by reverting this commit; earlier Apps retain their original terminal-upgrade route. No settings or Goal migration is introduced. Merge remains a maintainer decision.

Signed-off-by: huangruiteng <huangrt01@163.com>

@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

评审提交 185e1e34cc54dc444adeab792a96fa856a2af556,对照 09886ba7afec07dba709229cc9dfb7fabf15d03a。

[P2] Main 准备失败会删除原窗口的 Stable 更新缓存。 Stable App 已有经签名验证的更新缓存。运行 loopx upgrade --ref main,新 Main feed 返回更高版本,但下载的包未通过签名校验。 原窗口尚未退出,准备 helper 已删除原来的 Stable 缓存。独立对照中同样的 Main 包验签失败,Stable 缓存可继续读回。后续离线普通启动失去原本可用的已准备更新。 让准备 helper 用只读方式尝试复用匹配的缓存。保留同一个签名、版本和渠道校验 owner,把退役留给原来的消费路径。 准备时读取共享缓存 渠道不匹配时退役缓存

动机 / Motivation

从终端升级可写 macOS App 的用户,可以在等待签名包期间继续使用现有桌面工作区。 主干先让 App 退出并重开,再由新进程联网。候选把检查和下载移到退出之前,并用已验证的目标版本交接。 本地正例验证了同渠道缓存复用和离线目标交接。Python 调用顺序验证了签名拒绝发生在退出命令之前。 本轮执行覆盖原生更新模块、真实 loopback HTTP 和隔离的 CLI 进程适配器。已安装 App 的窗口、正式发布包和 Windows 仍待独立验证。

改动思路 / Approach

Rust 继续持有更新与签名判断,Python 负责进程交接。后台缓存与本次请求缓存拥有不同的生命周期。 让准备 helper 用只读方式尝试复用匹配的缓存。保留同一个签名、版本和渠道校验 owner,把退役留给原来的消费路径。 独立的请求目录和版本钉住方式保留了原生事务。当前的复用调用把已存在的自动缓存退役提前到了原窗口仍在运行的阶段。签名被拒后,终端虽然正确地保留窗口,普通启动可用的缓存已经丢失。 原生准备路径

具体改动 / Changes

关键代码讲解

Info.plist 与打包配置公开准备能力。Python 校验可写目录,调用原生准备入口,成功才带请求 ID 与目标版本重开 App。原生 updater 继续校验渠道与签名,随后从请求目录验证冻结的目标,离线执行交接。最终回执仍需要 App 版本和服务身份匹配。已有 Python 与 Rust 用例覆盖不退出、缓存复用及请求不匹配,README 说明了旧包和受保护目录的路径。 CLI 适配与读回 原生请求校验

以 09886ba7afec07dba709229cc9dfb7fabf15d03a 的 apps/desktop/loopx-control-plane/README.md 为准。signed-cache-isolation 在跨渠道准备失败时有缺口。original-channel-readback 和 native-recovery-ownership 分别由版本读回和受保护安装路径覆盖。 桌面更新合同

对主干的风险 / Risks

用已验签 Stable 缓存和本地 Main feed 复现。两次 HTTP 请求后,篡改包被拒,未生成请求缓存,原 Stable 缓存已消失。只禁用这一处退役动作的隔离对照仍拒绝同一包,原 Stable 字节可读回。CLI 对照确认主干先退出并重开,候选在退出前返回准备失败。预置一个已验签的 Stable 缓存,通过本地 Main feed 下载篡改包。断言签名失败、没有退出或交接,并用 Stable 渠道再次成功读回同一份已验签字节。

语义与 CI 对齐

本地 Python 28 通过、2 个 AppKit 用例跳过,两个聚焦原生用例和 Rust 格式检查通过。Quick premerge 六项直接检查与四项选中检查通过。独立跨渠道回归失败。托管 CI 未查询。

我的整体评价 / Verdict

请求修改。请让准备阶段的缓存复用保持只读,并补跨渠道失败后原缓存可读回的回归。变动涉及窗口何时关闭,公开 diff 未改面板布局。源码与进程顺序已复验。 本轮执行覆盖原生更新模块、真实 loopback HTTP 和隔离的 CLI 进程适配器。已安装 App 的窗口、正式发布包和 Windows 仍待独立验证。

English verdict: REQUEST_CHANGES. At 185e1e3, the native preparation rejects a tampered Main package before quit but deletes the running Stable channel's previously verified cache.

let Some(update) = update else {
return Ok(current.into());
};
let cached = crate::prepared_update::load(automatic_root, current, channel, key)

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] Main 准备失败会删除原窗口的 Stable 更新缓存。 Stable App 已有经签名验证的更新缓存。运行 loopx upgrade --ref main,新 Main feed 返回更高版本,但下载的包未通过签名校验。 原窗口尚未退出,准备 helper 已删除原来的 Stable 缓存。独立对照中同样的 Main 包验签失败,Stable 缓存可继续读回。后续离线普通启动失去原本可用的已准备更新。 让准备 helper 用只读方式尝试复用匹配的缓存。保留同一个签名、版本和渠道校验 owner,把退役留给原来的消费路径。 预置一个已验签的 Stable 缓存,通过本地 Main feed 下载篡改包。断言签名失败、没有退出或交接,并用 Stable 渠道再次成功读回同一份已验签字节。

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.

2 participants