Skip to content

Merge upstream v0.23.1 into dp-stable - #9

Merged
carlos-lopez-vecino merged 118 commits into
dp-stablefrom
chore/merge-upstream-v0.23.1
Sep 29, 2026
Merged

carlos-lopez-vecino merged 118 commits into
dp-stablefrom
chore/merge-upstream-v0.23.1

Conversation

@carlos-lopez-vecino

Copy link
Copy Markdown

Jira Issue

Summary

v0.23.1 is the target of the toolhive upgrade: v0.23.0 plus an auth fix. After the PAIINFRA-198 merge, dp-stable sits at v0.19.0. v0.20–v0.23 bring the v1beta1 API. Storage moves to v1beta1 with conversion: None, and the schemas are identical to v1alpha1. So one merge covers the whole range. The operator CRDs are shared by the prod (v0.19) and stg operators in every cluster, so this merge also adds shims that keep the new CRDs admitting the objects prod still has.

Nothing is deployed by this PR. The rollout is PAIINFRA-201.

  • Merges upstream v0.23.1 into dp-stable (117 upstream commits, 38 conflicting files). Merge with a merge commit, not squash.
  • MCPServer.spec.groupRef shim. Upstream v0.20 made it object-only, but every prod MCPServer uses a plain string. The field is now schemaless in the CRD, and MCPGroupRef.UnmarshalJSON decodes both "name" and {name: ...}. Any other shape decodes to an empty name, so a single bad object can't break list/watch decoding in the operator; GroupRefValidated then reports it.
  • VirtualMCPServer.spec.groupRef shim. It stays optional in the CRD, because prod objects don't set it until PAIINFRA-201. The controller still rejects objects without it.
  • Keeps every fork-only CRD field: sessionTTL, sessionStorage.usernameRef, allowedClientIds, optional oidcConfigRef.audience, redis address, and the secrets list keys (name+key).
  • Carries every DocPlanner patch. The non-obvious conflict resolutions are listed under "Special notes".

Type of change

  • Other (describe): upstream release merge

Test plan

  • Unit tests (task test)

  • Linting (task lint-fix; unrelated rewrites it made to upstream files were reverted)

  • Manual testing (describe below)

  • task build, go vet ./..., task operator-generate, task operator-manifests, task crdref-gen and task helm-docs all ran. The generated files are committed.

  • task test: two packages fail, and both fail the same way on dp-stable before this merge: pkg/vmcp/session/internal/security (TestRestoreHijackPrevention_AuthenticatedRoundTrip) and pkg/webhook/validating (TestValidatingMiddleware_HTTP422AlwaysDenies). pkg/vmcp/aggregator has a data race in a fork test that is flaky on both branches.

  • task lint: 86 findings, against 90 on dp-stable. Nothing new beyond the capped goconst list reporting different test files.

  • envtest integration suites (k8s 1.31): EmbeddingServer, MCPExternalAuthConfig, MCPGroup, MCPOIDCConfig, MCPTelemetryConfig and MCPToolConfig pass. MCPRegistry (17/21), MCPRemoteProxy (4) and MCPServer (1) fail with the same specs on dp-stable. The VirtualMCPServer composite-tool watch spec is flaky under -p on both branches, and passes when run alone.

  • New envtest cases in mcpserver_cel_validation_integration_test.go check the groupRef shim against a real API server:

    • the string form is admitted and decoded through both v1beta1 and v1alpha1;
    • the object form works;
    • a malformed value doesn't break List.
  • Prod objects against the new CRDs. I rendered toolhive-config / toolhive-config-stg from platform-apps@develop for every environment. Each toolhive CR was then dry-run created on an envtest API server and decoded into the new v1beta1 Go types:

    • control, current CRDs from dp-stable: 263/263 pass;
    • new CRDs, prod on base chart 0.17.2 (today's prod pin): 258/263. The 5 failures are the i0 hub VirtualMCPServers, whose outgoingAuth.backends.*.type: external_auth_config_ref was removed upstream in v0.21;
    • new CRDs, prod on base chart 0.18.0 (PAIINFRA-199): 180/180 pass, including all 95 MCPServers with a string groupRef.

Does this introduce a user-facing change?

No. Nothing is deployed until PAIINFRA-201.

Special notes for reviewers

Rollout gate for PAIINFRA-201: prod must run toolhive-config ≥ 0.18.0 before these CRDs roll out, or the 5 i0 hub VirtualMCPServers stop validating.

For review, compare against upstream: git diff v0.23.1 origin/chore/merge-upstream-v0.23.1 is the full fork delta (177 files, mostly fork code carried over from before this merge). git diff origin/dp-stable... is dominated by the upstream changes.

Non-obvious resolutions:

  • CEL can't validate the groupRef shim. The ticket planned a CEL rule, but Kubernetes can't build CEL type information for a schemaless field. That's true both from the parent and on the field itself; the API server rejects the CRD. Validation therefore happens in the operator, not at admission.
  • vMCP serve. Upstream moved it to pkg/vmcp/cli (Extract vMCP serve/validate logic into pkg/vmcp/cli/ stacklok/toolhive#4891). The fork's backend init/request timeouts, sessionTTL, write timeout and session-owner URL are ported to pkg/vmcp/cli/serve_timeouts.go, along with their tests.
  • Session manager. The fork implementation is kept, as in the v0.19 merge.
  • Backend session hints on restore (Add horizontal scaling tests for vmcp stacklok/toolhive#4724) are accepted but not forwarded. Sending a stored Mcp-Session-Id on Initialize would fail restores against backends that reject unknown sessions (e.g. mcp-for-argocd). The fork already recovers lost sessions by reconnecting.
  • Auth-server token handler. It keeps the fork's default audience for every grant type, which client credentials depend on. Upstream Grant default aud when resource is absent stacklok/toolhive#4805 limits that to authorization_code. The one upstream change taken: an empty resource counts as absent.
  • MCPOIDCConfigReference.resourceUrl was added by both sides, and git's rename detection produced a duplicate field. It's collapsed into upstream's.
  • Operator RBAC. Regenerating the operator ClusterRole overwrote the fork's release-qualified name (Release-qualify operator RBAC object names #5), so that change was reverted.
  • Legacy inline OIDC. v0.21 removed it; one fork test that used it now uses anonymous auth, since it only exercises authz mapping.

🤖 Generated with Claude Code

samuv and others added 30 commits April 13, 2026 19:51
* Add Kimi Code CLI as a supported MCP client

* Regenerate CLI and API docs for kimi-cli

* Fix indentation in AddAndVerifyMCPServer switch block

* Add kimi-cli to swagger.yaml ClientApp enum
* Support stateless streamable-HTTP remote servers

Remote MCP servers implementing the streamable-HTTP transport in
stateless mode (POST-only, no SSE) show zero tools when proxied by
ToolHive. The transparent proxy forwards all HTTP methods including
GET; stateless servers don't serve SSE on GET, so clients never
complete tool discovery.

Add a --stateless flag to thv run that gates GET, HEAD, and DELETE
requests at the proxy with 405, and switches the health check to a
POST-based JSON-RPC ping appropriate for servers without an SSE
endpoint.

Fixes stacklok#4514

Signed-off-by: Greg Katz <gkatz@indeed.com>

* Add --stateless flag to generated docs

Regenerate CLI and Swagger docs to include the new --stateless
flag added to the run command.

Signed-off-by: Greg Katz <gkatz@indeed.com>

* Apply stateless mode to all transports

Move stateless configuration outside the remote-URL guard so
--stateless works for both remote URLs and local container
workloads. Add Allow header to 405 responses per RFC 9110.
Update comments and flag text to remove remote-only language.

Signed-off-by: Greg Katz <gkatz@indeed.com>

* Add E2E test for stateless proxy mode

Verify that --stateless makes the proxy reject GET and DELETE
with 405 and forward POST to the upstream MCP server. Uses an
in-process mock server to avoid external dependencies.

Signed-off-by: Greg Katz <gkatz@indeed.com>

---------

Signed-off-by: Greg Katz <gkatz@indeed.com>
Accept the caller's metadata map directly rather than re-loading it
from storage. Any caller that detects a backend expiry already holds
the session metadata (e.g. from MultiSession.GetMetadata), so the
extra round-trip is unnecessary. Passing nil is treated as a silent
no-op. Safety is preserved: updateMetadata uses SET XX, so a
concurrently terminated (deleted) session is never resurrected.

Closes stacklok#4667

Co-authored-by: taskbot <taskbot@users.noreply.github.com>
* Add more claude rules

Generate more rules about style and testing, extrapolated from
the comments and bugs in latest prs and reviews

* changes from review

---------

Co-authored-by: taskbot <taskbot@users.noreply.github.com>
…k#4773)

* Add VS Code as a skill-supporting client

* Add VS Code Insiders as a skill-supporting client
…ory (stacklok#4796)

* Deduplicate client directories during skill extraction

VS Code and VS Code Insiders both resolve to ~/.copilot/skills/ for
user-scoped skills. When installing without --clients (the default),
the system expands to all skill-supporting clients and attempts to
extract to the same directory twice — the second extraction fails
because the directory already exists.

Add uniqueDirClients() to skip duplicate directories during extraction,
pre-checks, and rollback while still recording all client names in the
DB. Applied to all five extraction paths: fresh OCI, same-digest new
clients, digest upgrade, fresh git, and existing git.

Made-with: Cursor

* Seed uniqueDirClients with existing client directories

When a user first installs with --clients vscode and later re-installs
without --clients (expanding to both vscode and vscode-insider which
share ~/.copilot/skills/), toWrite contains only vscode-insider. The
previous uniqueDirClients had nothing to deduplicate, so the stat check
fired on the directory that vscode already created, returning a false
StatusConflict.

Add an occupiedDirs parameter to uniqueDirClients and a companion
existingClientDirs helper that builds the seed set from the existing
installed clients. The two affected paths (installExtractionSameDigest
NewClients and applyGitInstallExisting new-clients branch) now pass
the existing directories so that new clients sharing an already-owned
directory are skipped. When all new clients are skipped, the DB record
is still updated to include the new client names.
* Add Factory.ai as a supported MCP client

* Add Factory.ai as a supported MCP client

* Add Factory.ai as a supported MCP client
…acklok#4781)

Adds an integration test for the ListCapabilities → Close() path that
would fail if the context.Background() regression from stacklok#4613 were
reintroduced. Uses a proper streamable-HTTP server (NewStreamableHTTPServer)
so mcp-go issues a session ID and actually sends a DELETE on Close().

The assertion is a server-side atomic flag: if auth fails for the DELETE,
authRoundTripper drops the request before it reaches the server, the flag
stays false, and the test fails.

Closes stacklok#4725

Co-authored-by: taskbot <taskbot@users.noreply.github.com>
…onverter (stacklok#4784)

* Propagate protectedResourceAllowPrivateIP through OIDC resolver and converter

The protectedResourceAllowPrivateIP CRD field was defined but never
propagated through the OIDC resolver, and the vmcpconfig converter
incorrectly mapped it from jwksAllowPrivateIP. This meant the field
had no effect and could not be controlled independently.

Add ProtectedResourceAllowPrivateIP to the resolver OIDCConfig struct,
propagate it through inline, shared inline, and ConfigMap resolve paths,
fix the converter to use the correct source field, and add the missing
JwksAllowPrivateIP mapping.

Fixes bugs 2 and 3 from stacklok#3142.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add ConfigMap independence test for protectedResourceAllowPrivateIP

Address review feedback (F3): add a ConfigMap test case where
jwksAllowPrivateIP is true but protectedResourceAllowPrivateIP is
absent, verifying they resolve independently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Document runtime OR behavior on private IP fields and fix missing converter mappings

Add doc comments to JWKSAllowPrivateIP and ProtectedResourceAllowPrivateIP
on both InlineOIDCSharedConfig and InlineOIDCConfig noting that at runtime
either field being true enables private IPs for all OIDC HTTP requests.

Also add missing JWKSURL and IntrospectionURL mappings in
mapResolvedOIDCToVmcpConfigFromRef, which silently dropped these fields
for the MCPOIDCConfig reference path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Regenerate CRD manifests after doc comment updates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Regenerate CRD reference docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The constructor and functional options guarantee positive values
for healthCheckInterval and healthCheckPingTimeout, making two
zero-value guards unreachable. The redundant context timeout in
checkMCPStatus capped the configurable ping timeout at 5s; the
MCPPinger already enforces its own timeout internally.

Fixes stacklok#4790

Signed-off-by: Greg Katz <gkatz@indeed.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…4799)

* Add Goose, Gemini CLI, and Amp as skill-supporting clients

Enable skill installation for three additional AI clients, all
using the .agents/skills/ convention for both user and project
scope. Each client also discovers skills from its native path
(.gemini/skills/, ~/.config/agents/skills/, etc.) but .agents/
is the cross-client standard.

* Fix --client flag name and default behavior in skills docs

The CLI uses --clients (plural), and the default targets all
skill-supporting clients, not just the first one.
)

Add OpenAPI annotations to v0.1 skills API endpoints

Add swag annotations to the two registry skills handlers introduced
in stacklok#4753 so they appear in the generated OpenAPI spec:
- GET /registry/{name}/v0.1/x/dev.toolhive/skills (list, paginated)
- GET /registry/{name}/v0.1/x/dev.toolhive/skills/{ns}/{skill} (get)

Add @description doc comments to skillsV01Response and
skillsV01Metadata so swag emits them as named schema components.
Regenerate docs/server/ to include the new paths and schemas.
…metrichttp to v1.43.0 [SECURITY] (stacklok#4683)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…p from 1.41.0 to 1.43.0 (stacklok#4684)

Bump go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp

Bumps [go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp](https://github.com/open-telemetry/opentelemetry-go) from 1.41.0 to 1.43.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](open-telemetry/opentelemetry-go@v1.41.0...v1.43.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp
  dependency-version: 1.43.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* Add TelemetryConfigRef support to VirtualMCPServer CRD

VirtualMCPServer can now reference shared MCPTelemetryConfig resources
via spec.telemetryConfigRef, following the same pattern used by
MCPServer and MCPRemoteProxy. This enables shared telemetry
configuration, Kubernetes-native secret references for OTLP auth
headers, CA bundle ConfigMap references, and per-server serviceName
overrides.

Implements changes for issue stacklok#4792:
- Add TelemetryConfigRef field and CEL mutual exclusivity validation
- Add TelemetryConfigHash to status for change detection
- Add telemetry handler with batched statusManager pattern
- Add MCPTelemetryConfig watch in SetupWithManager
- Update converter to prefer TelemetryConfigRef over inline telemetry
- Add CA bundle volumes and sensitive header env vars to deployment
- Regenerate CRD manifests, RBAC, deepcopy, and mocks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Deprecate inline config.telemetry in favor of telemetryConfigRef

Add deprecation comments to config.Config.Telemetry field (for operator
context) and emit a warning log when inline telemetry is used during
conversion, guiding operators to migrate to spec.telemetryConfigRef.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add integration tests, docs, and example for TelemetryConfigRef

Add envtest-based integration tests for VirtualMCPServer
TelemetryConfigRef: hash tracking, config change detection, missing
ref condition, and CEL mutual exclusivity validation.

Update observability docs to show the preferred telemetryConfigRef
pattern for VirtualMCPServer and mark inline config.telemetry as
deprecated. Add example manifest demonstrating shared
MCPTelemetryConfig with VirtualMCPServer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address code review feedback for VirtualMCPServer TelemetryConfigRef

Fixes from code review on PR stacklok#4801 for issue stacklok#4792:

- Add VirtualMCPServer to MCPTelemetryConfig controller's findReferencingWorkloads,
  watch handler, and RBAC markers for deletion protection parity
- Eliminate redundant MCPTelemetryConfig API calls (3x per reconcile) by fetching
  once in handleTelemetryConfig and threading the config through to converter and
  deployment builder
- Change normalizeTelemetry to accept pre-fetched config instead of re-fetching
- Fix deprecation log level from Info to V(1) debug to avoid log flooding
- Remove stale TelemetryConfigRefValidated condition when TelemetryConfigRef is nil
- Update architecture docs with VirtualMCPServer telemetryConfigRef edges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Regenerate CRD API docs for TelemetryConfigRef

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The operator Helm chart was pinned to thv-registry-api v1.0.1.
Bump to the latest release v1.1.1.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Grant default `aud` when `resource` is absent

The `TokenHandler` only granted an audience claim when the client
included the RFC 8707 `resource` parameter. Clients that omit this
optional parameter received a token with an empty `aud`, which caused
`VirtualMCPServer` to reject every token its own auth server issued:
the incoming OIDC validator requires `audience` to be set and rejects
tokens that do not carry a matching claim.

When no `resource` parameter is present and `AllowedAudiences` contains
exactly one entry, the handler now defaults to granting that audience.
The intended audience is unambiguous in that case, and `AllowedAudiences`
is always non-empty (enforced at config validation time). The defaulting
is restricted to `authorization_code` grants; for `refresh_token` grants
fosite carries the originally-granted audience forward through the stored
session automatically, so re-granting would conflict with its matching
strategy.

Fixes stacklok#4794
Fosite validates that a granted audience appears in the
client's own `GetAudience()` list when handling refresh
tokens. The main `setupTestServer` helper registered the
test client without an `Audience` field, so the check
failed with HTTP 400 after the token handler began
defaulting to the sole `AllowedAudience` on the initial
`authorization_code` exchange.

DCR registration always sets `Audience` to `AllowedAudiences`;
align the test fixture with that production behaviour.
The subtests failed when `TOOLHIVE_DEV=true` was set in the shell
environment. `isDevMode()` reads that variable, causing the function to
use `http://` instead of `https://` and to skip SSRF rejection -- the
opposite of what the non-dev test cases assert.

Fix: remove `t.Parallel()` from `TestParseGitReference` and add
`t.Setenv("TOOLHIVE_DEV", "")` so the test always runs in a clean
environment, matching the pattern used by `TestParseGitReferenceDevMode`.
…tacklok#4807)

Add ConfigMap content assertions to telemetry integration tests

Verify the vmcp-config ConfigMap contains the correct telemetry
settings from the referenced MCPTelemetryConfig: endpoint (with
https:// prefix stripped), tracing/metrics enabled flags, and that
the ConfigMap updates when the MCPTelemetryConfig endpoint changes.

This ensures the full data path is tested end-to-end: CRD spec →
converter → ConfigMap → telemetry config, not just status conditions.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…k#4809)

* Change GroupRef from bare string to typed MCPGroupRef struct

GroupRef was a bare string while every other cross-CRD reference
(ExternalAuthConfigRef, ToolConfigRef, AuthServerRef, EmbeddingServerRef)
uses a typed struct. This inconsistency prevented extending GroupRef
with additional fields (like namespace) without a breaking change.

Define MCPGroupRef struct with Name field and nil-safe GetName() helper.
Replace GroupRef string with *MCPGroupRef on MCPServerSpec,
MCPRemoteProxySpec, MCPServerEntrySpec, and add a new top-level GroupRef
field on VirtualMCPServerSpec that takes precedence over the deprecated
config.groupRef string path.

Internal types (vmcpserver.Config, StatusResponse, BackendReconciler,
config.Config) remain as strings since they are not CRD API types.

This is a breaking wire-format change (groupRef: "name" becomes
groupRef: {name: "name"}) that must happen before v1beta1.

Closes stacklok#4634

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address review feedback on MCPGroupRef refactoring

- Add nil guard to MCPServerEntry validateGroupRef using GetName()
  instead of direct .Name access to prevent potential panic
- Add unit tests for MCPGroupRef.GetName() and
  VirtualMCPServer.ResolveGroupName() covering precedence logic
- Normalize nil-check pattern across all field index extractors to
  use GetName() == "" (handles both nil and empty name)
- Mark config.Config.Group as optional and deprecated since
  spec.groupRef is now the preferred path
- Improve Validate() error message to mention both spec.groupRef
  and config.groupRef paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update missed example and docs for MCPGroupRef wire format

- Fix vmcp_with_telemetry_ref.yaml (missed in initial pass)
- Update virtualmcpserver-api.md to document spec.groupRef as primary
  field and update all YAML examples to struct format
- Update virtualmcpcompositetooldefinition-guide.md and
  virtualmcpserver-observability.md examples
- Regenerate crd-api.md to show MCPGroupRef typed struct

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eters (stacklok#4712)

* Add --remote-auth-scope-param-name flag for non-standard OAuth scope parameters

Some OAuth providers use non-standard query parameter names for scopes
in the authorization URL. For example, Slack's OAuth v2 requires
user-token scopes in "user_scope" instead of the standard "scope"
parameter. This causes ToolHive's OAuth flow to fail with invalid_scope
errors when connecting to providers like Slack's MCP server.

Add a new --remote-auth-scope-param-name flag that allows users to
override the query parameter name used for scopes. When set, scopes are
sent under the specified parameter name and the standard "scope"
parameter is cleared. The oauth2Config.Scopes field is preserved so
token refresh requests continue to work correctly.

Signed-off-by: Gustavo Gomez <gmogmz@indeed.com>

* Address review feedback: fix empty scope= and OIDC fallback path

- Replace SetAuthURLParam("scope", "") with temporarily nil-ing
  oauth2Config.Scopes before AuthCodeURL, then restoring via defer.
  This omits the scope parameter entirely instead of producing an
  invalid empty scope= (RFC 6749 §3.3).
- Propagate ScopeParamName on the OIDC discovery fallback path in
  createOAuthConfig, so --remote-auth-scope-param-name works with
  --remote-auth-issuer as well.
- Strengthen test assertion to verify scope parameter is truly absent,
  not just empty-valued.

Signed-off-by: Gustavo Gomez <gmogmz@indeed.com>

* Fix missing swagger docs for scope_param_name field

Signed-off-by: Gustavo Gomez <gmogmz@indeed.com>

---------

Signed-off-by: Gustavo Gomez <gmogmz@indeed.com>
* Update VERSION for release

Release-Triggered-By: blkt

* Update Chart.yaml for release

Release-Triggered-By: blkt

* Update Chart.yaml for release

Release-Triggered-By: blkt

* Update Chart.yaml for release

Release-Triggered-By: blkt

* Update Chart.yaml for release

Release-Triggered-By: blkt

* Update values.yaml for release

Release-Triggered-By: blkt

* Update values.yaml for release

Release-Triggered-By: blkt

* Update values.yaml for release

Release-Triggered-By: blkt

* Update VERSION for release

Release-Triggered-By: blkt

* Update Chart.yaml for release

Release-Triggered-By: blkt

* Update README.md for release

Release-Triggered-By: blkt

* Update Chart.yaml for release

Release-Triggered-By: blkt

* Update README.md for release

Release-Triggered-By: blkt

* Update values.yaml for release

Release-Triggered-By: blkt
The values-openshift.yaml was a copy of the default values with outdated
image tags (v0.15.0) and different resource limits. It provided no
OpenShift-specific configuration and was misleading as a reference.
Remove it along with the README references that pointed to it.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add script engine public API types

Introduce the pkg/script/ package with the public interface for vMCP
code mode. The Executor interface provides Execute() for running
Starlark scripts and ToolDescription() for tools/list injection. Tool
binds MCP tool metadata with a dispatch callback. Config holds execution
parameters (step limit, parallel concurrency cap, tool call timeout).

Part of stacklok#4742

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add Starlark execution engine in internal/core

Core execution engine that wraps user scripts in a function body for
top-level return support, enforces step limits to prevent runaway
computation, and captures print() output as logs. This is the lowest
layer of the script package — it only knows about Starlark, not MCP.

Part of stacklok#4742

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Use require/table-driven tests per testing guidelines

Convert tests in description_test.go and execute_test.go to use
testify require and table-driven structure per project testing rules.

Part of stacklok#4742

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add Go/Starlark type conversions and MCP result parsing

Introduce internal/conversions with three concerns:
- Bidirectional Go ↔ Starlark type conversion (preserving JSON number
  fidelity by promoting whole float64s to Int)
- MCP result parsing that handles structured content with/without the
  mcp-go SDK wrapper, text content as JSON or plain string, and error
  results
- Tool name sanitization for converting MCP names to valid Starlark
  identifiers

Part of stacklok#4742

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add parallel(), call_tool(), and tool callable builtins

Introduce internal/builtins with:
- Tool callable factory supporting positional args (arg0, arg1, ...),
  keyword args, and mixed calling conventions
- call_tool("name", ...) for dispatching to tools by original name
  when the name is not a valid Starlark identifier
- parallel() builtin for concurrent fan-out with optional concurrency
  limit via semaphore
- BuildToolMap for collision-safe name → callback mapping

Part of stacklok#4742

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Wire executor to internal packages with integration tests

Connect the public Executor interface to internal/core, internal/builtins,
and internal/conversions. The executor builds a Starlark environment from
bound tools (each as a callable with positional+kwargs support), injects
data arguments, executes the script, and wraps the result as an
mcp.CallToolResult with optional log content.

Integration tests verify multi-tool scripts, loops/conditionals, parallel
fan-out, data arguments, call_tool dispatch, step limit enforcement, and
log capture.

Part of stacklok#4742

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address self-review findings

Fix correctness and safety issues found during code review:

- Fix data race: parallel() child threads now get their own log buffers
  instead of sharing the parent Print callback. Logs are merged after
  wg.Wait() completes.
- Propagate step limit to parallel() child threads so scripts cannot
  bypass CPU limits via parallel([lambda: infinite_loop()]).
- Thread context.Context into parallel() for cancellation support.
  Semaphore acquire and task launch check ctx.Done().
- Set thread.Load to return an error instead of nil (prevents panic if
  a script uses a load() statement).
- Reject data argument keys that shadow builtins or tool names instead
  of silently overwriting them.
- Return errors for invalid data argument types instead of silently
  dropping them.
- Simplify builtins package to expose a single Build() function that
  returns globals and reserved names. All other functions are unexported.
- Consolidate builtins tests into a single test file using table-driven
  tests.

Part of stacklok#4742

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address review comments

- Remove reserved set from builtins.Build() — caller checks globals
  keys directly to prevent data argument shadowing
- Consolidate integration tests into a single table-driven TestExecutor
- Add test for automatic JSON string → structured data conversion

Part of stacklok#4742

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address automated review comments

- Make per-tool-call timeout a caller responsibility: remove
  ToolCallTimeout from Config and document the contract on Tool.Call.
  Add test proving caller-set context deadlines are respected.
- Fix UTF-8 truncation in GenerateToolDescription: slice by rune count
  instead of byte index to avoid splitting multi-byte characters.
- Run go mod tidy to mark go.starlark.net as a direct dependency.

Part of stacklok#4742

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address reviewer feedback on parallel execution and edge cases

- Introduce pkg/foreach with generic bounded concurrent iteration using
  a worker pool. Goroutine count matches the concurrency limit, not the
  task count. Reusable outside pkg/script.
- Rewrite parallel() to use foreach.Concurrent instead of raw
  goroutines, fixing the goroutine explosion for large task lists.
- Divide step budget evenly across parallel children (stepLimit/N per
  child) to prevent amplification via nested parallel() calls.
- Document wrapScript triple-quoted string limitation.
- Strengthen collision test to assert dispatch reaches the correct tool.
- Clamp negative ParallelMax to 0 (unlimited) in resolveConfig.

Part of stacklok#4742

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
)

When the vMCP has an embedded auth server, ensureOIDCDiscovered()
fails fatally if the issuer URL is unreachable from inside the
cluster, preventing the local key provider from ever being consulted.

Make the discovery error non-fatal when a keyProvider is configured.
The local provider resolves signing keys in-process, so HTTP-based
OIDC discovery is not required for token validation. When discovery
fails with a keyProvider present, mark it as completed to avoid the
per-request retry penalty (3 attempts with backoff).

When the issuer IS reachable, discovery succeeds normally and the
HTTP JWKS fallback remains available for key ID misses against the
local provider.

Also improve the error message when the local provider cannot resolve
a key and no JWKS URL is available, and add a constructor log
distinguishing deferred discovery from non-fatal discovery.

Fixes stacklok#4747

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The "Edit with explicit port" test case used port 9090 in the
request body. The port-availability check (`networking.IsAvailable`)
makes a real TCP bind, so the test failed whenever port 9090
was already in use on the host machine.

Move the test case out of the static table and into a standalone
sub-test that allocates a free port at runtime via `net.Listen`
with `":0"`. The port is released before the HTTP handler runs so
the availability check can bind it. Also add the missing `net` and
`require` imports that this approach needs.
samuv and others added 23 commits April 21, 2026 11:04
* Split skillsvc.go into per-concern files

skillsvc.go had grown to ~2000 lines mixing ten concerns (service construction, OCI install, git install, extraction, client resolution, registry lookup, content fetching, build/push, uninstall, list/info).

Split it into focused files within the same skillsvc package: service, install, install_oci, install_git, install_extraction, clients, oci, registry, content, list, uninstall, build, scope. All symbols stay in the same package, so there are no circular-import risks and no exported API changes.

No behavioural changes, no renames, no signature changes.

* Split skillsvc_test.go into per-concern test files

skillsvc_test.go had grown to ~3900 lines. Split it into per-area test files mirroring the production layout, with shared fixtures extracted into testhelpers_test.go.

Each Test* function is moved verbatim — no behavioural changes to any test.

* Remove original skillsvc.go and skillsvc_test.go

The previous two commits added the split files but left the originals in place. Delete them now that all symbols have been moved to per-concern files.
…4957)

Commit c334edf ("vmcp: stop the TEI manager on Start failure and apply
image default in Serve") renamed the unexported defaultTEIImage constant
to the exported DefaultEmbeddingImage so Serve() can default the TEI
image alongside DefaultEmbeddingModel. The production files were
updated but embedding_manager_test.go was missed, leaving three
references to the now-undefined identifier. Golangci-lint catches it as
a typecheck error, which fails Lint Go Code and Test Go Code on every
PR opened against main.

Update the three remaining references so the package compiles and CI
goes green.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bump the default go transport builder image from golang:1.25-alpine to
golang:1.26-alpine so new go-scheme workloads build with the current
stable toolchain. Users can still pin an older release via
--runtime-image when a project needs it.

Also correct two pre-existing doc references that claimed the Go runtime
stage uses alpine:3.22 -- go.tmpl already pins alpine:3.23, so the docs
were drifted.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Return 502 Bad Gateway when OCI content pull fails

getContentFromOCI previously mapped registry.Pull failures to 400 Bad Request, but auth errors, unreachable registries, missing manifests, and network timeouts are upstream failures the caller cannot fix by changing their input. Align with getContentFromGit, which already returns 502 Bad Gateway for git resolve failures, so clients see consistent semantics across both backends.

* Return 502 Bad Gateway when OCI install pull fails

installFromOCI previously mapped registry.Pull failures to 400 Bad Request, which incorrectly blamed the caller for upstream registry problems (auth, network, manifest unknown). Return 502 Bad Gateway to match the behaviour just applied to GetContent and the existing git install path.
Bump the default uvx transport builder image from python:3.13-slim to
python:3.14-slim so new uvx-scheme workloads run on a supported Python
release with the latest security updates. Users can still pin an older
release via --runtime-image when a package needs it.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bump the default npx transport builder image from node:22-alpine to
node:24-alpine. Node 24 is the current active LTS release; Node 22 has
moved to maintenance. New npx-scheme workloads built with defaults now
run on the current LTS line. Users can still pin an older release via
--runtime-image when a package needs it.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tacklok#4932)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Radoslav Dimitrov <radoslav@stacklok.com>
…acklok#4960)

Follow-up to stacklok#4956: installFromOCI now returns 502 on upstream pull
failures (matching installFromGit), but the @failure list on the
installSkill swagger annotation still only advertised 400/409/500.

Add @failure 502 {string} string "Bad Gateway" and regenerate the
spec so clients see the real contract for POST /api/v1beta/skills.
* Add registry-aware workload creation

Add optional registry and server fields to POST /api/v1beta/workloads
so clients can create workloads by referencing a registry server:

  {"registry": "default", "server": "io.github.stacklok/fetch"}

thv resolves the server from the registry, extracts image/transport/
env vars/permissions/etc from metadata, and builds the RunConfig
server-side. User-provided fields always override registry defaults.

This eliminates client-side field extraction — Studio goes from
GET+extract+POST to just POST.

Ref: stacklok#4199

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Regenerate OpenAPI spec for registry-aware workload fields

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address review findings for registry-aware workload creation

- Fix handler validation: accept registry+server as alternative to
  image/url (was rejecting before resolution could run)
- Fix validation ordering: check registry+server pairing before
  attempting resolution
- Validate registry name: only "default" is currently supported,
  reject unknown values with clear error
- Regenerate OpenAPI spec

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Preserve registry metadata through BuildFullRunConfig

Return ServerMetadata from resolveRegistryServer and assign it to
the local serverMetadata/imageMetadata variables so downstream code
(registry info on RunConfig, tool validation, remote auth config,
proxy port) has access to it.

Skip the imageRetriever re-lookup when metadata was already resolved
from a registry reference — re-looking up by bare image ref would
fail since the image ref doesn't match any server name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Complete remote server metadata handling and address review feedback

Fix remote server path to get the same treatment as image servers:
- Extract ProxyPort from RemoteServerMetadata in registry assignment
- Build full remoteAuthConfig from registry OAuthConfig (scopes,
  issuer, authorize/token URLs, PKCE, headers, env vars)
- Previously only URL and Headers were copied; OAuth, EnvVars, and
  ProxyPort were silently dropped

Other fixes:
- Make resolveRegistryServer a method on WorkloadService using
  s.configProvider instead of creating a separate config.NewProvider()
- Improve validation: check partial registry+server before the
  generic "image or url required" error for better DX

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix remote auth overwrite, add image provenance verification

Critical fixes found by review:

Remote server path:
- Guard `if req.URL != ""` to skip createRequestToRemoteAuthConfig
  when metadata was resolved from registry (was unconditionally
  overwriting the registry-built remoteAuthConfig, dropping all
  OAuth fields, EnvVars, and ProxyPort)
- Add ClientSecret/BearerToken handling from user request on top
  of registry-resolved OAuth config

Image server path:
- Add image provenance verification (VerifyImage) for registry-
  resolved images. The normal imageRetriever path calls this
  internally but registry references bypass it.
- Export VerifyImage from retriever package for reuse

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add proper HTTP status codes to registry resolution errors

- Unknown registry → 400 Bad Request (was 500)
- Server not found → 404 Not Found (was 500)
- Registry provider unavailable → 503 Service Unavailable (was 500)
- Partial registry+server guard → 400 Bad Request (was 500)
- Fix misleading error message: "not found in registry" → "failed
  to look up in registry" for non-not-found errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add unit tests for registry-aware workload helpers

Cover applyImageDefaults (env var merge, user override precedence),
applyRemoteDefaults, applyRegistryDefaults dispatch, and the
resolveRegistryServer 400 path for unknown registries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Extract remote auth config builder helper

The registry-resolved RemoteServerMetadata branch and the
imageRetriever-returned remote branch in BuildFullRunConfig built
remote.Config from registry metadata in identical ways. Extract
buildRemoteAuthConfigFromMetadata so future changes to the OAuth
config shape land in one place.

Also drop the registry+server pairing guard from BuildFullRunConfig
since the route handler already rejects partial pairs with a 400
before the service is called.

Add unit tests covering nil OAuthConfig, full field population,
resource precedence (user > metadata > URL-derived), and CLI-string
formatting for user-provided ClientSecret and BearerToken.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tacklok#4964)

The dual-claim extraction, dot-notation traversal, and merge-order fix
from 88937fce lacked direct unit tests for helper functions and had no
integration tests exercising group-based Cedar policies through the
middleware stack. This closes both gaps.

Tier 1 — unit tests for parseCedarEntityID, sanitizeURIForCedar,
extractClientIDFromClaims, preprocessClaims, preprocessArguments,
mergeContexts, and IsAuthorized entity merge priority. Adds edge cases
to existing TestResolveNestedClaim (ambiguity) and
TestAuthorizeWithJWTClaims_DualClaim (same claim, missing JWT key,
non-array graceful handling, both dot-notation).

Tier 2 — integration tests for group-based list filtering (Entra dual,
Entra groups, Okta URI, Keycloak nested, prompts, resources), non-list
operations (tool call, prompt get, resource read), transitive
THVGroup→THVRole hierarchy through middleware, and upstream provider
group extraction.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add E2E tests for thv vmcp CLI quick mode and config-file mode

Add test/e2e/vmcp_cli_test.go covering the two serve modes: quick
mode (--group) and config-file mode (init → validate → serve --config),
each with an MCP client connectivity assertion that backend tools are
exposed. Includes a binding test confirming the server listens only on
127.0.0.1, and negative tests for missing flags and a nonexistent
config path.

Closes stacklok#4888

* fixes from review

---------

Co-authored-by: taskbot <taskbot@users.noreply.github.com>
The registry-resolved path in BuildFullRunConfig hardcoded
retriever.VerifyImageWarn while the imageRetriever path threaded
verification through its argument list. Keeping two inline literals means
the paths can silently drift if one is later made configurable.

Store the mode on WorkloadService (still defaulting to warn) and read it
in both call sites so changing behavior is a single-line edit.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The vmcp subcommand is not ready for release. Mark the parent
command Hidden so it does not appear in help output or shell
completion, while remaining functional for internal testing.

Regenerated CLI docs drop the now-hidden pages.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Update VERSION for release

Release-Triggered-By: JAORMX

* Update Chart.yaml for release

Release-Triggered-By: JAORMX

* Update Chart.yaml for release

Release-Triggered-By: JAORMX

* Update Chart.yaml for release

Release-Triggered-By: JAORMX

* Update Chart.yaml for release

Release-Triggered-By: JAORMX

* Update values.yaml for release

Release-Triggered-By: JAORMX

* Update values.yaml for release

Release-Triggered-By: JAORMX

* Update values.yaml for release

Release-Triggered-By: JAORMX

* Update VERSION for release

Release-Triggered-By: JAORMX

* Update Chart.yaml for release

Release-Triggered-By: JAORMX

* Update README.md for release

Release-Triggered-By: JAORMX

* Update Chart.yaml for release

Release-Triggered-By: JAORMX

* Update README.md for release

Release-Triggered-By: JAORMX

* Update values.yaml for release

Release-Triggered-By: JAORMX
…lok#4965)

When serverName is configured on the Cedar authorizer, resource entities
(Tool, Prompt, Resource) need an MCP parent so that server-scoped Cedar
policies like `resource in MCP::"github"` evaluate correctly. Without
this parent, those policies silently match nothing and deny all requests.

Add serverName parameter to CreateEntitiesForRequest and build an
MCP::"<serverName>" parent UID when non-empty. Thread a.serverName
through all four authorize* methods in core.go.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…lok#4905)

* Default redirectUri on upstream providers to {resourceUrl}/oauth/callback

The MCPExternalAuthConfig CRD documents that redirectUri defaults to
{resourceUrl}/oauth/callback when not specified, but the operator was
passing the empty string through to the proxy runner, which then
rejected it with "redirect_uri is required".

Pass resourceURL through BuildAuthServerRunConfig to buildUpstreamRunConfig
so empty RedirectURI fields on both OIDC and OAuth2 upstream providers are
resolved to the documented default before generating the runconfig. This
applies to both MCPServer and VirtualMCPServer code paths.

Fixes stacklok#4874

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add unit test for deriveResourceURL in converter_test.go

Address code review feedback: add TestDeriveResourceURL table-driven
test matching the convention established by TestDeriveAllowedAudiences
and TestDeriveScopesSupported. Covers nil IncomingAuth, nil OIDC,
empty Resource, and populated Resource cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Graduate CRDs from v1alpha1 to v1beta1

Signal API stability by promoting all ToolHive CRDs to v1beta1 while
preserving backwards compatibility — the CRDs serve both versions
simultaneously so existing v1alpha1 resources survive the upgrade
untouched and users migrate manifests at their own pace.

v1alpha1 and v1beta1 are schema-identical. The new
cmd/thv-operator/api/v1alpha1/ package defines only the root resource
types (MCPServer, MCPGroup, ...) as thin wrappers whose Spec and Status
fields reference the canonical types from v1beta1. controller-gen walks
field types when building the OpenAPI schema, so both versions resolve
to the same schema without any duplication of the actual model — I
verified empirically that all 12 CRDs produce structurally identical
schemas (modulo the legitimately-different root descriptions).

The upgrade is orchestrated through the `served` and `storage` flags
on each version entry: the new CRDs list both versions as served, mark
v1beta1 as storage, and mark v1alpha1 as deprecated with a warning.
No conversion webhook is configured, which defaults to strategy: None
— safe because the schemas are identical. Kubernetes swaps the
apiVersion string when serving an object at a different version than
it was stored at. Users can upgrade the CRD chart without deleting
anything, keep reading/writing at v1alpha1 indefinitely, and migrate
to v1beta1 by re-applying manifests. Once status.storedVersions no
longer contains v1alpha1, a future release can drop the v1alpha1
entry and remove the wrapper package.

What changed:

- Mechanical rename from v1alpha1 to v1beta1 across all Go imports,
  YAML apiVersion strings, Helm deploy manifests, chainsaw fixtures,
  examples, docs, and generated artifacts.
- New cmd/thv-operator/api/v1alpha1/ package: 12 root types + 12 list
  types as thin wrappers over v1beta1 Spec/Status, plus scheme
  registration. Each root type carries a +kubebuilder:deprecatedversion
  warning so kubectl prints a migration hint on every access.
- Register both schemes in cmd/thv-operator/main.go.
- Regenerated CRD manifests list both versions (v1alpha1 deprecated
  and non-storage, v1beta1 storage) with conversion.strategy: None.
- Removed the obsolete pre-upgrade Helm hook that blocked upgrades
  when v1alpha1 resources existed — no longer needed since those
  resources now survive the upgrade.
- Drive-by: regenerate pkg/audit/zz_generated.deepcopy.go which had
  drifted after DetectApplicationErrors was added to Config without
  re-running controller-gen.

Closes stacklok#2556

* Update test files to use v1beta1 after merge

Two test files that came in via the merge from main still reference
the v1alpha1 Go package, which no longer exists on this branch. The
operator binaries compile fine, but CI lint/test fail on the missing
mcpv1alpha1 symbol in the test package.

- cmd/thv-operator/controllers/mcpremoteproxy_deployment_test.go
- cmd/thv-operator/controllers/mcpserver_resource_overrides_test.go

Applied the same mcpv1alpha1 → mcpv1beta1 rename that the rest of
the branch uses. No behavioural change.
* Update VERSION for release

Release-Triggered-By: ChrisJBurns

* Update Chart.yaml for release

Release-Triggered-By: ChrisJBurns

* Update Chart.yaml for release

Release-Triggered-By: ChrisJBurns

* Update Chart.yaml for release

Release-Triggered-By: ChrisJBurns

* Update Chart.yaml for release

Release-Triggered-By: ChrisJBurns

* Update values.yaml for release

Release-Triggered-By: ChrisJBurns

* Update values.yaml for release

Release-Triggered-By: ChrisJBurns

* Update values.yaml for release

Release-Triggered-By: ChrisJBurns

* Update VERSION for release

Release-Triggered-By: ChrisJBurns

* Update Chart.yaml for release

Release-Triggered-By: ChrisJBurns

* Update README.md for release

Release-Triggered-By: ChrisJBurns

* Update Chart.yaml for release

Release-Triggered-By: ChrisJBurns

* Update README.md for release

Release-Triggered-By: ChrisJBurns

* Update values.yaml for release

Release-Triggered-By: ChrisJBurns
refreshCache() was returning stale cached data for any API error,
including auth failures (401/403 from registry and OAuth flow timeouts).
Users saw previously-cached registry content with no error after their
credentials expired or the OAuth browser flow timed out.

Fix in two parts:
- auth.Transport.RoundTrip now wraps all Token() errors with
  ErrRegistryAuthRequired, classifying token-acquisition failures as auth
  errors regardless of the underlying cause.
- refreshCache() checks for ErrRegistryAuthRequired/ErrRegistryUnauthorized
  before the stale-cache fallback; transient errors (network, 5xx) still
  degrade gracefully.

Fixes stacklok#4950

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…in (stacklok#4982)

* Bundle CRD manifests as a release asset

Adds a `thv-crds.tar.gz` tarball to each release containing the CRD
YAML manifests from `deploy/charts/operator-crds/files/crds/`.

Motivation: downstream consumers of the CRDs (notably
stacklok/docs-website, which generates per-CRD reference pages)
currently have to clone the entire toolhive repo at each release tag
just to read these 13 manifests. Shipping them as a ~94KB tarball
asset lets those consumers skip the clone and just `gh release
download` like they already do for `thv-cli-docs.tar.gz` and
`swagger.yaml`.

Purely additive — no changes to existing release assets or workflows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Re-export toolhive-core schemas as release assets

Reads the toolhive-core version from go.mod at release time, downloads
the four JSON schema files from that version of stacklok/toolhive-core,
and ships them alongside toolhive's own release assets.

Motivation: downstream consumers (notably stacklok/docs-website)
currently have to replicate this logic: read go.mod, derive the core
version, then fetch from a different repo's release. Re-exporting the
schemas here makes toolhive's release self-contained — one `gh release
download` call gets everything.

Paired with stacklok#4982 (CRD
manifests as release asset); together these eliminate the need for
docs-website to clone toolhive or hit a second repo during its
release-doc regeneration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Retire the docs-website repository_dispatch chain

docs-website no longer consumes `repository_dispatch: published-release`
events — its reference-doc regeneration now runs via a Renovate-driven
pipeline that reads the new `thv-crds.tar.gz` and re-exported core
schemas introduced earlier in this PR. See
stacklok/docs-website#748 for context.

Changes:

- Delete `.github/workflows/update-docs-website.yml` (the dispatch
  sender, called via workflow_call from `releaser.yml`).
- Remove the `update-docs-website` job from `releaser.yml` along with
  its `extract-release-actor` dependency, which only existed to feed
  the dispatch's `assign_to` input.
- Update `notify-release-failure`'s `needs:` list to drop the removed
  jobs.

Follow-up for maintainers: the `DOCS_REPO_DISPATCH_TOKEN` repo secret
can be retired — nothing references it after this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add advisory CRD schema compatibility CI check

The v1beta1 graduation in stacklok#4849 made a stability commitment that is
enforced only by convention today. Adds a new api-compat workflow that
runs crd-schema-checker on every PR touching operator API surface and
reports compatible/incompatible/unknown status in the job step summary.

Phase 1 is advisory: the job runs with continue-on-error so findings
are surfaced without blocking merges. Phase 2 will flip enforcement and
add the api-break-allowed escape hatch label. Part of stacklok#4972.

Baseline is the last published CRD chart from OCI, falling back to
origin/main when the chart for the latest tag is not yet published
(the release-to-publish lag window). NoBools and NoMaps style
validators are disabled — they are OpenShift API-style rules, not
compat-breaking checks, and fire on fields we legitimately use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address review feedback on API compat workflow

Apply findings from the multi-agent consensus review. Summary:

- SHA-pin crd-schema-checker to 3fee146022bf — the tool has no release
  tags, and @latest left CI non-deterministic and exposed the runner
  to supply-chain risk (continue-on-error does not sandbox the step).
- Add timeout-minutes: 10 — expected runtime is ~3 min; the bound
  protects against a hung helm pull or go install without the 6h
  default burning CI budget on a genuine hang.
- Drop 2>/dev/null from helm pull — its stderr is one useful line when
  the fallback path activates; redirecting hid transient ghcr.io
  outages behind the expected "tag not yet published" case.
- Narrow path filter to files/crds/** plus the workflow self-watch.
  templates/** is caught by operator-ci's generate-crds drift check;
  values.yaml and crd-helm-wrapper only change Helm conditionals and
  annotations the checker ignores, and the workflow's explicit --set
  flags force every CRD to render regardless.
- Route step outputs through env: blocks — defense-in-depth against
  future edits that might route PR-controlled data through them.

Count-floor and origin/main fallback rework deferred: the floor would
generate false positives on legitimate CRD add/remove, and the
origin/main rework needs design discussion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fail-closed when baseline chart is unavailable

Remove the origin/main fallback in baseline resolution. Falling back
silently compares subsequent PRs against an already-broken baseline
once a breaking change lands on main, narrowing the Phase 2
enforcement guarantee. Better to fail the job and wait for the
release chart to publish than to emit a false green.

Phase 1's continue-on-error means a failure here does not block PRs
during the release-to-OCI publish window. Phase 2 will cover that
window via the api-break-allowed label if needed.

Addresses review finding F5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Drop design doc reference from workflow comment

The design doc is a planning artifact and is not committed to the
repo, so the reference is a dead link for anyone reading the workflow
file. The workflow's own inline comments already explain the rollout
phases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Update VERSION for release

Release-Triggered-By: rdimitrov

* Update Chart.yaml for release

Release-Triggered-By: rdimitrov

* Update Chart.yaml for release

Release-Triggered-By: rdimitrov

* Update Chart.yaml for release

Release-Triggered-By: rdimitrov

* Update Chart.yaml for release

Release-Triggered-By: rdimitrov

* Update values.yaml for release

Release-Triggered-By: rdimitrov

* Update values.yaml for release

Release-Triggered-By: rdimitrov

* Update values.yaml for release

Release-Triggered-By: rdimitrov

* Update VERSION for release

Release-Triggered-By: rdimitrov

* Update Chart.yaml for release

Release-Triggered-By: rdimitrov

* Update README.md for release

Release-Triggered-By: rdimitrov

* Update Chart.yaml for release

Release-Triggered-By: rdimitrov

* Update README.md for release

Release-Triggered-By: rdimitrov

* Update values.yaml for release

Release-Triggered-By: rdimitrov
v0.23.1 is the upgrade target: v0.23.0 plus an auth fix. It moves the
API to v1beta1 (storage v1beta1, conversion None, schemas identical to
v1alpha1), so one merge covers v0.20 through v0.23.

Transition shims, so the shared CRDs keep admitting production objects
that are still managed by the v0.19 operator:
- MCPServer.spec.groupRef accepts both a plain string and {name}. The
  field is schemaless and MCPGroupRef.UnmarshalJSON decodes either
  form; any other shape decodes to an empty name so one bad object
  cannot break the operator cache. CEL cannot type-check a schemaless
  field, so invalid values surface through GroupRefValidated instead.
- VirtualMCPServer.spec.groupRef stays optional in the schema; the
  controller still rejects objects without it.

Conflict resolution:
- vMCP serve logic moved to pkg/vmcp/cli upstream; the fork's backend
  timeouts, session TTL, write timeout and session owner URL are
  ported there.
- The fork session manager is kept. DataStorage keeps Upsert, which the
  fork's HA session paths still use. Upstream's restored-metadata
  write-back (stacklok#4842) is taken; its lazy tool injection hooks are
  dropped because the fork already repairs cross-replica sessions on
  request initialization.
- Backend session hints from restore are accepted but not forwarded,
  since backends that reject unknown Mcp-Session-Id values would fail
  the restore; lost sessions are recovered through reconnect.
- The token handler keeps defaulting the audience on every grant type
  (client credentials rely on it) and treats an empty resource as
  absent.
- Fork-only CRD fields are kept: sessionTTL, sessionStorage.usernameRef,
  allowedClientIds, optional oidcConfigRef.audience, redis address and
  secrets list keys. The duplicate MCPOIDCConfigReference.resourceUrl
  both sides added is collapsed into upstream's.
- Regenerated deepcopy, CRDs, CRD API docs and chart READMEs; the
  release-qualified operator ClusterRole name is preserved.

PAIINFRA-200

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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Large PR Detected

This PR exceeds 1000 lines of changes and requires justification before it can be reviewed.

How to unblock this PR:

Add a section to your PR description with the following format:

## Large PR Justification

[Explain why this PR must be large, such as:]
- Generated code that cannot be split
- Large refactoring that must be atomic
- Multiple related changes that would break if separated
- Migration or data transformation

Alternative:

Consider splitting this PR into smaller, focused changes (< 1000 lines each) for easier review and reduced risk.

See our Contributing Guidelines for more details.


This review will be automatically dismissed once you add the justification section.

@carlos-lopez-vecino
carlos-lopez-vecino merged commit 1ebb380 into dp-stable Sep 29, 2026
8 of 17 checks passed
@carlos-lopez-vecino
carlos-lopez-vecino deleted the chore/merge-upstream-v0.23.1 branch September 29, 2026 13:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.