feat(cred): select credential ref per invocation (--ref flag + env) - #167
Conversation
Read/query commands always used the single active credential_ref from
config.yml, so concurrent processes targeting different accounts raced on that
shared file — a clobbered ref could leave gro pointing at a tokenless profile
("no OAuth token found") or a half-written value.
Add a per-invocation credential-ref selector honored by all commands, mirroring
the existing --backend precedence:
--ref <service>/<profile> > GOOGLE_READONLY_CREDENTIAL_REF env > config credential_ref
- root: persistent --ref flag recorded via keychain.SetCredentialRefOverride in
a new WireCredentialRefSelection hook that validates <service>/<profile> up
front. set-credential keeps its own local --ref write-target (shadow).
- keychain: effectiveRef() applies flag > env > config at the single open()
resolution site; an explicit override suppresses the one-time legacy migration
(it only ever targets the configured ref, same rationale as OpenRef).
- env-var name derived from the service so it tracks the <SERVICE>_ prefix.
Tests cover precedence + empty-flag fall-through, flag validation, the
PersistentPreRunE shadowing guard, and set-credential's local-shadow vs a read
command inheriting the persistent flag. Docs updated (README multi-account
section + docs/development.md).
Closes #166
piekstra-dev
left a comment
There was a problem hiding this comment.
Automated PR Review
Reviewed commit: 2485cf7cffb6
Profile: reviewer - Posting as: piekstra-dev
Summary
| Reviewer | Findings |
|---|---|
| go:implementation-tests | 1 |
| policies:conventions | 0 |
go:implementation-tests (1 finding)
Major - internal/keychain/keychain.go:83
The override-application in open() (
if ref, overridden := effectiveRef(cfg.CredentialRef); overridden { cfg.CredentialRef = ref; runMigration = false }) is never exercised end-to-end. credref_test.go only unit-tests the pure effectiveRef() precedence helper, and credref_wire_test.go only tests that root's PersistentPreRunE records the flag into the package-level override — no test proves that open() actually swaps cfg.CredentialRef and suppresses runMigration when an override is present. Contrast with the --backend feature, whose analogous binding (BindBackendFlag inside openWith) is directly exercised by TestOpenWith_FlagOverridesConfig in wire_test.go via an observable openWith() call. Here a regression (e.g. failing to set runMigration=false, or applying the override to the wrong field) would pass all current tests yet could re-run the one-time legacy token.json migration against an arbitrary --ref/env-selected profile, corrupting or misattributing legacy credential data. Since open() calls config.LoadConfigForRuntime() directly it can't be unit-tested as-is; factor the override-application into a small testable helper (e.g.applyCredentialRefOverride(cfg *config.Config) (runMigration bool)called from open()) and add an openWith-style test asserting cfg.CredentialRef and the migration flag change when SetCredentialRefOverride/env are set, mirroring TestOpenWith_FlagOverridesConfig.
Reviewer Coverage
| Reviewer | Status | Inspected | Skipped | Constraints |
|---|---|---|---|---|
| go:implementation-tests | complete_broad | internal/cmd/root/backend_wire_test.go, internal/cmd/root/credref_wire_test.go, internal/cmd/root/root.go, internal/keychain/credref_test.go, internal/keychain/keychain.go, internal/keychain/wire.go | unavailable | unavailable |
| policies:conventions | complete_broad | README.md, docs/development.md, internal/cmd/root/root.go, internal/keychain/keychain.go, internal/keychain/wire.go | unavailable | unavailable |
0 PR discussion threads considered. 0 summarized; 0 resolved.
Completed in 39s | unavailable | claude-sonnet-5 | cr 0.10.257
| Field | Value |
|---|---|
| Model | claude-sonnet-5 |
| Reviewers | go:implementation-tests, policies:conventions |
| Engine | claude_cli · claude-sonnet-5 |
| Reviewed by | cr · piekstra-dev |
| Duration | 39s wall · 1m 09s compute |
| Cost | unavailable |
| Tokens | 22 in / 4.2k out |
Per-workstream usage
| Workstream | Model | In | Out | Cache read | Cache create | Cost | Duration |
|---|---|---|---|---|---|---|---|
| orchestrator-selection | claude-sonnet-5 | 6 | 2.0k | 32.9k | 20.6k | unavailable | 24s |
| go:implementation-tests | claude-sonnet-5 | 4 | 1.0k | 8.8k | 122.3k | unavailable | 17s |
| policies:conventions | claude-sonnet-5 | 6 | 765 | 74.6k | 82.3k | unavailable | 16s |
| orchestrator-rollup | claude-sonnet-5 | 6 | 491 | 36.4k | 31.8k | unavailable | 12s |
Addresses review: the override-application in open() (swap credential_ref + suppress the one-time legacy migration) had no direct coverage — only the pure effectiveRef() precedence and the flag-wiring were tested, so a regression that dropped runMigration=false could re-run the token.json migration against an arbitrary --ref/env profile and misattribute credential data, undetected. Extract applyCredentialRefOverride(cfg, runMigration) from open() and add TestApplyCredentialRefOverride asserting cfg.CredentialRef is swapped AND runMigration is forced false on a flag/env override (and both untouched with none), mirroring TestOpenWith_FlagOverridesConfig.
piekstra-dev
left a comment
There was a problem hiding this comment.
Automated PR Review
Reviewed commit: 7aec86f309c0
Profile: reviewer - Posting as: piekstra-dev
Summary
| Reviewer | Findings |
|---|---|
| go:implementation-tests | 0 |
| policies:conventions | 1 |
| documentation:docs | 0 |
policies:conventions (1 finding)
Minor - internal/keychain/keychain.go:93
The --ref feature mirrors --backend's testing convention only partially: backend has both a pure precedence-helper test AND an openWith()-level test (wire_test.go: TestOpenWith_ConfigOnlyMemoryBackend / TestOpenWith_FlagOverridesConfig) proving the actual resolution site consumes the override end-to-end. credRef only has the pure-helper tests (effectiveRef, applyCredentialRefOverride in credref_test.go) — there's no equivalent open()/openWith()-level test proving the swapped cfg.CredentialRef is actually used to resolve service/profile and that runMigration=false is honored through to migrateLegacyOverwrite being skipped. Add an openWith-style test (memory backend, override set) asserting st.Ref()/st.Service() reflect the override, matching the parity already established for --backend.
Reviewer Coverage
| Reviewer | Status | Inspected | Skipped | Constraints |
|---|---|---|---|---|
| go:implementation-tests | complete_broad | internal/cmd/root/backend_wire_test.go, internal/cmd/root/credref_wire_test.go, internal/cmd/root/root.go, internal/keychain/credref_test.go, internal/keychain/keychain.go, internal/keychain/wire.go | unavailable | unavailable |
| policies:conventions | complete_broad | README.md, internal/cmd/root/root.go, internal/keychain/keychain.go, internal/keychain/wire.go | unavailable | unavailable |
| documentation:docs | complete_broad | README.md, docs/development.md | unavailable | unavailable |
0 PR discussion threads considered. 0 summarized; 0 resolved.
Completed in 3m 11s | unavailable | claude-sonnet-5 | cr 0.10.258
| Field | Value |
|---|---|
| Model | claude-sonnet-5 |
| Reviewers | go:implementation-tests, policies:conventions, documentation:docs |
| Engine | claude_cli · claude-sonnet-5 |
| Reviewed by | cr · piekstra-dev |
| Duration | 3m 11s wall · 5m 02s compute |
| Cost | unavailable |
| Tokens | unavailable |
Per-workstream usage
| Workstream | Model | In | Out | Cache read | Cache create | Cost | Duration |
|---|---|---|---|---|---|---|---|
| orchestrator-selection | claude-sonnet-5 | unavailable | unavailable | unavailable | unavailable | unavailable | 24s |
| go:implementation-tests | claude-sonnet-5 | unavailable | unavailable | unavailable | unavailable | unavailable | 2m 11s |
| policies:conventions | claude-sonnet-5 | unavailable | unavailable | unavailable | unavailable | unavailable | 1m 19s |
| documentation:docs | claude-sonnet-5 | unavailable | unavailable | unavailable | unavailable | unavailable | 52s |
| orchestrator-rollup | claude-sonnet-5 | unavailable | unavailable | unavailable | unavailable | unavailable | 13s |
Summary
Closes #166. Read/query commands could only use the single active
credential_reffromconfig.yml, so concurrent processes targeting different accounts raced on that shared file (a clobbered ref leftgroreading the wrong account or a tokenless profile →no OAuth token found).This adds a per-invocation credential-ref selector honored by all commands, resolved with the same precedence shape as
--backend:Design (mirrors the existing
--backendmachinery)--refflag, recorded viakeychain.SetCredentialRefOverridein a newWireCredentialRefSelectionhook that validates<service>/<profile>up front (clean--referror before any keyring work).set-credentialkeeps its own local--refwrite-target, which intentionally shadows the persistent one for that command only.effectiveRef()appliesflag > env > configat the singleopen()resolution site — so every read/API command picks it up with no per-command wiring. An explicit override suppresses the one-time legacytoken.jsonmigration (it only ever targets the configured/default ref — same rationale asOpenRef).<SERVICE>_prefix credstore uses for the backend env var), never hard-coded.Tests
--reffall-through--referror)PersistentPreRunEshadowing guard (mirrors the--backendregression test)set-credentiallocal-shadow vs a read command inheriting the persistent flagDocs
docs/development.md: precedence + resolution-site noteBuild, full
go test ./..., andgolangci-lint runare green locally.