Skip to content

feat(cred): select credential ref per invocation (--ref flag + env) - #167

Merged
piekstra merged 2 commits into
mainfrom
feat/per-invocation-credential-ref
Jul 16, 2026
Merged

feat(cred): select credential ref per invocation (--ref flag + env)#167
piekstra merged 2 commits into
mainfrom
feat/per-invocation-credential-ref

Conversation

@piekstra

Copy link
Copy Markdown
Contributor

Summary

Closes #166. Read/query commands could only use the single active credential_ref from config.yml, so concurrent processes targeting different accounts raced on that shared file (a clobbered ref left gro reading 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:

--ref <service>/<profile>  >  GOOGLE_READONLY_CREDENTIAL_REF env  >  config.yml credential_ref
gro mail search "is:unread" --ref google-readonly/work &
gro mail search "is:unread" --ref google-readonly/personal &   # independent, no config race

Design (mirrors the existing --backend machinery)

  • root: persistent --ref flag, recorded via keychain.SetCredentialRefOverride in a new WireCredentialRefSelection hook that validates <service>/<profile> up front (clean --ref error before any keyring work). set-credential keeps its own local --ref write-target, which intentionally shadows the persistent one for that command only.
  • keychain: effectiveRef() applies flag > env > config at the single open() resolution site — so every read/API command picks it up with no per-command wiring. An explicit override suppresses the one-time legacy token.json migration (it only ever targets the configured/default ref — same rationale as OpenRef).
  • env-var name is derived from the service (tracks the <SERVICE>_ prefix credstore uses for the backend env var), never hard-coded.

Tests

  • Precedence (flag > env > config) + empty---ref fall-through
  • Flag validation (malformed ref → --ref error)
  • PersistentPreRunE shadowing guard (mirrors the --backend regression test)
  • set-credential local-shadow vs a read command inheriting the persistent flag

Docs

  • README: new "Selecting an account per invocation (concurrent multi-account use)" section
  • docs/development.md: precedence + resolution-site note

Build, full go test ./..., and golangci-lint run are green locally.

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
piekstra requested review from piekstra-dev and removed request for piekstra-dev July 15, 2026 19:12

@piekstra-dev piekstra-dev left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread internal/keychain/keychain.go
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
piekstra requested review from piekstra-dev and removed request for piekstra-dev July 16, 2026 18:53

@piekstra-dev piekstra-dev left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread internal/keychain/keychain.go
@piekstra
piekstra merged commit 651f1ac into main Jul 16, 2026
11 checks passed
@piekstra
piekstra deleted the feat/per-invocation-credential-ref branch July 16, 2026 19:37
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.

Read commands can't select a credential profile per-invocation — concurrent multi-account use races on config.yml

2 participants