web: draw the chrome from sr-ht-ecore The nav/service-switcher, the login block, the environment banner and the brand were this service's own copy of code compare.sr.ht had already copied from somewhere else. They come from sourcecraft.dev/bigbes/sr-ht-ecore/chrome now: one chrome.Service built at startup from the shared config.ini, one chrome.Page per request, embedded in viewData so the shared partials find their fields on the dot. web/chrome.go is gone — buildNav, navItem, canonIndex, the login/logout/ profile URL building and the chrome half of viewData with it. The layout renders srht-env-banner and srht-nav instead of the local markup, the landing page's space list renders through srht-repo-list, and the template FuncMap starts from chrome.Funcs() (the local shortsha was a duplicate of the shared one). sameOrigin and the login redirect ask the chrome for our origin rather than keeping a second copy that could disagree with the links on the page. Three of ecore's policies differ from what this service did, and win, per that package's own doc: [sr.ht]site-name defaults to "sr.ht" rather than "sourcehut" and [sr.ht]environment to "development" rather than "production" when the key is absent, and the brand carries a fixed 15rem min-width so the switcher starts at the same x on every service. The instance's config.ini sets both keys, so on it only the brand width is visible. The nav tests that only restated ecore's rules — switcher order, the paste/pages/hub exclusion, the shape of a login URL — are dropped; ecore tests those. What is left covers this service's seam: that the identity authn resolved is the one the chrome is handed.
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).
feat(web,service): the owner mints and revokes agent tokens in a browser Issuing a credential required SSH to the host, which made the remote agent write plane unusable from anywhere else: to hand an agent a token the owner had to be at the machine. /tokens is that page — list, mint, revoke — behind the same owner-only gate and same-origin guard as approve/reject. The mint is owner-only, and that rule is what revocation depends on: an agent allowed to mint would survive having its own credential revoked by issuing itself another, and "revoke the token" is the entire incident response this design has. An agent asking for the page gets 403 rather than the read plane's login redirect — it is authenticated already, so bouncing it to meta would answer a question it did not ask. The plaintext is rendered in the response to the POST rather than after a redirect. A redirect would either drop the secret or carry it in a URL, where it lands in history and in every proxy log on the way; the cost is that a reload re-submits and mints a second token, which is one click to revoke on that same page, whereas a lost token is not recoverable. service.IssueAgentToken/ListAgentTokens/RevokeAgentToken hold the ACL and the mint, and `specsrht token` now goes through them too, so the CLI and the page cannot drift into two ideas of what issuing a token is. spec-ejq.3
feat: web — the Phase 2 read plane UI and its SCSS entry SourceHut chrome (nav service-switcher, login block, environment banner, error page, embedded hashed static assets) ported from compare.sr.ht, plus the read-plane pages: landing, space document tree, rendered document with frontmatter/backlinks, and search. The URL grammar is the design's pinned one: a document's address carries no extension, ".md" is raw source, ".json" is metadata plus body, and ?rev= pins any of the three to an immutable revision. Links rendered inside a pinned page keep the pin, so following one does not silently land on the approved head. One human and no visibility levels, so the read ACL is one line: the owner and its agents read, everyone else is redirected to meta's login (a browser) or refused with 401 (a client asking for .md/.json).