You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(server): regenerate an empty persisted secret instead of failing startup - #32053
When <state>/userdata/secrets/server-signing-key.bin is 0 bytes, the server exits with code 1 on every start: CryptoError: Mac.key: An HMAC key cannot be empty. The desktop app retries forever. ServerSecretStore.getOrCreateRandom returns any existing secret file without checking its length. Signing now goes through Mac.key, which rejects an empty key, so the empty file is fatal. The same happens for any other secret loaded through getOrCreateRandom (for example mcp-oauth-signing-key.bin).
Reproduced on main 6577c6b: start the server once, stop it, truncate server-signing-key.bin to 0 bytes, start it again.
Change
getOrCreateRandom treats an empty persisted secret as missing: it removes the file and creates a fresh random secret through the existing atomic create path (link, with the AlreadyExists re-read). Two callers in one process can both see the empty file (PayloadSigner.make loads its key lazily), so the removal runs under a store-level permit after a second read and only removes a file that is still empty. That section is uninterruptible, so an interrupted caller keeps the permit until its removal has settled. Non-empty secrets and missing files take the same path as before.
Existing sessions signed with the old key need a fresh sign-in after the repair.
Focused tests (vp test run src/auth/ServerSecretStore.test.ts src/auth/SessionStore.test.ts src/auth/McpOAuth.test.ts in apps/server): 3 files, 37 tests passed.
replaces an empty persisted secret with a fresh random one: fails on main with expected +0 to equal 32, passes here.
keeps the repaired secret when concurrent callers find it empty: two callers read the empty secret, and the second removal waits until the first caller has returned. Without the permit and re-read, the two callers return different keys; passes here.
vp run check:changed: format, lint, knip, graphql, typecheck (3 packages) and tests pass.
Runtime transcript, desktop mode, fresh base directory per side (synthetic state; driver below):
No UI changes, so there are no captures. Not checked: macOS and Windows file systems (the change uses the same readFile, remove and link calls as before).
AI-assisted; verified with the tests and tools listed above.
These tests reproduce #32026: getOrCreateRandom returns a 0-byte secret
file as the key instead of replacing it with a fresh random secret.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…artup
A 0-byte secret file under userdata/secrets was returned as-is by
getOrCreateRandom, so an empty server-signing-key.bin became an empty
HMAC key and the server exited on every start. getOrCreateRandom now
removes an empty persisted secret and runs the existing generate-and-create
path, so concurrent creators still converge through link's AlreadyExists.
Fixes#32026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… one key
Two callers that both read an empty secret could each remove and recreate
it, leaving the first caller holding a key that is no longer on disk.
PayloadSigner.make loads its key lazily, so concurrent first signatures hit
this in one process. Re-read under a store-level permit and remove only a
file that is still empty.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Interrupting a caller released the permit while Node's removal was still
in flight, so it could delete a key another caller had just repaired.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Macroscope's review found this PR approvable — Empty persisted secrets are regenerated instead of preventing startup, while valid secrets remain unchanged. Repair uses serialized rereading and the existing secure generation and persistence safeguards.
No code changes detected at 00c9bcc. Prior analysis still applies.
You can add or adjust custom eligibility rules. Learn more.
Reviewing files that changed from the base of the PR and between fa43bdc and 00c9bcc.
You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.
Use the checkbox below for a quick retry:
Trigger review
No actionable comments were generated in the recent review.
Walkthrough
getOrCreateRandom now rechecks empty persisted secrets under a semaphore and removes them if they remain empty. Tests cover replacement with a 32-byte secret and concurrent callers.
Changes: Empty Secret Recovery
Layer / File(s)
Summary
Semaphore-protected repair and tests apps/server/src/auth/ServerSecretStore.ts, apps/server/src/auth/ServerSecretStore.test.ts
getOrCreateRandom re-reads an empty secret under a one-permit semaphore and removes it if it is still empty. Tests verify a 32-byte replacement and matching results from concurrent callers.
The empty-secret repair is mergeable after normal checks; no concrete blocking issue remains.
Pre-merge checks: 4
Check name
Status
Explanation
Title check
The title clearly describes the empty-secret repair.
Description check
The description covers the problem, fix, linked issue, and verification. It does not identify the AI model and harness or confirm that the linked issue was triaged.
Linked Issues check
[#32026] getOrCreateRandom replaces an empty persisted secret through the existing atomic creation path. Tests cover replacement, persistence, and concurrent repair. The reported desktop test shows …
Out of Scope Changes check
[#32026] The secret-store change and its tests support empty-secret repair and safe concurrent calls. The summary shows no unrelated changes.
Full details: Linked Issues check
Explanation
[#32026] getOrCreateRandom replaces an empty persisted secret through the existing atomic creation path. Tests cover replacement, persistence, and concurrent repair. The reported desktop test shows the server starts with a regenerated key.
Finishing Touches
Generate docstrings
Commit to this branch
Create a new PR
Generate unit tests (beta)
Commit to this branch
Create a new PR
Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Reviewing files that changed from the base of the PR and between 6577c6b and fa43bdc.
Files selected for processing: 2
apps/server/src/auth/ServerSecretStore.test.ts
apps/server/src/auth/ServerSecretStore.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.
Ran separately built real servers on Linux x86_64 with Node v26.12.1, fresh disposable state, and T3CODE_DEV_SEED=0. Both recordings use the same 1600×1080 terminal presentation; credentials and local paths are omitted.
Same steps on both sides: start and stop to generate secrets; restart with the intact key and verify byte-for-byte preservation; stop; truncate userdata/secrets/server-signing-key.bin to zero bytes; restart with the same state and inspect startup output and file size. Successful starts also returned HTTP 200 from /.well-known/t3/environment.
AI-assisted; verified with the tests and tools listed above.
Not shown:
Electron's repeated startup-retry UI was not exercised; the underlying failure and recovery were demonstrated through the real built server.
Concurrent secret creation and other secret types were not exercised; this reproduction covers the persisted signing key.
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
bugfixFix PR opened by an issue lead; enters the PR looppr:needs-juliusNeeds Julius: route gate, unverifiable surface, or blockedsize:S10-29 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.
1 participant
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When
<state>/userdata/secrets/server-signing-key.binis 0 bytes, the server exits with code 1 on every start:CryptoError: Mac.key: An HMAC key cannot be empty. The desktop app retries forever.ServerSecretStore.getOrCreateRandomreturns any existing secret file without checking its length. Signing now goes throughMac.key, which rejects an empty key, so the empty file is fatal. The same happens for any other secret loaded throughgetOrCreateRandom(for examplemcp-oauth-signing-key.bin).Reproduced on main 6577c6b: start the server once, stop it, truncate
server-signing-key.binto 0 bytes, start it again.Change
getOrCreateRandomtreats an empty persisted secret as missing: it removes the file and creates a fresh random secret through the existing atomic create path (link, with the AlreadyExists re-read). Two callers in one process can both see the empty file (PayloadSigner.makeloads its key lazily), so the removal runs under a store-level permit after a second read and only removes a file that is still empty. That section is uninterruptible, so an interrupted caller keeps the permit until its removal has settled. Non-empty secrets and missing files take the same path as before.Existing sessions signed with the old key need a fresh sign-in after the repair.
Scope and approval
Fixes #32026
Verification
Focused tests (
vp test run src/auth/ServerSecretStore.test.ts src/auth/SessionStore.test.ts src/auth/McpOAuth.test.tsinapps/server): 3 files, 37 tests passed.replaces an empty persisted secret with a fresh random one: fails on main withexpected +0 to equal 32, passes here.keeps the repaired secret when concurrent callers find it empty: two callers read the empty secret, and the second removal waits until the first caller has returned. Without the permit and re-read, the two callers return different keys; passes here.vp run check:changed: format, lint, knip, graphql, typecheck (3 packages) and tests pass.Runtime transcript, desktop mode, fresh base directory per side (synthetic state; driver below):
Before, main 6577c6b:
After, fa43bdc:
Driver script
No UI changes, so there are no captures. Not checked: macOS and Windows file systems (the change uses the same
readFile,removeandlinkcalls as before).AI-assisted; verified with the tests and tools listed above.