Skip to content

Release v0.49.0 - #6649

Merged
reyortiz3 merged 1 commit into
mainfrom
release/v0.49.0
Sep 11, 2026
Merged

Release v0.49.0#6649
reyortiz3 merged 1 commit into
mainfrom
release/v0.49.0

Conversation

@toolhive-release-app

Copy link
Copy Markdown
Contributor

Release v0.49.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: reyortiz3
@github-actions github-actions Bot added the size/XS Extra small PR: < 100 lines changed label Sep 11, 2026
@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.90%. Comparing base (abd975b) to head (1c0c7d9).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6649      +/-   ##
==========================================
- Coverage   78.92%   78.90%   -0.02%     
==========================================
  Files         782      782              
  Lines       78065    78065              
==========================================
- Hits        61615    61601      -14     
- Misses      16445    16459      +14     
  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.

@reyortiz3
reyortiz3 merged commit e532cf0 into main Sep 11, 2026
44 checks passed
@reyortiz3
reyortiz3 deleted the release/v0.49.0 branch September 11, 2026 18:41
@github-actions

Copy link
Copy Markdown
Contributor

📝 Generated release notes for v0.49.0

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

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

🚀 Toolhive v0.49.0 is live!

A security- and auth-correctness release: a signer-pin bypass in thv skill upgrade is closed, the embedded auth server's documented zero-downtime key rotation finally works, and AWS STS role claims now fail closed instead of silently handing out the fallback role. This release also ships a dependency-light generated Go client for the management API, and moves the project to Go 1.27.

⚠️ Breaking Changes

  • pkg/vmcp/session.WithDialControl removed — vMCP embedders who set a dial-control hook on the session factory get a compile error; wrap the hook in the new WithDialControlResolver (migration guide below).
  • OAuth2 upstream token-endpoint auth method default reverted — only affects upgrades from v0.48.0: pre-registered oauth2 upstreams with a client secret and no explicit tokenEndpointAuthMethod go back to sending credentials in the POST body instead of HTTP Basic; set client_secret_basic explicitly if your IdP requires it (migration guide below).
  • AWS STS role claims must be a string or a list of strings — object, number, boolean, or null role claims now fail closed with HTTP 403 instead of silently receiving the fallback role, and a bare-string claim now selects its mapped role (migration guide below).
  • Root Go module now requires Go 1.27, and go:// workloads default to golang:1.27-alpine — builds pinned to Go 1.26 with GOTOOLCHAIN=local fail, and go:// servers that do not compile under Go 1.27 need an explicit image pin (migration guide below).
Migration guide: session.WithDialControlsession.WithDialControlResolver

Affects Go embedders of vMCP that called session.WithDialControl — the option added in v0.48.0 by #6547. The option was address-blind, so every backend received the same net.Dialer.Control hook and a per-backend dial policy could not be expressed. It is replaced in place rather than deprecated alongside a second option.

On v0.49.0 the old call fails to compile with undefined: session.WithDialControl.

⚠️ pkg/vmcp/client.WithDialControl is unchanged. Only the pkg/vmcp/session option was renamed — do not migrate client.WithDialControl call sites.

Before

factory := session.NewSessionFactory(registry,
    session.WithDialControl(denyPrivateRanges),
)

After

factory := session.NewSessionFactory(registry,
    // Same hook for every backend — identical to v0.48.0 behavior.
    session.WithDialControlResolver(
        func(_ string) func(network, address string, c syscall.RawConn) error {
            return denyPrivateRanges
        },
    ),
)

Per-backend policy — the capability this unlocks. Returning nil for a workload leaves that backend on http.DefaultTransport, byte-for-byte identical to the no-hook path:

session.WithDialControlResolver(
    func(workloadID string) func(string, string, syscall.RawConn) error {
        if allowsPrivateDialing(workloadID) {
            return nil
        }
        return denyPrivateRanges
    },
)

Migration steps

  1. Find every session.WithDialControl( call site in the pkg/vmcp/session package — not pkg/vmcp/client, whose identically-named option is unchanged.
  2. Rename it to session.WithDialControlResolver.
  3. Wrap your existing hook in func(workloadID string) func(network, address string, c syscall.RawConn) error { return hook } to preserve v0.48.0 semantics exactly.
  4. Optionally branch on workloadID to vary policy per backend; return nil to leave a backend untouched.
  5. Make sure your resolver is goroutine-safe — it is invoked concurrently from the per-backend session-init goroutines. A panicking resolver is recovered per backend and excludes only that backend.
  6. Rebuild. If you are enforcing SSRF/DNS-rebinding protection, confirm the returned hook still inspects address — deciding allow/deny from workloadID alone provides no network-level protection.

PR: #6567

Migration guide: OAuth2 upstream tokenEndpointAuthMethod default

Affects anyone on v0.48.0 with a pure oauth2-type upstream provider that uses a pre-registered clientId plus a client secret and leaves tokenEndpointAuthMethod unset.

#6543 (shipped in v0.48.0, and only in v0.48.0) added the token_endpoint_auth_method field, but also made an unset field silently default to client_secret_basic whenever a secret was configured — flipping every existing pre-registered upstream from POST-body credentials to HTTP Basic with no opt-in. v0.49.0 restores the historical default while keeping the new field.

The auth style is strict, not probing: an unset method sends credentials in the token-request POST body and does not retry with Basic. Against a Basic-only IdP the exchange fails with invalid_client — on both initial login and token refresh.

  • Upgrading from v0.47.x or earlier → no change; v0.49.0 matches what you already had.
  • On v0.48.0 with an IdP that required POST body → v0.48.0 broke you and v0.49.0 fixes it.
  • On v0.48.0 with a Basic-only IdP → you must now opt in explicitly.

OIDC-type upstreams and Dynamic Client Registration upstreams are unaffected.

Before

apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPExternalAuthConfig
spec:
  type: embeddedAuthServer
  embeddedAuthServer:
    upstreamProviders:
      - name: my-idp
        type: oauth2
        oauth2Config:
          clientId: my-client
          clientSecretRef:
            name: idp-client-secret
            key: client-secret
          tokenEndpoint: https://idp.example.com/oauth2/token
          # unset -> v0.48.0 silently used client_secret_basic

After

        oauth2Config:
          clientId: my-client
          clientSecretRef:
            name: idp-client-secret
            key: client-secret
          tokenEndpoint: https://idp.example.com/oauth2/token
          tokenEndpointAuthMethod: client_secret_basic   # now required to get Basic

Raw auth-server run config:

upstreams:
  - name: my-idp
    type: oauth2
    oauth2_config:
      client_id: my-client
      client_secret_env_var: MY_IDP_CLIENT_SECRET
      token_endpoint: https://idp.example.com/oauth2/token
      token_endpoint_auth_method: client_secret_basic   # add this

Migration steps

  1. Confirm this applies: you are coming from v0.48.0 and use a pre-registered (non-DCR) oauth2 upstream with a client secret.
  2. Check your IdP's token_endpoint_auth_methods_supported in its discovery document, or its client registration. If only client_secret_basic is accepted, act.
  3. Set tokenEndpointAuthMethod: client_secret_basic on every affected upstreamProviders[].oauth2Config (spec.embeddedAuthServer.upstreamProviders[] for MCPExternalAuthConfig, spec.authServerConfig.upstreamProviders[] for VirtualMCPServer), or token_endpoint_auth_method under upstreams[].oauth2_config in a raw run config.
  4. Apply and restart the workload, then verify a full login and a token refresh — refresh uses the same auth style.
  5. If your IdP accepts either style, or requires the POST body, do nothing.

The CRD schema is unchanged apart from doc text, so there is no CRD upgrade ordering concern.

PR: #6648

Migration guide: AWS STS role claim shapes now fail closed

Affects deployments using an awsSts external auth config with claim-based roleMappings. Matcher-expression-only configurations are unaffected.

Role mappings are evaluated with the CEL expression claim_value in claims[role_claim_key], and CEL's in only has list and map overloads. Two bugs followed: a string role claim raised a swallowed "no such overload" error and silently produced the fallback role even on an exact match, and an object role claim made in test map-key membership, matching spuriously. Both are now corrected, and unsupported shapes fail closed rather than quietly granting a role.

Two behavior changes, both deliberate:

  1. A bare-string role claim exactly equal to a configured claim now selects its mapped role instead of fallbackRoleArn. Strings that merely contain the value still do not match.
  2. A role claim that is an object, number, boolean, or null now fails closed — HTTP 403 Failed to determine IAM role from the aws_sts middleware, or a failed backend call with failed to select IAM role in vMCP outbound auth.

A missing role claim still falls back exactly as before.

Before

{ "sub": "user1", "groups": { "admins": true } }
{ "sub": "user2", "groups": 7 }

After

{ "sub": "user1", "groups": ["admins"] }
{ "sub": "user1", "groups": "admins" }

Migration steps

  1. Decode a representative token for each IdP feeding an awsSts config and inspect the claim named by awsSts.roleClaim (default groups).
  2. List of strings → no action, behavior unchanged.
  3. Bare string → no config change needed, but confirm the outcome is intended: those users now receive the mapped role rather than fallbackRoleArn. Verify the mapped role's IAM trust policy accepts these subjects and that its permissions suit that population.
  4. Object, number, boolean, or null → change the IdP claim mapping to emit a string or a JSON array of strings (in Keycloak, use a multivalued group/role mapper and flatten nested claims like realm_access.roles to a top-level key — roleClaim is a flat lookup, not a dot path). Alternatively point roleClaim at a correctly-shaped claim, or convert those mappings to matcher CEL expressions, which are evaluated against the raw claims and are unaffected.
  5. Before rolling out, watch for the new WARN lines role claim has unsupported shape, failing closed and claim-based role mapping evaluation failed, failing closed — they name the offending role_arn. Note that CEL expression evaluation failed, skipping mapping was promoted from Debug to Warn, so pre-existing matcher-expression bugs will now appear at default log level.
  6. In a mixed configuration, re-check priorities: a claim mapping with a lower priority number than a previously-winning matcher mapping now wins for string claims.

PR: #6306 — Closes #6305

Migration guide: Go 1.27 toolchain and go:// builder image

Two separate audiences.

go:// workload users. The default builder image for go:// workloads moved from golang:1.26-alpine to golang:1.27-alpine. Only freshly built go:// workloads with no override are affected. Go's compatibility promise makes a failure unlikely, but a server relying on a removed deprecated API will not compile.

Downstream Go importers of the root module. github.com/stacklok/toolhive now declares go 1.27.0 with no toolchain directive. Under the default GOTOOLCHAIN=auto Go downloads 1.27 transparently; under GOTOOLCHAIN=local, a pinned-toolchain CI, an air-gapped build, or a distro-packaged Go, the build fails hard with go: go.mod requires go >= 1.27. The nested github.com/stacklok/toolhive/sdk/go module deliberately keeps its go 1.26.0 floor and is not affected.

Before

# ~/.toolhive/config.yaml — previously relied on the golang:1.26-alpine default
runtime_configs: {}

After

# Pin the previous builder image persistently
runtime_configs:
  go:
    builder_image: "golang:1.26-alpine"
    additional_packages:
      - ca-certificates
      - git

Migration steps

  1. For a one-off go:// run, pin per invocation: thv run go://github.com/example/server --runtime-image golang:1.26-alpine.
  2. For a persistent pin, set runtime_configs.go.builder_image in ~/.toolhive/config.yaml as above. additional_packages replaces rather than appends to the built-in ["ca-certificates", "git"], so list them explicitly. Only the builder stage is customizable for Go workloads; the runtime stage is always alpine:3.23.
  3. If you import the root module, upgrade your toolchain to Go 1.27+, or keep GOTOOLCHAIN=auto and allow Go to fetch the toolchain on demand.
  4. If you only need the management API client, depend on github.com/stacklok/toolhive/sdk/go instead — it retains the go 1.26.0 floor.
  5. In GitHub Actions, point setup-go at the root go-version-file: go.mod rather than pinning a version.

PR: #6639

🆕 New Features

  • A new github.com/stacklok/toolhive/sdk/go module provides a typed, generated client covering all 77 documented management API operations, with safe default timeout and response-size handling, without pulling in ToolHive's full application dependency graph (#6637).
  • Cedar policies can now govern the MCP SEP-2640 Skills extension on direct-proxied servers: skills/get maps to Action::"get_skill" on the skill's exact URI, and skills/list responses are filtered to the skills the caller may get — previously both methods were refused outright by default-deny, and skills/list without a get_skill permit now returns an empty list instead of a 403 (#6512).
  • thv ai-plugin push --key <cosign.key> is available again for publishers using automatic local server discovery, now that key-signed plugins can be verified at install time with thv ai-plugin install --public-key and pinned in toolhive.lock.yaml for later sync/upgrade; remote or manually configured API URLs must still sign keylessly (#6528).
  • The embedded auth server and vMCP Redis session storage can now connect to an unauthenticated Redis/Valkey instance by omitting the ACL user configuration, logging a startup WARN that names the store so an unintended downgrade stays visible (#6551).

🐛 Bug Fixes

  • Security: thv skill upgrade --allow-signer-change no longer doubles as unsigned consent — it previously succeeded against an unsigned candidate, silently dropping a signer-pinned skill's recorded identity and rewriting the lock entry as unsigned: true; both thv skill upgrade and thv ai-plugin upgrade now report failed [unsigned-rejected] and name the uninstall … --scope project then install … --scope project --allow-unsigned sequence that records the exception explicitly (#6629).
  • The auth server's /.well-known/jwks.json now publishes configured fallback keys alongside the signing key (primary first, de-duplicated by kid), making the documented three-step zero-downtime signing-key rotation actually work instead of a hard cutover that invalidated every outstanding JWT (#6638 — Closes #6451).
  • Progress notifications that arrived just before a request's final response are no longer silently dropped in the Streamable HTTP proxy — queued notifications/progress frames are flushed to the SSE stream, in backend order, before the response closes it (#6491 — Closes #6349).
  • VirtualMCPServer Deployments using spec.podTemplateSpec no longer get a metadata.generation bump and a spurious DeploymentUpdated event on every statusReportingInterval tick, including the 30s default — pod-template drift detection was comparing user-merged label maps for exact equality (#6377 — Fixes #6340).
  • OAuth error responses from the embedded auth server now preserve Fosite's RFC 6749 error codes and hints (invalid_client, invalid_grant, …) where a wrapped error could previously degrade to a generic server_error (#6639).

🧹 Misc

  • vMCP session dial control is now resolved per backend workload rather than through a single address-blind hook, so a deployment can enforce a different dial policy for each backend at session initialization (#6567).
  • Fixed a missing miniredis import that broke typecheck — and therefore every test — in pkg/authserver/runner on main (#6636).
  • Fixed the Go SDK verification job, which was installing Go 1.26 for root-module generator tooling that now requires 1.27, and refreshed the stale generated SDK artifacts (#6645).

📦 Dependencies

Module Version
github.com/stacklok/toolhive-core v0.0.47

Also migrates all Redis call sites from the now-deprecated toolhive-core/redis compatibility facade to redisconn directly (#6646).

👋 Welcome to our newest contributor: @isaacgao4396 🎉

Full commit log

What's Changed

New Contributors

Full Changelog: v0.48.0...v0.49.0

🔗 Full changelog: v0.48.0...v0.49.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