Skip to content

Wire all secret callers to scoped and user providers#4343

Closed
amirejaz wants to merge 11 commits intomainfrom
phase4-migration
Closed

Wire all secret callers to scoped and user providers#4343
amirejaz wants to merge 11 commits intomainfrom
phase4-migration

Conversation

@amirejaz
Copy link
Copy Markdown
Contributor

Summary

  • All secret provider call sites were previously using the bare CreateSecretProvider, giving every caller access to the full flat keyspace. This PR wires each caller to the appropriate wrapper introduced in Phase 1 (Add scoped and user secret providers with system key isolation #4229), so secrets are isolated by design rather than by convention.
  • System callers (workload auth tokens, registry OAuth credentials, build auth files) now use CreateScopedSecretProvider, writing and reading under the __thv_<scope>_ prefix. These keys are invisible to user-facing secret commands.
  • User-facing callers (thv secret commands, REST API, MCP tool server, header secrets, build-env-from-secrets) now use CreateUserSecretProvider, which blocks access to any __thv_* reserved key.
  • RunConfig.WithSecrets and ValidateSecrets now take separate systemProvider and userProvider arguments so auth-token resolution and --secret flag resolution use the correct scope independently.

This is Phase 3 of the scoped secret store (part of #4192), tracking issue #4227. It must be released together with Phase 4 (#4244 — migration infrastructure).

Release note: Both this PR and the migration PR (#4244) must be merged before releasing. Neither is a breaking change on its own when no release occurs between the two merges.

Type of change

  • New feature (non-breaking change which adds functionality)

Test plan

  • go build ./... — clean build
  • go test ./pkg/runner/... ./pkg/auth/secrets/... ./pkg/workloads/... ./pkg/mcp/server/... — all pass
  • golangci-lint run — 0 issues

Special notes for reviewers

The WithSecrets signature change (one provider → two providers) is the most structurally significant change. The split is:

  • systemProvider — used for RemoteAuthConfig.ClientSecret, RemoteAuthConfig.BearerToken (both written by the workload auth subsystem under ScopeWorkloads)
  • userProvider — used for c.Secrets (--secret flags) and HeaderForward.AddHeadersFromSecret (both user-specified key names)

config_buildauthfile.go gets its own getScopedWorkloadsSecretsManager() helper rather than reusing the user-facing getSecretsManager(), since build auth files are system-owned (stored by thv config buildauthfile set, not by thv secret set).

Generated with Claude Code

@github-actions github-actions bot added the size/M Medium PR: 300-599 lines changed label Mar 24, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 24, 2026

Codecov Report

❌ Patch coverage is 40.74074% with 64 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.63%. Comparing base (e69251a) to head (c53dbc8).

Files with missing lines Patch % Lines
pkg/migration/secret_scope.go 0.00% 32 Missing ⚠️
pkg/auth/secrets/secrets.go 0.00% 15 Missing ⚠️
pkg/runner/runner.go 0.00% 6 Missing ⚠️
pkg/runner/config.go 66.66% 2 Missing and 1 partial ⚠️
pkg/runner/protocol.go 0.00% 2 Missing ⚠️
pkg/workloads/manager.go 0.00% 2 Missing ⚠️
pkg/api/v1/secrets.go 0.00% 1 Missing ⚠️
pkg/mcp/server/list_secrets.go 0.00% 1 Missing ⚠️
pkg/mcp/server/set_secret.go 0.00% 1 Missing ⚠️
pkg/runner/env.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4343      +/-   ##
==========================================
- Coverage   69.64%   69.63%   -0.02%     
==========================================
  Files         491      493       +2     
  Lines       50304    50388      +84     
==========================================
+ Hits        35036    35088      +52     
- Misses      12580    12616      +36     
+ Partials     2688     2684       -4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
@amirejaz amirejaz marked this pull request as draft March 24, 2026 14:55
@amirejaz amirejaz force-pushed the scoped-secret-providers branch from e52d21b to e13fd99 Compare March 26, 2026 10:46
@github-actions github-actions bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Mar 26, 2026
@github-actions github-actions bot added size/L Large PR: 600-999 lines changed and removed size/M Medium PR: 300-599 lines changed labels Mar 26, 2026
@amirejaz amirejaz marked this pull request as ready for review March 26, 2026 13:54
@github-actions github-actions bot added size/L Large PR: 600-999 lines changed and removed size/L Large PR: 600-999 lines changed labels Mar 26, 2026
@github-actions github-actions bot added size/L Large PR: 600-999 lines changed and removed size/L Large PR: 600-999 lines changed labels Mar 27, 2026
@github-actions github-actions bot added size/L Large PR: 600-999 lines changed and removed size/L Large PR: 600-999 lines changed labels Mar 27, 2026
@github-actions github-actions bot added size/L Large PR: 600-999 lines changed and removed size/L Large PR: 600-999 lines changed labels Mar 27, 2026
@amirejaz amirejaz requested a review from reyortiz3 March 27, 2026 12:10
@github-actions github-actions bot added size/L Large PR: 600-999 lines changed and removed size/L Large PR: 600-999 lines changed labels Mar 27, 2026
Base automatically changed from scoped-secret-providers to main March 30, 2026 13:44
amirejaz and others added 10 commits March 31, 2026 19:16
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>
@github-actions github-actions bot added size/L Large PR: 600-999 lines changed and removed size/L Large PR: 600-999 lines changed labels Mar 31, 2026
@amirejaz
Copy link
Copy Markdown
Contributor Author

Closing in favor of a new PR from the correct head branch (phase3-wire-callers). The new PR will show only wire callers changes without including migration commits.

@amirejaz amirejaz closed this Mar 31, 2026
@github-actions github-actions bot removed the size/L Large PR: 600-999 lines changed label Mar 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants