Skip to content

fix(auth): run pelican under a PTY so fdp login can re-consent - #12

Merged
sammuli merged 1 commit into
mainfrom
fix/login-pty-stdout
Jul 1, 2026
Merged

fix(auth): run pelican under a PTY so fdp login can re-consent#12
sammuli merged 1 commit into
mainfrom
fix/login-pty-stdout

Conversation

@sammuli

@sammuli sammuli commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Problem

fdp login fails once the cached pelican OAuth refresh token expires:

WARNING Failed to parse token: "exp" not satisfied
WARNING Failed to renew an expired token: oauth2: "invalid_grant" "invalid refresh token"
Failed to get a token: This program must be run in a terminal to acquire a new token
Login failed: pelican token request failed (exit 1)

Root cause (confirmed by a controlled PTY experiment against pelican 7.25.0): pelican gates fresh interactive token acquisition on its stdout being a TTY. _pelican_get_token captured pelican's stdout with subprocess.PIPE (inheriting only stdin/stderr), so pelican always saw a non-terminal stdout. While the refresh token was valid pelican renewed silently (no terminal needed), which is why login worked; the moment the refresh token lapsed, pelican needed the device-code/browser flow and refused.

stdin stderr stdout result
PTY PTY pipe ❌ "must be run in a terminal"
PTY PTY PTY ✅ prints the device-approval URL

Fix

  • New _run_in_pty() runs pelican with stdin/stdout/stderr all on a pseudo-terminal. Pelican sees a real terminal; its approval URL/prompts are streamed live to our stderr; output is captured for token extraction; our stdin is forwarded when it is a TTY. Silent renewal still works unchanged.
  • _pelican_get_token uses it; _extract_token hardened to pull the JWT out of the interleaved warning/URL/token stream (bare JWT or per-line JSON).

Tests

  • Login tests moved to the new _run_in_pty seam.
  • New TestRunInPty: a child that refuses unless stdout.isatty() (reproduces the bug) + interleaved-stream extraction tests.
  • pytest tests/test_auth.py → 40 passed; auth/cli/env modules → 73 passed, 1 skipped.
  • Verified against real pelican: in a non-TTY-stdout context (the failing case) the patched code now reaches the device-approval URL instead of erroring.

🤖 Generated with Claude Code

pelican gates interactive token acquisition on its stdout being a TTY:
once the cached refresh token expires it must re-consent via the
device-code/browser flow, and refuses to start that flow when stdout is
a pipe. `_pelican_get_token` captured stdout with subprocess.PIPE, so
`fdp login` worked only while silent renewal was possible and failed
with "This program must be run in a terminal to acquire a new token"
the moment the refresh token lapsed.

Add `_run_in_pty()`, which runs pelican with stdin/stdout/stderr all on
a pseudo-terminal: pelican sees a real terminal, its approval URL and
prompts are streamed live to our stderr, the output is captured for
token extraction, and our stdin is forwarded when it is a TTY. Harden
`_extract_token` to pull the JWT out of the resulting interleaved
warning/URL/token stream (bare JWT or per-line JSON).

Tests: switch the login tests to the new `_run_in_pty` seam and add
`TestRunInPty` (a child that refuses unless stdout.isatty(), reproducing
the bug) plus interleaved-stream extraction tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sammuli
sammuli merged commit c21964f into main Jul 1, 2026
1 check passed
@sammuli
sammuli deleted the fix/login-pty-stdout branch July 1, 2026 01:17
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.

1 participant