Skip to content

Add client-side API token auto-refresh (GL-1709)#444

Closed
timmarkhuff wants to merge 3 commits into
mainfrom
thuff/gl-1709-sdk-token-autorefresh
Closed

Add client-side API token auto-refresh (GL-1709)#444
timmarkhuff wants to merge 3 commits into
mainfrom
thuff/gl-1709-sdk-token-autorefresh

Conversation

@timmarkhuff

@timmarkhuff timmarkhuff commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Treats the API token supplied to Groundlight(...) (or GROUNDLIGHT_API_TOKEN) as a bootstrap credential rather than the credential used on every request. The SDK mints a short-lived working token from it, caches that token on disk, and rotates it on a background thread, so a long-lived bootstrap token is never sent on normal API calls. This limits the blast radius of a leaked token and lets the server enforce a bounded token TTL without breaking long-running integrations.

Design doc: GL-1709 plan (docs/gl-1709-api-token-security-plan.md in zuuul). Server-side endpoints land in zuuul PR #6579.

What's included

  • src/groundlight/token_manager.py (new) - the auto-refresh engine:
    • One slot file per bootstrap token at ~/.groundlight/tokens/<snippet>.json (mode 0600, dir 0700), overridable via GROUNDLIGHT_TOKEN_DIR.
    • Startup reuses a cached, unexpired token; otherwise mints under a cross-process file lock (filelock) with atomic slot writes (temp + os.replace).
    • Token naming derives from the current token's name plus a random 6-char hex suffix, strips any existing suffix so names don't grow, truncates to 64 chars, and falls back to sdk-auto xxxxxx.
    • Per-instance daemon thread (gl-token-refresh-<snippet>) rotates once per day with double-checked locking, and deletes the previous token after a 2x grace window so tokens don't accumulate.
    • 401 fallback: a rejected working token triggers a one-time re-mint from the bootstrap token and a retry.
  • Client integration (client.py): wires the manager into Groundlight.__init__, adds close() and context-manager support (with Groundlight() as gl:).
  • internalapi.py: GroundlightApiClient.call_api re-mints and retries once on UnauthorizedException.
  • config.py: named constants (TOKEN_TTL_DAYS=30, REFRESH_INTERVAL_DAYS=1, CLEANUP_GRACE_FACTOR=2) and env-var names.
  • Generated client: regenerated from spec/public-api.yaml to add the ApiTokensApi (create/list/delete/get-by-snippet) and related models.
  • Dependency: filelock promoted to a direct dependency (Unlicense).
  • Tests: test/unit/test_token_manager.py (21 tests, no network) covering naming, cold start, caching, expiry, rotation, cleanup, 401 re-mint, and TTL.

Decisions to confirm before merge

  • Opt-in for now. Auto-refresh is gated behind auto_refresh_token= / GROUNDLIGHT_AUTO_REFRESH_TOKEN (default off). The GL-1709 plan calls for always-on, but with it always-on Groundlight() mints on every startup, which 404s wherever the server endpoints aren't deployed yet (and breaks the SDK's own test suite). Flipping the default to on is a one-line change once the server side is live. Marked TODO(GL-1709).
  • Dropped platformdirs. Its current release requires Python 3.10 but the SDK supports 3.9. Used pathlib.Path.home() for the token dir instead (Linux is the priority per the plan).

Blockers gated by TODO(GL-1709)

  • Switch token-name lookup from paginated list-and-match to the dedicated GET /v1/api-tokens/by-snippet/{snippet} endpoint. The replacement method (_get_token_name_by_snippet) is already written but not called, since that endpoint isn't live yet.
  • Flip auto-refresh to default-on once the server endpoints (zuuul #6579) are deployed everywhere.

Test plan

  • pytest test/unit/test_token_manager.py (21 passing, no network)
  • ruff, pylint (10.00/10), mypy, toml-sort clean on changed files
  • Existing unit suite still initializes with the gate off
  • End-to-end against an environment with the token-management endpoints live (mint / daily rotation / cleanup / 401 re-mint)

Tim Huff and others added 3 commits July 9, 2026 11:29
Treat the supplied API token as a bootstrap credential: mint a short-lived
working token, cache it per-token on disk, and rotate it on a background
daemon thread so the bootstrap token is never used directly for API calls.
Includes a 401 re-mint fallback, context-manager support, and unit tests.

Gated opt-in for now via auto_refresh_token / GROUNDLIGHT_AUTO_REFRESH_TOKEN
until the server-side token-management endpoints are live everywhere.

Co-authored-by: Cursor <cursoragent@cursor.com>
The auto-refresh work added a close() lifecycle method to Groundlight, which
the CLI auto-registers as a command and looks up in _COMMAND_GROUPS, causing
KeyError: 'close'. Exclude it like other non-shell methods.

Co-authored-by: Cursor <cursoragent@cursor.com>
A due-but-failing rotation left the token overdue, so the next wait was 0s and
the refresh loop would spin and hammer the API during an outage. Wait a fixed
backoff after a failed cycle instead. Adds a test for the loop's failure path.

Co-authored-by: Cursor <cursoragent@cursor.com>
@timmarkhuff timmarkhuff closed this Jul 9, 2026
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