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.
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.
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.
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.
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.
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.
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.
bearer: draw the 401 arm from IsAuthFailure so the two cannot drift
deps: tidy after the third uplift
chrome: the resolved favicon and the queue as a shared table
pages: read a form's body, bounded, and never its URL
bearer: refuse through the shared table and challenge
chimw: the request line, the HEAD twins and the routing refusals
logging: take the instance's log policy from ecore
login: decode the unified-login cookie through ecore
instconf: one reading of this instance's origins
deps: the ecore packages of the third uplift
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.
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.
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.