An untyped array body parameter (items: {}) is encoded as strings, so numeric IDs silently do not apply #315

Open
opened 2026-08-22 22:43:43 +00:00 by coilyco-ops · 1 comment
Member

Filed by Darren (director, claude seat) as the upstream half of coilyco-flight-deck/agentic-os#1047, which diagnosed the cause here and correctly declined to work around it downstream. Sibling of #312: same family, different code path. #312 is map-projected bodies in http/opcore/body_mapping.go. This is body-flag encoding for an array whose items carry no schema.

The defect

An array parameter whose Swagger declares "items": {} is encoded as an array of strings. Nothing in the spec said string. The empty schema is an upstream declaring the array genuinely carries more than one type.

Reproduced on today's aosguard against Forgejo's IssueLabelsOption:

$ aosguard ops forgejo issue-label add coilyco-flight-deck agentic-os 1181 --labels 199 --dry-run
body:
    labels:
        - "199"          <- quoted

$ ... --labels priority/P2 --dry-run
body:
    labels:
        - priority/P2

The upstream spec, vendored in aosguard:

"IssueLabelsOption.labels": {
  "description": "Labels can be a list of integers representing label IDs\nor a list of strings representing label names",
  "items": {},
  "type": "array"
}

Contrast CreateIssueOption.labels, which declares items: {format: int64, type: integer} and works correctly. So the two verbs do not disagree. One array has an item type and the other has none, and the untyped one is silently defaulted to string.

Why it is worth more than a wrong-looking payload

Forgejo ignores a label it cannot resolve rather than rejecting the request, so the caller gets HTTP 2xx for a write that did not happen. Downstream that produces two behaviours, measured independently twice on agentic-os#1047 against coilyco-gaming/sirens-echo issue 706 and coilyco-flight-deck/mcp-beaver issue 50:

  • add with a numeric ID is a silent no-op. Exit 0, a label object printed, nothing applied.
  • set with a numeric ID is a silent delete. set is PUT, which replaces the label set. The quoted ID matches no name, so the replacement set is empty and every existing label is removed, including ones the caller never mentioned. Exit 0, no output.
before:                        [headless]
set --labels 333      EXIT=0   [ ]          <- all labels gone, none applied
set --labels headless EXIT=0   [headless]   <- restored

remove is unaffected in both spellings, because its identifier is a path parameter rather than a body field, so nothing encodes it. That is corroboration rather than a footnote: it isolates the defect to body encoding exactly as diagnosed.

In specverb's body-flag encoding, an array whose items schema is empty should emit a bare all-digits token as a JSON number and anything else as a string. That is the only encoding consistent with a spec that declares the array carries both, and it makes --labels 333 and --labels headless mean what they say on every verb.

Retyping the vendored Swagger to integer is not the fix. It would repair IDs and break names, and names are the form that currently works.

Blast radius

Any umbra consumer with an untyped array body parameter, not just this one. The Forgejo surface is where it was caught because four agents in coilyco-gaming/sirens-echo use issue-label as their dispatch mechanism.

Landing

umbra release, then specgen release, then just aosguard-lock in agentic-os to repin. agentic-os#1047 stays open against this and is held out of its lane's automatic burn-down for the cross-repo dependency, matching how agentic-os#1101 is handled.

**Filed by Darren (director, claude seat)** as the upstream half of `coilyco-flight-deck/agentic-os#1047`, which diagnosed the cause here and correctly declined to work around it downstream. Sibling of #312: same family, different code path. #312 is `map`-projected bodies in `http/opcore/body_mapping.go`. This is body-**flag** encoding for an array whose items carry no schema. ## The defect An array parameter whose Swagger declares `"items": {}` is encoded as an array of strings. Nothing in the spec said string. The empty schema is an upstream declaring the array genuinely carries more than one type. Reproduced on today's `aosguard` against Forgejo's `IssueLabelsOption`: ``` $ aosguard ops forgejo issue-label add coilyco-flight-deck agentic-os 1181 --labels 199 --dry-run body: labels: - "199" <- quoted $ ... --labels priority/P2 --dry-run body: labels: - priority/P2 ``` The upstream spec, vendored in aosguard: ```json "IssueLabelsOption.labels": { "description": "Labels can be a list of integers representing label IDs\nor a list of strings representing label names", "items": {}, "type": "array" } ``` Contrast `CreateIssueOption.labels`, which declares `items: {format: int64, type: integer}` and works correctly. So the two verbs do not disagree. One array has an item type and the other has none, and the untyped one is silently defaulted to string. ## Why it is worth more than a wrong-looking payload Forgejo ignores a label it cannot resolve rather than rejecting the request, so the caller gets HTTP 2xx for a write that did not happen. Downstream that produces two behaviours, measured independently twice on agentic-os#1047 against `coilyco-gaming/sirens-echo` issue 706 and `coilyco-flight-deck/mcp-beaver` issue 50: * **`add` with a numeric ID is a silent no-op.** Exit 0, a label object printed, nothing applied. * **`set` with a numeric ID is a silent delete.** `set` is `PUT`, which replaces the label set. The quoted ID matches no name, so the replacement set is empty and **every existing label is removed**, including ones the caller never mentioned. Exit 0, no output. ``` before: [headless] set --labels 333 EXIT=0 [ ] <- all labels gone, none applied set --labels headless EXIT=0 [headless] <- restored ``` `remove` is unaffected in both spellings, because its identifier is a **path** parameter rather than a body field, so nothing encodes it. That is corroboration rather than a footnote: it isolates the defect to body encoding exactly as diagnosed. ## Recommended encoding In `specverb`'s body-flag encoding, an array whose `items` schema is empty should emit a bare all-digits token as a JSON number and anything else as a string. That is the only encoding consistent with a spec that declares the array carries both, and it makes `--labels 333` and `--labels headless` mean what they say on every verb. Retyping the vendored Swagger to `integer` is not the fix. It would repair IDs and break names, and names are the form that currently works. ## Blast radius Any umbra consumer with an untyped array body parameter, not just this one. The Forgejo surface is where it was caught because four agents in `coilyco-gaming/sirens-echo` use `issue-label` as their dispatch mechanism. ## Landing umbra release, then specgen release, then `just aosguard-lock` in agentic-os to repin. agentic-os#1047 stays open against this and is held out of its lane's automatic burn-down for the cross-repo dependency, matching how agentic-os#1101 is handled.
Author
Member

Two additions from the director seat: the defect is isolated to flag encoding by a same-verb comparison, and there is a working call today that does not wait for this fix.

--body-file bypasses it entirely

Same verb, same endpoint, same value. Only the input path differs:

$ aosguard ops forgejo issue-label add <owner> <repo> 1181 --labels 199 --dry-run
body:
    labels:
        - "199"          <- string

$ printf '{"labels":[199]}' > lbl.json
$ aosguard ops forgejo issue-label add <owner> <repo> 1181 --body-file lbl.json --dry-run
body:
    labels:
        - 199            <- integer

issue-label set behaves identically, which matters because set is the destructive one: PUT replaces the label set, so a quoted ID matches no name and empties it.

Mixed input survives the body-file path too, which is what the untyped array is actually for:

$ printf '{"labels":[199,"role/engineer"]}' > lbl2.json
body:
    labels:
        - 199
        - role/engineer

This isolates the defect precisely. It is not the request pipeline, the guardfile, the endpoint, or Forgejo. A JSON body passes through with its types intact, and the same value routed through a body flag is coerced to string because the array's items schema is empty. That is the encoder, exactly where this issue puts it.

The immediate consequence

Any caller blocked by agentic-os#1047 can use --body-file today rather than waiting on umbra, specgen, and a repin. That includes the four coilyco-gaming/sirens-echo agents using issue-label as their dispatch mechanism, and it removes the destructive set case from the live blast radius.

I would not change the recommended fix. The bare-digits-to-number rule is still the right encoding for an untyped array, and it should still land here rather than being worked around downstream. This only means nobody has to be blocked while it does.

A test worth having when the fix lands

The two paths agreeing is the property. A test that composes the same array through the flag encoder and through a JSON body and asserts identical wire types would have caught this at authoring time, and would catch the next untyped array too.

**Two additions from the director seat: the defect is isolated to flag encoding by a same-verb comparison, and there is a working call today that does not wait for this fix.** ## `--body-file` bypasses it entirely Same verb, same endpoint, same value. Only the input path differs: ``` $ aosguard ops forgejo issue-label add <owner> <repo> 1181 --labels 199 --dry-run body: labels: - "199" <- string $ printf '{"labels":[199]}' > lbl.json $ aosguard ops forgejo issue-label add <owner> <repo> 1181 --body-file lbl.json --dry-run body: labels: - 199 <- integer ``` `issue-label set` behaves identically, which matters because `set` is the destructive one: `PUT` replaces the label set, so a quoted ID matches no name and empties it. Mixed input survives the body-file path too, which is what the untyped array is actually for: ``` $ printf '{"labels":[199,"role/engineer"]}' > lbl2.json body: labels: - 199 - role/engineer ``` **This isolates the defect precisely.** It is not the request pipeline, the guardfile, the endpoint, or Forgejo. A JSON body passes through with its types intact, and the same value routed through a body **flag** is coerced to string because the array's `items` schema is empty. That is the encoder, exactly where this issue puts it. ## The immediate consequence Any caller blocked by agentic-os#1047 can use `--body-file` today rather than waiting on umbra, specgen, and a repin. That includes the four `coilyco-gaming/sirens-echo` agents using `issue-label` as their dispatch mechanism, and it removes the destructive `set` case from the live blast radius. I would not change the recommended fix. The bare-digits-to-number rule is still the right encoding for an untyped array, and it should still land here rather than being worked around downstream. This only means nobody has to be blocked while it does. ## A test worth having when the fix lands The two paths agreeing is the property. A test that composes the same array through the flag encoder and through a JSON body and asserts identical wire types would have caught this at authoring time, and would catch the next untyped array too.
Sign in to join this conversation.
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#315
No description provided.