Skip to content

Commit d41a0ea

Browse files
Update stacklok/toolhive-registry-server to v1.5.0 (#1039)
* Update stacklok/toolhive-registry-server to v1.5.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Refresh reference assets for toolhive-registry-server v1.5.0 * Document registry-server v1.5.0 authz and api.timeout --------- Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
1 parent 2700850 commit d41a0ea

6 files changed

Lines changed: 348 additions & 116 deletions

File tree

.github/upstream-projects.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
projects:
3030
- id: toolhive-registry-server
3131
repo: stacklok/toolhive-registry-server
32-
version: v1.4.13
32+
version: v1.5.0
3333
docs_paths:
3434
- docs/toolhive/guides-registry
3535
- docs/toolhive/concepts/registry-criteria.mdx

docs/toolhive/guides-registry/audit-logging.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Resource lifecycle events:
6969
| `registry.entries.list` | Entries listed for a registry |
7070
| `entry.publish` | Entry published to a managed source |
7171
| `entry.delete` | Entry deleted |
72+
| `entry.claims.read` | Entry claims retrieved |
7273
| `entry.claims.update` | Entry claims updated |
7374
| `user.info` | Caller identity info retrieved (`/v1/me`) |
7475

docs/toolhive/guides-registry/authorization.mdx

Lines changed: 98 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,12 @@ auth:
7777

7878
### Available roles
7979

80-
| Role | Grants access to |
81-
| ------------------ | ------------------------------------------------------------- |
82-
| `superAdmin` | All operations; bypasses all claim checks |
83-
| `manageSources` | Create, update, delete, and list sources via the admin API |
84-
| `manageRegistries` | Create, update, delete, and list registries via the admin API |
85-
| `manageEntries` | Publish and delete MCP server versions and skills |
80+
| Role | Grants access to |
81+
| ------------------ | -------------------------------------------------------------------- |
82+
| `superAdmin` | All operations; bypasses all claim checks |
83+
| `manageSources` | Create, update, delete, and list sources via the admin API |
84+
| `manageRegistries` | Create, update, delete, and list registries via the admin API |
85+
| `manageEntries` | Publish, delete, and manage claims on MCP server versions and skills |
8686

8787
### Role rule matching
8888

@@ -197,10 +197,30 @@ A caller who requests `GET /registry/platform/v0.1/servers` must have JWT claims
197197
that include `org: "acme"` and `team: "platform"`. Otherwise, the server returns
198198
`403 Forbidden`.
199199

200-
### Claim containment
200+
### Claim matching rules
201201

202-
The server uses **containment** (superset check) for claim validation: the
203-
caller's claims must be a superset of the resource's claims. For example:
202+
The server compares caller claims against a resource's claims with two rules
203+
that differ only in how they handle array-valued claims. Every check does an AND
204+
across keys (the caller must satisfy every key present on the resource), and
205+
both rules default-deny on unlabeled resources.
206+
207+
| Rule | Within-array test | Used for |
208+
| -------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
209+
| **Visibility** | OR - the caller must share **any one** array value | Reads and access gates: list and get on sources, registries, and entries; the registry access gate on `/registry/{name}`; delete on sources, registries, and entry versions. |
210+
| **Subset** | AND - the caller must hold **every** array value | Writes: create or update a source or registry; publish an entry; update the claims on a published entry. Applied to the **incoming** claims in the request, not to the resource's existing tags. |
211+
212+
For scalar (string) claims the two rules behave identically. The difference only
213+
shows up on array values.
214+
215+
For example, a resource tagged `team: ["platform", "data"]` is an allow-list:
216+
under the visibility rule, a caller whose JWT has `team: "platform"` can see and
217+
delete it. Under the subset rule, the same caller could **not** create or update
218+
a resource tagged that way, because they don't hold both `platform` and `data`
219+
in their own JWT. This split prevents a caller from stamping a resource with
220+
wider visibility than their own identity while keeping shared resources visible
221+
to every eligible team.
222+
223+
Examples on scalar claims:
204224

205225
| Resource claims | Caller JWT claims | Result |
206226
| --------------------------------- | --------------------------------- | ---------------------------------- |
@@ -209,6 +229,19 @@ caller's claims must be a superset of the resource's claims. For example:
209229
| `{}` (no claims) | `{org: "acme"}` | Denied (default-deny on unlabeled) |
210230
| `{org: "acme"}` | `{org: "contoso"}` | Denied |
211231

232+
:::info[Upgrading from earlier releases]
233+
234+
Registry Server v1.4.x treated array-valued claims as AND-within-array for every
235+
check, so a resource tagged `team: ["platform", "data"]` was invisible to a
236+
caller who held only one of the team values. From v1.5.0, reads and access gates
237+
use the visibility (OR) rule instead, so callers who share any of the tagged
238+
values can see and delete the resource. Writes still enforce the subset (AND)
239+
rule to prevent visibility escalation. If you relied on AND-within-array to keep
240+
an array-tagged resource hidden from callers who held only one value, retag the
241+
resource with the specific set you want to require.
242+
243+
:::
244+
212245
### Unlabeled resources
213246

214247
When `auth.authz` is configured, sources, registries, and entries with no claims
@@ -288,11 +321,32 @@ curl -X POST \
288321
See the [Registry API reference](../reference/registry-api.mdx) for the full
289322
server payload schema, including `packages`, `remotes`, and `_meta` fields.
290323

291-
### Update claims on an existing entry
324+
### Read and update claims on an existing entry
292325

293-
Use `PUT /v1/entries/{type}/{name}/claims` to change the claims on every version
294-
of a previously published entry. The `type` path parameter is either `server` or
295-
`skill`.
326+
Use `GET /v1/entries/{type}/{name}/claims` to fetch the claims on a previously
327+
published entry, and `PUT /v1/entries/{type}/{name}/claims` to change them. The
328+
`type` path parameter is either `server` or `skill`, and the endpoints require
329+
the `manageEntries` role. Because entry names contain a slash separating
330+
namespace from server name, URL-encode the slash as `%2F` so the path is treated
331+
as a single `{name}` path parameter.
332+
333+
```bash title="Read claims on a published server"
334+
curl -X GET \
335+
"https://registry.example.com/v1/entries/server/io.github.acme%2Fmy-server/claims" \
336+
-H "Authorization: Bearer $TOKEN"
337+
```
338+
339+
The response envelope is always a JSON object, even when the entry has no
340+
claims:
341+
342+
```json title="Response: 200 OK"
343+
{
344+
"claims": {
345+
"org": "acme",
346+
"team": "platform"
347+
}
348+
}
349+
```
296350

297351
```bash title="Update claims on a published server"
298352
curl -X PUT \
@@ -307,35 +361,52 @@ curl -X PUT \
307361
}'
308362
```
309363

310-
Because entry names contain a slash separating namespace from server name,
311-
URL-encode the slash as `%2F` so the path is treated as a single `{name}` path
312-
parameter.
364+
Pass an empty `claims` object (`{"claims": {}}`) to clear claims entirely.
365+
Successful updates return `204 No Content`.
366+
367+
Both endpoints apply the standard [claim matching rules](#claim-matching-rules)
368+
to the caller's JWT:
369+
370+
- The GET returns `403 Forbidden` unless the caller can see the entry under the
371+
visibility rule (or is a super-admin).
372+
- The PUT requires that the caller can see the existing entry (visibility)
373+
**and** that the new claims are a subset of the caller's JWT (subset), so a
374+
caller can't broaden an entry's visibility past their own identity.
313375

314-
Pass an empty `claims` object (`{"claims": {}}`) to clear claims entirely. The
315-
same authorization rules apply: your JWT claims must satisfy both the existing
316-
claims on the entry (so you can modify it) and the new claims you're setting (so
317-
you can't escalate visibility beyond what you're entitled to). Successful
318-
updates return `204 No Content`.
376+
Both endpoints are scoped to managed-source entries. Entries synced from a Git,
377+
API, file, or Kubernetes source get their claims from upstream (the source
378+
manifest, or the `toolhive.stacklok.dev/authz-claims` annotation for
379+
Kubernetes), and every sync overwrites them, so any API write would be
380+
ephemeral. To inspect the claims on synced entries, use
381+
`/v1/sources/{name}/entries` or `/v1/registries/{name}/entries`.
319382

320383
## Admin API claim scoping
321384

322385
When authorization is enabled, the admin API endpoints for managing sources and
323386
registries are also scoped by claims:
324387

325-
- **List sources/registries**: Only returns resources whose claims the caller's
326-
JWT satisfies.
388+
- **List sources/registries**: Only returns resources the caller can see under
389+
the visibility rule.
327390
- **Get source/registry by name**: Returns `404 Not Found` (not `403`) when the
328-
caller's claims don't match. This prevents information disclosure about
391+
caller can't see the resource. This prevents information disclosure about
329392
resources the caller cannot access.
330393
- **List source/registry entries**: `GET /v1/sources/{name}/entries` and
331394
`GET /v1/registries/{name}/entries` return the raw entries (servers and skills
332395
with versions and claims) in a source or registry. These endpoints follow the
333396
same claim scoping: the parent source or registry must be accessible to the
334397
caller.
335398
- **Create source/registry**: The request claims must be a subset of the
336-
caller's JWT claims.
337-
- **Update/delete source/registry**: The caller's JWT claims must satisfy the
338-
existing resource's claims.
399+
caller's JWT claims (subset rule).
400+
- **Update source/registry**: The caller must be able to see the existing
401+
resource (visibility rule) and the incoming claims must be a subset of the
402+
caller's JWT (subset rule).
403+
- **Delete source/registry**: The caller must be able to see the resource
404+
(visibility rule). On shared resources tagged with an array of allowed values,
405+
any caller who can see the resource can delete it, so list, get, and delete
406+
now agree.
407+
408+
See [Claim matching rules](#claim-matching-rules) for how visibility and subset
409+
differ on array-valued claims.
339410

340411
## Auth-only mode
341412

docs/toolhive/guides-registry/configuration.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ sources:
248248
- name: mcp-upstream
249249
api:
250250
endpoint: https://registry.modelcontextprotocol.io
251+
timeout: '30s'
251252
syncPolicy:
252253
interval: '1h'
253254
@@ -260,6 +261,11 @@ registries:
260261

261262
- `endpoint` (required): Base URL of the upstream MCP Registry API (without
262263
path)
264+
- `timeout` (optional): Per-request HTTP timeout as a Go duration (for example,
265+
`30s`, `1m`). Must be greater than zero and no larger than `5m`. Defaults to
266+
`10s`. Raise this for public or occasionally slow upstreams whose
267+
time-to-first-byte exceeds the default. The same bound is enforced whether the
268+
source is loaded from the config file or created through the admin API.
263269

264270
:::note
265271

docs/toolhive/guides-registry/publish-servers.mdx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -337,10 +337,24 @@ subsequent lookups.
337337

338338
:::
339339

340-
### Update claims on a published server
340+
### Read and update claims on a published server
341341

342-
To change the authorization claims on an existing server, send a `PUT` request
343-
to the claims path:
342+
To read the current claims on a published server, send a `GET` request to the
343+
claims path. The response envelope is always a JSON object, even when the entry
344+
has no claims:
345+
346+
```bash
347+
curl -X GET \
348+
https://registry.example.com/v1/entries/server/io.github.acme%2Fdeploy-helper/claims \
349+
-H "Authorization: Bearer <TOKEN>"
350+
```
351+
352+
```json title="Response: 200 OK"
353+
{ "claims": { "org": "acme", "team": "platform" } }
354+
```
355+
356+
To change the claims on an existing server, send a `PUT` request to the same
357+
path:
344358

345359
```bash
346360
curl -X PUT \
@@ -350,10 +364,12 @@ curl -X PUT \
350364
-d '{"claims": {"org": "acme", "team": "platform"}}'
351365
```
352366

353-
The JWT presented on both the original publish and this update must satisfy the
354-
claims being set. See
367+
Both endpoints require the `manageEntries` role, and your JWT must satisfy the
368+
entry's existing claims. On update, the new claims must also be a subset of your
369+
JWT. These endpoints only cover managed-source entries; synced entries get their
370+
claims from upstream. See
355371
[Claims on published entries](./authorization.mdx#claims-on-published-entries)
356-
for the authorization rules.
372+
for the full authorization rules.
357373

358374
## Next steps
359375

0 commit comments

Comments
 (0)