Automated SciToken acquisition in the fdp CLI - #11
Merged
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onment and fdp env; pin generic config keys Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The backend/preset is a deployment-level choice (toksearch.llm resolves it from --backend / $FDP_LLM_BACKEND / ~/.fdp/config.toml / built-in default), not per-device. Remove the per-device --backend auto-injection from _build_llm_cmd; update the --backend help text and tests. The fdp_schema default_llm_preset field is left in place (now unused) to avoid an AttributeError coupling with older fdp releases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a ptdata_indexed locator declares index_pattern, _tokamak_env now emits PTDATA_JSON_INDEX_PATTERN so libfdpio can resolve the latest matching subdir at read time. Backward compat: unset pattern → key absent (proven by test_omits_pattern_when_unset). Parity fixture updated to drop the pinned timestamp suffix from index_dir and add the glob pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scaffold fdp/auth.py with decode_exp(), which extracts the 'exp' claim from a JWT without signature verification. Five unit tests cover the happy path, expired tokens, missing exp, malformed base64, and non-JWT inputs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AttributeError (e.g. int/None input calling .split) was not caught, contradicting the docstring guarantee. Add AttributeError to the except tuple and add a test to document the behaviour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds get_valid_token(handle, explicit=None) with four-level precedence: explicit arg (verbatim) > $BEARER_TOKEN (verbatim) > device cache (~/.fdp/cache/<name>.token, unexpired JWT only) > legacy (~/.fdp/token, unexpired JWT only). Devices without a bearer_token locator return None. Also adds os/time/Path imports and private helpers (_bearer_env, _is_unexpired, _cache_path, _read_token_file). 7 new tests in TestGetValidToken cover each branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An empty or None explicit is treated as 'no override' — document this in get_valid_token docstring and add a one-line docstring to _is_unexpired. In tests: hoist all mid-file and per-method imports to the top-level block, add addCleanup for BEARER_TOKEN in env-var test, and strengthen test_fresh_cache_used to assert the exact cached token is returned (not just non-None). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add AuthError, CachedToken, _extract_token, _pelican_get_token, _write_cache, login, and logout to fdp/auth.py. login() shells out to pelican credentials token get, writes the bare JWT to ~/.fdp/cache/<device>.token with mode 0o600 via atomic os.replace, and returns a CachedToken summary. logout() removes the cache file and returns whether one existed. _extract_token handles bare-JWT, JSON dict (access_token/token keys), and bare JSON string outputs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tests Remove stray `import subprocess` and redundant `import tempfile` from test_auth.py. Add TestExtractToken.test_access_token_beats_token to lock in access_token > token key priority. Strengthen write-scope test with positional argv[4] assertion. In _write_cache, add explicit os.chmod on the cache dir so 0700 holds regardless of umask or pre-existing dir. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ensure_token() that resolves a usable bearer token and optionally triggers the interactive login() flow when none exists. Auto-login is gated on: device declares bearer auth, FDP_NO_AUTO_LOGIN is unset, and the session is interactive (TTY auto-detect or caller-forced bool). Warns and returns None when it cannot acquire. Add _auto_login_allowed() helper and import sys/warnings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When FDP_NO_AUTO_LOGIN is set the user opted out deliberately; emitting a "run fdp login" warning is noise they did not ask for. The warning is still emitted in the non-interactive case (e.g. fdp env), where the caller may genuinely be unaware that no token is available. Tests tightened to assert: no UserWarning in the opt-out path, and assertWarns(UserWarning) in the non-interactive path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the inline token-resolution logic in resolve_bearer_token and setup_environment with delegation to fdp.auth.get_valid_token / ensure_token, so expiry-checking, cache lookup, and interactive login are all owned by the auth module. Adds auto_login kwarg to setup_environment (used by `fdp run`). Updates test_bearer_token_from_file to write a real unexpired JWT since the legacy path now requires a valid JWT. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ency - Rename _bearer_env → bearer_env (public API; used by environment.py). - get_valid_token now warns when the legacy ~/.fdp/token exists but is expired or not a valid JWT, so upgrading users get a migration hint. - setup_environment's no-token warning respects FDP_NO_AUTO_LOGIN (consistent with ensure_token's existing opt-out guard). - Remove dead _has_bearer_auth from environment.py (superseded by auth.bearer_env when resolve_bearer_token was delegated to auth). - Tests: wrap test_expired_legacy_skipped in assertWarns; add test_unusable_legacy_file_warns; add test_warns_when_bearer_device_has_no_token and test_no_warn_when_opted_out to TestSetupEnvironment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire auth.login/logout into new `fdp login [--write]` and `fdp logout` subcommands; thread auto_login=True into setup_environment for `fdp run`; fix do_env to emit the device-declared bearer env-var name instead of hardcoded BEARER_TOKEN. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap _resolve_device_handle() in do_login and do_logout with a (ValueError, KeyError) guard so unknown or missing devices produce a clean error message + sys.exit(1) instead of an uncaught traceback. Refactor all four TestCliLoginLogout tests to use the existing _patch_catalog(stack) helper, which patches entry_points AND registers a teardown callback that clears _cat._cache, eliminating the catalog state leak that the previous inline pattern left behind. Also strengthen test_login_dispatches_to_auth_login to assert that the real device handle (schema.name == "d3d") is passed to auth.login. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ests Use os.open() with O_CREAT|O_TRUNC|0o600 in _write_cache so the temp token file is never briefly world-readable under a permissive umask. Add two tests for custom bearer-env-var derivation and read-through (test_auth.py), plus an end-to-end auto_login test that runs setup_environment(auto_login=True) through to ensure_token (test_environment.py). 130 passed / 6 skipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Summary
Adds automated SciToken (bearer token) acquisition to the
fdpCLI, so users no longer hand-manage~/.fdp/token. A newfdp/auth.pymodule owns the credential lifecycle;fdpmints/refreshes tokens via thepelicanGitHub-OAuth flow (pelican credentials token get) and feeds them into the existingBEARER_TOKENpath.New behavior
fdp login [--write]— mint/refresh a token, cached at~/.fdp/cache/<device>.token(bare JWT,0600, atomic write).fdp logout— delete the cached token.fdp run …— auto-acquires a token when none is valid and the session is interactive (stdin+stderr TTYs);FDP_NO_AUTO_LOGIN=1disables it for batch jobs.fdp env— never launches the OAuth flow; now emits the device's declared bearer env-var name instead of a hardcodedBEARER_TOKEN.-t/--bearer-token→$BEARER_TOKEN→ managed cache → legacy~/.fdp/token(file sources expiry-checked; an unusable legacy file warns with a migration hint).Design notes
pelican(already a runtime dep) rather than reimplementing OAuth. Verified against a live client: the command emits a bare JWT on stdout, sends the device-code prompt/warnings to stderr, and requires a TTY — so the subprocess pipes stdout only and inherits stdin+stderr.Tokamak.pelican_root; the target env var from the locator'sauth.env. No catalog schema changes.Design + plan:
docs/superpowers/specs/2026-06-22-fdp-scitoken-automation-design.md,docs/superpowers/plans/2026-06-22-fdp-scitoken-automation.md.Test Plan
Automated
pixi run test→ 130 passed, 6 skipped (skips requiretoksearch_d3dinstalled). 65 dedicated tests acrosstest_auth.py/test_environment.py/test_cli.py; allpelican/OAuth calls mocked.build_and_testgreen.Manual
Requires a real terminal (pelican's OAuth flow needs a TTY) in an env that has both this branch's
fdpand a device contributor (toksearch_d3dprovides thed3dcatalog entry). The barefdpdev env has no device andfdp loginerrors with "No tokamak contributors are installed."Setup — none needed. The
repos/toksearch_d3dpixi env already hasfdpeditable-installed fromrepos/fdp, so it runs whatever branch that repo is checked out to:Run the steps below from
repos/toksearch_d3dwithpixi run …. To switch back afterward, justgit checkout maininrepos/fdp— nothing to uninstall. (That env is Python 3.10, so apip install -eoffdpis refused by the>=3.11metadata floor — but the existing editable install runs fine under 3.10; the floor is aboutml-metadatawheels, notfdpsyntax.)fdp loginmints a token.pixi run fdp login→ device-code prompt on stderr; after approval printsLogged in to d3d (read); token valid until <UTC>.Verify:stat -c '%a %n' ~/.fdp/cache/d3d.token→600, and the file starts witheyJ…(a JWT).unset BEARER_TOKEN; pixi run fdp env | grep BEARER_TOKEN→export BEARER_TOKEN='<cached JWT>'.fdp run(interactive). Force the no-valid-token state, then run:True. (Restore later:mv ~/.fdp/token.bak ~/.fdp/token.)Falsewith no OAuth prompt. Opt-out is silent; the non-TTY case warns to stderr.fdp envnever launches the flow. With sources invalid + interactive TTY:pixi run fdp env >/dev/null→ exports + warning, but no OAuth prompt.fdp logout.pixi run fdp logout→ "Removed cached token."; a second call → "No cached token to remove.";ls ~/.fdp/cache/d3d.token→ No such file.Cleanup:
mv ~/.fdp/token.bak ~/.fdp/token 2>/dev/null; thengit checkout maininrepos/fdpif you want the env back on the releasedfdp. Themvdance forces the no-valid-token state without deleting dated token files — just restore the symlink. Iffdp loginpicks a device other thand3d, adjust the cache path (pixi run fdp catalog listshows names).🤖 Generated with Claude Code