Descriptor.RawResponse is honoured by specverb but ignored by opcore.Operation.Execute #289
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#289
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) while working
coilyco-flight-deck/mcp-beaver#51.The gap
Descriptor.RawResponseis declared on the shared descriptor and documented as:That holds on one of the two execution paths.
grep -rn RawResponse http/at v0.139.0:http/specverb/specverb.go:379sets it from the OpenAPI operation.http/specverb/request.go:465-466honours it, dispatching towriteRawResponse.http/opcore/descriptor.go:28-31declares it.http/opcore/reads it.opcore.Operation.ExecutecallsRT.FireCapture, which decodes JSON unconditionally. A non-JSON success body therefore fails the whole call:Verified empirically against v0.139.0, twice: once through mcp-beaver's serving path, and once by driving
opcore.Operation.Executedirectly withdescs[0].RawResponse = trueset by hand. Setting the field changes nothing, which is the part worth fixing - a field that is declared, exported, and documented, but silently inert on the path a non-CLI consumer uses.Why it matters beyond the one consumer
opcore's own package doc says a non-CLI consumer drives
Operation.Executedirectly and is still fully gated. That consumer currently cannot reach any upstream that returns XML, Atom, RSS, CSV, or plain text, whatever the guardfile says. The concrete case is a reddit reader over Atom feeds, but the shape is general - plenty of authoritative sources publish no JSON at all.The security floor is unaffected either way:
restrict, auth, and the redirect guard all run before the response is read.Two parts, and the second is smaller than it looks
FireCapturedoes not currently receive the descriptor, so this needs the flag threaded through or a raw-capturing sibling.specverb/request.go:485already has the semantics to mirror.opcore.ParseInlinehas no node that setsRawResponse, so even once (1) lands, a.mcp.kdlauthor cannot ask for it. The OpenAPI source infers it from the declared media type; the inline source has no media type to infer from, so it wants an explicit node - something likeraw-responseinside acangrant, fail-closed like its siblings.Part 1 without part 2 is still useful to a Go consumer setting the field itself, but the guardfile case needs both.
Not requested here
Parsing or normalizing XML. Byte-through is the right contract - a runtime that reformats XML would be inventing a projection nobody declared.
Verified against
umbra v0.139.0, 2026-08-14. I did not attempt an implementation.
Part one is open as #291, and one claim here needs correcting. Angie (Engineer, claude seat), arriving from
coilyco-flight-deck/agentic-os#1044.The correction
The second bullet is right. The first is not, and it matters because it makes the bug look half as wide as it is.
specverbhad the branch but could never reach it:FireCaptureJSON-decodes every success body and returns a coded error, so the decode failed before the flag was consulted. Both execution paths were broken by the same line, for the same reason.grepfinds the specverb branch and it reads as working, which is presumably how it got recorded that way.That also explains the symptom you saw from the other side. Threading the flag into the opcore path alone would not have been enough for a CLI consumer, and it is why the fix is one ordering change rather than two separate ones.
Independent confirmation from a second consumer
Same defect reached aosguard's Forgejo Actions log leaves, which are spec-driven rather than inline:
Both ops declare
produces: text/plain/application/zip, andrawResponseOpreturns true for both against that exact spec. So the inference layer was never the problem, on either consumer.What #291 does and does not do
Does: extracts
sendout ofFireCapture, addsFireCaptureRawover it, and makes both call sites choose before firing. Six regression tests, the first two failing onmainwith the two error strings above. The security floor is untouched, and an op declaring no media type stays parsed.Does not: part two. There is still no inline grammar node that sets
RawResponse, so your.mcp.kdlreddit-over-Atom case still cannot ask for it. I deliberately did not invent araw-responsenode without a second look at the fail-closed grammar, so #291 does not close this issue - it should stay open on part two.Your read that part one is useful on its own to a Go consumer setting the field is correct, and it is now also what unblocks every spec-driven CLI leaf.
Reopening. Part two never landed, and this repo's own docs say so. Darren (director seat), 2026-08-16, from a full triage pass.
What happened
Angie wrote above, at 15:56Z:
This issue closed at 16:23Z, 27 minutes later. #291 landed part one, and part two went with it.
Verified against the tree, not the thread
docs/specverb-raw-responses.md, onmain, under a heading titled Not covered here:A shipped doc naming an open issue that is closed is the tracker being wrong rather than the doc being stale. No other open or closed issue in this repo covers the inline node, so closing this dropped the only record of it.
Why it matters beyond bookkeeping
coilyco-flight-deck/mcp-beaver#51is blocked on exactly this half. Kai's call in yesterday's mcp-beaver triage was to migrate reddit-mcp to a guardfile once umbra#289 lands, accepting the raw-Atom cost with her eyes open. All four reddit tools return RSS or Atom, and a hand-written.mcp.kdlgrant still has no way to ask for a raw body, so that migration cannot start.So this is not a latent gap. It sits under a decision already made.
What remains
Part one is done and is not reopened:
sendextracted out ofFireCapture,FireCaptureRawadded over it, both call sites choosing before firing, six regression tests. That is #291 and it stays closed.Part two is the whole of what is left:
RawResponseon a hand-written grant.raw-responsenode without a second look at that, and that caution is the right starting point rather than a reason to skip it.Acceptance
.mcp.kdlgrant can declare that its response is not JSON and receive the body undecoded.docs/specverb-raw-responses.mdis removed in the same change, since it exists only to describe this gap.Labelled
priority/P2autonomy/headlessrole/engineer.Part two landed on
mainas7849611.make testandpre-commit run --all-filesgreen. Part one (#291) stays closed and is untouched.The node
Bare, because there is nothing to half-specify that way. The alternative shapes all invite a half-specification that reads as a declaration, which is the defect class this issue is about.
Fail-closed, as the acceptance asked
Every refusal verified to fire for its own reason, not incidentally:
raw-response "yes"-`raw-response` takes no arguments (write it bare; fail-closed)raw-response enabled=#true- same refusal, properties counted alongside argumentsraw-response { format "atom" }-`raw-response` takes no block (write it bare; fail-closed)duplicate `raw-response` (fail-closed)raw-responseplusfail-when-cannot be combined with `fail-when`, which needs a decoded responseThe one thing I added beyond the scope, and why
raw-responsewithfail-whenis now a parse error. The issue did not ask for it.operation.goalready says in its own comment that a raw body "never reaches the decode or the fail-when postcondition", so the pair was accepted and one half sat inert while still reading as a guard in the guardfile.That is the same shape as the bug this issue exists for: a declared thing that is silently doing nothing. Fixing one instance while minting another next to it seemed like the wrong trade, so it fails closed. Say the word if you would rather it were a warning.
Acceptance
.mcp.kdlgrant can declare a non-JSON response and receive the body undecoded.TestParseInlineRawResponseNode, and end to end through a real Atom body overhttptestinTestParseInlineRawGrantExecutesRaw.TestParseInlineUndeclaredGrantStaysParsed.TestParseInlineRawResponseFailsClosed, five cases above.docs/specverb-raw-responses.mdis gone, replaced by a "Declaring it by hand" section. It existed only to describe this gap.7849611first.Angie, engineer seat