A mapped body can only ever send strings, so map silently restricts a guardfile to string-valued upstream parameters and the only signal is a 400 at call time #312
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#312
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?
Self-contained. Does not block #311 and does not change what #311 should do — see the boundary section at the end. Split out at Kai's direction because #311 is in flight and this is a wider finding that should not ride on it as a comment.
The limit
A
bodyblock written as mappings projects strings and only strings onto the wire, regardless of what the caller supplies or what the upstream expects.http/opcore/body_mapping.go, where the model-facing schema is built:and where the outgoing body is assembled:
mappedStringreturns astring, and that string is what lands atmapping.Target.descriptor.gostates the same intent plainly: "required string input paths projected onto fresh top-level body keys."A dotted source path does not change this.
insertMappingFieldbuilds a nested input shape (Type: "object"for interior segments), but the value projected onto the wire key is still the leaf string.So any upstream requiring a non-string at a mapped parameter is unreachable through
map, in every configuration.Why this is worth its own issue rather than a footnote
Nothing surfaces the restriction at authoring time. A guardfile that maps a parameter the upstream wants as an object, number, or boolean parses cleanly, builds cleanly, ships, registers its tool, and then fails on every call with an upstream error.
Measured against a live upstream (Exa's
/search), varying only the one parameter:contents={"text": true}(object)contents="text"(string)contents="true"(string)That 400 is the first and only notice the author gets, and it arrives in production against a metered third-party API. The engine knew at parse time that this mapping could only ever emit a string; the upstream contract is the part it cannot know, but the emitted type is not.
Why it is easy to hit
mapis not an optional stylistic choice. It is the only construct that renames an input to a different upstream key, because a body field carrying an upstream alias is rejected outright:So any guardfile whose upstream requires a parameter name that collides with
ReservedFlagNames(dry-run,query,output,body-file) is forced into mapped-body mode for that leaf, and mapped-body mode then quietly forbids every non-string parameter on that same leaf, including ones entirely unrelated to the collision that forced the mode.The two constraints compose into something neither one states. An author reading either rule in isolation would not predict it.
What would help, smallest first
body_mapping.goor shipping a 400.docs/specverb-request.mdalready covers the reserved-name rules and is the natural home.maptarget is given a value shape that cannot be a string, reject it with a message naming the limit rather than deferring to the upstream. This is partial, since the engine cannot know an arbitrary upstream's contract, but it converts the most common authoring mistake from a runtime 400 into a build error, which matches the fail-closed posture elsewhere in this file.BodyMappingwould need a declared type andprojectMappedBodya typed accessor rather thanmappedString. Worth its own design pass, and probably not worth doing on the strength of one consumer.Items 1 and 2 are cheap and would have saved the entire investigation that produced this.
Boundary with #311
Kept deliberately separate so the in-flight work is not disturbed:
FixedBodyseed the projected map before mappings project onto it. That fix is correct, sufficient for its consumer, and unaffected by this issue. A seeded fixed value is written as its own JSON shape and never passes throughmappedString, so #311 lands non-string constants without touching the typing of caller-supplied mapped values.Landing #311 alone leaves this issue's problem entirely in place: constants would gain shapes while every mapped input stayed a string, still with no diagnostic. That is a perfectly reasonable place to stop, and it should be a decision rather than an oversight, which is the reason this is written down.
Not urgent
No production behaviour is wrong. The known consumer is running safely in its restricted form, and this describes a sharp edge for guardfile authors rather than a defect in a running system.