authn: accept tokens.sr.ht working tokens beside the agent token A second agent credential plane, next to the existing one rather than in place of it. The agent_token table, every agent configured with it, and the refs rule and provenance requirement around it are untouched; the local plane is removed in a later phase, not this one. The resolver tries the instance plane first and falls back to the local store on exactly two refusals, bearer.ErrInvalid and bearer.ErrNotOurs. spec's local token has no prefix to discriminate on — it is 32 random bytes in base64, which is precisely what "did not decode as one of ours" looks like — so the fallback replaces the shape test bench and cover can afford. ErrRevoked, ErrForbidden and ErrUnavailable are terminal: a withdrawn credential must not get a second chance at the old door, and an unreachable daemon must not silently degrade into the legacy plane. Grants ride on the principal and are checked where the action is known, never in the middleware, which runs upstream of the router: spec:propose in service.Propose, below both write surfaces, and spec:read in each read surface's gate. /mcp checks per tool rather than at its Gate, because one endpoint carries both kinds and a surface-wide read grant would refuse a propose-only token at initialize. Principal.Authorize is a no-op off the instance plane, which is what keeps the local token working. The instance plane brings an owner where the local token had none, so a working token belonging to anybody but [sr.ht] owner-name is refused rather than admitted as a second identity: Principal.Owner is read by the provenance committer, the refs rule's principal kind and the coreauth AuthContext, all written for one human. StatusFor is the one status table. ErrUnavailable is 503 and never 401 — reading "I could not ask tokens.sr.ht" as "revoked" would refuse every live instance token while a daemon that is deliberately off the hot path restarts. An instance with no [tokens.sr.ht] section builds no instance plane and starts anyway, serving its own agent token as before.
refactor(authn): one Principal.CanRead() for the read-plane ACL (spec-ejq.1) graph's gate, web's mayRead and mcpsrv's Gate each hand-spelled 'IsOwner() || IsAgent()' — three copies of the read ACL, which graph's own comment warned is how a corpus leaks when they drift. Define it once as authn.Principal.CanRead and route all three through it. coreauth.Derive keeps its own owner||agent test on purpose: it answers a different question (is this an owner-backed identity to bridge to AUTH_INTERNAL), and coupling it to the read ACL would misroute a future read-only viewer kind to the owner's UserID. Closes spec-ejq.1
feat(authn): unified-login identity, agent token validation, provenance trailers Cookie identity is fail-open to anonymous: a missing, tampered or foreign-key cookie yields no user rather than an error, so anonymous browsing never breaks on a bad cookie. Agent tokens are the opposite. A bad bearer is a permanent 401, a store outage a transient 503, and revoked rows are returned so operators can distinguish a killed token from one that never existed. TokenStore is declared here rather than imported from db/, so the two packages can be built in parallel and service/ wires them together.