fix(auth): read OS credentials under Node as well as Bun - #3655
Draft
cpacker wants to merge 1 commit into
Draft
Conversation
Letta stores Cloud credentials via Bun.secrets, but the published CLI and
listeners run under Node, where Bun.secrets does not exist. Node therefore
treated secure storage as unavailable even when the OS credential already
existed. Because migrateTokensToSecrets deletes the settings-file copy once
tokens move to the Keychain, a Node-run process had no token source at all:
Keychain unreachable and the file fallback already erased. That is the
repeated-logout symptom.
Select a SecretBackend at runtime instead of assuming Bun:
- Bun: keep using Bun.secrets.
- Node/macOS: delegate to an owning Bun runtime when present, else use
/usr/bin/security directly.
- Node/Windows: Credential Manager via PowerShell using Bun's exact generic
credential representation (target ${service}/${name}, username name, raw
UTF-8 bytes, enterprise persistence).
- Node/Linux: secret-tool using Bun's exact Secret Service attributes.
On macOS, Bun writes now opt into allowUnrestrictedAccess. Bun's default ACL
restricts an item to the writing executable, which is what makes headless
processes hang on a Keychain approval dialog. This migrates existing items to
a per-user ACL on next write, while Bun remains the runtime.
Secret values are never placed in process arguments, command output is
bounded, and subprocesses time out. Availability checks are structural rather
than reading a live credential, which itself triggered Keychain access.
This change deliberately does not alter which runtime executes the published
CLI; the polyglot shebang is untouched. Flipping that is a separate change
with its own migration concerns.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 3, 2026
Contributor
|
@just-cameron The replacement base is fully green after the macOS and Reflection reruns. When you have a chance, please review #3655 first; #3656 and #3657 are stacked on it. — Overlord (agent-c2adbf5c-8419-4211-8cd8-3740db164974) |
just-cameron
approved these changes
Aug 4, 2026
cpacker
marked this pull request as draft
August 5, 2026 06:15
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Splits #3423 into a reviewable stack. This PR is 1 of 3 and deliberately does not change which runtime executes the published CLI.
Problem
Letta stores Cloud credentials with
Bun.secrets, but the published CLI and listeners can run under Node, whereBun.secretsdoes not exist. Node therefore treated secure storage as unavailable even when the OS credential already existed.The failure is worse than a missing fallback.
migrateTokensToSecretsdeletes the settings-file copy once tokens move into the OS store (settings-manager.ts:524). So once a user has run under Bun even once, a Node-run process has no token source at all — Keychain unreachable and the file fallback already erased. That is the repeated-logout symptom, and it hits global installs and listeners.A second symptom:
Bun.secrets.set()defaults to a Keychain ACL restricted to the writing executable. Any other process reading that item triggers a GUI approval dialog, which on a headless dev box blocks indefinitely.Approach
src/utils/secrets.tsselects aSecretBackendat runtime instead of assuming Bun:Bun.secrets./usr/bin/securitydirectly.${service}/${name}, usernamename, raw UTF-8 bytes, enterprise persistence.secret-toolusing Bun's exact Secret Service attributes:service,account,xdg:schema=com.oven-sh.bun.Secret.On macOS, Bun writes now opt into
allowUnrestrictedAccess. This migrates existing restricted items to a per-user ACL on next write, while Bun is still the runtime — fixing the approval-prompt hang without needing the runtime flip.Secret values are never placed in process arguments; they go over stdin. Command output is bounded and subprocesses time out after ten seconds.
bun -eruns in a fresh temp cwd withBUN_CONFIG/BUN_OPTIONSstripped so a projectbunfig.tomlcannot inject code into the credential helper.Availability checks are now structural rather than reading a live credential, since the probe itself caused the Keychain access it was trying to detect.
What this PR deliberately excludes
The polyglot-shebang removal from
scripts/postinstall-patches.jsis not here. Flipping existing macOS users from Bun to Node while their Keychain items are still ACL-restricted is the one genuinely risky part of #3423, and it should land only after this PR's ACL migration has reached adoption.Security tradeoff
allowUnrestrictedAccessuses a per-user rather than per-executable ACL. Credentials stay encrypted by Keychain, but other processes running as the same user can invoke the same credential APIs. This is required for Node and Bun listeners to share credentials without GUI prompts, and is the same tradeoff Bun's own option exposes.Validation
bun run check— 12/12 passsrc/utils/secrets.test.ts— 14 passsrc/utils/secret-backends.test.ts— 17 pass (bidirectional interop betweenBun.secretsand each Node backend, pinning target/schema/persistence/encoding)Known follow-up
src/utils/secret-backends.tsis 1,000 lines — exactly at the ceiling. It holds four platform backends; splitting it per-platform is worth doing before anyone else needs to touch it.Stacked: this PR → #3656 → #3657
🤖 Generated with Claude Code