Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2ef6d8b
Add explicit credential storage schema
rianjs Jun 20, 2026
c898ece
Resolve explicit credential stores at runtime
rianjs Jun 20, 2026
15081ee
Refresh secrets storage inventory UX
rianjs Jun 20, 2026
2c488b0
Discover 1Password desktop accounts and vaults
rianjs Jun 20, 2026
0338cb1
Make credential writes choose explicit destinations
rianjs Jun 20, 2026
4982b02
Require explicit scripted credential destinations
rianjs Jun 20, 2026
f7834f7
Remove ambient credential store UX
rianjs Jun 20, 2026
918a78b
Bump version to 0.5
rianjs Jun 20, 2026
ea80d10
Clean up credential storage lint
rianjs Jun 20, 2026
0064358
Skip disabled init menu rows
rianjs Jun 20, 2026
8f46c42
Explain secrets storage discovery before probing
rianjs Jun 20, 2026
bab7465
Separate built-in secrets store from actions
rianjs Jun 20, 2026
610a735
Split 1Password desktop account and vault selection
rianjs Jun 20, 2026
a9188ae
Tighten secrets storage copy
rianjs Jun 21, 2026
8a3e034
Remove redundant 1Password desktop section
rianjs Jun 21, 2026
103a8f5
Clarify 1Password account URL examples
rianjs Jun 21, 2026
895448f
Show secrets backend probe results
rianjs Jun 21, 2026
d59af4a
Add secrets backend discovery modes
rianjs Jun 21, 2026
3b07b93
Fix 1Password desktop account selection
rianjs Jun 21, 2026
30d5367
Use per-command 1Password discovery timeouts
rianjs Jun 21, 2026
deb1574
Prioritize owned 1Password vaults
rianjs Jun 21, 2026
6297e01
Allow credential stores before review profiles
rianjs Jun 21, 2026
84c00c3
Allow committing profileless credential stores
rianjs Jun 21, 2026
502d13e
Remove ambient profile fallback
rianjs Jun 21, 2026
c5ad1a1
Bump minor version to 0.6
rianjs Jun 21, 2026
e91fe77
Polish init menu prerequisites
rianjs Jun 21, 2026
0296dc9
Allow committing last store deletion
rianjs Jun 21, 2026
f5d9d68
Make LLM runtimes standalone
rianjs Jun 21, 2026
b8cbda8
Open first profile editor directly
rianjs Jun 21, 2026
adbb57f
Refresh review profile editor layout
rianjs Jun 21, 2026
1f562b6
Tighten profileless config validation
rianjs Jun 21, 2026
a1fae2e
Stage credential-store deletion immediately
rianjs Jun 21, 2026
854047c
Align init main menu styling
rianjs Jun 21, 2026
7963f7c
chore: fix ci lint fallout
rianjs Jun 21, 2026
ccdfbfa
chore: retrigger ci
rianjs Jun 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 100 additions & 87 deletions README.md

Large diffs are not rendered by default.

61 changes: 44 additions & 17 deletions cmd/cr/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func TestRunConfigShowJSON(t *testing.T) {
}

var stdout, stderr bytes.Buffer
code := run([]string{"config", "show", "--json"}, strings.NewReader(""), &stdout, &stderr)
code := run([]string{"--profile", "home", "config", "show", "--json"}, strings.NewReader(""), &stdout, &stderr)
if code != 0 {
t.Fatalf("run exit code = %d, want 0; stderr = %q", code, stderr.String())
}
Expand Down Expand Up @@ -144,6 +144,13 @@ cases:
func TestRunCredentialCommandsDoNotLeakSecrets(t *testing.T) {
statedirtest.Hermetic(t)
t.Setenv("CODEREVIEW_KEYRING_PASSPHRASE", "test-passphrase")
path, err := config.Path()
if err != nil {
t.Fatalf("config Path: %v", err)
}
if err := config.Save(path, mainCredentialCommandTestConfig()); err != nil {
t.Fatalf("config Save: %v", err)
}
const (
firstSecret = "distinctive-secret-one"
secondSecret = "distinctive-secret-two"
Expand All @@ -158,25 +165,26 @@ func TestRunCredentialCommandsDoNotLeakSecrets(t *testing.T) {
wantCode int
}{
{
name: "init",
name: "set initial credential",
args: []string{
"--backend", "file",
"init",
"--non-interactive",
"--git-token-from-env", "CR_TOKEN_ONE",
"set-credential",
"--store", "test-file",
"--name", "codereview/default",
"--key", "git_token",
"--from-env", "CR_TOKEN_ONE",
},
},
{
name: "config show",
args: []string{"--backend", "file", "config", "show", "--json"},
args: []string{"--profile", "default", "config", "show", "--json"},
wantCode: 0,
},
{
name: "existing credential failure",
args: []string{
"--backend", "file",
"set-credential",
"--ref", "codereview/default",
"--store", "test-file",
"--name", "codereview/default",
"--key", "git_token",
"--from-env", "CR_TOKEN_TWO",
},
Expand All @@ -185,9 +193,9 @@ func TestRunCredentialCommandsDoNotLeakSecrets(t *testing.T) {
{
name: "set overwrite",
args: []string{
"--backend", "file",
"set-credential",
"--ref", "codereview/default",
"--store", "test-file",
"--name", "codereview/default",
"--key", "git_token",
"--from-env", "CR_TOKEN_TWO",
"--overwrite",
Expand All @@ -196,7 +204,7 @@ func TestRunCredentialCommandsDoNotLeakSecrets(t *testing.T) {
},
{
name: "config clear",
args: []string{"--backend", "file", "config", "clear", "--json"},
args: []string{"--profile", "default", "config", "clear", "--json"},
wantCode: 0,
},
}
Expand All @@ -216,14 +224,15 @@ func TestRunCredentialCommandsDoNotLeakSecrets(t *testing.T) {

func mainTestConfig() config.File {
return config.File{
DefaultProfile: "home",
Keyring: config.KeyringConfig{Backend: "memory"},
Profiles: map[string]config.Profile{
"home": {
Git: config.GitConfig{
Host: "github.com",
AuthMode: config.GitAuthModePAT,
CredentialRef: "codereview/home",
Host: "github.com",
AuthMode: config.GitAuthModePAT,
Credential: config.CredentialLocation{
Store: config.LocalOSCredentialStoreID,
Name: "codereview/home",
},
},
LLM: config.LLMConfig{
Provider: config.LLMProviderAnthropic,
Expand All @@ -236,6 +245,24 @@ func mainTestConfig() config.File {
}
}

func mainCredentialCommandTestConfig() config.File {
cfg := mainTestConfig()
cfg.Secrets = config.SecretsConfig{
Stores: map[string]config.SecretsStore{
"test-file": {
DisplayName: "Test file store",
Backend: config.SecretsStoreBackend{
Kind: config.SecretsBackendKind("file"),
},
},
},
}
profile := cfg.Profiles["home"]
profile.Git.Credential = config.CredentialLocation{Store: "test-file", Name: "codereview/default"}
cfg.Profiles = map[string]config.Profile{"default": profile}
return cfg
}

func assertNoSecretLeak(t *testing.T, output string, secrets ...string) {
t.Helper()
for _, secret := range secrets {
Expand Down
62 changes: 23 additions & 39 deletions docs/config-command-surface-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ bootstrap, secret ingress, or the full long-term profile management API.

This document is the semantic parent for:

- `#167`: `cr config path` and default-profile commands
- `#167`: `cr config path`
- `#166`: `cr config route` management commands
- `#169`: `cr config resolve-profile` route preview
- `#168`: `cr config agent-source` management commands
- `#294`: `cr config secrets-profile` management commands
- `#294`: `cr config credential-store` inspection commands

Related but out of scope for this batch:

Expand Down Expand Up @@ -49,10 +49,10 @@ The codebase already supports:
- GitHub App reviewer auth

The missing piece is command coverage. Today `cr config` exposes `show`,
`clear`, and `llm models`, but path/default/route/route-preview/agent-source
operations still require manual YAML edits. Named secrets-management profiles
also need a non-interactive command surface, but that surface must remain
config-only until the later secrets-profile resolver/selection tickets land.
`clear`, and `llm models`, but path/route/route-preview/agent-source
operations still require manual YAML edits. Credential-store inspection is
available through `cr config credential-store`; creating, editing, and deleting
configured stores belongs to the interactive `cr init` secrets-storage flow.

## Shared Command Rules

Expand All @@ -63,7 +63,7 @@ These rules apply across the batch unless a ticket explicitly narrows them.
- Commands that operate on a profile should use the existing global
`--profile <name>` flag.
- When a command is not repository-aware, `--profile` selects the target
profile and otherwise falls back to `default_profile`.
profile; commands that need a profile must fail if none is selected.
- Commands introduced in this batch must not invent a second profile-selection
mechanism.

Expand Down Expand Up @@ -103,13 +103,11 @@ These rules apply across the batch unless a ticket explicitly narrows them.

## Ticket Ownership Matrix

### `#167`: path and default profile
### `#167`: path

Owns:

- `cr config path [--json]`
- `cr config default get [--json]`
- `cr config default set <profile>`

Must not own:

Expand All @@ -120,8 +118,6 @@ Must not own:

Required semantics:

- `default set` changes only `default_profile`
- `default set` validates that the target profile exists
- path inspection reports resolved config location only; broader state-path
expansion is optional follow-up work, not required by this ticket

Expand All @@ -137,7 +133,6 @@ Must not own:

- route preview output beyond what is needed for route list/mutation
- runtime route-resolution redesign
- default-profile mutation

Required semantics:

Expand Down Expand Up @@ -189,8 +184,7 @@ Must not own:
Required semantics:

- reuse the same repository-resolution path already used by runtime commands
- report whether resolution came from explicit `--profile`, matched route, or
fallback to `default_profile`
- report whether resolution came from explicit `--profile` or a matched route
- remain a local preview command, not an execution path
- if the existing runtime resolution path does not expose enough metadata for
preview output, `#169` may add a shared resolution-result wrapper/helper that
Expand Down Expand Up @@ -237,40 +231,30 @@ Path normalization contract:
This keeps mutation semantics deterministic and local-string-based rather than
filesystem-state-based.

### `#294`: secrets-management profile mutation
### `#294`: credential-store inspection

Owns:

- `cr config secrets-profile list [--json]`
- `cr config secrets-profile get <id> [--json]`
- `cr config secrets-profile set <id> [--backend <kind>] [--label <text>] [--clear-label] [--op-timeout <duration>] [--op-vault-id <id>] [--op-item-title-prefix <text>] [--op-item-tag <text>] [--op-item-field-title <text>] [--op-connect-host <url>] [--op-connect-token-env <name>] [--op-service-token-env <name>] [--op-desktop-account-id <id>]`
- `cr config secrets-profile default get [--json]`
- `cr config secrets-profile default set <id>`
- `cr config secrets-profile default unset`
- `cr config secrets-profile remove <id>`
- `cr config credential-store list [--json]`
- `cr config credential-store get <id> [--json]`

Must not own:

- secret ingress or secret-value printing
- runtime credential routing away from legacy `keyring.backend`
- review-profile references to secrets-management profiles before the later selection tickets
- creating, editing, deleting, or selecting credential stores for a write
- any user-facing credential-store default

Required semantics:

- `list` and `default get` report the effective secrets-profile inventory/default, including the projected `legacy-default` compatibility entry when no explicit profiles exist
- `default set` and `default unset` mutate only `config.secrets.default_profile`
- `default set` rejects `legacy-default` because it is reserved/projected, not configured state
- `set` is patch-style:
- create requires `--backend`
- update preserves omitted fields
- `--clear-label` explicitly clears the label
- `--label` plus `--clear-label` is a usage error
- whitespace-only `--label` is rejected
- update requires at least one mutation flag
- `--backend op`, `--backend op-connect`, and `--backend op-desktop` also require `--op-vault-id`; `op-connect` additionally requires `--op-connect-host`
- `remove` is idempotent for already-absent configured profiles, but rejects `legacy-default` and blocks removing the configured default profile until it is unset or moved
- `list` reports the effective credential-store inventory, including the
read-only built-in `local-os` store
- `get` reports one effective credential store by id
- configured stores are listed after `local-os` in stable order
- `local-os` is projected and is never persisted under `secrets.stores`
- JSON output must not include an `is_default` field for credential stores
- backend validation must reuse shared credstore metadata rather than duplicating backend-name knowledge in command code
- this ticket is config-only: mixed configs with both `keyring.backend` and `secrets.*` keep legacy runtime backend behavior until the later resolver ticket changes credential selection
- old `config secrets-profile ...` subcommands are rejected instead of acting as
compatibility aliases

## `init` Boundary

Expand Down Expand Up @@ -300,7 +284,7 @@ The likely future design questions remain:

- whether profile mutation should be patch-like or declarative replacement
- whether field-specific subcommands and `profile set` should coexist
- how profile mutation should interact with credential refs and route safety
- how profile mutation should interact with credential locations and route safety

Those questions belong to later work after the current true-up lands.

Expand Down
Loading
Loading