The inline grammar cannot set a request header, so reddit 403s every guardfile that reaches it #303
Labels
No labels
burndown-2026-06
sunday-sprint
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/umbra#303
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Filed by Angie (engineer,
claudeseat) while building the reddit guardfile forcoilyco-flight-deck/mcp-beaver#51. Found by running it against live reddit rather than by reading the grammar, which is why it had not surfaced.The gap
opcore.ParseInline'scanbody acceptspath | query | body | method | raw-response | set | fail-when | describe. There is noheader. The full guardfile grammar has one (http/guardfile/guardfile.go:593), so this is specifically the inline half - the half mcp-beaver renders.opcore's client also sets no
User-Agentof its own, so a generated server reaches an upstream as Go's default.Why that is not academic
reddit blocks it. Reproduced three times in a row against a spec that lints clean and builds exactly the right URL:
The contrast that identifies the cause: the same URL fetched with a named User-Agent answers 200, and with reddit-mcp's own
daily-routines/1.0 (by /u/coilysiren)answers 429 - rate limited, which is a served response rather than a block. So reddit is refusing the anonymous client specifically, not the request.reddit-mcp has always sent a descriptive agent (
src/reddit_mcp/server.py:45). A guardfile has no way to.Scope past reddit
reddit is the case that surfaced it, not the extent of it. A descriptive User-Agent is the stated etiquette for most volunteer and nonprofit APIs the fleet reads, and several enforce it. Any of them is unwrappable today for the same reason.
Shape worth considering
A
headernode in thecanbody, matching the full grammar's spelling:Points that want deciding rather than assuming:
wrapmay be the better home - with the same argumentrate-limitmade in mcp-beaver.umbra/<version>by default would fix the reddit case without any grammar at all - and the grammar would then be for the cases wanting a contact address in the string.Authorizationmust stay off limits.authowns it, and aheadernode that could overwrite it would be a second, unreviewed credential path.Acceptance
Authorizationcannot be set through it.Blocks
coilyco-flight-deck/mcp-beaver#51, whose other blocker (umbra#289'sraw-responsenode) is cleared and verified on v0.148.0.Correcting this issue: my diagnosis was wrong, and the real cause is fixed.
The correction
I claimed reddit blocks umbra's unnamed client, and cited a measurement. That measurement was taken through curl, where
Go-http-client/1.1and an empty agent both answered 403 while any other agent reached the rate limiter. I generalised it to Go's client without re-running it there, which was unsound - curl and Go differ in more than the agent string.Re-measured from Go's
net/http, one request each:Nothing blocks on the agent from the client that matters. The issue title and its premise were both wrong.
What was actually blocking
The placeholder credential. The inline grammar required an
authblock and offered no way to say "this upstream takes no credential", so a public API had to be given a fake one. Isolated from Go:User-AgentonlyUser-Agent+Authorization: Bearer unused-public-feedAuthorization: Bearer unused-public-feedonlyAn endpoint that serves anonymous callers freely can still reject a caller presenting a credential it cannot verify, and that rejection looks exactly like a block on the client. That is why it read as a User-Agent problem.
What landed
dafac75- the defaultUser-Agent. Kept, but on the etiquette argument alone: naming your client is the stated expectation for most volunteer and nonprofit APIs a Guardfile reads.docs/specverb-user-agent.mdnow carries the correction rather than the claim, so nobody later reads it as a fix.ac86bf1-auth none, released in v0.154.0.authorizereturns without touching the request, no value provider runs, and no secret is read. Theauthblock stays required, because a spec that simply omits it is a spec that forgot;noneis how an author says the omission is deliberate.auth nonecarrying avalueis an error rather than a no-op. Measurements indocs/specverb-auth-none.md.Verified end to end
mcp-beaver's reddit guardfile now serves real Atom off reddit.com on both public reads, on v0.154.0.examples/steam-storefront.mcp.kdlmoved toauth noneas well - its own comment had called the placeholder "the least-bad option" and named this gap as worth fixing upstream, which was right for a sharper reason than it knew.What stays open here
The half this issue asked for that is still missing: a
headernode, so an author can put a contact address in the agent where an API asks for one. Nothing in the fleet needs it today, andAuthorizationmust stay out of scope for it either way -authowns that, and a second path to it would be an unreviewed credential surface.Retitling would be fair; I have left the title as filed so the correction above is not orphaned from the claim it corrects.
The remaining half landed on
mainas599429f.make testandpre-commit run --all-filesgreen.Taking the correction above as the starting point rather than the issue body: the default agent and
auth noneare done, and what was still missing is theheadernode so an author can put a contact address in the string where an API asks for one.Wrap-level, taking the option this issue argued for
Agreed, so that is where it went. Applied to every leaf before the default, so a declared agent wins and an undeclared guardfile is unchanged.
Two headers refused rather than accepted
Authorization- this issue's own constraint, and it holds:authowns it, and a second path would be an unreviewed credential surface. Case-insensitive, soauthorizationis refused too.Content-Type- not asked for, and worth saying why. The runtime sets it from the request body, so a declared one would be silently overwritten. Accepting a header that never arrives is the same defect class as #297 and #289: a declaration that reads as effective while doing nothing. Refusing it costs nothing, since no guardfile can usefully set it anyway.A duplicate name (compared case-insensitively, since header names are), an empty name or value, and a wrong argument count also fail closed.
Acceptance
TestWrapHeaderReachesTheWire.Authorizationcannot be set through it.TestWrapHeaderRefusesReservedNames, which also checks the refusal namesauthas the owner rather than failing generically.TestDefaultUserAgentWhenNoHeaderDeclared.httptestand assert on the headers the upstream actually saw, which is the only thing that proves a declared header left the client. The live-reddit property was already verified end to end in the correction above on v0.154.0, byauth nonerather than by this node.Note for the next reader
The premise correction is now reflected in the docs rather than only in this thread.
docs/specverb-request.mdcarries the User-Agent story as etiquette with the retraction attached, anddocs/specverb-policy.mdcarriesauth nonewith its measurements. Both pages were merged during the #299 band migration, so the oldspecverb-user-agent.mdandspecverb-auth-none.mdpaths are gone and their content lives on those two.Angie, engineer seat