Skip to content

Commit a4b6710

Browse files
Document verified v0.49.0 behavior changes
- Restore client_secret_post as the default token_endpoint_auth_method for pre-registered OAuth 2.0 upstreams on both K8s and vMCP embedded auth server pages. - Document AWS STS role-claim shape rule: string or list of strings, other shapes fail closed with a 403. - Add Skill entity, get_skill action, and skills/list response filtering to the Cedar authorization policy reference. - Remove the stale "plugin push is keyless-only" note and describe the restored --key cosign-key-pair signing for locally discovered ToolHive servers.
1 parent 7e68716 commit a4b6710

5 files changed

Lines changed: 105 additions & 55 deletions

File tree

docs/toolhive/guides-cli/ai-plugins.mdx

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -248,22 +248,37 @@ thv ai-plugin push ghcr.io/my-org/plugins/my-plugin:v1.0.0 \
248248
--identity-token "$IDENTITY_TOKEN"
249249
```
250250

251+
To sign with a cosign key pair on disk instead, pass `--key`:
252+
253+
```bash
254+
thv ai-plugin push ghcr.io/my-org/plugins/my-plugin:v1.0.0 \
255+
--key cosign.key
256+
```
257+
258+
Set `COSIGN_PASSWORD` in the `thv serve` environment to decrypt an encrypted
259+
key. Key-pair signing requires the locally discovered ToolHive server; a push
260+
against a remote or manually configured API URL (for example, via
261+
`TOOLHIVE_API_URL`) returns `403`. Use `--identity-token` or the default keyless
262+
flow against those servers.
263+
251264
To publish without a signature, pass `--no-sign`:
252265

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

257-
To install an unsigned plugin project-scoped, pass `--allow-unsigned` to
258-
`thv ai-plugin install`.
259-
260-
:::note
270+
`--key`, `--identity-token`, and `--no-sign` are mutually exclusive.
261271

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

266-
:::
280+
To install an unsigned plugin project-scoped, pass `--allow-unsigned` to
281+
`thv ai-plugin install`.
267282

268283
Push uses your existing container registry credentials (for example, from
269284
`docker login` or `podman login`). Authenticate before pushing.

docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -761,15 +761,14 @@ For OAuth 2.0 servers that return identity in the token response itself, see
761761

762762
#### Select the token endpoint auth method
763763

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

770-
Set `tokenEndpointAuthMethod: client_secret_post` on `oauth2Config` for
771-
providers that require credentials in the request body instead of the
772-
`Authorization` header:
769+
Set `tokenEndpointAuthMethod: client_secret_basic` on `oauth2Config` for
770+
providers that require credentials in the HTTP `Authorization` header instead of
771+
the request body:
773772

774773
```yaml title="MCPExternalAuthConfig: token endpoint auth method"
775774
oauth2Config:
@@ -780,18 +779,20 @@ oauth2Config:
780779
name: upstream-idp-secret
781780
key: client-secret
782781
# highlight-next-line
783-
tokenEndpointAuthMethod: client_secret_post
782+
tokenEndpointAuthMethod: client_secret_basic
784783
```
785784
786785
Allowed values are `none`, `client_secret_basic`, and `client_secret_post`. DCR
787786
clients continue to use the method negotiated at registration time.
788787

789-
:::info[Changed in v0.48.0]
788+
:::info[Changed in v0.49.0]
790789

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

796797
:::
797798

docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -472,23 +472,25 @@ ToolHive obtains the client ID and secret from the DCR response.
472472

473473
### Select the token endpoint auth method
474474

475-
When a client secret is configured, ToolHive sends the credentials to the
476-
upstream OAuth 2.0 token endpoint using HTTP Basic auth (`client_secret_basic`).
477-
This matches the RFC 7591 default for confidential clients and covers most
478-
providers. Public clients without a secret use the `none` method.
479-
480-
Set `tokenEndpointAuthMethod: client_secret_post` on `oauth2Config` for
481-
providers that require credentials in the request body instead of the
482-
`Authorization` header. Allowed values are `none`, `client_secret_basic`, and
475+
Pre-registered OAuth 2.0 upstream clients send credentials to the token endpoint
476+
in the request POST body (`client_secret_post`) by default, whether or not a
477+
client secret is configured. Public clients without a secret use the `none`
478+
method.
479+
480+
Set `tokenEndpointAuthMethod: client_secret_basic` on `oauth2Config` for
481+
providers that require credentials in the HTTP `Authorization` header instead of
482+
the request body. Allowed values are `none`, `client_secret_basic`, and
483483
`client_secret_post`. DCR clients continue to use the method negotiated at
484484
registration time.
485485

486-
:::info[Changed in v0.48.0]
486+
:::info[Changed in v0.49.0]
487487

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

493495
:::
494496

docs/toolhive/integrations/aws-sts.mdx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,8 +341,19 @@ awsSts:
341341
roleClaim: roles # look at the "roles" claim instead of "groups"
342342
```
343343

344+
The role claim must be a **string** or a **list of strings**. A bare string
345+
matches when it equals a mapping's `claim` value exactly. Any other shape
346+
(object, number, boolean, or null) is rejected: the request fails with
347+
`403 Failed to determine IAM role` from the STS middleware, and vMCP outbound
348+
STS calls fail with `failed to select IAM role`. Watch the proxy logs for
349+
`role claim has unsupported shape, failing closed`, which identifies the
350+
offending mapping.
351+
352+
A missing role claim continues to fall back to `fallbackRoleArn`.
353+
344354
For more complex matching logic, use CEL expressions in the `matcher` field
345-
instead of `claim`:
355+
instead of `claim`. Matcher expressions run against the raw claims map and are
356+
not affected by the shape rule above:
346357

347358
```yaml
348359
roleMappings:

docs/toolhive/reference/authz-policy-reference.mdx

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ automatically.
2626
| `Tool` | `Tool::"<tool_name>"` | A tool resource (used for `tools/call`) |
2727
| `Prompt` | `Prompt::"<prompt_name>"` | A prompt resource (used for `prompts/get`) |
2828
| `Resource` | `Resource::"<sanitized_uri>"` | A data resource (used for `resources/read`). The URI is [sanitized](#resource-uri-sanitization) for Cedar compatibility |
29+
| `Skill` | `Skill::"<skill_uri>"` | A skill resource (used for `skills/get`). The exact skill URI is used verbatim as the entity ID |
2930
| `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) |
3031
| `THVGroup` | `THVGroup::"<group_name>"` | A group membership entity. Used with Cedar's `in` operator for [group-based policies](#group-membership) |
3132

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

4749
### List operations
4850

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

5658
### Always-allowed MCP methods
5759

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

229+
### Skill get attributes (`skills/get`)
230+
231+
When a client retrieves a skill, the resource entity (`Skill::`) has these
232+
attributes:
233+
234+
| Attribute | Type | Description |
235+
| ----------- | ------ | ------------------------------ |
236+
| `name` | String | The skill URI |
237+
| `uri` | String | The skill URI (same as `name`) |
238+
| `operation` | String | Always `"get"` |
239+
| `feature` | String | Always `"skill"` |
240+
241+
A `skills/get` request with a missing, empty, or non-string `uri` parameter, or
242+
with duplicate `uri` members, is rejected before it reaches an authorizer.
243+
227244
### Feature list attributes (list operations)
228245

229246
:::info[Not currently used]
@@ -470,10 +487,11 @@ permit(
470487

471488
## List operation filtering
472489

473-
List operations (`tools/list`, `prompts/list`, `resources/list`, and
474-
`resources/templates/list`) bypass request-level authorization entirely.
475-
ToolHive forwards the list request to the MCP server, then filters the response
476-
to include only items the caller is authorized to access.
490+
List operations (`tools/list`, `prompts/list`, `resources/list`,
491+
`resources/templates/list`, and `skills/list`) bypass request-level
492+
authorization entirely. ToolHive forwards the list request to the MCP server,
493+
then filters the response to include only items the caller is authorized to
494+
access.
477495

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

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

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

498518
:::note
499519

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

506527
:::
507528

0 commit comments

Comments
 (0)