Conversation
jaxxstorm
commented
Apr 15, 2026
- feat(config): add oauth-backed profiles and age secret encryption
- feat(config): add age key path and interactive profile setup
- feat(config): add interactive setup and age key reuse
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7d4324f0da
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Adds OAuth-backed authentication and AGE-based encryption for persisted config secrets, plus interactive setup flows, to make tscli usable without long-lived API keys while improving credential-at-rest security.
Changes:
- Add AGE encryption config + encrypted sibling fields for profile secrets, including encryption setup and interactive
config setupflows. - Expand runtime auth to support OAuth client-credentials exchange for general API requests (not just lifecycle commands).
- Update CLI/docs/tests for new commands (
config encryption setup,config setup) and renamed profile command (config profiles set).
Reviewed changes
Copilot reviewed 55 out of 56 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| test/docs/docs_test.go | Adds assertions that docs mention encryption + OAuth profile concepts. |
| test/cli/testdata/leaf_commands.txt | Updates expected leaf commands to include new config commands and rename profiles upsert -> profiles set. |
| test/cli/test_harness_test.go | Adds CLI execution helpers that can provide stdin input to commands. |
| test/cli/persistent_prerun_runtime_config_test.go | Adds integration coverage for OAuth profiles and decrypting encrypted profiles at runtime. |
| test/cli/example_output_test.go | Adds example coverage for new setup/encryption commands and input-driven examples. |
| test/cli/config_profiles_integration_test.go | Expands integration coverage for encryption setup, interactive setup, encrypted persistence, and delete semantics. |
| pkg/tscli/client.go | Adds OAuth bearer-token transport for tsapi client when API key is absent. |
| pkg/oauth/exchange.go | Adds ExchangeClientCredentialsAtURL helper to support configurable token endpoints. |
| pkg/config/profiles_test.go | Updates deletion behavior tests and adds promotion test when deleting active profile. |
| pkg/config/profiles.go | Adds encrypted secret fields, decryption during resolution, command-auth resolver, and active-profile delete semantics. |
| pkg/config/encryption_test.go | Adds unit tests for AGE config validation, encrypt/decrypt helpers, identity inspection, and path expansion. |
| pkg/config/encryption.go | Implements AGE config loading/validation, encryption/decryption, identity resolution, and persistence helpers. |
| pkg/config/config.go | Canonicalizes persisted encryption settings by removing empty keys/blocks. |
| openspec/specs/multi-tailnet-config-profiles/spec.md | Updates spec for encrypted secret fields and OAuth-backed API auth behavior. |
| openspec/specs/interactive-config-setup/spec.md | Adds spec for interactive config setup onboarding/management flow. |
| openspec/specs/config-secret-encryption/spec.md | Adds spec for AGE-based secret encryption and setup workflows. |
| openspec/specs/config-and-auth-documentation/spec.md | Updates documentation requirements to include encryption and OAuth-backed API usage. |
| openspec/changes/archive/2026-04-15-improve-interactive-encryption-setup/tasks.md | Adds archived change plan/tasks for interactive encryption setup improvements. |
| openspec/changes/archive/2026-04-15-improve-interactive-encryption-setup/specs/interactive-config-setup/spec.md | Archives modified interactive setup requirements. |
| openspec/changes/archive/2026-04-15-improve-interactive-encryption-setup/specs/config-secret-encryption/spec.md | Archives modified encryption requirements. |
| openspec/changes/archive/2026-04-15-improve-interactive-encryption-setup/proposal.md | Archives proposal for interactive encryption improvements. |
| openspec/changes/archive/2026-04-15-improve-interactive-encryption-setup/design.md | Archives design notes for interactive encryption improvements. |
| openspec/changes/archive/2026-04-15-improve-interactive-encryption-setup/.openspec.yaml | Adds archive metadata. |
| openspec/changes/archive/2026-04-15-improve-config-setup/tasks.md | Adds archived tasks for initial config setup flow. |
| openspec/changes/archive/2026-04-15-improve-config-setup/specs/multi-tailnet-config-profiles/spec.md | Archives modified multi-tailnet profile requirements. |
| openspec/changes/archive/2026-04-15-improve-config-setup/specs/interactive-config-setup/spec.md | Archives added interactive setup requirements. |
| openspec/changes/archive/2026-04-15-improve-config-setup/proposal.md | Archives proposal for config setup flow. |
| openspec/changes/archive/2026-04-15-improve-config-setup/design.md | Archives design for config setup flow. |
| openspec/changes/archive/2026-04-15-improve-config-setup/.openspec.yaml | Adds archive metadata. |
| openspec/changes/archive/2026-04-15-add-oauth-credentials-and-config-encryption/tasks.md | Archives tasks for OAuth + encryption feature set. |
| openspec/changes/archive/2026-04-15-add-oauth-credentials-and-config-encryption/specs/multi-tailnet-config-profiles/spec.md | Archives modified requirements for OAuth-backed API usage + encryption. |
| openspec/changes/archive/2026-04-15-add-oauth-credentials-and-config-encryption/specs/config-secret-encryption/spec.md | Archives added encryption requirements. |
| openspec/changes/archive/2026-04-15-add-oauth-credentials-and-config-encryption/specs/config-and-auth-documentation/spec.md | Archives modified documentation requirements. |
| openspec/changes/archive/2026-04-15-add-oauth-credentials-and-config-encryption/proposal.md | Archives proposal for OAuth + encryption. |
| openspec/changes/archive/2026-04-15-add-oauth-credentials-and-config-encryption/design.md | Archives design for OAuth + encryption. |
| openspec/changes/archive/2026-04-15-add-oauth-credentials-and-config-encryption/.openspec.yaml | Adds archive metadata. |
| internal/cli/root.go | Switches prerun config validation to new command-auth resolver. |
| go.sum | Adds new module sums for AGE and Bubble Tea dependencies. |
| go.mod | Adds filippo.io/age and related indirect deps. |
| docs/configuration.md | Documents encryption keys, encrypted secret fields, and updated profile commands. |
| docs/authentication.md | Documents OAuth-backed API auth and optional AGE encryption workflow. |
| docs/commands/tscli_config_profiles_set.md | Adds generated docs for renamed config profiles set. |
| docs/commands/tscli_config_profiles.md | Updates command list to reference set instead of upsert. |
| docs/commands/tscli_config_encryption_setup.md | Adds generated docs for config encryption setup. |
| docs/commands/tscli_config_encryption.md | Adds generated docs for config encryption command group. |
| docs/commands/tscli_config.md | Adds link to config encryption command group. |
| docs/commands/_sidebar.md | Updates sidebar links for new/renamed commands. |
| docs/commands/README.md | Updates generated README links for new/renamed commands. |
| cmd/tscli/config/setup/cli_test.go | Adds unit tests for setup model behavior and key path handling. |
| cmd/tscli/config/setup/cli.go | Implements interactive tscli config setup (Bubble Tea + prompt fallback) including encryption provisioning. |
| cmd/tscli/config/profiles/upsert/cli.go | Renames command to set and adds interactive prompting for missing auth values. |
| cmd/tscli/config/profiles/deleteprofile/cli.go | Updates delete command description to reflect new delete semantics. |
| cmd/tscli/config/encryption/setup/cli.go | Implements tscli config encryption setup with path/env/command private key sourcing and reuse detection. |
| cmd/tscli/config/encryption/cli.go | Adds config encryption command group. |
| cmd/tscli/config/cli.go | Registers config setup and config encryption under the config command group. |
| README.md | Updates examples to use config profiles set instead of upsert. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if strings.TrimSpace(privateKeySource) == "" { | ||
| fmt.Fprintln(cmd.OutOrStdout(), "Private key source [path|env|command]: ") | ||
| value, err := reader.ReadString('\n') | ||
| if err != nil { | ||
| return err | ||
| } | ||
| privateKeySource = strings.TrimSpace(value) | ||
| } |
There was a problem hiding this comment.
This prompt uses Fprintln, which prints a newline before reading input. That makes the “prompt” behave like a message rather than an inline prompt (cursor moves to the next line) and is inconsistent with the other prompts in this command. Prefer Fprint so the user can type on the same line and stdout matches expected prompt formatting.
| type oauthBearerTransport struct { | ||
| rt http.RoundTripper | ||
| clientID string | ||
| clientSecret string | ||
| tokenURL string | ||
|
|
||
| mu sync.Mutex | ||
| accessToken string | ||
| } | ||
|
|
||
| func (t *oauthBearerTransport) RoundTrip(req *http.Request) (*http.Response, error) { | ||
| accessToken, err := t.token(req.Context()) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| clone := req.Clone(req.Context()) | ||
| clone.Header = req.Header.Clone() | ||
| clone.Header.Set("Authorization", "Bearer "+accessToken) | ||
| return t.rt.RoundTrip(clone) | ||
| } | ||
|
|
||
| func (t *oauthBearerTransport) token(ctx context.Context) (string, error) { | ||
| t.mu.Lock() | ||
| defer t.mu.Unlock() | ||
| if t.accessToken != "" { | ||
| return t.accessToken, nil | ||
| } | ||
| resp, err := oauth.ExchangeClientCredentialsAtURL(ctx, t.clientID, t.clientSecret, t.tokenURL) | ||
| if err != nil { | ||
| return "", fmt.Errorf("exchange oauth credentials: %w", err) | ||
| } | ||
| t.accessToken = resp.AccessToken | ||
| return t.accessToken, nil | ||
| } |
There was a problem hiding this comment.
oauthBearerTransport caches the access token forever (t.accessToken), but the token response includes expiry. After expiration, all API calls will start failing until the process restarts. Track the token expiry (e.g., store expiresAt/expiresIn) and refresh when expired (or on 401) to keep long-running commands/retries reliable.
| ``` | ||
| -h, --help help for setup | ||
| --private-key string AGE private key stored in config when --private-key-source=config | ||
| --private-key-command string Command that returns the AGE private key when --private-key-source=command | ||
| --private-key-source string How to provide the AGE private key: config, env, or command | ||
| --public-key string AGE public key used to encrypt persisted secrets | ||
| ``` |
There was a problem hiding this comment.
This generated command doc doesn’t match the actual config encryption setup command implementation: it documents --private-key and --private-key-source=config but the command uses --private-key-path and private-key-source=path (and doesn’t define --private-key). Regenerate the docs (or adjust the command flags) so help text and docs stay consistent.
| if tailnet == "" { | ||
| return nil, fmt.Errorf("tailnet is required") | ||
| } | ||
| if apiKey == "" { | ||
| if apiKey == "" && (oauthClientID == "" || oauthClientSecret == "") { | ||
| return nil, fmt.Errorf("api-key is required") | ||
| } |
There was a problem hiding this comment.
In New(), the credential validation now allows OAuth, but the returned error still says "api-key is required". This will be misleading when users forget to supply OAuth credentials. Consider updating the message to reflect that either an API key or an OAuth client id/secret pair is required (or include which pieces are missing).