Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/upstream-projects.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ projects:

- id: toolhive
repo: stacklok/toolhive
version: v0.60.0
version: v0.61.0
# toolhive is a monorepo covering the CLI, the Kubernetes
# operator, and the vMCP gateway. It also introduces cross-
# cutting features that land in concepts/, integrations/,
Expand Down
26 changes: 17 additions & 9 deletions docs/toolhive/guides-vmcp/delegate-agent-identity.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,13 @@ registration and rejects a replayed `client_assertion` (the same `jti` presented
twice) on the second attempt. ToolHive also accepts the token endpoint URL as
the `aud` value, or an `aud` array that contains either value. These forms are
deprecated, and a future release will require the issuer as the only audience.

ToolHive returns HTTP 401 `invalid_client` for every rejected assertion: a bad
signature, an unknown `kid`, a wrong `aud`, a reused `jti`, an expired
assertion, an `nbf` or `iat` in the future, or a lifetime longer than the server
allows. A request that sets `client_assertion_type` but omits `client_assertion`
gets HTTP 400 `invalid_request`.

Everything else about the exchange, subject-token validation against
`trustedIssuers`, actor resolution, and the resulting `act` claim, works exactly
as in the pre-provisioned case above; the issued token's `act.sub` is the
Expand Down Expand Up @@ -303,12 +310,13 @@ must register again.

## Troubleshooting

| Error | Likely cause |
| -------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_client` (exact hint varies by cause: wrong secret, wrong `token_endpoint_auth_method`, or a public client attempting a confidential-only grant) | The delegate client must be confidential. Verify `inboundGrants.tokenExchange.delegateClients[].clientSecretRef` is set and the client authenticated with the matching secret and method. |
| `invalid_grant`: "The subject token does not authorize this client to act on behalf of the subject." | The subject token's `may_act.sub` doesn't match the actor ToolHive resolved for this request. |
| `invalid_grant`: "This client is not authorized to exchange subject tokens from the external actor's issuer." | The authenticated client isn't in `issuerPolicies[].allowedDelegateClients` for the issuer that minted the subject token. |
| `invalid_grant`: "The subject token was issued to a different client." | The subject token's own `client_id` claim doesn't match the authenticated delegate client, and the client isn't in `allowedDelegateClients`. |
| `invalid_request`: "The subject token is invalid or could not be verified." | The subject token failed signature, issuer, or audience validation against every configured `trustedIssuers` entry. |
| CRD rejected: `issuerPolicies[]: allowedDelegateClients: Required value` | An `inboundGrants.tokenExchange.issuerPolicies` entry is missing `allowedDelegateClients`. Add it. |
| CRD rejected: "delegateClients with an HTTP issuer require insecureAllowConfidentialOverLoopbackHTTP to be explicitly enabled" | A delegate client uses a plain-HTTP issuer. Use an `https://` issuer, or for local development set `insecureAllowConfidentialOverLoopbackHTTP: true` with a loopback issuer. |
| Error | Likely cause |
| -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_client` (exact hint varies by cause: wrong secret, wrong `token_endpoint_auth_method`, or a public client attempting a confidential-only grant) | The delegate client must be confidential. Verify `inboundGrants.tokenExchange.delegateClients[].clientSecretRef` is set and the client authenticated with the matching secret and method. |
| HTTP 401 `invalid_client` for a `private_key_jwt` client | The `client_assertion` failed validation. Check that it's signed with a key whose `kid` is in the registered JWKS, that `aud` is the issuer, that `exp`, `nbf`, and `iat` are valid against the server clock, and that each request uses a fresh `jti`. |
| `invalid_grant`: "The subject token does not authorize this client to act on behalf of the subject." | The subject token's `may_act.sub` doesn't match the actor ToolHive resolved for this request. |
| `invalid_grant`: "This client is not authorized to exchange subject tokens from the external actor's issuer." | The authenticated client isn't in `issuerPolicies[].allowedDelegateClients` for the issuer that minted the subject token. |
| `invalid_grant`: "The subject token was issued to a different client." | The subject token's own `client_id` claim doesn't match the authenticated delegate client, and the client isn't in `allowedDelegateClients`. |
| `invalid_request`: "The subject token is invalid or could not be verified." | The subject token failed signature, issuer, or audience validation against every configured `trustedIssuers` entry. |
| CRD rejected: `issuerPolicies[]: allowedDelegateClients: Required value` | An `inboundGrants.tokenExchange.issuerPolicies` entry is missing `allowedDelegateClients`. Add it. |
| CRD rejected: "delegateClients with an HTTP issuer require insecureAllowConfidentialOverLoopbackHTTP to be explicitly enabled" | A delegate client uses a plain-HTTP issuer. Use an `https://` issuer, or for local development set `insecureAllowConfidentialOverLoopbackHTTP: true` with a loopback issuer. |
6 changes: 3 additions & 3 deletions docs/toolhive/reference/helm/operator-crds.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ Control CRD installation and preservation with these Helm values.
This reference lists values declared in the chart's default `values.yaml`.
Templates can also accept optional settings and inherited `global` values.

Chart: `toolhive-operator-crds`. Chart version: `0.60.0`. Source:
[`values.yaml`](https://github.com/stacklok/toolhive/blob/v0.60.0/deploy/charts/operator-crds/values.yaml)
at `v0.60.0`.
Chart: `toolhive-operator-crds`. Chart version: `0.61.0`. Source:
[`values.yaml`](https://github.com/stacklok/toolhive/blob/v0.61.0/deploy/charts/operator-crds/values.yaml)
at `v0.61.0`.

## Values

Expand Down
18 changes: 9 additions & 9 deletions docs/toolhive/reference/helm/operator.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ Configure the ToolHive operator with these Helm values.
This reference lists values declared in the chart's default `values.yaml`.
Templates can also accept optional settings and inherited `global` values.

Chart: `toolhive-operator`. Chart version: `0.60.0`. Source:
[`values.yaml`](https://github.com/stacklok/toolhive/blob/v0.60.0/deploy/charts/operator/values.yaml)
at `v0.60.0`.
Chart: `toolhive-operator`. Chart version: `0.61.0`. Source:
[`values.yaml`](https://github.com/stacklok/toolhive/blob/v0.61.0/deploy/charts/operator/values.yaml)
at `v0.61.0`.

## Values

Expand Down Expand Up @@ -73,7 +73,7 @@ features:
gc:
gogc: 75
gomemlimit: 110MiB
image: ghcr.io/stacklok/toolhive/operator:v0.60.0
image: ghcr.io/stacklok/toolhive/operator:v0.61.0
imageDiscovery:
enabled: false
resources: {}
Expand Down Expand Up @@ -158,8 +158,8 @@ serviceAccount:
labels: {}
name: toolhive-operator
tolerations: []
toolhiveRunnerImage: ghcr.io/stacklok/toolhive/proxyrunner:v0.60.0
vmcpImage: ghcr.io/stacklok/toolhive/vmcp:v0.60.0
toolhiveRunnerImage: ghcr.io/stacklok/toolhive/proxyrunner:v0.61.0
vmcpImage: ghcr.io/stacklok/toolhive/vmcp:v0.61.0
volumeMounts: []
volumes: []
```
Expand Down Expand Up @@ -318,7 +318,7 @@ Key containing the PEM-encoded CA certificate in the Secret.

**Type:** `string`

**Default:** `"ghcr.io/stacklok/toolhive/operator:v0.60.0"`
**Default:** `"ghcr.io/stacklok/toolhive/operator:v0.61.0"`

Container image for the operator.

Expand All @@ -334,15 +334,15 @@ Image pull policy for the operator container and image-discovery containers.

**Type:** `string`

**Default:** `"ghcr.io/stacklok/toolhive/proxyrunner:v0.60.0"`
**Default:** `"ghcr.io/stacklok/toolhive/proxyrunner:v0.61.0"`

Container image for ToolHive proxy runners created by the operator.

### `operator.vmcpImage`

**Type:** `string`

**Default:** `"ghcr.io/stacklok/toolhive/vmcp:v0.60.0"`
**Default:** `"ghcr.io/stacklok/toolhive/vmcp:v0.61.0"`

Container image for Virtual MCP Server (vMCP) deployments created by the
operator.
Expand Down
41 changes: 27 additions & 14 deletions static/api-specs/toolhive-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ components:
ExchangeRefreshTokens enables refresh-token issuance through token
exchange for this client. Presence of the block enables it; a nil block
refuses both grant kinds. Valid only with restricted, and requires
APIAudience.
SubjectTokenAudiences and offline_access or offline in scopes_supported.
properties:
id_jag_max_lifetime:
description: |-
Expand All @@ -285,10 +285,13 @@ components:
properties:
audiences:
description: |-
Audiences are the RFC 8707 resource values this client may request a
token for. Required, and must be a subset of RunConfig.AllowedAudiences:
a declared client must not receive every allowed audience just because
this was left empty.
Audiences are output only: the upper bound on the audiences of the
access tokens this client can obtain through any grant. They must be a
subset of RunConfig.AllowedAudiences: a declared client must not receive
every allowed audience just because this was left empty. Required, unless
token_exchange.exchange_refresh_tokens is set and token exchange is the
client's only grant (grant_types omitted, or just the token-exchange
grant), in which case it may be empty.
items:
type: string
type: array
Expand Down Expand Up @@ -381,19 +384,14 @@ components:
register at runtime (DCR or CIMD), so startup cannot check that they
exist. A configured delegate client cannot be listed: its tokens carry
its own client_id, so listing it would trust everything it accepts.
Valid only with restricted; empty means the client's own tokens only,
except that exchange_refresh_tokens requires a non-empty list and does
not accept the client's own tokens.
Valid only with restricted. On plain token exchange, empty means the
client's own tokens only. The exchange-grant path (exchange_refresh_tokens)
accepts only the listed source clients, never the client's own tokens, so
the list must be non-empty there.
items:
type: string
type: array
uniqueItems: false
api_audience:
description: |-
APIAudience is the client's own API audience. The client may request it
even when the subject token does not cover it. It must be one of the
client's Audiences. Valid only with restricted.
type: string
exchange_refresh_tokens:
$ref: '#/components/schemas/authserver.DelegateClientExchangeRefreshTokens'
self_issued_subject_tokens:
Expand All @@ -404,6 +402,21 @@ components:
this client does not lift that restriction. With any, the client may
exchange any self-issued token.
type: string
subject_token_audiences:
description: |-
SubjectTokenAudiences are the audiences a self-issued subject token must
carry, matched exactly: the token must contain at least one of them. The
check applies on every exchange path, including the client's own tokens.
Listing a value grants nothing: the client can receive that audience only
if its audiences also cover it, and startup refuses that while
exchange_refresh_tokens is set. Each entry must be one of the server's
allowed_audiences. Valid only with restricted, and required with
exchange_refresh_tokens. An empty list is the same as an omitted one:
no requirement.
items:
type: string
type: array
uniqueItems: false
required:
- self_issued_subject_tokens
type: object
Expand Down
Loading