What happened
The Software update panel has been showing this under Check for updates on a 0.2.0-dev.9.20260831 install since 2026-09-01, with the button doing nothing visible when pressed:
root was signed by 0/3 keys
Two separate problems are visible in that one line.
1. The error state latches, so the panel shows one failure forever
app-update-service.ts:
.catch((error) => status.state === 'error' ? status : publishError('check', error))
Once the status is error, every later failure returns the existing status unchanged. A user who hits one transient failure sees that first message for the life of the process, and pressing the button appears to do nothing even when it ran and failed differently. The stale text is indistinguishable from a stuck check.
2. A provenance-verification failure is labelled as a check failure
The message comes from @tufjs/models inside getTrustedRoot, reached from app-update-attestation.ts when verifying a downloaded update's Sigstore bundle. It is rendered under the Check for updates row, so it reads as though update discovery is broken. publishError('check', error) is the only surface, and the phase that actually failed is not carried into it.
The message is also raw library text. root was signed by 0/3 keys names an internal threshold with no action a user can take.
What the state actually was
Worth recording, because the metadata was not the problem:
- Cached root: version 15, expires 2026-11-20, 5 signatures against the 5 root keyids, threshold 3.
15.root.json from tuf-repo-cdn.sigstore.dev is byte-identical to the cached copy; 16/17/18.root.json are 404, so 15 is current.
- The keys are
keytype: ecdsa, scheme: ecdsa-sha2-nistp256, which the shipped @tufjs/models@5.0.0 handles (utils/key.js:23), so this is not the older ecdsa-sha2-nistp256-keytype incompatibility.
- The shipped
@sigstore/tuf is 5.0.0, matching the workspace.
- Verification of that exact cache succeeds now:
getTrustedRoot({ cachePath }) against a copy of it returns a trusted root with 2 CAs and 2 tlogs, and a bootstrap into an empty cache also succeeds.
The cache directory held only root.json and targets/, with no timestamp.json, snapshot.json or targets.json — the shape of a seed that was written but never refreshed. So the original failure looks transient (network or a partial write on 09-01), and everything after it was the latch.
Removing ~/Library/Application Support/Maka/update-trust/sigstore clears it.
Suggested changes
- Let a later failure replace an earlier one, or keep the first and record the latest attempt's time and reason so the panel can say "last attempt failed at HH:MM" rather than repeating a stale sentence.
- Carry the failing phase into the status, so a provenance failure does not render under Check for updates.
- Map the verification failures users can act on to text they can act on. For this one: the update's signature could not be verified against the Sigstore trust root, and the trust cache can be cleared.
简体中文
现象
0.2.0-dev.9.20260831 安装版的「软件更新」面板从 2026-09-01 起一直在「检查更新」下显示 root was signed by 0/3 keys,点按钮看不出任何变化。这一行里其实藏着两个问题。
一、错误态会粘住,面板永远显示同一次失败。 app-update-service.ts 里 status.state === 'error' ? status : publishError('check', error):一旦进入错误态,后续失败原样返回旧状态。用户遇到一次偶发失败后,整个进程生命周期里都只看得到第一条消息,即使按钮确实跑过、而且是以别的原因失败的。停滞的文案和"卡住"无法区分。
二、产物验签失败被标成了检查失败。 这条消息来自 getTrustedRoot 内部的 @tufjs/models,由 app-update-attestation.ts 在校验已下载更新的 Sigstore bundle 时触发,却渲染在「检查更新」那一行下面,读起来像是更新发现坏了。publishError('check', error) 是唯一出口,真正失败的阶段没有被带进状态里。文案也是库的原始文本,root was signed by 0/3 keys 说的是一个内部阈值,用户无从下手。
当时的真实状态
值得记录,因为元数据本身没问题:缓存 root 是 v15、2026-11-20 过期、5 个签名对应 5 个 keyid、阈值 3;CDN 上的 15.root.json 与本地字节完全一致,16/17/18 都是 404;密钥是 keytype: ecdsa,随包的 @tufjs/models@5.0.0 认得(utils/key.js:23),所以不是旧的 keytype 兼容问题;随包 @sigstore/tuf 是 5.0.0,与工作区一致。用这份缓存的副本现在验,成功;清空缓存重新引导,也成功。
缓存目录里只有 root.json 和 targets/,没有 timestamp.json、snapshot.json、targets.json,正是"种子写了但从未刷新成功"的形态。所以最初那次失败像是偶发(网络或 09-01 的一次部分写入),之后一直显示的是被latch住的旧错误。删掉 ~/Library/Application Support/Maka/update-trust/sigstore 即可恢复。
建议
让后一次失败可以覆盖前一次,或者保留首次错误但记录最近一次尝试的时间与原因;把失败阶段带进状态,别让验签失败渲染在「检查更新」下;把用户可以处理的验证失败映射成他们能处理的文案。
What happened
The Software update panel has been showing this under Check for updates on a
0.2.0-dev.9.20260831install since 2026-09-01, with the button doing nothing visible when pressed:Two separate problems are visible in that one line.
1. The error state latches, so the panel shows one failure forever
app-update-service.ts:Once the status is
error, every later failure returns the existing status unchanged. A user who hits one transient failure sees that first message for the life of the process, and pressing the button appears to do nothing even when it ran and failed differently. The stale text is indistinguishable from a stuck check.2. A provenance-verification failure is labelled as a check failure
The message comes from
@tufjs/modelsinsidegetTrustedRoot, reached fromapp-update-attestation.tswhen verifying a downloaded update's Sigstore bundle. It is rendered under the Check for updates row, so it reads as though update discovery is broken.publishError('check', error)is the only surface, and the phase that actually failed is not carried into it.The message is also raw library text.
root was signed by 0/3 keysnames an internal threshold with no action a user can take.What the state actually was
Worth recording, because the metadata was not the problem:
15.root.jsonfromtuf-repo-cdn.sigstore.devis byte-identical to the cached copy;16/17/18.root.jsonare 404, so 15 is current.keytype: ecdsa,scheme: ecdsa-sha2-nistp256, which the shipped@tufjs/models@5.0.0handles (utils/key.js:23), so this is not the olderecdsa-sha2-nistp256-keytype incompatibility.@sigstore/tufis 5.0.0, matching the workspace.getTrustedRoot({ cachePath })against a copy of it returns a trusted root with 2 CAs and 2 tlogs, and a bootstrap into an empty cache also succeeds.The cache directory held only
root.jsonandtargets/, with notimestamp.json,snapshot.jsonortargets.json— the shape of a seed that was written but never refreshed. So the original failure looks transient (network or a partial write on 09-01), and everything after it was the latch.Removing
~/Library/Application Support/Maka/update-trust/sigstoreclears it.Suggested changes
简体中文
现象
0.2.0-dev.9.20260831安装版的「软件更新」面板从 2026-09-01 起一直在「检查更新」下显示root was signed by 0/3 keys,点按钮看不出任何变化。这一行里其实藏着两个问题。一、错误态会粘住,面板永远显示同一次失败。
app-update-service.ts里status.state === 'error' ? status : publishError('check', error):一旦进入错误态,后续失败原样返回旧状态。用户遇到一次偶发失败后,整个进程生命周期里都只看得到第一条消息,即使按钮确实跑过、而且是以别的原因失败的。停滞的文案和"卡住"无法区分。二、产物验签失败被标成了检查失败。 这条消息来自
getTrustedRoot内部的@tufjs/models,由app-update-attestation.ts在校验已下载更新的 Sigstore bundle 时触发,却渲染在「检查更新」那一行下面,读起来像是更新发现坏了。publishError('check', error)是唯一出口,真正失败的阶段没有被带进状态里。文案也是库的原始文本,root was signed by 0/3 keys说的是一个内部阈值,用户无从下手。当时的真实状态
值得记录,因为元数据本身没问题:缓存 root 是 v15、2026-11-20 过期、5 个签名对应 5 个 keyid、阈值 3;CDN 上的
15.root.json与本地字节完全一致,16/17/18 都是 404;密钥是keytype: ecdsa,随包的@tufjs/models@5.0.0认得(utils/key.js:23),所以不是旧的 keytype 兼容问题;随包@sigstore/tuf是 5.0.0,与工作区一致。用这份缓存的副本现在验,成功;清空缓存重新引导,也成功。缓存目录里只有
root.json和targets/,没有timestamp.json、snapshot.json、targets.json,正是"种子写了但从未刷新成功"的形态。所以最初那次失败像是偶发(网络或 09-01 的一次部分写入),之后一直显示的是被latch住的旧错误。删掉~/Library/Application Support/Maka/update-trust/sigstore即可恢复。建议
让后一次失败可以覆盖前一次,或者保留首次错误但记录最近一次尝试的时间与原因;把失败阶段带进状态,别让验签失败渲染在「检查更新」下;把用户可以处理的验证失败映射成他们能处理的文案。