Skip to content

perf(vault): cached credentials and safe singleflight refresh - #46

Merged
shreyanshjain7174 merged 3 commits into
mainfrom
perf/vault-cache
Oct 17, 2026
Merged

shreyanshjain7174 merged 3 commits into
mainfrom
perf/vault-cache

Conversation

@shreyanshjain7174

Copy link
Copy Markdown
Contributor

Plan

Phase 0, item 7 of the AgentGate scale-out prompt: vault credential cache and singleflight refresh.

Every request did a SQLite read plus AES decrypt to fetch a token. Concurrent requests for an expiring OAuth token all refreshed at once. Providers that rotate refresh tokens then invalidate each other, which can lock the user out.

What changed

  • CachedStore. Caches decrypted tokens per (user, service).
    • An entry expires at the earlier of a 60s TTL and token expiry minus 30s. LRU-bounded at 10,000 entries.
    • Write-through: Put and Delete update the DB first, and a DB error leaves the entry invalidated.
    • AGENTGATE_VAULT_CACHE_TTL sets the TTL (0 disables).
  • Singleflight refresh. One refresh per (user, service) at a time. Waiters share the result. Failures are shared with current waiters, not cached.
    • The refresh runs on a detached context with its own 15s deadline. A waiter that cancels returns at once without stopping the refresh.
    • A panic in the refresh is recovered. Waiters get an error and the next call starts fresh.
  • OAuth exchange. RefreshFunc takes a context. exchangeToken uses NewRequestWithContext and a client with a 15s timeout. Before, http.PostForm on the default client could hang forever.
  • Conditional write. CompareAndPut(user, service, expectedRefreshToken, tok) on every store. SQLite compares inside a transaction and the UPDATE matches the old ciphertext.
    • A refresh that finishes after an unlink no longer brings the credential back.
    • A re-link made during a refresh is no longer overwritten by the old grant.
  • Rotated refresh tokens get up to 3 write attempts, even past the refresh deadline. The new access token is still returned. Failures log user, service and attempt count, never token values.
  • Cancelled refresh waits return 504 vault_refresh_timeout, not token_expired.
  • The OAuth callback page now renders the service name through html/template (found by Semgrep).

Numbers

BenchmarkVaultGet ns/op B/op allocs/op
SQLite store 5,785 1,648 51
Cached 69 32 1

Validation

  • go build ./...: passed
  • go vet ./...: passed
  • go test -race ./... -count=2: passed
  • semgrep --config auto --error --quiet internal/vault internal/oauth internal/gateway cmd/agentgw: passed, no findings

Tests cover:

  • cache hits, TTL and skew, invalidation and the LRU bound;
  • 50 concurrent requests causing 1 refresh;
  • a shared failure not being cached;
  • waiter cancellation;
  • a hung refresh timing out with recovery on the next call;
  • delete and re-link during a refresh;
  • rotated-token write retries, including after the deadline;
  • panic recovery.

Review

Three rounds by a second model (Claude Opus 5.5). It found:

  • a hung shared refresh blocking a credential until restart;
  • refreshes overwriting newer writes or unlinked credentials;
  • an unrecovered panic in the shared goroutine.

All are fixed with tests.

Known limits

  • Decrypted tokens sit in process memory while cached. The cache is bounded and entries drop on delete.
  • Replicas cache and refresh independently. The CAS write keeps them from overwriting each other's newer tokens.

shreyanshjain7174 and others added 3 commits October 18, 2026 05:08
Prevent concurrent OAuth refreshes from invalidating rotating tokens. Keep decrypted tokens in a bounded, expiry-aware local LRU.

Signed-off-by: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Apply a separate 15s deadline to shared refreshes.
Use conditional writes so deletes and re-links beat stale tokens.
Retry rotated-token writes without logging credentials.

Escape OAuth callback HTML to satisfy the expanded Semgrep scan.

Signed-off-by: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Always release waiters and clear flights after a panic.
Use fixed errors and logs so panic values cannot expose credentials.
Persist successful rotations even after the exchange deadline.

Signed-off-by: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@shreyanshjain7174
shreyanshjain7174 merged commit c81fe3c into main Oct 17, 2026
1 check passed
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