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(graph): GraphQL-native webhook surface (Phase 5a) The webhook types, mutations, and resolvers, adapted from the pages.sr.ht core-go template for spec's single-owner model. - SDL: WebhookEvent (PROPOSAL_OPENED/MERGED/REJECTED), WebhookSubscription interface + UserWebhookSubscription, WebhookDelivery, WebhookPayload interface + ProposalEvent (carries a Proposal), cursor wrappers, `webhook` payload root field, and a `type Mutation` with createUserWebhook / deleteUserWebhook. No OAuth `client` field and no @access/@private directives — spec has no OAuth clients or scopes, so the owner gate is the entire ACL. - Models: hand-written database.Model impls (UserWebhookSubscription, WebhookDelivery) so gqlgen autobinds rather than generates them; events via pq.Array; cursor keyset pagination. - Resolvers: all owner-gated via authn (spec's ACL), using core-go's webhook engine — Validate, NewAuthConfig (INTERNAL, via the coreauth bridge), FilterWebhooks, WebhookContext.Exec for the sample, and the `webhook`→Payload(ctx) root. Proposal writes deliberately stay off this surface (only webhook mutations; the schema test now asserts exactly that). - gqlgen.yml binds Cursor to core-go's model.Cursor; generated code regenerated with the pinned gqlgen v0.17.36 (reproducible). Compiles and vets clean; existing graph read tests still pass. Runtime context wiring and event firing are the next slices.
feat(graph): the read-only GraphQL schema at /query Eight query fields over the service layer, no Mutation and no Subscription — the design defers mutations until the proposal state machine settles, and TestSchemaHasNoMutations stands guard on that. Access is fail-closed and gated before parse, matching web's ACL exactly, so introspection is treated as content too. A federating api.sr.ht must therefore present a token or skip us, which costs one log line. A malformed rev is reported as a GraphQL error rather than folded into null. service/ deliberately hides malformed-versus-absent from probing, but the caller here is already authenticated as the owner or its own agent, and a bare null for rev=proposals/42 is indistinguishable from an absent document — it reads as a silently dropped argument. Proposal listing declares its port but is unwired: service/ exposes no proposal read yet, and returning an empty list would tell a reviewer their queue is clear when it is merely unread.