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 @@ -44,7 +44,7 @@ projects:

- id: toolhive
repo: stacklok/toolhive
version: v0.48.0
version: v0.49.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
31 changes: 23 additions & 8 deletions docs/toolhive/guides-cli/ai-plugins.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -248,22 +248,37 @@ thv ai-plugin push ghcr.io/my-org/plugins/my-plugin:v1.0.0 \
--identity-token "$IDENTITY_TOKEN"
```

To sign with a cosign key pair on disk instead, pass `--key`:

```bash
thv ai-plugin push ghcr.io/my-org/plugins/my-plugin:v1.0.0 \
--key cosign.key
```

Set `COSIGN_PASSWORD` in the `thv serve` environment to decrypt an encrypted
key. Key-pair signing requires the locally discovered ToolHive server; a push
against a remote or manually configured API URL (for example, via
`TOOLHIVE_API_URL`) returns `403`. Use `--identity-token` or the default keyless
flow against those servers.

To publish without a signature, pass `--no-sign`:

```bash
thv ai-plugin push ghcr.io/my-org/plugins/my-plugin:v1.0.0 --no-sign
```

To install an unsigned plugin project-scoped, pass `--allow-unsigned` to
`thv ai-plugin install`.

:::note
`--key`, `--identity-token`, and `--no-sign` are mutually exclusive.

Plugin push is keyless-only. If you need to sign with a cosign key pair, publish
a [skill](./skills-management.mdx) instead, which supports both keyless and
key-pair signing via `--key`.
To install a key-pair-signed plugin project-scoped, pass
`--public-key <cosign.pub>` with the matching cosign public key on the first
install. ToolHive verifies the signature and pins the key in the project's
`toolhive.lock.yaml` so subsequent `thv ai-plugin sync` and
`thv ai-plugin upgrade` runs reuse it automatically. Distribute the public key
alongside the artifact. Keyless-signed plugins need no such out-of-band step,
since the signer identity is verifiable from the transparency-log entry.

:::
To install an unsigned plugin project-scoped, pass `--allow-unsigned` to
`thv ai-plugin install`.

Push uses your existing container registry credentials (for example, from
`docker login` or `podman login`). Authenticate before pushing.
Expand Down
29 changes: 15 additions & 14 deletions docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -761,15 +761,14 @@ For OAuth 2.0 servers that return identity in the token response itself, see

#### Select the token endpoint auth method

When a client secret is configured, the embedded auth server sends the client
credentials to the upstream token endpoint using HTTP Basic auth
(`client_secret_basic`). This matches the RFC 7591 default for confidential
clients and covers most providers. Public clients without a secret use the
`none` method.
Pre-registered OAuth 2.0 upstream clients send credentials to the token endpoint
in the request POST body (`client_secret_post`) by default, whether or not a
client secret is configured. Public clients without a secret use the `none`
method.

Set `tokenEndpointAuthMethod: client_secret_post` on `oauth2Config` for
providers that require credentials in the request body instead of the
`Authorization` header:
Set `tokenEndpointAuthMethod: client_secret_basic` on `oauth2Config` for
providers that require credentials in the HTTP `Authorization` header instead of
the request body:

```yaml title="MCPExternalAuthConfig: token endpoint auth method"
oauth2Config:
Expand All @@ -780,18 +779,20 @@ oauth2Config:
name: upstream-idp-secret
key: client-secret
# highlight-next-line
tokenEndpointAuthMethod: client_secret_post
tokenEndpointAuthMethod: client_secret_basic
```

Allowed values are `none`, `client_secret_basic`, and `client_secret_post`. DCR
clients continue to use the method negotiated at registration time.

:::info[Changed in v0.48.0]
:::info[Changed in v0.49.0]

Pre-registered OAuth 2.0 upstream clients with a configured secret previously
sent credentials in the request body. They now default to `client_secret_basic`.
If your provider rejects Basic auth on the token endpoint (rare), set
`tokenEndpointAuthMethod: client_secret_post` to restore the previous behavior.
If you are upgrading from v0.48.0, the default for pre-registered OAuth 2.0
upstream clients with a configured secret has changed back from
`client_secret_basic` to `client_secret_post`. If your provider only accepts
HTTP Basic auth on the token endpoint, set
`tokenEndpointAuthMethod: client_secret_basic` explicitly. Upgrades from v0.47.x
or earlier see no change in default behavior.

:::

Expand Down
28 changes: 15 additions & 13 deletions docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -472,23 +472,25 @@ ToolHive obtains the client ID and secret from the DCR response.

### Select the token endpoint auth method

When a client secret is configured, ToolHive sends the credentials to the
upstream OAuth 2.0 token endpoint using HTTP Basic auth (`client_secret_basic`).
This matches the RFC 7591 default for confidential clients and covers most
providers. Public clients without a secret use the `none` method.

Set `tokenEndpointAuthMethod: client_secret_post` on `oauth2Config` for
providers that require credentials in the request body instead of the
`Authorization` header. Allowed values are `none`, `client_secret_basic`, and
Pre-registered OAuth 2.0 upstream clients send credentials to the token endpoint
in the request POST body (`client_secret_post`) by default, whether or not a
client secret is configured. Public clients without a secret use the `none`
method.

Set `tokenEndpointAuthMethod: client_secret_basic` on `oauth2Config` for
providers that require credentials in the HTTP `Authorization` header instead of
the request body. Allowed values are `none`, `client_secret_basic`, and
`client_secret_post`. DCR clients continue to use the method negotiated at
registration time.

:::info[Changed in v0.48.0]
:::info[Changed in v0.49.0]

Pre-registered OAuth 2.0 upstream clients with a configured secret previously
sent credentials in the request body. They now default to `client_secret_basic`.
If your provider rejects Basic auth on the token endpoint (rare), set
`tokenEndpointAuthMethod: client_secret_post` to restore the previous behavior.
If you are upgrading from v0.48.0, the default for pre-registered OAuth 2.0
upstream clients with a configured secret has changed back from
`client_secret_basic` to `client_secret_post`. If your provider only accepts
HTTP Basic auth on the token endpoint, set
`tokenEndpointAuthMethod: client_secret_basic` explicitly. Upgrades from v0.47.x
or earlier see no change in default behavior.

:::

Expand Down
13 changes: 12 additions & 1 deletion docs/toolhive/integrations/aws-sts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,19 @@ awsSts:
roleClaim: roles # look at the "roles" claim instead of "groups"
```

The role claim must be a **string** or a **list of strings**. A bare string
matches when it equals a mapping's `claim` value exactly. Any other shape
(object, number, boolean, or null) is rejected: the request fails with
`403 Failed to determine IAM role` from the STS middleware, and vMCP outbound
STS calls fail with `failed to select IAM role`. Watch the proxy logs for
`role claim has unsupported shape, failing closed`, which identifies the
offending mapping.

A missing role claim falls back to `fallbackRoleArn`.

For more complex matching logic, use CEL expressions in the `matcher` field
instead of `claim`:
instead of `claim`. Matcher expressions run against the raw claims map and are
not affected by the shape rule above:

```yaml
roleMappings:
Expand Down
59 changes: 40 additions & 19 deletions docs/toolhive/reference/authz-policy-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ automatically.
| `Tool` | `Tool::"<tool_name>"` | A tool resource (used for `tools/call`) |
| `Prompt` | `Prompt::"<prompt_name>"` | A prompt resource (used for `prompts/get`) |
| `Resource` | `Resource::"<sanitized_uri>"` | A data resource (used for `resources/read`). The URI is [sanitized](#resource-uri-sanitization) for Cedar compatibility |
| `Skill` | `Skill::"<skill_uri>"` | A skill resource (used for `skills/get`). The exact skill URI is used verbatim as the entity ID |
| `FeatureType` | `FeatureType::"<feature>"` | A feature category entity. Values: `tool`, `prompt`, `resource`. Not currently used for authorization; list operations are handled via [response filtering](#list-operation-filtering) |
| `THVGroup` | `THVGroup::"<group_name>"` | A group membership entity. Used with Cedar's `in` operator for [group-based policies](#group-membership) |

Expand All @@ -43,15 +44,16 @@ These actions are evaluated against your Cedar policies:
| `Action::"call_tool"` | `tools/call` | Call a specific tool |
| `Action::"get_prompt"` | `prompts/get` | Retrieve a specific prompt |
| `Action::"read_resource"` | `resources/read` | Read a specific data resource |
| `Action::"get_skill"` | `skills/get` | Retrieve a specific skill |

### List operations

List methods (`tools/list`, `prompts/list`, `resources/list`, and
`resources/templates/list`) bypass request-level authorization entirely.
ToolHive allows the list request through and filters the response to include
only items the caller is authorized to access using the individual-access
actions above. See [List operation filtering](#list-operation-filtering) for
details.
List methods (`tools/list`, `prompts/list`, `resources/list`,
`resources/templates/list`, and `skills/list`) bypass request-level
authorization entirely. ToolHive allows the list request through and filters the
response to include only items the caller is authorized to access using the
individual-access actions above. See
[List operation filtering](#list-operation-filtering) for details.

### Always-allowed MCP methods

Expand Down Expand Up @@ -224,6 +226,21 @@ these attributes:
| `feature` | String | Always `"resource"` |
| `arg_<key>` | Varies | Request argument values |

### Skill get attributes (`skills/get`)

When a client retrieves a skill, the resource entity (`Skill::`) has these
attributes:

| Attribute | Type | Description |
| ----------- | ------ | ------------------------------ |
| `name` | String | The skill URI |
| `uri` | String | The skill URI (same as `name`) |
| `operation` | String | Always `"get"` |
| `feature` | String | Always `"skill"` |

A `skills/get` request with a missing, empty, or non-string `uri` parameter, or
with duplicate `uri` members, is rejected before it reaches an authorizer.

### Feature list attributes (list operations)

:::info[Not currently used]
Expand Down Expand Up @@ -470,10 +487,11 @@ permit(

## List operation filtering

List operations (`tools/list`, `prompts/list`, `resources/list`, and
`resources/templates/list`) bypass request-level authorization entirely.
ToolHive forwards the list request to the MCP server, then filters the response
to include only items the caller is authorized to access.
List operations (`tools/list`, `prompts/list`, `resources/list`,
`resources/templates/list`, and `skills/list`) bypass request-level
authorization entirely. ToolHive forwards the list request to the MCP server,
then filters the response to include only items the caller is authorized to
access.

For each item in the list response, ToolHive runs a policy check using the
corresponding individual-access action:
Expand All @@ -484,24 +502,27 @@ corresponding individual-access action:
| `prompts/list` | `Action::"get_prompt"` against each `Prompt::"<name>"` |
| `resources/list` | `Action::"read_resource"` against each `Resource::"<sanitized_uri>"` |
| `resources/templates/list` | `Action::"read_resource"` against each `Resource::"<sanitized_uri>"` built from the URI template |
| `skills/list` | `Action::"get_skill"` against each `Skill::"<skill_uri>"` |

This means you don't need separate list policies. Your `call_tool`,
`get_prompt`, and `read_resource` policies automatically control what appears in
list responses. For resources and resource templates, the per-item check uses
the [sanitized](#resource-uri-sanitization) entity ID, while the original URI
(or URI template) remains available via the `resource.uri` attribute.
`get_prompt`, `read_resource`, and `get_skill` policies automatically control
what appears in list responses. For resources and resource templates, the
per-item check uses the [sanitized](#resource-uri-sanitization) entity ID, while
the original URI (or URI template) remains available via the `resource.uri`
attribute. Skill URIs are used verbatim as the entity ID.

Filtered resource template responses are served with
`Cache-Control: private, no-store` so a shared HTTP cache cannot replay one
caller's filtered view to a different caller.

:::note

Because list responses are filtered using `call_tool`, `get_prompt`, and
`read_resource` policies, an item only appears in a list response when the
corresponding individual-access policy permits it. For example, if no
`call_tool` policy permits a given tool, that tool won't appear in `tools/list`
responses.
Because list responses are filtered using `call_tool`, `get_prompt`,
`read_resource`, and `get_skill` policies, an item only appears in a list
response when the corresponding individual-access policy permits it. For
example, if no `call_tool` policy permits a given tool, that tool won't appear
in `tools/list` responses. A `skills/list` response with no matching `get_skill`
permit returns an empty list rather than a 403.

:::

Expand Down
7 changes: 4 additions & 3 deletions docs/toolhive/reference/cli/thv_ai-plugin_push.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ Push a built AI-tool plugin to an OCI registry

Push a previously built plugin artifact to a remote OCI registry.

Push signs keylessly by default. Use --no-sign to publish unsigned; plugin push
does not support key-pair signing and has no --key flag.
Push signs keylessly by default. Use --key to sign with a cosign key pair
instead, or --no-sign to publish unsigned.

```
thv ai-plugin push [reference] [flags]
Expand All @@ -28,7 +28,8 @@ thv ai-plugin push [reference] [flags]

```
-h, --help help for push
--identity-token string OIDC identity token (or a path to a file containing one) for keyless signing. If omitted, one is acquired automatically: from the GitHub Actions OIDC token when running with id-token: write permission, otherwise via an interactive browser sign-in
--identity-token string OIDC identity token (or a path to a file containing one) for keyless signing. Mutually exclusive with --key. If omitted, one is acquired automatically: from the GitHub Actions OIDC token when running with id-token: write permission, otherwise via an interactive browser sign-in
--key string Path to a cosign private key to sign the pushed artifact. Requires the locally discovered ToolHive server; for a remote or manually configured API URL, use keyless signing. Encrypted keys are decrypted with COSIGN_PASSWORD read from the 'thv serve' process, which performs the signing. Consumers installing the result project-scoped must pass --public-key with the matching cosign public key the first time; distribute it alongside the artifact. Keyless signing needs no such out-of-band step, since the signer identity is verifiable from the artifact itself
--no-sign Push without signing (consumers will need an explicit unsigned exception to install project-scoped)
```

Expand Down
Loading