Skip to content

fix(auth): read OS credentials under Node as well as Bun - #3655

Draft
cpacker wants to merge 1 commit into
mainfrom
fix/secret-backends-node
Draft

fix(auth): read OS credentials under Node as well as Bun#3655
cpacker wants to merge 1 commit into
mainfrom
fix/secret-backends-node

Conversation

@cpacker

@cpacker cpacker commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

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, where Bun.secrets does not exist. Node therefore treated secure storage as unavailable even when the OS credential already existed.

The failure is worse than a missing fallback. migrateTokensToSecrets deletes 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.ts selects a SecretBackend at runtime instead of assuming Bun:

  • Bun — keep using Bun.secrets.
  • Node / macOS — delegate read/write/delete to an owning Bun runtime when one is present, else use /usr/bin/security directly.
  • Node / Windows — Credential Manager through PowerShell using Bun's exact generic-credential representation: target ${service}/${name}, username name, raw UTF-8 bytes, enterprise persistence.
  • Node / Linuxsecret-tool using 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 -e runs in a fresh temp cwd with BUN_CONFIG/BUN_OPTIONS stripped so a project bunfig.toml cannot 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.js is 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

allowUnrestrictedAccess uses 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 pass
  • src/utils/secrets.test.ts — 14 pass
  • src/utils/secret-backends.test.ts — 17 pass (bidirectional interop between Bun.secrets and each Node backend, pinning target/schema/persistence/encoding)
  • Full unit suite — 5,935 pass, 0 fail

Known follow-up

src/utils/secret-backends.ts is 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

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>
@letta-integration

Copy link
Copy Markdown
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)

@cpacker
cpacker marked this pull request as draft August 5, 2026 06:15
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