graph: accept a meta.sr.ht token, so /query can be federated api.sr.ht forwards ONE client Authorization header to every service a federated query touches — AuthMiddleware copies it verbatim into the request context, and the Internal credential it can mint is used only to fetch schemas at startup. So a federated caller arrives here holding whatever credential the client had, and the only credential that works across the whole instance is a meta.sr.ht personal access token. /query refused those, which made federating it one `api-origin=` line that would produce 401s. api-meta.json was the stronger half of the same refusal: with an empty scope list meta.sr.ht renders no checkbox for this service, so no PAT could be scoped for it even in principle — no amount of code in graph/ could have worked around that. It now publishes SPECS, derived from authn.ScopeRead rather than spelled twice, with a test on each side asserting the two agree. A scope published and not checked admits what should be refused, one checked and not published cannot be minted at all, and neither is visible from inside one file. The plane is built in cmd/specsrht beside the graph.New call and NOT on the *authn.Resolver every surface shares, which is the whole scope of the exception: the REST write plane, /mcp and the push hook resolve through that resolver, which knows nothing of PlaneMeta, so a PAT is not a way around the tokens.sr.ht grant they require. Routing is metapat.PlaneOf on the presented credential rather than a fall-through from a failed plane, because an instance with no [tokens.sr.ht] section has no working-token validator to fail first. Two things this service needs that the cov and bench donors did not. MetaAuth is told the instance owner and refuses a PAT belonging to anybody else with the same ErrNotInstanceOwner a foreign working token gets: a PAT is the credential every account on the instance can mint for itself, so without that check the widest credential in existence would have been the one that skipped the narrowest identity rule, and any user could have read the corpus. And a PAT resolves to KindAgent, never KindOwner, so webhook management and proposal approval stay with the human at a browser. The sentinels are spec's own — ErrMissingScope, ErrInvalidPersonalToken, ErrMetaUnavailable — rather than reuses of bearer's, because bearer's prose names tokens.sr.ht and every word of it is wrong for this plane; an operator reading "tokens.sr.ht could not be reached" while that daemon is up goes looking in the wrong place. What must agree between the planes is the status, not the wording, and StatusFor is the one table that decides it for both. The two vocabularies do not overlap and are refused differently, which the tests pin: a PAT naming spec.sr.ht/spec:read is a well-formed OAuth grant for a scope this service does not publish (403), while a working token naming spec.sr.ht/SPECS does not parse as a grant string at all (401). A 403 names the permission of the plane the caller actually used — telling a PAT holder to obtain "spec:read" would send them looking for a checkbox meta.sr.ht cannot render. cmd/specsrht's api-meta assertion is not Postgres-gated here, unlike the bench donor's: it ran locally and went red on the stale empty-list assertion, rather than hiding until CI.