feat: automatically rotate SDK API tokens#445
Open
timmarkhuff wants to merge 29 commits into
Open
Conversation
Rotate short-lived working tokens through a locked disk cache while preserving bootstrap credentials for recovery. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the new close method from being auto-registered as a shell command so CLI initialization and tests continue to work. Co-authored-by: Cursor <cursoragent@cursor.com>
Back off after failed refreshes, preserve pending cleanup metadata, and make 401 recovery safe for streamed and raw HTTP requests. Co-authored-by: Cursor <cursoragent@cursor.com>
Fall back to the bootstrap token when a server does not yet expose token management, preserving compatibility during deployment. Co-authored-by: Cursor <cursoragent@cursor.com>
Replay generated stream bodies correctly, cover note requests, and make locking and shutdown safe for in-flight refresh work.
The bootstrap token has only one job: mint the first working token. Keeping it alive afterward is a security risk -- a leaked bootstrap can silently mint tokens indefinitely. Revoke it immediately after the first slot is written. Consequences: - 401 recovery no longer falls back to bootstrap to re-mint; it adopts a fresher cached token from another process or raises loudly requiring human intervention (provision a new bootstrap token). - refresh() raises on a missing slot rather than re-minting from bootstrap, since the bootstrap is gone. Updates plan doc and Google Doc accordingly. Co-authored-by: Cursor <cursoragent@cursor.com>
…ing rotation The bootstrap token's name (suffix-stripped) is now written into the slot cache as base_name once, at first mint. Subsequent rotations read it from the slot instead of doing a paginated list call to rediscover the current token's name. Key changes: - TokenSlot gains a base_name field; old slots with no field fall back to a live lookup via the new _resolve_base_name helper. - _initialize_token looks up the bootstrap token once; the result seeds base_name and is passed directly to _revoke_bootstrap, eliminating the second paginated scan that revocation previously required. - _mint_replacement now receives base_name as a parameter rather than doing its own lookup. - _new_token_name simplified: it no longer accepts None or strips suffixes (that responsibility moved to _resolve_base_name). Co-authored-by: Cursor <cursoragent@cursor.com>
The GET /v1/api-tokens/by-snippet/<snippet> endpoint (zuuul#6579) is now deployed. Replace the paginated list scan with a single direct call, removing _find_token_by_snippet and TOKEN_PAGE_SIZE entirely. - _initialize_token and _resolve_base_name both call _get_token_by_snippet; NotFoundException means "token not found" and falls back gracefully. - Revert TOKEN_TTL_DAYS to 30 and REFRESH_INTERVAL_DAYS to 1 (temporary short values were only for live rotation testing). - Update tests: list_api_tokens mocks replaced with get_api_token_by_snippet; _page helper and EXPECTED_PAGE_COUNT removed. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the configured token alive for delayed cleanup after the first working token is minted, and document the interim hardcoded testing TTL/refresh cadence until identity token_ttl discovery lands. Co-authored-by: Cursor <cursoragent@cursor.com>
Allow null last_used_at on API token responses so minting a never-used token does not fail OpenAPI deserialization during client init, and annotate next_previous for mypy. Co-authored-by: Cursor <cursoragent@cursor.com>
Short testing refresh intervals raced with urllib3 mocks and inflated call counts. Close the configure-time probe client and pause refresh on the retry-test fixture. Co-authored-by: Cursor <cursoragent@cursor.com>
Short testing refresh intervals left orphaned refresh threads that raced urllib3 mocks in HTTP retry tests. Default tests to GROUNDLIGHT_DISABLE_TOKEN_REFRESH=1 and close client fixtures. Co-authored-by: Cursor <cursoragent@cursor.com>
Rotate whenever the current token is due: revoke previous, demote current, mint a replacement. Drop grace-blocked minting that could outrun short test TTLs, and clarify configured-token error messages. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the helper for local testing only; it does not belong in the branch under review. Co-authored-by: Cursor <cursoragent@cursor.com>
Pass the server's unauthorized detail through to ApiTokenError so expired or revoked tokens are reported clearly, and silence the known broad-exception catches that pylint flags in TokenManager. Co-authored-by: Cursor <cursoragent@cursor.com>
Never-expire configured tokens skip minting and refresh entirely. Expiring tokens mint without a client TTL and refresh every observed_ttl/30 from the working token's lifetime. Co-authored-by: Cursor <cursoragent@cursor.com>
Replay multipart file bytes on 401 retry, derive refresh cadence from server created_at/expires_at with a non-positive TTL clamp, and drop GROUNDLIGHT_DISABLE_TOKEN_REFRESH so rotate-vs-not is only expires_at. Also rename bootstrap_* to configured_*, require current cache fields, treat by-snippet 404 as no-rotation, and simplify mint previous wiring. Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve generated/model.py timestamp conflict while keeping ApiToken and VLM models. Co-authored-by: Cursor <cursoragent@cursor.com>
Drop an unnecessary multi-line import diff and describe never-expire tokens via expires_at instead of ttl. Co-authored-by: Cursor <cursoragent@cursor.com>
Annotate mixed multipart snapshot/file dict value types and make created_at Optional so the lint job can pass. Co-authored-by: Cursor <cursoragent@cursor.com>
Edge config and readiness endpoints do not validate API tokens, so routing those calls through unauthorized recovery is not needed here. Co-authored-by: Cursor <cursoragent@cursor.com>
Rely on the previous-token grace period and background refresh instead of retrying OpenAPI and raw requests after unauthorized responses. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.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
expires_at.expires_at=null) are used as-is: no mint, no on-disk cache, no background refresh thread (same behavior as pre-rotationGroundlight).expires_at), park the configured token aspreviousfor delayed cleanup, and refresh everyobserved_ttl / 30using servercreated_at/expires_at.by-snippetlookup, create, delete, list).Rotation policy (client)
expires_atset. No environment-variable override.(expires_at - created_at) / 30(e.g. 30-day TTL => daily refresh; 3-minute TTL => every 6 seconds).Dependency
GET /v1/api-tokens/by-snippet/<snippet>(zuuul#6579) and the ApiIdentity server contract (zuuul#6614–#6616).Test plan
expires_at, dynamic refresh interval, previous-parking, cleanup, and non-positive TTL clamp