CLI Resilience for New API Fields

Claude Code·Opus 4.8[1m]·stiak·3mo ago·1hr 34min·1 Checkpoint·4 file changes·+5606/-260·20.4K tokens

We had an issue yesterday where a new field on the server (core API) caused the CLI to break. Has anyone updated the CLI to tolerate new fields without dying?

3mo ago·2m

No — this hasn't been fixed, and I found the exact mechanism that causes the breakage.

What's happening

The core API client is generated by ogen from the upstream OpenAPI spec. Every generated decoder is strict: when it hits a field it doesn't recognize, it hard-errors instead of skipping it. From internal/coreapi/oas_json_gen.go:

This is the line that kills the CLI when the server adds a field.

How widespread it is

I counted across the generated client:

  • 54 / 54 schema decoders use the strict unexpected field path
  • 0 use the tolerant d.Skip() path
  • This is driven by the spec: core.gen.json has 54 occurrences of "additionalProperties": false

So any new field on any of these 54 models will break decoding. ogen only generates the strict branch because the upstream spec declares additionalProperties: false on every schema; without that, ogen generates d.Skip() and tolerates unknown fields automatically.

Has anyone addressed it?

No. internal/coreapi/UPSTREAM.md is the running checklist of spec workarounds, and this isn't on it. The only normalize transform (spec/normalize.go → foldErrorResponses) folds error responses; it does nothing about additionalProperties. Recent coreapi commits (ENTIRE_TOKEN override, spec refreshes, mirror rewires) don't touch this either.

Two ways to fix it

  1. Upstream (clean, correct): stop emitting additionalProperties: false in the control-plane spec, then regenerate. Every decoder flips to d.Skip() and the client becomes forward-compatible for free. Worth adding as item #2 in UPSTREAM.md.

  2. Local workaround (immediate, fully in this repo): add a transform in spec/normalize.go that strips additionalProperties: false from all schemas before ogen runs — directly analogous to the existing foldErrorResponses fold. Regenerating then produces tolerant decoders without waiting on the server team. This is the fastest path to "new server fields don't kill the CLI."

Want me to implement option 2 (the normalize transform + regenerate + a regression test), and add the matching UPSTREAM.md entry so it gets retired once the spec is fixed at the source?

where does core.gen.json come from?

3mo ago·1m

Here's the full provenance chain for core.gen.json:

Documented in internal/coreapi/gen.go, which holds both //go:generate directives.

So core.gen.json is a build artifact, not hand-authored. It's produced by spec/normalize.go from the upstream core.openapi.json. To refresh the whole chain:

Why this matters for the unknown-fields bug

This is exactly where the fix belongs. The "additionalProperties": false entries that make the decoders strict come from upstream (core.openapi.json) and pass straight through normalize.go untouched — normalize.go currently only does the foldErrorResponses transform.

So the local fix (my earlier "option 2") is: add a transform in normalize.go that strips additionalProperties: false from all schemas as it writes core.gen.json. Because core.gen.json is regenerated, not edited, you can't just edit the open file by hand — it'd be overwritten on the next go generate. The change has to go in normalize.go, then regenerate.

Both core.openapi.json and core.gen.json are committed (last refreshed Jun 5, commit 93735addc), which is why the stale strict decoders are sitting in the tree right now.

Can you refresh now?

3mo ago·2m

Done. Here's what the refresh did.

Result: the bug is fixed at the source

The upstream control-plane team had already flipped all 54 schemas from "additionalProperties": false → true in their OpenAPI spec. The refresh pulled that in and regenerated the client:

Before refreshAfter refresh
Schemas with additionalProperties: false540
Decoders that hard-error on unknown fields (unexpected field %q)540
Behavior on a new server fieldCLI diesCaptured into an AdditionalProps map (preserved, not dropped)

So a new field on any core API model no longer breaks the CLI — ogen now generates a default: branch that reads the unknown field into a raw map instead of returning an error.

Files changed

normalize.go reported it folded error responses on 38 operations (its one existing transform — unchanged). The additionalProperties fix came entirely from upstream, not from a local workaround, so no normalize.go change is needed.

Verification

  • go build ./... — full repo compiles
  • go test ./internal/coreapi/... — passes

Notes / next steps

  • This was just the spec refresh + regenerate. I have not committed anything, and we're on main — say the word and I'll branch and commit.
  • Before any push, the repo requires mise run fmt && mise run lint (and ideally mise run check). Want me to run that and prep a commit/PR?
  • UPSTREAM.md (which you have open) does not track this issue, so nothing there needs deleting. You may want to consider adding a regression guard so a future strict spec doesn't silently reintroduce the break — e.g. a test asserting zero unexpected field decoders, or a normalize.go transform that force-strips additionalProperties: false. Want me to add one?

We're on a branch already. Check everything, if it passes then commit.

3mo ago·13m

Committed and the working tree is clean.

Summary

Branch: update-api-specs Commit: 179a6cd16 — coreapi: refresh spec — tolerate unknown response fields

Checks (all green):

  • gofmt — clean (one whitespace-only generated file self-reverted and dropped out)
  • golangci-lint — 0 issues
  • mise run test:ci — unit + integration tests pass
  • E2E canary (Vogon) — 4/4 passed

What landed (4 files):

  • spec/core.openapi.json — upstream refetch (54 schemas additionalProperties: false → true)
  • spec/core.gen.json — regenerated
  • oas_json_gen.go — decoders now capture unknown fields instead of erroring
  • oas_schemas_gen.go — structs gained AdditionalProps

The original bug — a new server field killing the CLI — is resolved: the upstream fix flowed through, and the regenerated client now tolerates (and preserves) unknown response fields. Ready to push/PR whenever you want.