Server-owned credential authority: one refresher per provider grant (ADR-0055) #223
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab!223
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/222"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #222
What this does
Kills the token-family fork: since #202, every run got a one-time snapshot of the master credential store with no write-back, so a long-lived instance's CLI self-refresh minted a new OAuth token family and invalidated the refresh token held by the master and every sibling snapshot — recurring host-level "not logged in", spawn refusals, and wipe-on-stop able to destroy the only valid family.
The invariant now enforced, exactly as settled in the grilled design: one refresher per provider grant; instances are consumers, never refreshers. Lab never parses token contents and never speaks any provider's OAuth protocol.
Implementation, by settled decision
internal/credrotatedrives each provider's own CLI against the master store every 15 min (RefreshCredentials); the CLI decides whether to rotate. claude-code uses the probe-confirmedclaude -p --model haikutrigger under a pinned masterCLAUDE_CONFIG_DIR(compat §3b records the 2026-07-24 probe evidence). Pokes are gated on a non-empty master sig — a logged-out host is never poked.CredentialsSig("")change (poke-rotation, adopt-back, or operator re-login) re-injects into every live instance HOME through the existing singleInjectCredentialscall site, same tick, and re-stamps each run's persisted baseline. The scan runs every 30 s — well inside the probed "minutes" invalidation window. Bootstrap fan-out on the first tick heals the fleet after lab downtime.runs.cred_sig, migration 0019; stamped at launch from the post-inject observation) and adopts that family into the master immediately, newest credential-file mtime winning across multiple self-refreshers, then fans out.instancehome.Managerfunnelscredrotate.AdoptCheckinto all seven wipe/sweep paths (stop, launch rollback, AFK stop, reaper, parked teardown, startup + runtime sweeps). Installed beforeStartupReconcile, so the startup sweep of orphan homes after downtime is covered — a wipe can never destroy the only valid family.RefreshCredentials(ctx)/CredentialsSig(home)/AdoptCredentials(instanceHome)onprovider.AgentProvider; core owns schedule, comparison, ordering, tie-breaks; adapters own every file path and CLI recipe. Conformance pins the""-means-master and missing-master-empty-sig conventions andAdoptCredentials' error guards for every adapter.ADR-0055 records the design, rejected alternatives, and probe evidence.
Two deliberate deviations from the brief's letter
CredentialsSigreturns(sig string, mtime time.Time), not barestring. The settled design puts the newest-mtime adopt tie-break in core, but core never learns adapter file paths — so the seam surfaces the newest credential-file mtime as typed filesystem metadata (never token content). The sig itself stays opaque, equality-compared only.codex exec --sandbox read-only --skip-git-repo-check ok, flags confirmed against codex-cli 0.144.4--help). This host has no codex login, and probing a live grant double-spends its refresh token — the same reason the issue dropped the grace-window probe. The issue explicitly scoped this as blocking only that adapter'sRefreshCredentials; compat §11 records the recipe, the two open probe questions, and an operator-supervised verification procedure, plus a live re-verification checklist row.Known bounded trade-off: a stop-path
AdoptCheckcan wait behind an in-flight poke (shared mutex; one poke per provider per 15 min, bounded by a 3 min refresh timeout, typically seconds).Verification
go build ./...,go vet ./...,gofmt -l,golangci-lint run(0 issues), fullgo test ./...green;internal/credrotatealso green under-race.TestTick_MasterRotationFansOut), no fan-out on no-op poke (TestTick_NoopPokeDoesNotFanOut,TestTick_PokeRotationFansOutSameTick), self-refresh adopt + newest-mtime tie-break (TestTick_SelfRefreshAdoptedAndFannedOut,TestTick_NewestSelfRefreshWins), adopt-before-wipe ordering (TestWipeFiresPreWipeHookBeforeRemoval,TestSweepAllFiresPreWipeHookOnlyForReapedOrphans,TestAdoptCheck), opaque-sig/""-means-master conformance (TestConformance/*/credential-authority), adapter coverage incredauthority_test.gofor both providers with stubbed binaries.🤖 Generated with Claude Code
[autoland] verdict: pass
Clean PASS. Verified via CI aggregate success — 'ci / native' (run #295, 7m55s), the full native build/test/vet/lint/-race lane. Conventional commit + Closes #222 (afk auto-close intact). Diff scope matches issue #222's grilled design; migrations present for both dialects; ADR-0055 recorded. Codex RefreshCredentials ships as an issue-sanctioned unverified candidate recipe (non-blocking per the issue's explicit scope; documented in ADR-0055 + compat §11 with an operator verification procedure).