web: draw the whole web tier from sr-ht-ecore The chrome moved last time; this moves the four packages around it. pages discovers the page templates and carries the shared error body, so the hand-maintained page list, the render helper and error.html go; assets finds the hashed stylesheet and serves the static tree, so the local regexp, the href glob and handleStatic go; middleware brings the private-cache policy and a panic guard that aborts a connection rather than appending an error page to a truncated one; and ecoretest replaces the hand-built config and the TestMain that minted its own keys. The one that matters is csrf. The guard was a predicate three handlers remembered to call, which made unprotected the default for any form added later. It is now csrf.Require on the router, so it covers the routes that are not written yet, it runs before routing — an unrouted POST is refused rather than 404'd — and the refusal is the shared sentence. Host comparison becomes case-insensitive, which is strictly more permissive and only for an operator typo in the config. threads.html becomes _threads.html, the partial spelling pages.Load discovers, and the review page takes container-fluid: two prose columns do not fit the centred container, which is what cover and dolt already concluded for their wide views. What stays here: the sentinel-to-status mapping in fail, and a renderError that wraps ecore's error body in this service's view struct.
authn: remove the local agent-token plane The agent_token table and everything that minted, verified, listed or revoked it. Agent credentials are tokens.sr.ht working tokens now: signed, expiring, owned by a meta.sr.ht account and carrying grants, verified locally by sr-ht-ecore's bearer package. One door, and nothing behind it — a credential the instance plane refuses is refused, where it used to be offered to a second store that might say yes. DEPLOY GATE: do not deploy this until every agent configured with the shared secret holds a tokens.sr.ht token with spec:propose (spec:read to read). Migration 0005 drops the table, migrate-on-upgrade runs it on deploy, and deploying early locks out every agent at once — including the SSH push path. hooks/ no longer reads agent_token directly. The push path goes through the same authn.Resolver the HTTP surfaces use and demands spec:propose, because a push by an agent is a proposal by another transport. The refs rule and the provenance requirement are untouched on both counts: a universal grant is still only an agent to the receive path, and X-Agent / X-Agent-Session are still mandatory on every agent write. bearer.ErrNotOurs is now a permanent refusal. A meta.sr.ht PAT used to fall through to the local store and miss there; with no store to fall through to it earns a 401 rather than the 503 an unclassified error would. [tokens.sr.ht] origin becomes a required config key: with no issuer there is no credential to check, so the daemon fails startup instead of serving reads and refusing every agent write one request at a time. /tokens redirects to the daemon that issues (tokens SPEC ch. 7).
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.