refactor(dashboard): sweep tranche 3 onto read_bounded_json (#5587) - #7308
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Mechanical application of an established, register-audited guard pattern; per-site cap decisions are documented, pinned by tests, and fully reversible. Suggestions
[DESIGN-REVIEWED] 0a11a85 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsAll three candidates are low-confidence by their own discovery pass, and each falls apart under falsification: Candidate 1 (batch-delete/queue-reorder 64 KB cap): the discovery pass itself could not establish a concrete input — it admits ">1600 items" is required and "the harm may be purely theoretical." That is a "might," not a real (a). The refusal is a 413, not a crash/data-loss/security hole. Drop. Candidate 2 (cron enable/ack now 400 a malformed present body): I opened Candidate 3 (reset_conversation 400 on malformed body): same intentional-and-documented contract; the early return performs no teardown, preserving the reset-race invariant. Drop. Step 2: I examined the only genuine logic — the No findings. [OPUS-REVIEWED] 0a11a85 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
e4ffdcb to
a65455c
Compare
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All evidence is gathered. The mechanism ( First-Principles-Verdict: CONCERNS The sweep is sound, but the malformed-body-now-400 flip lands on five chat-slot routes the description's three-route list never names. What this change shipsIntent: stop valid-JSON-non-object bodies from turning 55 dashboard routes into 500s — a FIX (tranche 3 of issue #5587).
Watch
[FIRST-PRINCIPLES-REVIEWED] 0a11a85 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
a65455c to
6fe3b6d
Compare
bolichen97
left a comment
There was a problem hiding this comment.
Description / code mismatch
Both wire-contract changes the Description declares are real in the code, but neither of the two specs that document those contracts is part of this change, so the authoritative prose now states the opposite of what ships.
1. The cron spec still documents the enable/ack default-on-malformed tolerance and the invalid_json code this change reverses
The Description says —
Sites that defaulted an ABSENT body keep doing so (
allow_absent), but a present-and-malformed body is a 400 instead of silently collapsing to defaults (api_cron_enable,api_cron_ack,api_workflow_run_rerun— the same defect class #5587 originally fixed inapi_memory_promote).
The code does — exactly that. api_cron_enable and api_cron_ack call _shared.read_bounded_json(..., allow_absent=True), so an ABSENT body still defaults, while a present scalar/array/null body is refused with 400 body_not_object and an unparseable body with 400 invalid_json. The spec paragraph is unchanged and contradicts this on both of its route-specific clauses: it states that POST /api/crons/{id}/enable and /ack "fall back to their defaults" on a body they cannot use, and that PATCH /api/crons/{id} and DELETE /api/lessons answer code: invalid_json for a non-object body — docs/system-specs/modules/learn-cron-dashboard.md:959-965.
Risk — AGENTS.md § Specification management is a MUST: "MUST update the spec in the SAME commit when an API, schema, or documented behavior changes." Left stale, the authoritative cron spec tells the next reader — and the next agent following the router table to it — that enable/ack silently default a non-object body and that update / lessons-delete answer invalid_json, which is the inverse of what ships. A contributor writing a client from the spec, or a later tranche reading it to decide a fold, gets the wrong contract, and the divergence is invisible because no gate compares prose to code.
Required change — In the same commit, rewrite the **JSON body shape** paragraph at docs/system-specs/modules/learn-cron-dashboard.md:959-965 to state the shipped contract: these routes go through _shared.read_bounded_json; a scalar/array/null body is 400 body_not_object, an unparseable body is 400 invalid_json, an over-cap body is 413 payload_too_large, and only an ABSENT body still defaults on /enable and /ack (allow_absent). While there, the POST /api/chat/slots/{slot}/end-wait code list at docs/system-specs/modules/learn-cron-dashboard.md:857-859 ("400 wait_id_required, 404 slot_not_found, 409 wait_not_in_flight") is now incomplete: chat_handlers.py::api_chat_slot_end_wait replaces its if not isinstance(body, dict): body = {} normalization with read_bounded_json(request, allow_absent=True), so that route gained 400 body_not_object / invalid_json.
2. The workflows spec still documents code: invalid_json for a non-object body on every workflow mutation route
The Description says —
Non-object bodies: 400
body_not_object(was a 500, or a custom 400 message on a few routes).
The code does — the /api/workflows/** mutation handlers route through _shared.read_bounded_json, which answers 400 body_not_object for a valid-JSON-non-object body and reserves invalid_json for a body it cannot parse at all. The spec paragraph documenting the wire contract for those routes still names code: invalid_json for a non-object body — docs/system-specs/modules/workflows.md:869-871.
Risk — That paragraph is the documented wire contract for the /api/workflows/** mutation surface, and it now names a machine-readable code the backend no longer emits for that input, so any client written from the spec branches on a code it will never see. Same AGENTS.md MUST as above. Unlike the cron case there is no compensating prose for this surface: body_not_object is documented once in the tree (docs/system-specs/modules/pptx-maker.md:186), and no doc states read_bounded_json's non-object contract centrally — its only other docs mention, docs/system-specs/modules/app-notifications.md:23, covers the size bound alone.
Required change — In the same commit, update docs/system-specs/modules/workflows.md:869-871 to name code: body_not_object for a valid-JSON-non-object body and invalid_json for an unparseable one. Recording the cap split in the same paragraph is welcome but is not what blocks: api_workflow_run_promote is the one capped route, so an over-cap body there is 413 payload_too_large, while the other seven read uncapped (max_bytes=None) because their bodies carry script source or a free-form intent.
6fe3b6d to
a5bd7f6
Compare
a5bd7f6 to
5b488e5
Compare
5b488e5 to
3e34091
Compare
Route the 55 bare `await request.json()` sites in chat_handlers.py and
handlers/{mcp,cron,workflows,files}.py through the shared body guard, so
a valid-JSON-non-object body answers 400 body_not_object instead of the
AttributeError 500 the follow-on .get() raised.
Every site records its cap decision in the guard test's register: 38
control-field bodies take the shared 64 KB cap, 3 sites whose field
bounds can exceed it take explicit per-route ceilings (cron create and
update sized to MAX_CRON_MESSAGE in worst-case UTF-8; mcp apply sized to
its change-count cap), and 14 sites whose bodies carry a chat message,
script source, prompt, server config, or file contents stay uncapped by
explicit max_bytes=None. No new cap deferrals: the pending-cap ratchet
stays at 13.
Sites that previously defaulted an ABSENT body keep doing so via
allow_absent, but a present-and-malformed body is a 400 instead of
silently collapsing to defaults (cron enable/ack, workflow rerun). SEL
denial audits on the files handlers derive their error label from the
guard's machine code, so a 413 is no longer filed as a JSON parse
failure. Capped sites stream request.content, so their test harnesses
feed real bytes through the shared test/body_stream_helpers.py stub
instead of mocking request.json.
Refs #5587
3e34091 to
0a11a85
Compare
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
Summary
Tranche 3 of the #5587 sweep. Routes the 55 bare
await request.json()sites inchat_handlers.pyandhandlers/{mcp,cron,workflows,files}.pythrough the shared body guardread_bounded_json, so a valid-JSON-non-object body ([],"s",5,null) answers 400body_not_objectinstead of the AttributeError 500 the follow-on.get()raised.Refs #5587 — partial fix, do not close. Out of scope per the issue: the four deliberately-divergent guards (
handlers_channel._json_object,handlers/hooks.py::_json_object,handlers/artifacts.py::_read_json_body,handlers/session_storage.py::_json_body) and their fold decision.Cap decisions (all recorded in the guard test's
_CAP_REGISTER)api_crons_create/api_cron_update(_MAX_CRON_BODY_BYTES= 4×MAX_CRON_MESSAGE+ 64 KB headroom, since a maximal multibyte message exceeds 64 KB) and_do_mcp_apply(_MCP_APPLY_MAX_BODY_BYTES= 1 MB, its change-count cap runs only after decoding).max_bytes=None._CONTROL_FIELDS_CAP_PENDINGratchet stays at 13.Behavior changes
body_not_object(was a 500, or a custom 400 message on a few routes).payload_too_largefor oversized bodies, enforced before decoding.allow_absent), but a present-and-malformed body is a 400 instead of silently collapsing to defaults (api_cron_enable,api_cron_ack,api_workflow_run_rerun— the same defect class dashboard: consolidate the two JSON-object body-guard helpers, then sweep the ~85 unshaped request.json() handlers #5587 originally fixed inapi_memory_promote).handlers/files.pyderive their error label from the guard response'scode, so a 413 is audited aspayload_too_large, not as a JSON parse failure.website/srccode switches on the retiredinvalid_body/ per-routeinvalid_jsoncodes (remaining greps are unrelated i18n keys), so no frontend change rides along.Testing
request.content, so every affected mocked-jsonharness now feeds real bytes via the new sharedtest/body_stream_helpers.py(BodyStreamPayload/attach_body), which also collapses what would have been ~13 copy-pasted stream stubs./interruptbody restores both_stop_stateand_auto_run; oversized/api/mcp/applybody is 413 before decoding.error-code-baseline.jsonregenerated per its own--updateremedy (removed codeless 400 literals).Pre-push blind review
Two model-pinned read-only lanes on the staged diff:
/interruptleft_auto_rundisabled on a refused body;_do_mcp_applyopted out of the cap despite bounded control data._Payloadconsolidation,user_metanarrowing comment), 1 verified-no-action.Remaining files (tracked on #5587)
~131 bare sites remain across ~51 dashboard files, largest first:
handlers/messaging.py(27),handlers/prompts.py(6),handlers/security.py(6),handlers/source_providers.py(6),handlers/core.py(5),handlers/updates.py(5),chat_folders.py(5),handlers/agents.py(4),handlers/hooks.py(4),handlers/side.py(4), plus ~41 files with 1–3 sites each.