feat(19.7-02): identity kinds, ceilings, prompts and the MCP services page

- personal tokens only behind mcp.allow_personal_tokens (401 without a
  lookup otherwise); an admin token yields a backend-only identity with
  its scope limit per dispatched method, a user token a frontend-only one
- read/write ceiling and prompts proven by the tracer's subtests; the
  audit line asserted to carry the tool and never the arguments or token
- the mounted *pier.Pier is published on the application (test seams in
  export_test.go)
- docs/services/mcp.md with the tested acme.blog example, site.yaml note,
  cross-links from authentication.md, registration.md and the surf README
This commit is contained in:
Jakub Zych
2026-10-22 18:34:39 +02:00
parent 806e87c510
commit 8c6c0a3654
12 changed files with 589 additions and 13 deletions

View File

@@ -137,7 +137,7 @@ To protect routes, return that middleware from `pact.HasMiddleware` under a name
A guard that resolves more than a user, such as an API token record, implements `bouncer.CredentialGuard`; the middleware stores that record too, and handlers read it with `bouncer.Credential`.
A framework dispatcher that calls a route in-process on behalf of a caller it has already authenticated (the MCP module running a tool through the plugin's own route) does not mint a token for the call. It attaches a `bouncer.Dispatch` marker to the request context with `bouncer.WithDispatch`: the pre-resolved principal, its optional credential and the exact guard names that may honour it. The marker is context-only. Nothing reads a header, cookie or query string for it, so an HTTP request can never carry one, and only a dispatcher inside the framework sets it; plugins never do. `bouncer.Registry.Middleware` honours a marker only for a guard name the marker lists (`bouncer.Dispatched`) and then skips the guard, which covers every guard registered through the registry, such as a plugin's API token guard. `cabana.ResolveBackend` honours a marker for the `backend` guard when its principal is a backend one; a frontend marker is refused as unauthenticated, and a scoped principal is still checked against the cover codes. A marker for one guard has no effect on a route behind another.
A framework dispatcher that calls a route in-process on behalf of a caller it has already authenticated (the MCP module running a tool through the plugin's own route) does not mint a token for the call. It attaches a `bouncer.Dispatch` marker to the request context with `bouncer.WithDispatch`: the pre-resolved principal, its optional credential and the exact guard names that may honour it. The marker is context-only. Nothing reads a header, cookie or query string for it, so an HTTP request can never carry one, and only a dispatcher inside the framework sets it; plugins never do. `bouncer.Registry.Middleware` honours a marker only for a guard name the marker lists (`bouncer.Dispatched`) and then skips the guard, which covers every guard registered through the registry, such as a plugin's API token guard. `cabana.ResolveBackend` honours a marker for the `backend` guard when its principal is a backend one; a frontend marker is refused as unauthenticated, and a scoped principal is still checked against the cover codes. A marker for one guard has no effect on a route behind another. The one dispatcher in the framework is the [MCP endpoint](mcp.md): pier resolves the caller from the bearer of every MCP request, asks the tool's guard provider for the principal and sets the marker on the in-process call, so the route sees the same principal an HTTP request with that user's token would carry, while the bearer itself never reaches the route.
### Accepting admin tokens on plugin routes