Tell an unkeyed build why it cannot self-update (LC-607) #567

Merged
longjacksonle merged 1 commit from fix/lc607-updater-fails-before-network into main 2026-07-20 20:09:32 +02:00

Part of LC-607. Does not close it: the update channel still needs a decision on where releases are published for end users. This fixes the part that is wrong under every possible answer to that question.

The bug

fetch_manifest fetched the manifest and its detached signature before checking whether the build has an embedded public key. An unkeyed build refuses every manifest at the verify step anyway, so the fetch could only change which error the user saw, and it showed the wrong one.

Every desktop binary anyone could have installed is unkeyed. publish-release.yml is the only workflow that injects LETS_CHAT_UPDATE_PUBLIC_KEY, and it has never completed a run; the binaries currently sitting in the latest package came from main-push builds, which do not reference the variable. The compiled-in default source is the private psa-systems-private packages org, where an anonymous GET returns 401.

So the real-world output was:

$ lets-chat-desktop --check-update
check-update failed: fetch manifest from https://dev.a8n.run/api/packages/psa-systems-private/generic/lets-chat/latest/latest.json: ... 401

Three things wrong with that. It blames the network for something no network could fix. It prints an internal private-org URL to end users. And it makes an outbound request to a source that could never have satisfied it.

The change

Check PUBLIC_KEY_HEX before touching the network:

$ lets-chat-desktop --check-update
check-update failed: update signing is not configured in this build; refusing to self-update

Behaviour is unchanged for a keyed release build, which skips the early return and proceeds exactly as before.

Verification

unkeyed_build_reports_not_configured_without_fetching points LETS_CHAT_UPDATE_URL at a loopback address the net guard rejects outright, then asserts the not-configured message comes back. That is what makes it evidence about ordering rather than a restatement of the code: if the key check did not come first, the failure would be the guard's.

Confirmed not vacuous by deleting the early return and re-running:

assertion `left == right` failed: unkeyed build must fail on the missing key, not on the transport
  left: "fetch manifest from https://127.0.0.1:1/generic/x/latest/latest.json: initial target 127.0.0.1 resolves to a non-public address; refusing to connect"
 right: "update signing is not configured in this build; refusing to self-update"

just check clean, desktop tests pass.

What is still open on LC-607

The channel itself. The packages owner is a private org, so no client can fetch from it regardless of signing; there is no latest.json published; and the keypair was never provisioned. Steps 2 and 3 are mechanical once the publish target is chosen. Worth noting there is no urgency trap here: since every shipped binary is unkeyed and fails closed, no existing install is silently doing the wrong thing, and no already-distributed client gets stranded by whatever key is eventually generated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FhDVMViNHqUbwmfe2aXTv5

Part of LC-607. Does **not** close it: the update channel still needs a decision on where releases are published for end users. This fixes the part that is wrong under every possible answer to that question. ## The bug `fetch_manifest` fetched the manifest and its detached signature *before* checking whether the build has an embedded public key. An unkeyed build refuses every manifest at the verify step anyway, so the fetch could only change which error the user saw, and it showed the wrong one. Every desktop binary anyone could have installed is unkeyed. `publish-release.yml` is the only workflow that injects `LETS_CHAT_UPDATE_PUBLIC_KEY`, and it has never completed a run; the binaries currently sitting in the `latest` package came from main-push builds, which do not reference the variable. The compiled-in default source is the private `psa-systems-private` packages org, where an anonymous GET returns 401. So the real-world output was: ``` $ lets-chat-desktop --check-update check-update failed: fetch manifest from https://dev.a8n.run/api/packages/psa-systems-private/generic/lets-chat/latest/latest.json: ... 401 ``` Three things wrong with that. It blames the network for something no network could fix. It prints an internal private-org URL to end users. And it makes an outbound request to a source that could never have satisfied it. ## The change Check `PUBLIC_KEY_HEX` before touching the network: ``` $ lets-chat-desktop --check-update check-update failed: update signing is not configured in this build; refusing to self-update ``` Behaviour is unchanged for a keyed release build, which skips the early return and proceeds exactly as before. ## Verification `unkeyed_build_reports_not_configured_without_fetching` points `LETS_CHAT_UPDATE_URL` at a loopback address the net guard rejects outright, then asserts the not-configured message comes back. That is what makes it evidence about ordering rather than a restatement of the code: if the key check did not come first, the failure would be the guard's. Confirmed not vacuous by deleting the early return and re-running: ``` assertion `left == right` failed: unkeyed build must fail on the missing key, not on the transport left: "fetch manifest from https://127.0.0.1:1/generic/x/latest/latest.json: initial target 127.0.0.1 resolves to a non-public address; refusing to connect" right: "update signing is not configured in this build; refusing to self-update" ``` `just check` clean, desktop tests pass. ## What is still open on LC-607 The channel itself. The packages owner is a private org, so no client can fetch from it regardless of signing; there is no `latest.json` published; and the keypair was never provisioned. Steps 2 and 3 are mechanical once the publish target is chosen. Worth noting there is no urgency trap here: since every shipped binary is unkeyed and fails closed, no existing install is silently doing the wrong thing, and no already-distributed client gets stranded by whatever key is eventually generated. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FhDVMViNHqUbwmfe2aXTv5
fix(desktop): tell an unkeyed build why it cannot self-update (LC-607)
All checks were successful
check-secrets / Kingfisher (push) Successful in 7s
check-secrets / TruffleHog (push) Successful in 7s
check-secrets / Nosey parker (push) Successful in 5s
check-secrets / TruffleHog (pull_request) Successful in 12s
check-secrets / Nosey parker (pull_request) Successful in 12s
check-secrets / Kingfisher (pull_request) Successful in 14s
Check / clippy + fmt + tests (pull_request) Successful in 19m52s
Create release / Create release from merged PR (pull_request) Has been skipped
08b4f0e9ff
`fetch_manifest` fetched the manifest and its signature before checking whether the build has an embedded public key. An unkeyed build refuses every manifest at the verify step regardless, so the fetch could only change which error the user was shown, and it showed the wrong one.

Every desktop binary anyone could have installed is unkeyed: `publish-release.yml` is the only workflow that injects `LETS_CHAT_UPDATE_PUBLIC_KEY`, and it has never completed a run. The compiled-in default source is the private `psa-systems-private` packages org, where an anonymous GET is 401. So `--check-update` printed a bare 401 against an internal URL, which reads like a server outage or a permissions problem the user might resolve, when the actual state is that this build has no self-update at all and no URL would have helped.

Checking `PUBLIC_KEY_HEX` first reports the real reason, stops printing an internal org URL to end users, and makes no request to a source that could never have satisfied it. Behaviour is unchanged for a keyed release build.

This does not fix the update channel, which needs a decision on where releases are published for end users. LC-607 stays open for that.

Verified by deleting the early return: the test then fails with the net guard's "resolves to a non-public address" instead of the not-configured message, which is what makes it evidence that no request is attempted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhDVMViNHqUbwmfe2aXTv5
longjacksonle deleted branch fix/lc607-updater-fails-before-network 2026-07-20 20:09:32 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!567
No description provided.