Add crash-safe migration for legacy system secret keys#4346
Open
Add crash-safe migration for legacy system secret keys#4346
Conversation
586f764 to
516c58c
Compare
bfbee20 to
1fd9f57
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4346 +/- ##
==========================================
- Coverage 69.61% 69.58% -0.04%
==========================================
Files 494 496 +2
Lines 50457 50518 +61
==========================================
+ Hits 35126 35151 +25
- Misses 12635 12669 +34
- Partials 2696 2698 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
1fd9f57 to
1d6f2dc
Compare
516c58c to
a943793
Compare
1d6f2dc to
67d36b9
Compare
reyortiz3
reviewed
Mar 26, 2026
| DefaultGroupMigration bool `yaml:"default_group_migration,omitempty"` | ||
| TelemetryConfigMigration bool `yaml:"telemetry_config_migration,omitempty"` | ||
| MiddlewareTelemetryMigration bool `yaml:"middleware_telemetry_migration,omitempty"` | ||
| SecretScopeMigration bool `yaml:"secret_scope_migration,omitempty"` |
Contributor
There was a problem hiding this comment.
maybe consider in the future something like Migrations map[string]bool?
Contributor
Author
There was a problem hiding this comment.
Good idea — a Migrations map[string]bool would let us add future one-shot migrations without touching the struct shape each time. Flagging as a follow-up refactor once this stack lands; don't want to mix a config-schema change into this PR.
reyortiz3
reviewed
Mar 26, 2026
reyortiz3
reviewed
Mar 26, 2026
reyortiz3
reviewed
Mar 26, 2026
| // leaves the secret reachable under the new key. Keys that do not exist in | ||
| // provider are silently skipped, making the function safe to retry. | ||
| func MigrateSystemKeys(ctx context.Context, provider Provider, migrations []KeyMigration) error { | ||
| for _, m := range migrations { |
Contributor
There was a problem hiding this comment.
nit: keyMigrations instead of migrations?
67d36b9 to
4f55374
Compare
a4e622b to
f55ffdc
Compare
5ec8b68 to
f9d6728
Compare
107da0e to
4a005ee
Compare
When a user upgrades ToolHive, system secrets may still exist under bare keys (e.g. BEARER_TOKEN_foo) until the secret scope migration completes. If migration fails or hasn't run yet, ScopedProvider callers would be unable to find their secrets under the new scoped key (__thv_workloads_BEARER_TOKEN_foo), breaking workload auth. Add a transparent fallback in ScopedProvider.GetSecret: on a not-found response for the scoped key, also try the bare (pre- migration) key. Once migration completes and bare keys are deleted, the fallback finds nothing and becomes a natural no-op — no config check or injection needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add t.Parallel() to TestScopedProvider_GetSecret_MigrationFallback and its subtests to satisfy the paralleltest linter requirement. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update every call site that creates a secrets provider to use the appropriate wrapper introduced in Phase 1: - System callers (workload auth tokens, registry credentials, build auth files) now use CreateScopedSecretProvider, placing secrets under the __thv_<scope>_ prefix and hiding them from user-facing commands. - User-facing callers (thv secret commands, REST API, MCP tool server, header secrets, build-env-from-secrets) now use CreateUserSecretProvider, blocking access to __thv_* reserved keys. - RunConfig.WithSecrets and ValidateSecrets now accept separate system and user providers so auth-token resolution and --secret flag resolution use the correct scope independently. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Verify that the UserProvider wiring in CLI secret commands correctly blocks access to __thv_* keys: - set with __thv_ prefix is rejected - get with __thv_ prefix is rejected with "reserved for system use" - delete with __thv_ prefix is rejected - two distinct __thv_ key names are blocked, confirming the check is not tied to a single hardcoded name Each test uses an isolated XDG_CONFIG_HOME/HOME temp directory so the user's real secrets store is never touched. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add GetUserSecretsProvider() to pkg/auth/secrets alongside the existing GetSecretsManager(), centralising the config-read + CreateUserSecretProvider boilerplate in one place - Remove getScopedWorkloadsSecretsManager() from config_buildauthfile.go; it was identical to authsecrets.GetSecretsManager() — use that directly - Replace inline boilerplate in config_buildenv.go/validateSecretExists and secret.go/getSecretsManager with calls to GetUserSecretsProvider(), eliminating duplicated setup-check + provider-create blocks - Rename local variable manager → provider in GetSecretsManager() to match the naming convention used throughout the new provider code Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Existing users may have registry tokens and workload auth secrets stored under bare keys (BEARER_TOKEN_, OAUTH_CLIENT_SECRET_, REGISTRY_OAUTH_, etc.) that pre-date the scoped provider wrappers. This migration renames them into the __thv_<scope>_ namespace on first startup so they are accessible via the new scoped providers and hidden from user-facing secret commands. Key design properties: - Write-before-delete ordering: the new key is written before the old is deleted, so a crash mid-migration leaves the secret reachable. - Idempotent: a missing old key is silently skipped, making retries safe. - One-shot: guarded by the SecretScopeMigration config flag; once set, the migration is a cheap config read and returns immediately. - Discovery-based: DiscoverMigrations lists all secrets and matches known system prefixes, so no static registry of workload names is required. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Follow-up from the SecretScope type change: update SystemKeyPrefixMappings Scope field from string to SecretScope, and replace the removed package-level scopedKey() function with inline key construction. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename local variable migrations → keyMigrations in DiscoverMigrations for clarity (distinguishes the slice from the function parameter name used in MigrateSystemKeys) - Replace inline strings.HasPrefix(key, SystemKeyPrefix) with isSystemKey(key) to reuse the existing helper and keep the intent explicit Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace CreateUserSecretProvider and CreateScopedSecretProvider with the new CreateProvider(managerType, WithUserFacing()) and CreateProvider(managerType, WithScope(scope)) API introduced in the phase2-factory-helpers PR. Affected callers: - cmd/thv/app/registry_login.go - cmd/thv/app/header_flags.go - pkg/auth/secrets/secrets.go - pkg/runner/env.go - pkg/runner/runner.go - pkg/runner/protocol.go - pkg/mcp/server/set_secret.go - pkg/mcp/server/list_secrets.go - pkg/workloads/manager.go - pkg/api/v1/secrets.go Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5882adb to
500e0bd
Compare
4a005ee to
d9168dc
Compare
d9168dc to
8dabc6a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
BEARER_TOKEN_,OAUTH_CLIENT_SECRET_,REGISTRY_OAUTH_, etc.) that pre-date the scoped provider wrappers. Without migration, those keys would be unreachable after Phase 3 wires callers to useScopeWorkloads/ScopeRegistryproviders.MigrateSystemKeysandDiscoverMigrationsinpkg/secrets/migration.go: discovers bare system keys by known prefix and renames them into the__thv_<scope>_namespace using write-before-delete ordering for crash safety.CheckAndPerformSecretScopeMigrationinpkg/migration/secret_scope.go: guards migration behind aSecretScopeMigrationconfig flag (same pattern as existing migrations) and is triggered fromcmd/thv/main.goalongside the other startup migrations.This is Phase 4 of the scoped secret store (part of #4192), tracking issue #4226. It is stacked on Phase 3 (#4343) and must be released at the same time.
Type of change
Test plan
go build ./...— clean buildgo test ./pkg/secrets/... ./pkg/migration/...— all pass, includingTestMigrateSystemKeysandTestDiscoverMigrationsgolangci-lint run— 0 issuesSpecial notes for reviewers
Key design properties of the migration:
GetSecretreturns a not-found error and the entry is silently skipped.DiscoverMigrationslists all secrets and matches againstSystemKeyPrefixMappings; no static workload registry is required.SecretScopeMigrationconfig flag prevents re-running on every startup after migration completes.CreateSecretProvider(bare, no wrapper) so it can enumerate and rename both old bare keys and already-scoped__thv_*keys without restriction.Generated with Claude Code