Repository navigation
fix(api): give POST /api/agent/execute_command a JSON body model both SDKs can send (#15527) - #15541
Merged
Merged
Conversation
4 tasks
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No new hardcoded values of either class — Known backlog in |
This was referenced Sep 2, 2026
…to issue-15536-unserved-backend
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #15527, #15536, #15534.
Thinking Path
Three issues were in scope. I verified each against
Dev_new_guibefore building, andonly one of the three could be delivered from this base.
#15527 — measured, confirmed, delivered.
autobot-backend/api/agent.pydeclaredcommand_data: dictbesideuser_role: str = Form("user"). OneFormdefault makesFastAPI read the whole body as
application/x-www-form-urlencoded, where every fieldarrives as a string, so
command_datacould never validate. The checked-in contractagreed: the operation published
application/x-www-form-urlencodedwith a requiredcommand_dataobject.Route or clients? The route. Every caller sends JSON —
send_commandin the PythonSDK,
sendCommandin the TypeScript SDK — and nothing in the repo sends a form to it.The sibling route in the same module,
POST /command_approval, already takes a JSONCommandApprovalPayloadwith the identicaluser_role: str = "user"field, so theJSON model is the pattern this file already follows and the
Formdefault was theoutlier. Making the clients form-encode would have meant three callers bending to one
mistake.
Wire-contract note. Changing a route's request content type is a wire-contract change.
It is safe here in the strict sense that no body was accepted before: every candidate
shape answered 422, so no external caller can have a working integration to break. An
out-of-repo client that was already receiving 422 will now receive 200 or 400.
Where the model had to live.
api/schemas_agent.pyis the canonical home, and it sitsexactly on its file-size ceiling — the
scripts/python_file_size_known_large.py/repo_tests/python_file_size_ratchet_baseline.pypair pins it to its exact line count andonly ever shrinks, so it could not take one more model. Per the constraint, I split rather
than raised:
api/schemas_agent_requests.pynow holds the five request payloadsapi/agent.pybinds to a body, moved verbatim, plus the newCommandExecutePayload.schemas_agent.pydrops from 2283 to 2225 lines and both ratchet entries are lowered inthe same commit.
api/agent.pylands back on exactly 1380, its unchanged ceiling.#15536 — DEFERRED, with evidence. The premise that the debt is "already pinned in a
shrink-only
_KNOWN_UNSERVEDbaseline" does not hold on this base:repo_tests/slm_frontend_calls_reach_served_routes_test.pydoes not exist onDev_new_gui. It lives on the unmerged PR #15538. That guard'stest_the_known_unserved_baseline_is_exactfails in both directions, so any call Iremoved here would leave a stale entry behind and redden #15538 the moment it lands.
Branch protection on
Dev_new_guihasrequired_status_checks.strict = false, so astale #15538 can merge without a rebase and take
Dev_new_guired with it. #15536 alsocollides at file level with #15538, which edits
autobot-slm-frontend/src/views/settings/admin/UserManagementSettings.vue— the onlyexternal caller of the
getUsers()half. So #15536 isblocked_by#15538, notblocked_bya missing decision. Full measurement and the per-call caller/route verdictsare posted on the issue.
#15534 — left
needs-decision, as filed. The question is whether VNC input forwardingbelongs on the SLM at all; nothing in the code answers it, and adding seven endpoints to
make four buttons look correct is the wrong fix. Options and a recommendation are posted
on the issue rather than guessed at here. A forced batch would have been worse than the
two it is not in.
What Changed
autobot-backend/api/schemas_agent_requests.pyapi/agent.pyrequest payloads moved verbatim out ofschemas_agent.py, plusCommandExecutePayload(command: str,user_role: str = "user"). 93 lines, well under the 600 cap.autobot-backend/api/schemas_agent.pyMetadataimport dropped; the personality models given their own section header. 2283 -> 2225 lines.autobot-backend/api/agent.pyexecute_commandtakespayload: CommandExecutePayload; theFormdefault and thedictbody are gone.Formstays imported —/pauseand/resumestill use it, and they are form-only, which is not the defect. Exactly 1380 lines, unchanged ceiling.autobot-backend/api/agent_test.pyResearchTaskRequestto its new module.autobot-frontend/src/types/generated/api.tsBody_execute_command_api_agent_execute_command_postremoved,CommandExecutePayloadadded in alphabetical position, the operation's request body moved toapplication/json, and the path entry's description updated to the new docstring.libs/autobot-sdk-python/.../resources/agents.pysend_command(command)posts{"command": command}.session_idremoved — the route has no session concept and never read it.libs/autobot-sdk-ts/src/resources/agents.tssendCommand(command), same body, same reason.repo_tests/route_form_and_json_body_are_not_mixed_test.pyscripts/python_file_size_known_large.py,repo_tests/python_file_size_ratchet_baseline.pyVerification
The route's declared content type, before and after. Measured out of tree, with both
signatures mounted on a bare FastAPI app so body parsing is what is observed:
Row 4 is the exact body the Python SDK builds, and row 3 is
user_rolesemanticsunchanged: absent means
"user", supplied is honoured.admin_checkis untouched. Thechecked-in contract now says the same thing —
api.tsmoved from"application/x-www-form-urlencoded": components["schemas"]["Body_execute_command_..."]to
"application/json": components["schemas"]["CommandExecutePayload"].One behaviour change worth stating: a body with no
commandnow answers 422 rather thanthe hand-rolled 400 in
_validate_command_request. Nothing can regress on it, because nobody reached that branch before. An empty-string
commandstill takes the 400 path.The new guard fires. Population floors are evaluated before the substantive assertion,
so a collapsed sweep fails by name. Three mutations, verbatim:
api/agent.py—1 failed, 6 passed;autobot-backend/api/agent.py:845 execute_command Form=['user_role'] body=['command_data']_PACKAGESat a directory that does not exist —2 failed, 5 passed;FIX THE SWEEP: found only 0 route handlers, floor is 2400.Formtoken in_MARKERSasFormData—5 failed, 2 passed;FIX THE SWEEP: found only 0 routes declaring a Form field, floor is 20.Mutation 3 is the one that matters: it proves the literal token matched is one the subject
actually emits.
test_the_extractor_sees_form_in_the_source_it_readspins that to two realhandlers,
pause_agent_apiandresume_agent_api, which still declareuser_role: str = Form("user")— so drift off the real spelling loses them by nameinstead of reporting zero mixtures everywhere.
Precision was measured, not assumed. A naive sweep reports 4 hits on this tree. Three
are correct code:
chat_knowledge.upload_file_to_chatandconversation_files.upload_conversation_filedeclare path parameters beside aFormfield, and
code_source.upload_packagereachesDependsthroughAnnotated. The guardhandles all three and has regression tests for each, so the tree reports exactly 1 hit
before the fix and 0 after.
Suites run. Restored to the committed state, then:
repo_tests/route_form_and_json_body_are_not_mixed_test.py—7 passedrepo_tests/python_file_size_ratchet_test.py— the pair is consistent,38 passedfor both files togetherrepo_tests/sdk_request_url_test.py,sdk_request_signature_params_test.py,sdk_response_model_contract_test.py—133 passed, 1 failed. The failure istest_every_sdk_response_model_is_paired_or_explicitly_unpairedon fiveDataResponse[Session*]generics; it passes when that file runs alone, touches nothingin this diff, and is a pre-existing cross-file import-order artifact.
black --check,isort --check-only,flake8clean on every changed Python file; noline over 120 characters.
Probes were written outside the repository and never staged;
git statusis clean.Not closing #15527. Three of its four acceptance criteria are met. The fourth asks for
the
_UNSATISFIABLEentry to come out ofrepo_tests/sdk_request_body_test.py— a filethat does not exist on
Dev_new_gui; it is on the unmerged PR #15526. That entry ischecked to still be necessary, so #15526 must drop it when it rebases onto this. Flagged
there.
Model Used
Claude Opus 5 (1M context).