feat(keyring): wire --backend flag and keyring.backend config key - #140
Conversation
Surface the cli-common credstore backend selector as a first-class user control: --backend persistent flag on the gro root command, and the existing keyring.backend config key now flows through the same BindBackendFlag helper. Precedence is flag > env > config > auto-detect, matching credstore's internal ordering. Bumps cli-common to v0.2.0 (which transitively bumps the Go directive to 1.26), bumps CI runners to Go 1.26 across ci.yml, release.yml, and test-chocolatey.yml, and bumps golangci-lint to v2.12.2 (v2.0.2 rejects Go 1.26 targets). Wiring is cobra-layer-only: WireBackendSelection validates the flag and records it in a package-level override; openWith binds the flag pair against cfg.Keyring.Backend at the single credstore.Open call site. WireBackendSelection is exported so any future shadowing subcommand can call it explicitly (cobra does NOT chain PersistentPreRunE); gro has no shadowers today, guarded by a regression test. Closes #139
|
Findings Minor: Minor: The docs sweep missed stale narrow backend wording. Minor: The central Nit: |
…s; add openWith binding tests Addresses PR review minors: - gro config show now emits keyring.backend (selector name) alongside the resolved Backend, with the config.yml value when set. - README and the agent-guidance doc sweep: set-credential prose and config-field list now name keyring.backend / --backend / env var consistently with the keyring section. - New internal/keychain/wire_test.go: TestOpenWith_ConfigOnlyMemoryBackend and TestOpenWith_FlagOverridesConfig prove openWith actually consumes cfg.Keyring.Backend via BindBackendFlag and that the flag override wins over config. Memory backend keeps the tests platform- and passphrase-free.
|
Verdict: Gaps found (minor) — coverage is solid on the wiring, thin on the surfaced output. Strong coverage on the new
Concrete gaps:
Nothing structural; the secret-handling contract is well-tested. Add a small |
Addresses TDD-review gap: TestRunShowReportsKeyringBackendSelector seeds keyring.backend=file in config.yml and asserts both the text 'keyring.backend: file (config.yml)' line and the JSON 'keyring_backend' field are emitted. TestRunShowOmitsKeyringBackendWhenUnset locks in the omitempty path so unset configs produce neither output.
monit-reviewer
left a comment
There was a problem hiding this comment.
Automated PR Review
Reviewed commit: 7e8dcc6
Summary
| Reviewer | Findings |
|---|---|
| documentation:docs-reviewer | 1 |
| harness-engineering:harness-architecture-reviewer | 3 |
| harness-engineering:harness-enforcement-reviewer | 1 |
| harness-engineering:harness-knowledge-reviewer | 1 |
documentation:docs-reviewer (1 findings)
💡 Suggestion - README.md:165
Counting inconsistency: text states 'The backend can be selected three ways' but then lists four options (--backend flag, GOOGLE_READONLY_KEYRING_BACKEND env var, keyring.backend config, and auto-detect). Should say 'four ways' or clarify that three are user-configurable options with auto-detect as the fallback.
harness-engineering:harness-architecture-reviewer (3 findings)
internal/keychain/wire.go:6
Package-level mutable globals (backendFlagValue, backendFlagWasSet, backendMu) create a hidden side-channel between the cobra PersistentPreRunE layer and openWith(). This bypasses normal Go scoping, demands careful cleanup in every test (resetState + t.Cleanup), and would silently misbehave if two goroutines concurrently called Execute(). Any code path that calls openWith without first going through WireBackendSelection will silently consume whatever backend override was last written. A dependency-injection approach — passing the resolved backend into openWith as a parameter, or threading it through cmd.Context() — would eliminate the ambient state without adding API complexity.
💡 Suggestion - internal/cmd/root/root.go:62
WireBackendSelection calls cccredstore.BindBackendFlag with a throwaway &cccredstore.Options{} (result immediately discarded) solely to validate the flag early, then separately calls keychain.SetBackendFlagOverride. openWith later calls BindBackendFlag a second time. If BindBackendFlag's semantics change (e.g. normalization of backend name, or context-sensitive cross-validation), the cobra-layer validation and the keychain-layer binding could diverge silently. Consider removing the throwaway validation call, or making validation the responsibility of the credstore package's own API.
💡 Suggestion - internal/keychain/keychain.go:105
Validation asymmetry: --backend flag validation is eager (fails at startup), but keyring.backend config validation is deferred until first credential access. A typo in keyring.backend in config.yml is silent until the user runs a command that touches the keyring. This is user-visible and worth surfacing via a startup warning or in gro config show output.
harness-engineering:harness-enforcement-reviewer (1 findings)
💡 Suggestion - internal/cmd/root/backend_wire_test.go:21
resetState's t.Cleanup closure has a misleading comment implying it strips added child commands, but the body only resets the flag override. Child cleanup is delegated to per-test defer removeChild() calls. If a test's AddCommand panics before the defer is registered, or a future test omits the defer, rootCmd is permanently mutated for subsequent tests. TestWireBackendSelection_RealCommandTreeInheritsFlag — which walks all children — would then walk leaked probe commands. Prefer subtest-scoped command trees or reset rootCmd's commands inside the cleanup.
harness-engineering:harness-knowledge-reviewer (1 findings)
go.mod:24
The bump from cli-common v0.1.0 → v0.2.0 pulls in unexpected indirect dependencies including: forks of well-known packages under an unknown 'byteness' GitHub org (replacing 99designs/keyring, 99designs/go-keychain, gsterjov/go-libsecret), a beta-tagged 1Password SDK (onepassword-sdk-go v0.4.1-beta.1), a WebAssembly runtime (tetratelabs/wazero + extism/go-sdk + dylibso/observe-sdk), and Jaeger tracing. The WASM runtime stack executing inside a credential-storage operation is a particularly high-impact attack surface. The 'byteness/*' forks replacing established packages are a supply-chain risk. This dependency graph should be audited and confirmed as expected from the cli-common v0.2.0 release before merging.
4 info-level observations excluded. Run with --verbose to include.
2 PR discussion threads considered.
Completed in 3m 10s | $1.37 | sonnet | daemon 0.2.120 | Glorfindel
| Field | Value |
|---|---|
| Model | sonnet |
| Reviewers | hybrid-synthesis, documentation:docs-reviewer, harness-engineering:harness-architecture-reviewer, harness-engineering:harness-enforcement-reviewer, harness-engineering:harness-knowledge-reviewer, harness-engineering:harness-self-documenting-code-reviewer, security:security-code-auditor |
| Engine | claude · sonnet |
| Reviewed by | pr-review-daemon · monit-pr-reviewer |
| Duration | 3m 10s wall · 10m 21s compute (Reviewers: 2m 13s · Synthesis: 54s) |
| Cost | $1.37 |
| Tokens | 266.1k in / 39.7k out |
| Turns | 10 |
Per-workstream usage
| Workstream | Model | In | Out | Cache read | Cache create | Cost |
|---|---|---|---|---|---|---|
| hybrid-synthesis | sonnet | 23.8k | 2.7k | 0 | 23.8k (1h) | $0.14 |
| documentation:docs-reviewer | haiku | 6.5k | 6.2k | 0 | 6.5k (1h) | $0.04 |
| harness-engineering:harness-architecture-reviewer | sonnet | 69.1k | 5.8k | 32.3k | 36.8k (1h) | $0.26 |
| harness-engineering:harness-enforcement-reviewer | sonnet | 32.3k | 4.9k | 0 | 32.3k (1h) | $0.22 |
| harness-engineering:harness-knowledge-reviewer | sonnet | 78.3k | 7.1k | 6.9k | 71.4k (1h) | $0.40 |
| harness-engineering:harness-self-documenting-code-reviewer | sonnet | 35.8k | 6.9k | 2.2k | 33.6k (1h) | $0.24 |
| security:security-code-auditor | haiku | 20.4k | 5.9k | 0 | 20.3k (1h) | $0.06 |
Re-reviews only run when @monit-reviewer is re-requested as a reviewer — push as many commits as you need, then re-request when ready. PRs targeting branches other than main, master are skipped, even when @monit-reviewer is re-requested.
monit-reviewer
left a comment
There was a problem hiding this comment.
Automated PR Review
Reviewed commit: e75258e | Previous: 7e8dcc6 (incremental)
Summary
| Reviewer | Findings |
|---|---|
| harness-engineering:harness-enforcement-reviewer | 1 |
| harness-engineering:harness-knowledge-reviewer | 2 |
harness-engineering:harness-enforcement-reviewer (1 findings)
💡 Suggestion - internal/cmd/root/backend_wire_test.go:57
rootCmd.SetArgs(...) is called in multiple tests but never reset via t.Cleanup. Cobra's SetArgs mutates the package-level rootCmd persistently; with go test -shuffle=on a stale args slice from a prior test could bleed into TestWireBackendSelection_RealCommandTreeInheritsFlag or other tests that call Execute(). Add t.Cleanup(func() { rootCmd.SetArgs(nil) }) immediately after each rootCmd.SetArgs(...) call.
harness-engineering:harness-knowledge-reviewer (2 findings)
go.mod:24
The cli-common v0.1.0→v0.2.0 bump replaces the community-standard 99designs/keyring family with lesser-known byteness/* forks, and pulls in a large set of unexpected transitive dependencies for a security-critical credential-store library: a full WebAssembly runtime (tetratelabs/wazero ~5 MB), extism/go-sdk, pre-release 1Password SDKs (onepassword-sdk-go v0.4.1-beta.1, connect-sdk-go at a pre-release commit), Uber Jaeger tracing, and dylibso/observe-sdk. The byteness/go-libsecret fork has a timestamp of 20260108, suggesting very recent creation. Per the settled thread (go.mod:24), the author acknowledged the tradeoff was deliberate and documented in cli-common#23, with the WASM/1Password deps gated by AllowedBackends init() registration. However, the original thread settlement covered supply-chain risk in general — the specific concern about the byteness/* forks replacing 99designs/* (not previously called out with this specificity) and the beta SDK in a security-critical path remain worth a one-time explicit sign-off. This finding is not a re-raise of the settled thread but a more specific residual concern about the fork provenance.
💡 Suggestion - internal/keychain/keychain.go:106
When credstore.BindBackendFlag returns an error inside openWith, the error is always prefixed with '--backend:' regardless of whether the failure originated from the CLI flag or the keyring.backend config key. If a user sets an invalid keyring.backend in config.yml and never passes --backend on the command line, the error message will mislead them into thinking the problem is a CLI flag they didn't use. Consider using a context-aware prefix or surfacing both sources in the error text.
2 info-level observations excluded. Run with --verbose to include.
8 PR discussion threads considered.
Completed in 3m 51s | $1.18 | sonnet | daemon 0.2.120 | Glorfindel
| Field | Value |
|---|---|
| Model | sonnet |
| Mode | Re-review · Cycle 2 · Session resumed |
| Reviewers | hybrid-synthesis, documentation:docs-reviewer, harness-engineering:harness-architecture-reviewer, harness-engineering:harness-enforcement-reviewer, harness-engineering:harness-knowledge-reviewer, harness-engineering:harness-self-documenting-code-reviewer, security:security-code-auditor |
| Engine | claude · sonnet |
| Reviewed by | pr-review-daemon · monit-pr-reviewer |
| Duration | 3m 51s wall · 10m 54s compute (Reviewers: 2m 36s · Synthesis: 53s) |
| Cost | $1.18 |
| Tokens | 206.4k in / 44.6k out |
| Turns | 8 |
Per-workstream usage
| Workstream | Model | In | Out | Cache read | Cache create | Cost |
|---|---|---|---|---|---|---|
| hybrid-synthesis | sonnet | 32.7k | 2.6k | 7.5k | 25.3k (1h) | $0.14 |
| documentation:docs-reviewer | haiku | 6.5k | 8.6k | 0 | 6.5k (1h) | $0.05 |
| harness-engineering:harness-architecture-reviewer | sonnet | 32.3k | 5.0k | 2.2k | 30.2k (1h) | $0.21 |
| harness-engineering:harness-enforcement-reviewer | sonnet | 32.3k | 6.3k | 2.2k | 30.2k (1h) | $0.23 |
| harness-engineering:harness-knowledge-reviewer | sonnet | 32.3k | 5.9k | 2.2k | 30.2k (1h) | $0.23 |
| harness-engineering:harness-self-documenting-code-reviewer | sonnet | 35.8k | 8.3k | 17.7k | 18.1k (1h) | $0.21 |
| security:security-code-auditor | haiku | 20.4k | 7.1k | 0 | 20.4k (1h) | $0.07 |
| discussion-summarizer | — | 14.0k | 713 | 7.5k | 6.5k (1h) | $0.04 |
Re-reviews only run when @monit-reviewer is re-requested as a reviewer — push as many commits as you need, then re-request when ready. PRs targeting branches other than main, master are skipped, even when @monit-reviewer is re-requested.
| cloud.google.com/go/compute/metadata v0.9.0 // indirect | ||
| github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect | ||
| github.com/99designs/keyring v1.2.2 // indirect | ||
| github.com/1Password/connect-sdk-go v1.5.4-0.20250417152128-c154b387248b // indirect |
There was a problem hiding this comment.
🟡 Medium (harness-engineering:harness-knowledge-reviewer): The cli-common v0.1.0→v0.2.0 bump replaces the community-standard 99designs/keyring family with lesser-known byteness/* forks, and pulls in a large set of unexpected transitive dependencies for a security-critical credential-store library: a full WebAssembly runtime (tetratelabs/wazero ~5 MB), extism/go-sdk, pre-release 1Password SDKs (onepassword-sdk-go v0.4.1-beta.1, connect-sdk-go at a pre-release commit), Uber Jaeger tracing, and dylibso/observe-sdk. The byteness/go-libsecret fork has a timestamp of 20260108, suggesting very recent creation. Per the settled thread (go.mod:24), the author acknowledged the tradeoff was deliberate and documented in cli-common#23, with the WASM/1Password deps gated by AllowedBackends init() registration. However, the original thread settlement covered supply-chain risk in general — the specific concern about the byteness/* forks replacing 99designs/* (not previously called out with this specificity) and the beta SDK in a security-critical path remain worth a one-time explicit sign-off. This finding is not a re-raise of the settled thread but a more specific residual concern about the fork provenance.
Reply to this thread when addressed.
There was a problem hiding this comment.
See thread on the earlier instance of this finding (#140 (comment)). The cli-common v0.2.0 dep graph is accepted as a deliberate tradeoff, gated by AllowedBackends so the WASM/1Password stacks never execute in gro. No new CGO.
| rootCmd.SetArgs([]string{"probe-flagset", "--backend", "memory"}) | ||
|
|
||
| if err := rootCmd.Execute(); err != nil { | ||
| t.Fatalf("Execute: %v", err) |
There was a problem hiding this comment.
🔵 Low (harness-engineering:harness-enforcement-reviewer): rootCmd.SetArgs(...) is called in multiple tests but never reset via t.Cleanup. Cobra's SetArgs mutates the package-level rootCmd persistently; with go test -shuffle=on a stale args slice from a prior test could bleed into TestWireBackendSelection_RealCommandTreeInheritsFlag or other tests that call Execute(). Add t.Cleanup(func() { rootCmd.SetArgs(nil) }) immediately after each rootCmd.SetArgs(...) call.
Reply to this thread when addressed.
There was a problem hiding this comment.
Fixed — resetState's t.Cleanup now also calls rootCmd.SetArgs(nil) so a panicking test or shuffled execution order can't leak stale args into the next test.
| // auto-selection and store credentials somewhere unintended. | ||
| return nil, fmt.Errorf("invalid keyring.backend %q in config (only \"file\" is supported)", b) | ||
| // Bind the --backend flag (from the cobra layer, recorded by | ||
| // root.WireBackendSelection) and cfg.Keyring.Backend together at one |
There was a problem hiding this comment.
🔵 Low (harness-engineering:harness-knowledge-reviewer): When credstore.BindBackendFlag returns an error inside openWith, the error is always prefixed with '--backend:' regardless of whether the failure originated from the CLI flag or the keyring.backend config key. If a user sets an invalid keyring.backend in config.yml and never passes --backend on the command line, the error message will mislead them into thinking the problem is a CLI flag they didn't use. Consider using a context-aware prefix or surfacing both sources in the error text.
Reply to this thread when addressed.
There was a problem hiding this comment.
Fixed — openWith now attributes the error to whichever knob produced the invalid value: '--backend:' when flagSet=true, 'keyring.backend:' otherwise. The current contract is that BindBackendFlag only fails on the flag side (config passes through to credstore.Open), but the branch is in place for the day that broadens.
… SetArgs in tests - openWith now prefixes BindBackendFlag errors with 'keyring.backend:' when the flag is absent (instead of always '--backend:'), so a config-side typo surfaces a message that names the right knob. - resetState now resets rootCmd.SetArgs on cleanup to keep package-level state clean across tests under go test -shuffle=on.
|
These are low-value, please approve the PR |
|
No architectural drift from the daemon edits. The added One nit: |
Summary
--backendpersistent flag ongroroot and wires it throughcredstore.BindBackendFlagtogether with the existingkeyring.backendconfig key. Precedence:--backend>GOOGLE_READONLY_KEYRING_BACKEND>keyring.backend> auto.cli-commonv0.1.0 → v0.2.0 (Go directive auto-bumps to 1.26), CI Go 1.24/1.22 → 1.26, golangci-lint v2.0.2 → v2.12.2.credstore.Opencall site ininternal/keychain/keychain.go.WireBackendSelectionis exported so any future shadowing subcommand can call it explicitly (cobra does NOT chainPersistentPreRunE).Test plan
go build ./...,go test ./...(1392 passed),go vet ./...,golangci-lint runcleangro --helpshows--backendwith the full precedence usage textGOOGLE_READONLY_KEYRING_BACKEND(verified by grep)internal/cmd/root/backend_wire_test.gocover: flag-set on subcommand path, invalid flag, config-side passthrough viaBindBackendFlag, deferred-validation of invalid config, shadowing-subcommand regression, and a real-tree walk asserting every leaf resolves--backendto the same*pflag.Flagpointer as the root (catches local shadowing).Closes #139