~bigbes/sr-ht-spec

824788ab — Eugene Blikh 2 days ago
mcpsrv: mark /mcp uncacheable, fail closed on origin, split tool errors from faults

Three gaps between this surface and the cov/dolt pattern the siblings settled
on. The write tools are untouched, the per-tool grant scheme is untouched, and
the transport stays stateful.

The endpoint set no Cache-Control and no Vary at all. Every answer here depends
entirely on the credential the request carried and says nothing about it in its
URL, and some of them are the whole approved corpus, so a shared cache was free
to keep one and replay it to the next caller. cache.go is a local copy of
ecore/mcphttp.PrivateCache, byte for byte on the header values so the retrofit
is a delete and an import once that commit is published. It commits the headers
on Write and Flush as well as WriteHeader: the SDK answers a POST with an event
stream that never calls WriteHeader, so a wrapper hooking only that one sets
nothing on the response an agent actually gets, while passing every other header
test. Measured — with only WriteHeader hooked the streamed answer leaves with
the SDK's own `no-cache, no-transform` and no Vary.

Vary names both planes, where dolt.sr.ht names Authorization alone. dolt is
right for dolt: its /mcp is bearer-only, so naming Cookie would promise a cache
a dependency the surface never reads. It is wrong here. authn.Resolver.Resolve
prefers a bearer token when one is present but falls through to
login.UsernameFromRequest when none is, and an owner cookie resolves to
KindOwner — which is exactly what Gate admits. On this service the cookie is
the difference between the whole corpus and a 401. This is the one string a
future mcphttp retrofit has to reconcile between the two services.

An origin with no host in it was a warning and then an unguarded endpoint. The
Host allowlist is the only thing protecting /mcp once the SDK's own rebinding
guard is disabled, so that path turned one unparseable config value into a
silently open endpoint indistinguishable in every functional test from a
correctly guarded one. It is a construction error now. The daemon cannot reach
it either way: service.Config.Validate already refuses to start unless the
origin parses and carries a host.

Errors from below travelled to the agent as tool results carrying their own
text, so a dead git object store and a missing document were the same kind of
answer. A tool result means "the call was understood and the thing you asked
for is not there", so an agent reading one for a store outage concludes the
document does not exist and rewrites a specification around a document that is
perfectly real — and the store's own words reached it. errors.go splits the
two: service.ErrNotFound is a tool result whose sentence is built from the
call's own arguments, and everything else is a jsonrpc protocol error saying
"internal server error" with the detail logged. The old tests asserted the
behaviour being removed — that the agent was shown the words "on fire" — and
are replaced by ones that pin the split in both directions.
bd5b615a — Eugene Blikh 2 days ago
docs: drop a stray closing tag from ci.md

An XML tag from some tool's output that was written into the file and
committed with it. Predates this branch — it is in ef7bddf too.
6d7dbdb5 — Eugene Blikh 2 days ago
ci: publish this build's own coverage and benchmarks

Serving a revision is two passes over the whole corpus — FromDocuments builds
the archive, LinkPass renders every document to fill the link graph — and a
proposal view is a third over two whole revisions. All three grow with the
space rather than with the request, and this repository had no Benchmark at
all; doc/ and prosediff/ now measure them over a synthetic 208-document space
and a 24-chapter specification carrying one of every change kind.

The manifest gains a coverage and a bench task, both after publish so a
rejected upload cannot cost a shipped apk, and both guarded: the profile must
be non-empty, the benchmark names must be in the file (an empty benchfmt body
uploads and reports success), and a build handed no token says so and exits 0
with the artifact still attached.
ef7bddf8 — Eugene Blikh 3 days ago
ci: publish the apk into artifacts.sr.ht as well

The abuild output goes on to the S3 bucket phoebe re-indexes, and now also to
the ~bigbes/main channel of artifacts.sr.ht, which indexes and signs it in the
same request. One PUT per file with the shared working token; a 409 says the
job ran twice and is a warning rather than a failure.
6e59bf0f — Eugene Blikh 8 days ago
api: advertise an empty scope list, not null

core-go serves the second argument of WithSchema verbatim at
/query/api-meta.json, so a nil slice reaches the wire as `"scopes": null`.
meta.sr.ht discovers every *.sr.ht service at import time and iterates that
field when rendering /oauth2/personal-token, so the null turned the whole
instance's personal-token page into a 500 — no token could be minted at all
while spec.sr.ht was up. The other custom services answer 404 there and are
skipped, which is why only this one broke it.

Empty is the accurate value: the service is owner-only and defines no
AccessScope enum to grant against.
cb60d4f7 — Eugene Blikh 9 days ago
ci: run the suites against a real Postgres on the builder

93 test files across 16 packages had never run on builds.sr.ht. The manifest
brings up a Postgres in the VM, exports SPECSRHT_TEST_PG and runs gofmt, go vet
and `make test` before abuild.

The DSN guard is the point of it. db/db_test.go, service/fixture_test.go and
cmd/specsrht-migrate/main_test.go all gate on that variable, so an unset DSN
does not fail anything — it skips 67 tests (counted, with and against a local
Postgres) and leaves the build green over the whole persistence layer, the
schema/migrations agreement check included. options="!check" in the APKBUILD
means this task is the only place the suites run at all, so the guard is what
makes that line true.

TEST_TIMEOUT lands with it rather than after the first goroutine dump: the
toolchain's default is a silent ten minutes.
8255ff90 — Eugene Blikh 9 days ago
ci: export the version instead of sed-ing a tracked APKBUILD

The version reaches abuild as $PKGVER now, and the tracked APKBUILD is never
rewritten: Go reads vcs.modified from `git status --porcelain`, so a sed of a
tracked file stamps every packaged binary dirty for the whole abuild run.

The export alone would not have fixed it. .gitignore covered the intermediate
web/static/main.css and not the content-hashed main.min.<sha>.css that `make
css` renames it to — the file that survives, and that build() writes BEFORE go
build — so every packaging run compiled with an untracked file in the tree.
/src/, /pkg/ and /tmp/ are the three directories abuild works in inside this
checkout, GOTMPDIR among them.

Also: the cacher bootstrap splits into install + init so the installer's PATH
export is sourced by the next task, and goes through install.sh for its
checksum verification; scss collapses into the --exec miss-and-seed form;
cache_restore takes --optional plus the half-restored module cache repair;
cache_save moves after publish and drops the exists-guards; publish gates on a
readable ~/.apk-ci.env; tags build.

package() stages what build() produced (make install-files) instead of
recompiling both binaries in a fresh fakeroot process with no cache pins, and
stops installing static assets that //go:embed already put in the binary. The
`[ -d ./cmd/... ]` skip guards are gone: a deleted cmd/ was a green build that
packaged nothing.

The reasoning lives in docs/ci.md, because the manifest is a varchar(16384)
and a submission over the cap gets no CI at all.
9b5827b6 — Eugene Blikh 9 days ago
bearer: draw the 401 arm from IsAuthFailure so the two cannot drift
9cec0f54 — Eugene Blikh 9 days ago
deps: tidy after the third uplift
471d9706 — Eugene Blikh 9 days ago
chrome: the resolved favicon and the queue as a shared table
636dc7a8 — Eugene Blikh 9 days ago
pages: read a form's body, bounded, and never its URL
b643b0be — Eugene Blikh 9 days ago
bearer: refuse through the shared table and challenge
c87a11bd — Eugene Blikh 9 days ago
chimw: the request line, the HEAD twins and the routing refusals
0a32fd7a — Eugene Blikh 9 days ago
logging: take the instance's log policy from ecore
b7d1bf89 — Eugene Blikh 9 days ago
login: decode the unified-login cookie through ecore
be33cce1 — Eugene Blikh 9 days ago
instconf: one reading of this instance's origins
ed79a1bb — Eugene Blikh 9 days ago
deps: the ecore packages of the third uplift
cd1b8b01 — Eugene Blikh 9 days ago
deps: auxilia whose scribe.Err reads the whole error chain

Until now scribe.Err type-asserted the outermost error for slog.LogValuer,
so a culpa error under a single fmt.Errorf logged as a flat string and lost
its code, hint and stacktrace with no warning. It walks the chain now, which
is what makes the culpa wrapping in this service visible in the journal.
3d811988 — Eugene Blikh 9 days ago
service: wrap the merge and reject failures with culpa

Three sites, all of them the outermost wrap on an error that becomes a 500:
the merge path's non-staleness arm, the reject path's default arm, and the
merged-but-the-row-did-not case, whose remedy now rides on the error as a hint
instead of sitting in a sentence one wrap away from being buried.

Outermost is the whole criterion. scribe.Err type-asserts the error it is
handed for slog.LogValuer rather than searching the chain, so a culpa error with
an fmt.Errorf above it logs as a plain string and the stacktrace is lost — which
is also why the sentinel arms stay fmt.Errorf: they carry no cause worth a
stack, and their text is what a 404 or 409 shows a viewer.

errors.Is and errors.As traverse culpa's wrap, so the sentinel mapping in the
surfaces is unchanged.
643fa0b5 — Eugene Blikh 9 days ago
logging: log through slog and scribe rather than stdlib log

sr-ht-ecore's panic middleware now reports through log/slog, and it reports
through the *default* logger — nothing can hand a middleware in another module
this service's *slog.Logger. So the daemon's scribe handler becomes the one
install point, and the packages that were still calling log.Printf go through
the default logger too: the read plane's render and encode failures, its 5xx
mapping, and the credential resolver's fail-closed line. Each carries the fields
that used to be interpolated into the sentence — method, path, status, page,
doc — and the error itself through scribe.Err.

The handler grows what it was missing: file:line, because most of what reaches
it is a failure and 'which of the six render sites' is the first question;
colour dropped when stderr is not a terminal; and the masks. This daemon handles
the unified-login cookie and tokens.sr.ht working tokens, and a struct logged
whole is how a live credential outlives its own request in a log file — masking
in the handler covers the log line nobody reviewed as well as these.

cmd/specsrht-migrate keeps stdlib log on purpose: it is a one-shot CLI whose
'specsrht-migrate: ...' progress an operator reads at the terminal during an
upgrade, and log.Fatalf is its error exit.
Next