fix(tui): stop fleet readonly schema probe from auto-vivifying null enums - #5944
Conversation
Hmbown
left a comment
There was a problem hiding this comment.
Reviewed at 8104a75b0. The bug is real, the fix is correct, and I reproduced both states locally. Approving in substance — two small reuse notes and a DCO trailer below.
What I verified
I checked the head out into a detached worktree, reverted only crates/tui/src/tools/registry.rs to its parent (8104a75b0^), and kept your new test. It fails exactly as you describe:
tool bash schema carries null at ["$.properties.action.enum"]:
{"additionalProperties":false,"properties":{"action":{"enum":null},"command":{...},...}}
Restoring the fix: 48 passed; 0 failed for tools::registry
(RUST_MIN_STACK=16777216 cargo test -p codewhale-tui --lib -- tools::registry).
Two things worth recording because they are not obvious from the diff:
- The auto-vivification is two levels deep, not one.
schema["properties"]["action"]insertsNull, and then["enum"]on thatNullconverts it into an object before inserting — which is why the artifact is{"enum": null}and not a barenull. - Neither
schema_sanitize::sanitizenorschema_canonicalize::canonicalize_schema(registry.rs:270-271) strips it downstream. The failure output above is post-sanitize, post-canonicalize. So the guard genuinely has to live at the probe — there is no net underneath it.
Nits
1. pointer_mut is already the house idiom for this exact probe — crates/tui/src/tools/subagent/mod.rs:14869 and :14907:
let Some(actions) = tool
.input_schema
.pointer_mut("/properties/action/enum")
.and_then(serde_json::Value::as_array_mut)
else { continue; };and the comment right above it (mod.rs:14863-14867) describes this same bug, down to the lowercase-bash shape:
Indexing
["properties"]["action"]["enum"]mutably would fabricate an"action": {"enum": null}property on schemas that have no action discriminator (the lowercasebashcommand/timeout shape) — a phantom node that fails Moonshot MFJS validation. Only shape enums that already exist.
So this is the same known defect in a second location, and the established one-line fix collapses your three-step ladder to:
let Some(actions) = schema
.pointer_mut("/properties/action/enum")
.and_then(Value::as_array_mut)
else { return; };Same fail-closed behavior, non-mutating, and it makes the two sites greppable as one pattern. Your long comment is worth keeping either way — it is better than the one in subagent.
2. The Run arm two lines above still auto-vivifies — crates/tui/src/tools/registry.rs:508:
if let Some(properties) = schema["properties"].as_object_mut() {If a Run-family schema ever lands without a top-level properties, this writes "properties": null into the wire schema and then returns — the identical failure class one match arm earlier. It is latent today (Run always has properties), but since this PR is specifically about closing that hole, schema.get_mut("properties").and_then(Value::as_object_mut) there would close it for good. Your new test would not currently catch it, because Run is filtered out of the reviewer catalog unless verification == Bounded.
3. DCO — 8104a75b0 carries no Signed-off-by: trailer. The Check Signed-off-by job is advisory (.github/workflows/dco.yml exits 0 either way), so it is green, but CONTRIBUTING asks for it. git commit --amend -s && git push --force-with-lease when convenient.
Unrelated to this change: the Test (windows-latest) failure on this run does not touch tools::registry.
Nice catch, and thank you for the regression test that walks the whole projected catalog for nulls rather than asserting on bash alone — that generalizes to the next tool that grows an action enum.
…nums
`project_readonly_evidence_schema` used `schema["properties"]["action"]["enum"]`
to probe for an action enum. serde_json's IndexMut auto-vivifies missing keys
by inserting Null, so schemas with no action property (e.g. lowercase `bash`)
ended up with `properties.action = {"enum": null}`. Strict OpenAI-compatible
validators then rejected the whole request with `Invalid schema for function
'bash': null is not of type "array"`, observed on Fleet read-only workers.
Probe with non-mutating `get_mut` instead, and add a regression test that
asserts a reviewer wire catalog carries no null schema fields.
…arm (Hmbown#5944 follow-up) Maintainer follow-up on @gaord's fix, applying the two review nits: - The read-only projection probes with schema.pointer_mut("/properties/action/enum"), the same idiom tools/subagent already uses for this exact bug, so the two sites read as one pattern. - The Run arm two lines above still auto-vivified "properties" on a schema without one; it now uses get_mut and cannot write "properties": null. - Rebased onto current main (the tests module had grown a neighbour) and added the changelog receipt crediting the author. RUST_MIN_STACK=16777216 cargo test -p codewhale-tui --lib -- tools::registry: 51 passed; 0 failed. cargo fmt --check clean; cargo clippy -p codewhale-tui --lib --all-targets -D warnings clean; sync-changelog --check clean. No-Issue: community fix from a live Fleet 400 report; no tracking issue. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJrzNAmppg4vt3LNJbaeri Signed-off-by: CodeWhale Bot <bot@codewhale.net>
8104a75 to
7371e95
Compare
| - Read-only Fleet workers no longer send `"action": {"enum": null}` in their | ||
| projected `bash` schema. The read-only projection probed the action enum | ||
| with a mutating index, which auto-vivified the key on schemas that have no | ||
| action property, and strict OpenAI-compatible validators then rejected the | ||
| whole request (`null is not of type "array"`). The probe is non-mutating | ||
| now, in both the read-only projection and the `Run` arm next to it, and a | ||
| regression test walks the whole projected catalog for nulls | ||
| (#5944, thanks @gaord). |
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
…eps the endpoint's receipt (Hmbown#5926) The founder's receipt: of the eight servers the footer called failed, seven only needed a login. Three surfaces now keep the states apart. /mcp lists the servers that need a login first, as their own Needs login group above Needs attention, and opens with the cursor already on the first such row, so the Enter the screen advertises runs `/mcp login <server>` straight away. New ExtensionsGroupNeedsLogin key in all 15 locale packs. A token refresh that fails to parse the provider's answer keeps the endpoint's receipt - status line, content type, and a 200-byte excerpt with every credential-shaped value (access_token, refresh_token, client_secret, id_token, bearer schemes) masked before the cut - instead of rmcp's bare 'Failed to parse server response', so a provider outage answering an HTML 502 reads differently from a parser defect. The receipt rides a RecordingOAuthHttpClient that executes what rmcp's stock client would and records the latest token-endpoint answer; only the current refresh's answer may explain that refresh's failure. The remedy wording itself landed earlier as Hmbown#5959. A footer snapshot test pins 'MCP . 1 connected . 1 auth required . 1 failed' so an expired login never regresses into the failed count (the chip landed with the boot-surface half of Hmbown#5926). Gates on this head (rebased onto main f974685): cargo fmt --all clean; cargo clippy --workspace --all-targets --all-features --locked -D warnings clean; full suite cargo test -p codewhale-tui --lib --locked 11877 passed / 0 failed / 13 ignored (targeted module filters 71 passed, the eight new tests verified by name, localization 50 passed). Earlier runs under the shared CI runner showed isolated load flakes (deepseek translate, tmux clipboard, fleet concurrent manager loops) that each pass in isolation and in the final clean run; one zai compatibility-stream failure at the previous base was root-caused to the tools-registry probe defect fixed on main by Hmbown#5944's follow-up. Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Summary
Fix
project_readonly_evidence_schema: it probed for an action enum with IndexMut, which auto-vivifies missing keys as null. A read-only Fleet worker projecting a lowercase bash tool then carriedproperties.action = {"enum": null}, which strict OpenAI-compatible validators reject. Probe with non-mutatingget_mutinstead.Testing
Note: main currently has 5 pre-existing nonminimal_bool clippy errors (tools/subagent, tui/ui/apply, tui/views/fleet_roster, tui/phase_strip), unrelated to this change.
No-Issue: community fix from a live Fleet 400 report; no tracking issue.
Maintainer follow-up (2026-09-06)
Rebased onto current main and applied the two review nits in a signed follow-up commit: the probe uses
pointer_mut("/properties/action/enum")liketools/subagent, and theRunarm usesget_mutso it cannot write"properties": null. Changelog receipt added crediting @gaord.Gate:
cargo test -p codewhale-tui --lib -- tools::registry51 passed / 0 failed; fmt, clippy-D warnings, andsync-changelog --checkclean.