~bigbes/sr-ht-spec

ref: b7d1bf89a0bba51749babd45ce9f9c14b72f2470 sr-ht-spec/migrations d---------
a4d8cc52 — Eugene Blikh 9 days ago
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).
e183a11e — Eugene Blikh 24 days ago
feat(core,db): comment anchors and the comment table (spec-by6.3.1)

DESIGN.md held the comment schema back until the review UI existed, because a
schema is the expensive thing to get wrong here. The UI exists, so this lands
the anchoring model it specifies: (doc_id, heading_path, block_index,
block_hash), resolved content-first — the block's hash wherever it moved to,
then position under the same headings, and outdated when neither matches. A
comment that lost its place says so rather than being relocated to a best
guess, because a comment on the wrong paragraph reads as authoritative.

Two refinements the specified model left open:

block_index is the block's position WITHIN its heading path, not within the
document. prosediff numbers blocks document-globally, but the positional
fallback exists precisely for blocks whose content changed, and a global index
is destroyed by any insertion above it — so a global index would fail exactly
when it is needed.

A hash match is not unique. Documents repeat themselves ("TBD" under half the
headings), so the comment's own section wins outright and the nearest index
breaks what that does not. Without it, which duplicate a comment landed on
would depend on document order.

Anchor state is derived, never stored. A comment is not outdated in general, it
is outdated at a revision, and the proposal branch moves under it as the agent
revises; a column would cache a function of a moving input.

The table encodes the rest as constraints: a root carries the whole anchor and
a reply carries none of it (one predicate over all six columns, so a
half-written anchor is unwritable), agent provenance on proposal's rule, and
resolution only on a thread root.

Verified against a real Postgres 16, which caught what Go-level validation did
not: pq.Array sends a nil slice as SQL NULL, so a comment on a block before the
first heading tripped ck_comment_anchor and every document preamble was
uncommentable.

schema.sql keeps the comment table last, matching migration order — the
agreement test compares the two statement for statement. Its absent-list, which
required a design change rather than a quiet migration to add this table, is
the gate this commit passes through.

spec-by6.3.1
36976713 — Eugene Blikh 25 days ago
feat(db): webhook + user tables (Phase 5a foundation)

The DB foundation for GraphQL-native webhooks, matching the core-go /
pages.sr.ht convention:

- "user" table — spec is single-owner, but the webhook subscription is
  user-scoped in core-go's model, so a user row is the owner's identity
  and the FK target. Columns mirror core-go auth.LookupUser so the table
  is ready if the service ever adopts core-go auth; today only id/username
  are used (owner seeded at startup).
- webhook_event enum (PROPOSAL_OPENED/MERGED/REJECTED), auth_method enum.
- gql_user_wh_sub / gql_user_wh_delivery — column names match core-go's
  webhooks engine exactly (it selects/inserts by these names). The
  events check uses cardinality() not array_length() so an empty event
  list is rejected at the DB (array_length returns NULL there, which a
  CHECK does not reject — a latent gap in the upstream convention).

Webhook tables use bare `timestamp` (not spec's TIMESTAMPTZ) to match
core-go's `NOW() at time zone 'utc'` inserts. Verified: schema.sql and
the 0003 migration both apply and reverse cleanly; FK/check/cascade all
behave.
c23aec57 — bigbes 27 days ago
feat: projects — a saved filter over one global index, not a container

A project is a named space set; querying one filters the single global
index. The meta-project is an implicit filter at a reserved address rather
than a row: a stored +everything would need a sync job on every space
creation, and its one failure mode is silently omitting a space.

SpaceFilter distinguishes All from an empty member list, because a freshly
created project has no members and must mean selects-nothing. Collapsing
the two would make every new project silently match the whole corpus.
dc1e5d0c — Eugene Blikh 27 days ago
feat: Postgres schema, initial migration and the db/ query layer

The six tables of the design's Postgres schema — space, document_id,
proposal, agent_token, index_stamp, digest_mark — plus a typed query layer
over them. project and comment stay deliberately absent.

Two invariants are enforced in SQL rather than in Go, because a
check-then-write in Go has a window and this does not:

  - Global document ID uniqueness is the doc_id PRIMARY KEY, and the merge
    path's multi-row upsert guards its ON CONFLICT branch on the owning
    space, so a batch claiming another space's ID leaves that row untouched
    and surfaces as a CollisionError naming where the ID really lives.
  - Proposal transitions run as UPDATE ... WHERE state = 'open'; the current
    state is read back only to name it in the error. CHECK constraints make
    an unknown state, a merged row without an approval kind or merged_rev,
    and empty provenance unwritable by anything, including SQL that does not
    go through this package.

MergeProposal wraps the transition and the registry re-pointing in one
transaction: a merged proposal whose documents are still registered at their
pre-merge paths would break link resolution and the next staleness check.

Agent tokens store only a SHA-256 hash; the lookup boundary re-verifies with
subtle.ConstantTimeCompare and rejects revoked tokens distinctly.

Tests skip when SPECSRHT_TEST_PG is unset, so the pure-logic half — batch
duplicate detection, token comparison, argument guards, and the agreement
between schema.sql and migrations/0001_initial.sql — still runs with no
database available.