Skip to content

fix(login): keep a completed login when the keyring is missing, stop offering a browser there isn't - #2088

Open
jdx wants to merge 1 commit into
mainfrom
claude/device-code-flow-fallbacks-f56bbb
Open

fix(login): keep a completed login when the keyring is missing, stop offering a browser there isn't#2088
jdx wants to merge 1 commit into
mainfrom
claude/device-code-flow-fallbacks-f56bbb

Conversation

@jdx

@jdx jdx commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

https://entire.io/gh/entireio/cli/trails/1116

The transcript this fixes

dev% go run ./cmd/entire login
SSH session detected; using device-code flow (a browser opened here couldn't reach this machine).
Device code: 6ULF-BR4P
Login URL:   https://us.auth.entire.io/cli/auth?user_code=6ULF-BR4P

Press Enter to open in browser...

Warning: failed to open browser: start browser command "xdg-open": exec: "xdg-open": executable file not found in $PATH
Open this URL in your browser to approve this login: https://us.auth.entire.io/cli/auth?user_code=6ULF-BR4P
Waiting for approval... save login: store refresh token in credential store: exec: "dbus-launch": executable file not found in $PATH
...
exit status 1

Three separate problems, in the order they hurt.

1. A login the user already completed was thrown away

The credential write happens after approval, so on a box with no D-Bus the user entered the code, approved in a browser on another machine — and then got exit status 1 plus instructions to re-run the entire flow with ENTIRE_TOKEN_STORE=file.

The default backend is now keyring-then-file (internal/entireclient/tokenstore/fallback.go):

  • The keyring is still tried first, so a working keyring is always preferred and a machine that grows one later goes back to using it.
  • When it refuses the write, the tokens land in the already-documented 0600 file instead of being discarded, and the fallback is announced on stderr — once per process, never silent.
  • Reads follow the same order, so later commands find the tokens with no env var set.
  • Ctrl-C during a keyring call stays an abort: it propagates untouched and never answers a user's interrupt by writing a bearer token to disk.
  • A keyring failure with nothing in the file surfaces the keyring error rather than ErrNotFound, so a broken keyring never masquerades as "not logged in".
  • Delete clears both backends, so logout removes every copy a fallback write may have left.

On a keyring-less machine:

Warning: OS keyring (Secret Service (D-Bus)) is unavailable: exec: \"dbus-launch\": executable file not found in \$PATH
Credentials saved to ~/.config/entire/tokens.json instead (mode 0600). Set ENTIRE_TOKEN_STORE=file to skip the keyring, or ENTIRE_TOKEN_STORE=keyring to fail instead of writing a file.

The one product judgment call here, flagged for a human: auto-fallback-with-a-warning over failing. Discarding an approved login seemed clearly worse than storing it in the location the README already documents — but it does mean a machine that silently lost its keyring starts writing tokens to disk. ENTIRE_TOKEN_STORE=keyring is the new opt-out for "never put my bearer tokens on disk"; =file still skips the keyring entirely. Happy to make it a prompt instead if that's the wrong default.

2. It offered a browser that doesn't exist

[Enter] Open browser [c] Copy URL was advertised unconditionally, so on a headless SSH box the keystroke could only buy a nested exec: \"xdg-open\" error — right after the CLI told the user to press it.

The prompt now advertises only what the machine can honour: an opener on \$PATH plus a graphical session (or a WSL bridge) for the browser, and clipboard.Unsupported for the copy hint. With neither available the prompt line disappears and the key reader never starts, which also leaves Ctrl-C native instead of taking a TTY into raw mode to collect keystrokes that could only be refused.

This gates what is advertised, not a veto: pressing Enter is still honoured wherever the opener binary exists, so an unusual-but-working setup isn't locked out — and where it doesn't exist, the answer is a sentence rather than an exec error.

3. The error was glued to the status line

Waiting for approval... save login: store refresh token… — the non-interactive device path printed Waiting for approval… with no newline and never closed it, so whatever came next ran into it. The status line is now always closed.

Also

  • README's headless section: interactive login on a headless machine needs no env var now, and both overrides are documented.
  • tokenstore package doc describes the new arrangement.

Testing

  • New white-box tests for the fallback store: fallback write + read-back, once-per-process warning, healthy keyring never spilling to disk, keyring-miss-consults-file, failure-over-ErrNotFound, interrupt writes nothing, both-backends-failing (ErrFileFallbackFailed), delete across both backends, provenance in BackendDescription, and the env-var matrix.
  • New login tests: the prompt-line matrix, Enter-without-an-opener refusing to exec, no-usable-actions leaving the terminal alone, and the non-interactive waiting line being closed.
  • mise run fmt && mise run lint clean; unit, integration, and both canary suites pass. Verified the fallback end-to-end on a real keyring-less machine (warning, write, read-back, provenance, delete).

🤖 Generated with Claude Code

…offering a browser there isn't

A device-code login on a headless SSH box printed the code, offered
"[Enter] Open browser" (which could only produce `exec: "xdg-open":
executable file not found`), and then — after the user had already
approved in a browser elsewhere — exited 1 on `dbus-launch` not existing,
telling them to re-run the whole flow with ENTIRE_TOKEN_STORE=file.

Three fixes:

- The default credential backend is now keyring-then-file. The keyring is
  still tried first, so a working one is always preferred and a machine
  that grows one goes back to using it; when it refuses the write the
  tokens land in the documented 0600 file instead of being discarded, and
  the fallback is announced on stderr (once per process), never silent.
  Reads follow, so later commands need no env var. Ctrl-C during a keyring
  call stays an abort and never writes a token to disk.
  ENTIRE_TOKEN_STORE=keyring is the new opt-out for "never put my bearer
  tokens on disk".

- The URL prompt advertises only actions this machine can honour: an
  opener on $PATH plus a graphical session (or WSL bridge) for the
  browser, and clipboard.Unsupported for the copy hint. With neither, the
  prompt line disappears and the key reader never starts, leaving Ctrl-C
  native instead of taking a TTY into raw mode for keystrokes that could
  only be refused. Pressing Enter anyway is still honoured wherever an
  opener exists, and answered with a sentence where it isn't.

- The non-interactive device flow left "Waiting for approval… " open, so
  the save error was glued onto it. The status line is now always closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jdx
jdx requested a review from a team as a code owner August 20, 2026 20:11
Copilot AI lite review requested due to automatic review settings August 20, 2026 20:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves entire login robustness on headless/keyring-less machines by (1) introducing a keyring→file tokenstore fallback so completed logins aren’t discarded, and (2) making the device-code URL prompt only advertise actions the current machine can actually perform (browser open / clipboard copy), while ensuring the “Waiting for approval…” status line is always newline-terminated.

Changes:

  • Add a default keyring→file fallback credential store and document/extend ENTIRE_TOKEN_STORE behavior (file / keyring).
  • Gate interactive login URL prompt hints/actions on real browser/clipboard availability; avoid starting raw-mode key reads when no action is usable.
  • Update README guidance for headless auth and add targeted unit tests for fallback store and login UX.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
README.md Updates headless authentication docs to reflect fallback-to-file behavior and env overrides.
internal/entireclient/tokenstore/tokenstore.go Switches default backend to a fallback store; adds keyring override and updates backend provenance description.
internal/entireclient/tokenstore/fallback.go New fallback store implementation (keyring first, file fallback) with one-time warning behavior.
internal/entireclient/tokenstore/fallback_test.go New tests covering fallback store behavior, warning behavior, and env selection helpers.
cmd/entire/cli/login.go Closes the non-interactive waiting line; introduces URL prompt action gating (browser/clipboard/keys) and improved Enter handling.
cmd/entire/cli/login_tty_test.go Updates TTY-focused login tests for the new availableActions interactor interface.
cmd/entire/cli/login_test.go Adds tests for prompt-line matrix, “no usable actions” behavior, Enter-without-opener behavior, and waiting-line newline closure.
cmd/entire/cli/browser_open_windows.go Adds browserOpenerAvailable() for Windows (always true via ShellExecute).
cmd/entire/cli/browser_open_other.go Adds opener availability detection for non-Windows and improves opener error messages.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +144 to 148
// are rather than the keyring that refused them.
if path, ok := FellBackToFileStore(); ok {
return fmt.Sprintf("file %s (%s unavailable)", path, keyringProviderName())
}
return keyringProviderName()
Comment on lines +164 to +169
// The keyring already failed earlier in this process; the warning has
// been printed and the file store is where this belongs.
if err := s.file.Set(service, user, password); err != nil {
return err
}
return nil
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants