Skip to content

Release v0.48.0 - #6631

Merged
JAORMX merged 1 commit into
mainfrom
release/v0.48.0
Sep 10, 2026
Merged

JAORMX merged 1 commit into
mainfrom
release/v0.48.0

Conversation

@toolhive-release-app

Copy link
Copy Markdown
Contributor

Release v0.48.0

Version Bump

minor release

Files Updated

  • VERSION
  • deploy/charts/operator-crds/Chart.yaml (path: version)
  • deploy/charts/operator-crds/Chart.yaml (path: appVersion)
  • deploy/charts/operator/Chart.yaml (path: version)
  • deploy/charts/operator/Chart.yaml (path: appVersion)
  • deploy/charts/operator/values.yaml (path: operator.image)
  • deploy/charts/operator/values.yaml (path: operator.toolhiveRunnerImage)
  • deploy/charts/operator/values.yaml (path: operator.vmcpImage)
  • Helm chart docs (via helm-docs)

Next Steps

  1. Review this PR
  2. Merge to main
  3. Release automation will handle the rest

Checklist

  • Version bump is correct
  • All CI checks pass

Release-Triggered-By: JAORMX
@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.87%. Comparing base (4411a30) to head (eedede9).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6631      +/-   ##
==========================================
- Coverage   78.89%   78.87%   -0.02%     
==========================================
  Files         781      781              
  Lines       77784    77784              
==========================================
- Hits        61364    61356       -8     
- Misses      16415    16423       +8     
  Partials        5        5              

☔ View full report in Codecov by Harness.
📢 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.

@JAORMX
JAORMX merged commit c25e508 into main Sep 10, 2026
43 checks passed
@JAORMX
JAORMX deleted the release/v0.48.0 branch September 10, 2026 12:40
@github-actions

Copy link
Copy Markdown
Contributor

📝 Generated release notes for v0.48.0

Auto-generated by the release-notes skill. Review and, if good, apply with:

gh release edit v0.48.0 --notes-file <paste-below>.md
Click to expand release notes

🚀 Toolhive v0.48.0 is live!

A security-hardening release for the signing and authorization paths: key-bearing skill pushes now require a capability that only the local server owner can present, malformed MCP list responses fail closed instead of slipping past your policies, and key-pinned plugins finally work beyond install. Alongside that, OIDC upstreams gain Dynamic Client Registration and proxies gain a configurable request read timeout.

⚠️ Breaking Changes

  • Pre-registered OAuth2 upstreams now send their secret as HTTP Basic — if your identity provider only accepts credentials in the token-request body, add token_endpoint_auth_method: client_secret_post to that upstream before upgrading (migration guide below).
  • thv skill push --key now only works against the auto-discovered local server — keyed pushes via TOOLHIVE_API_URL, direct REST calls, or an older thv against a v0.48.0 server return 403; drop the override or sign keylessly (migration guide below).
  • thv skill upgrade --allow-signer-change no longer unpins key-pinned skills wholesale — it now drops a pin only for a skill that genuinely moved to keyless signing, and two blocked states changed exit code from 4 to 3 (migration guide below).
  • storage.UpstreamTokenStorage gained a method — Go embedders with their own auth-server storage implementation must add CompareAndSwapUpstreamTokens to compile (migration guide below).
Migration guide: OAuth2 upstreams now use HTTP Basic by default

Affects pre-registered (non-DCR) type: oauth2 upstreams that have a client secret configured, on both the CLI/run-config and operator surfaces. OIDC upstreams, DCR upstreams, and public clients with no secret are unaffected.

Before v0.48.0, OAuth2UpstreamRunConfig had no auth-method field, so the method was always empty — and authStyleFromMethod("") maps to oauth2.AuthStyleInParams, a hard selection of POST-body credentials with no auto-detect fallback. v0.48.0 defaults the method to client_secret_basic whenever a secret is configured, so the wire format of every upstream token request and refresh changes with no edit on your side. If the provider rejects Basic, the exchange fails and is not retried in POST-body form.

This is the RFC-preferred placement (RFC 6749 §2.3.1 requires servers to support Basic; RFC 7591 §2 makes it the default), so most providers cope. Leave the field unset wherever your provider accepts Basic.

Before

# authserver run config — v0.47.1 behavior: credentials in the POST body
upstreams:
  - name: acme
    type: oauth2
    oauth2_config:
      authorization_endpoint: https://idp.example.com/oauth/authorize
      token_endpoint: https://idp.example.com/oauth/token
      client_id: thv-upstream
      client_secret_env_var: UPSTREAM_CLIENT_SECRET

After

upstreams:
  - name: acme
    type: oauth2
    oauth2_config:
      authorization_endpoint: https://idp.example.com/oauth/authorize
      token_endpoint: https://idp.example.com/oauth/token
      client_id: thv-upstream
      client_secret_env_var: UPSTREAM_CLIENT_SECRET
      token_endpoint_auth_method: client_secret_post   # restores pre-0.48.0 wire format

Operator equivalent:

apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPExternalAuthConfig
spec:
  type: embeddedAuthServer
  embeddedAuthServer:
    upstreamProviders:
      - name: acme
        type: oauth2
        oauth2Config:
          clientID: thv-upstream
          clientSecretRef:
            name: upstream-oauth
            key: client_secret
          tokenEndpointAuthMethod: client_secret_post   # restores pre-0.48.0 wire format

Migration steps

  1. Inventory every type: oauth2 upstream that sets client_secret_file, client_secret_env_var, or clientSecretRef. DCR and OIDC upstreams need no action.
  2. Check each provider's token_endpoint_auth_methods_supported in its RFC 8414 / OIDC discovery document. If client_secret_basic is absent — or the provider is a custom non-discovery API — you must act.
  3. Also flag any client secret containing characters outside A-Za-z0-9-._~. x/oauth2 percent-encodes Basic credentials per RFC 6749 Appendix B, so a provider that skips form-decoding will read a different secret than it did from the POST body. Prefer client_secret_post, or rotate to an alphanumeric secret.
  4. Operator users: upgrade the operator-crds chart firsttokenEndpointAuthMethod does not exist in the v0.47.1 CRD schema and will be pruned or rejected.
  5. Add token_endpoint_auth_method: client_secret_post (run config) or tokenEndpointAuthMethod: client_secret_post (CRD) to the affected upstreams, then roll out v0.48.0.
  6. Verify with one interactive login plus one refresh per upstream. A wrong choice surfaces as invalid_client from the upstream token endpoint.

PR: #6543 — Fixes #6536

Migration guide: `thv skill push --key` requires the auto-discovered local server

POST /api/v1beta/skills/push accepts key, a path to a cosign private key that the thv serve process opens and signs with. Without independent authorization the management API is a signing oracle — and checking the peer address is not sufficient, because a reverse proxy can forward an untrusted request over loopback or a UNIX socket and make the caller look local.

v0.48.0 generates a 128-bit key-signing capability at server startup, stores it only in the owner-protected discovery file (0600, under $XDG_STATE_HOME/toolhive/server/server.json), and requires it on every key-bearing push. The CLI attaches it only when it resolves the server through automatic discovery.

Keyless signing (--identity-token or auto-acquired), --no-sign, and every non-push skills endpoint are unchanged.

Affected invocations — all previously succeeded, all now return 403

  1. TOOLHIVE_API_URL is set, even to the same local server — the env var short-circuits discovery, so no capability is attached.
  2. Discovery fails while the server is reachable at the default URL — missing, stale, or unhealthy server.json; a thv serve started by a different OS user (the 0600 file is unreadable); or a containerized thv serve with a published port.
  3. Any direct REST caller — curl, scripts, or third-party tooling built on pkg/skills/client.NewClient(baseURL) without WithKeySigningCapability.
  4. Version skew — an older thv CLI (≤ v0.47.1) against a v0.48.0 thv serve never sends the header.

Before

export TOOLHIVE_API_URL=http://127.0.0.1:8080
thv serve &
thv skill push ghcr.io/acme/my-skill:1.0.0 --key ./cosign.key

After

# Option 1 — keep --key, drop the override (same host, same OS user)
unset TOOLHIVE_API_URL
thv skill push ghcr.io/acme/my-skill:1.0.0 --key ./cosign.key

# Option 2 — sign keylessly (recommended; works in CI and remotely)
thv skill push ghcr.io/acme/my-skill:1.0.0
thv skill push ghcr.io/acme/my-skill:1.0.0 --identity-token "$ID_TOKEN"

# Option 3 — unsigned, if signing is not required
thv skill push ghcr.io/acme/my-skill:1.0.0 --no-sign

Migration steps

  1. Upgrade thv and thv serve to the same version before relying on --key.
  2. If you set TOOLHIVE_API_URL, unset it for keyed pushes, or switch to keyless signing.
  3. For CI, prefer keyless: the CLI acquires an ambient OIDC token (needs id-token: write), or pass --identity-token.
  4. Direct API callers: replace "key": "/path/to/cosign.key" with "identity_token": "<oidc-token>" in the push body. To keep key, you must be the server owner on the same host — read key_signing_capability from $XDG_STATE_HOME/toolhive/server/server.json and send it as X-Toolhive-Key-Signing-Capability. The credential is refused over plaintext remote transport and is never followed across redirects.
  5. If you switch a published artifact from keyed to keyless, tell consumers — their --public-key pins will need the key-to-keyless upgrade path or a project-scoped reinstall.

PR: #6560

Migration guide: `thv skill upgrade --allow-signer-change` is now scoped per entry

Affects thv skill upgrade on key-pinned skill entries. Previously the flag skipped the signer guard entirely and forwarded itself to the install, which dropped any recorded key — so it unpinned every key-pinned skill in the project, and a transient verifier fault during planning was enough to re-anchor an artifact that still carried a good signature by the pinned key. v0.48.0 measures each candidate against its pin once and only treats a conclusive keyed mismatch followed by a verifying keyless signature as a genuine move.

Most of this turns previously-broken outcomes into working ones: a key-pinned entry whose candidate is key-signed only used to drop the pin and then fail keyless verification at install time, and now succeeds. A genuine key-to-keyless move is still permitted by the flag, unchanged. Two outcomes do change from success to something else:

  • Dual-signed candidate (a valid pinned-key bundle and a valid keyless bundle) with --allow-signer-change: the upgrade still succeeds, but the key pin is now retained rather than re-anchored to keyless. A publisher mid-migration who used the flag to move consumers off a key will find it no longer re-anchors.
  • Undecided verdict (registry/transport/context fault, or a bundle that verifies neither way) with --allow-signer-change: previously the guard was skipped and the install could re-anchor to keyless and succeed; the entry now failed and the pin is left alone. This is the fail-open being closed.

Two blocked states also moved between exit codes — neither ever succeeded, but the code and the machine-readable status/reason in --format json changed:

Case Before After
Unsigned candidate under a pinned key signer-change-blocked (exit 4) failed [unsigned-rejected] (exit 3)
Keyless candidate whose signature does not verify signer-change-blocked (exit 4) failed (exit 3)

Before

# Dropped every key pin in the project, including ones still verifying
thv skill upgrade --allow-signer-change

After

# To intentionally drop a pin whose candidate still verifies against it:
thv skill uninstall <name> --scope project
thv skill install <source> --scope project --public-key <path>   # or omit to land keyless

Migration steps

  1. To intentionally drop a key pin whose candidate still verifies against the pinned key, use the uninstall/reinstall sequence the tool now prints — --allow-signer-change is no longer that mechanism. Add --project-root if you are not in the project directory.
  2. If an upgrade now reports failed with a verification error, re-run it: an operational fault no longer silently unpins.
  3. Update CI that distinguishes exit 4 (policy rejection) from exit 3 (failure), or that parses outcomes[].status, for keyed skill entries.

PR: #6628

Migration guide: `UpstreamTokenStorage` gained `CompareAndSwapUpstreamTokens`

Affects Go consumers who implement storage.UpstreamTokenStorage (or wrap storage.Storage) outside this repository — for example a Postgres-backed token store, or a decorator passed to authserver.New, runner.NewEmbeddedAuthServer, or upstreamtoken. There is no configuration, CLI, or protocol break, and StoreUpstreamTokens is untouched and still works. In-repo only RedisStorage and MemoryStorage implement it, and the generated mocks were regenerated, so mock users are unaffected.

Before

type MyStore struct{ /* ... */ }

func (s *MyStore) GetUpstreamTokens(ctx context.Context, sessionID, providerName string) (*storage.UpstreamTokens, error)
func (s *MyStore) StoreUpstreamTokens(ctx context.Context, sessionID, providerName string, tokens *storage.UpstreamTokens) error

After

// Add this method. The compare and the write must be atomic with respect to
// other writers of the same row.
func (s *MyStore) CompareAndSwapUpstreamTokens(
	ctx context.Context,
	sessionID, providerName, expectedRefreshToken string,
	tokens *storage.UpstreamTokens,
) error {
	// Return storage.ErrConcurrentRefresh when the stored refresh token no
	// longer equals expectedRefreshToken. Treat an absent/nil row and an empty
	// stored RefreshToken as matching an empty expectedRefreshToken.
	// A decorator can simply forward to the wrapped store.
}

Migration steps

  1. Add CompareAndSwapUpstreamTokens to every external UpstreamTokenStorage implementation, making the compare and write atomic against other writers of the same row.
  2. Return storage.ErrConcurrentRefresh on mismatch; treat an absent or nil row and an empty stored refresh token as matching an empty expectedRefreshToken.
  3. Decorators wrapping storage.Storage can forward straight to the wrapped store.
  4. Note one runtime difference on the refresh path: a refresh that races a logout or a TTL eviction now returns an error (one re-authentication) instead of silently resurrecting the deleted row.

PR: #6548

🆕 New Features

  • OIDC upstreams can now register themselves with your identity provider — set dcrConfig on a type: oidc upstream instead of pre-provisioning a clientId, and ToolHive derives the registration endpoint from the issuer's discovery document (#6544).
  • Proxies that need to accept slow or large uploads are no longer capped at 30 seconds — set thv run --proxy-read-timeout or MCPServer.spec.proxyReadTimeout to raise the request read timeout (#6285).
  • Plugins installed with thv ai-plugin install --public-key now work beyond install: sync reports them as current instead of drifting on every run, sync --check passes on an intact project, upgrade evaluates candidates against the pinned key, and sync --adopt explains that the plugin must be installed with --public-key instead of failing opaquely (#6525).
  • Projects embedding vMCP as a Go library can install their own dial-time SSRF/DNS-rebinding guard on the connections opened during session initialization, closing a gap relative to the existing tool-call and aggregation guards — note this is an embedder API only, with no CLI or operator flag to enable it yet (#6547).
  • Internal scaffolding landed for SPIFFE/SVID client authentication in the embedded auth server; the feature remains disabled and configuration-rejected in this build, and existing secret-, private_key_jwt-, and jwt-bearer-authenticated clients are unaffected (#6562).

🐛 Bug Fixes

  • When authorization policies are enabled, a managed MCP server returning a malformed or ambiguously encoded tool, prompt, or resource list now gets an error instead of having the list forwarded with your policies silently not applied (#6553).
  • Virtual MCP's find_tool now fails closed rather than returning a partially filtered result, and authorized tools are filtered consistently across both the text and structured parts of the response (#6555).
  • Dropping a backend from a Kubernetes vMCP deployment now promptly closes that backend's leftover per-session connections, including long-lived SSE streams, instead of leaking them until each client disconnects; affected clients keep their session, but their surviving backends reconnect on the next request (#6549) — Go embedders implementing server.SessionManager themselves need to add the new EvictStaleSessions method.

Known issue in the find_tool hardening above: with Cedar authorization enabled on Virtual MCP with the tool optimizer, a find_tool search matching zero tools currently returns a generic internal error instead of an empty list, because the optimizer emits "tools": null for that case. Other deployments are unaffected.

🧹 Misc

  • ToolHive's AI-plugin architecture, lifecycle, trust anchors, and CLI reference documentation now match shipped behavior, with stale claims about public-key verification, Codex registration, and keyless publishing corrected (#6627).
  • The issue-triage workflow runs again after switching to a model the Stacklok gateway actually serves (#6557).
  • Renovate can resolve the pinned arduino/setup-task action again, clearing a persistent dependency-dashboard warning without changing CI execution (#6565).

📦 Dependencies

Module Version
github.com/lestrrat-go/jwx/v3 v4 (major)
github.com/cenkalti/backoff/v5 v7 (major)
google.golang.org/grpc v1.83.2
golang.org/x/crypto v0.57.0
golang.org/x/sys v0.48.0
golang.org/x/term v0.46.0
golang.org/x/sync v0.23.0
golang.org/x/mod v0.41.0
golang.org/x/time v0.16.0
golang.org/x/exp/jsonrpc2 digest 85c1c22
go.starlark.net digest 89a6a09
modernc.org/sqlite v1.58.0
github.com/spiffe/go-spiffe/v2 v2.8.1
github.com/getsentry/sentry-go v0.49.0
github.com/go-chi/chi/v5 v5.3.2
github.com/gofrs/flock v0.13.1
github.com/charmbracelet/x/ansi v0.11.8
github.com/tidwall/gjson v1.19.0
github.com/pelletier/go-toml/v2 v2.4.3
github.com/pressly/goose/v3 v3.28.0
github.com/go-git/go-billy/v5 v5.9.1
github.com/moby/moby/api v1.56.0
github.com/moby/moby/client v0.6.0
github.com/testcontainers/testcontainers-go v0.44.0
github.com/1password/onepassword-sdk-go v0.4.1
github.com/ory/x v0.0.729
golang.ngrok.com/ngrok/v2 v2.2.0
aws-sdk-go-v2 monorepo grouped update
auth and security libraries grouped update
opentelemetry and observability grouped update
testing libraries grouped update
github/codeql-action v4.38.0
codecov/codecov-action v7 (major)
actions/setup-node v7 (major)
slackapi/slack-github-action v4 (major)
helm/kind-action v1.15.0
zizmorcore/zizmor-action v0.6.4
codespell-project/actions-codespell v2.4.3
anthropics/claude-code-action v1.0.220
core workflow actions grouped major update
setup and language actions grouped major update
container build actions grouped update
helm / helm/helm v3.22.0
kubernetes-sigs/kind v0.33.0
node v24 (major)
ghcr.io/stacklok/thv-registry-api v1.5.2
ghcr.io/modelcontextprotocol/inspector v2 (major)
ghcr.io/zizmorcore/zizmor v1.30.1
registry v3 (major)
dockerfile template base images grouped update
Full commit log

What's Changed

🔗 Full changelog: v0.47.1...v0.48.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release size/XS Extra small PR: < 100 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant