feat(specverb): honor the spec's declared response media type #278

Merged
coilysiren merged 1 commit from feat/honor-response-media-type into main 2026-08-12 05:03:21 +00:00
Member

Fixes the last thing between the Forgejo 16 upgrade and actually reading CI logs from an agent.

Symptom

$ aosguard ops forgejo action-job logs coilyco-gaming sirens-echo 27514
aosguard: invalid character `-` after top-level value

The log body starts with an ISO timestamp, so the JSON decoder reads 2026 as a number and then hits the hyphen. curl against the same endpoint returns HTTP 200 and 23935 bytes.

Cause

Everything except the response path was already correct:

  • Forgejo 16 serves GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs.
  • The vendored spec (16.0.1) declares produces: ["text/plain"] with a string response.
  • The guardfile grants it and even annotates it "exact plaintext bytes".

But nothing in this engine read the declared media type. runtime.fire called respfmt.Render unconditionally, and Render unconditionally json.Unmarshals. Every op-based verb assumed JSON regardless of what its spec said.

output "raw" existed, but only on fetch blocks, which do not surface as CLI verbs.

Change

Descriptor gains RawResponse, set in resolveDescriptor from the operations declared 200 or 206 content type. When set, fire` writes the body through untouched.

Deciding from the spec rather than from the bytes matters. A parse-and-fall-back-to-raw heuristic would make behaviour depend on payload content and would mask genuine JSON errors, which is the wrong shape for a security-boundary engine.

Fail-safe direction preserved

  • JSON, +json suffixes, and media types carrying parameters all keep the parsed path.
  • A spec that declares no content keeps the parsed path.
  • Only a non-success response declaring text does not flip the success path.

Projections are refused, not ignored

--query over a raw body returns a user error rather than silently returning everything. The caller asked to filter a payload the engine cannot parse, and quietly answering a different question is worse than failing.

Verification

make build, make vet, and go test ./http/specverb/ pass, including two new tests covering the media-type matrix and the fail-safe defaults.

Pre-existing failures, confirmed by stashing and re-running on clean main: five pkg/broker server tests fail, and make lint reports one gosec hardcoded-credential finding in the generated describe tree. Neither is touched by this change.

Downstream

aosguard pins a cli-guard ref, so this needs a pin bump and a specgen rebuild before action-job logs works on a host. No guardfile change is required: the grant and the vendored spec are already right.

🤖 Generated with Claude Code

Fixes the last thing between the Forgejo 16 upgrade and actually reading CI logs from an agent. ## Symptom ``` $ aosguard ops forgejo action-job logs coilyco-gaming sirens-echo 27514 aosguard: invalid character `-` after top-level value ``` The log body starts with an ISO timestamp, so the JSON decoder reads `2026` as a number and then hits the hyphen. `curl` against the same endpoint returns HTTP 200 and 23935 bytes. ## Cause Everything except the response path was already correct: * Forgejo 16 serves `GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs`. * The vendored spec (16.0.1) declares `produces: ["text/plain"]` with a string response. * The guardfile grants it and even annotates it "exact plaintext bytes". But nothing in this engine read the declared media type. `runtime.fire` called `respfmt.Render` unconditionally, and `Render` unconditionally `json.Unmarshal`s. Every op-based verb assumed JSON regardless of what its spec said. `output "raw"` existed, but only on `fetch` blocks, which do not surface as CLI verbs. ## Change `Descriptor` gains `RawResponse`, set in `resolveDescriptor` from the operation`s declared 200 or 206 content type. When set, `fire` writes the body through untouched. Deciding from the spec rather than from the bytes matters. A parse-and-fall-back-to-raw heuristic would make behaviour depend on payload content and would mask genuine JSON errors, which is the wrong shape for a security-boundary engine. ### Fail-safe direction preserved * JSON, `+json` suffixes, and media types carrying parameters all keep the parsed path. * A spec that declares no content keeps the parsed path. * Only a non-success response declaring text does **not** flip the success path. ### Projections are refused, not ignored `--query` over a raw body returns a user error rather than silently returning everything. The caller asked to filter a payload the engine cannot parse, and quietly answering a different question is worse than failing. ## Verification `make build`, `make vet`, and `go test ./http/specverb/` pass, including two new tests covering the media-type matrix and the fail-safe defaults. **Pre-existing failures, confirmed by stashing and re-running on clean `main`:** five `pkg/broker` server tests fail, and `make lint` reports one gosec hardcoded-credential finding in the generated describe tree. Neither is touched by this change. ## Downstream aosguard pins a cli-guard ref, so this needs a pin bump and a specgen rebuild before `action-job logs` works on a host. No guardfile change is required: the grant and the vendored spec are already right. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(specverb): honor the spec's declared response media type
All checks were successful
ci / test (pull_request) Successful in 43s
ci / lint (pull_request) Successful in 29s
ci / secrets (pull_request) Successful in 6s
1dede3a002
An operation whose success response declares text/plain or application/zip
was still parsed as JSON and reformatted, so the call failed on the first
byte that is not JSON. Forgejo's Actions job log is the motivating case:

  aosguard ops forgejo action-job logs <owner> <repo> <job>
  -> invalid character '-' after top-level value

The log starts with an ISO timestamp, so the decoder reads 2026 as a
number and then chokes on the hyphen. The endpoint, the guardfile grant,
and the vendored spec were all already correct. Only the response path
assumed JSON.

Descriptor gains RawResponse, set from the operation's declared 200 or
206 content type. A non-JSON media type writes the body through
untouched. The JSON family, including +json suffixes and types carrying
parameters, keeps the parsed path, and a spec that declares nothing keeps
it too, so the fail-safe direction is unchanged.

A projection over a raw body is refused rather than ignored. Silently
returning everything would answer a different question than --query
asked.

Pre-existing on main and untouched here: five pkg/broker server tests
fail, and gosec flags a hardcoded-credential pattern in the generated
describe tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/umbra!278
No description provided.