feat(serve): serve POST /worker/resources/list and /worker/resources/read [TOO-1936] - #922
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests.
🚀 New features to boost your workflow:
|
35dc47d to
9de50ed
Compare
0acfcad to
d64200b
Compare
9de50ed to
682dd87
Compare
d64200b to
f41cd44
Compare
682dd87 to
eb459e3
Compare
f41cd44 to
1db7d9e
Compare
Greptile SummaryThis PR adds authenticated worker endpoints for listing and reading resources, registers them by default, and maps resource and request-validation failures onto protocol error responses.
Confidence Score: 4/5The PR is not yet safe to merge because invalid UTF-8 request bodies still produce the worker-fault response this change is intended to eliminate. The malformed-body fix catches JSONDecodeError and ValidationError, but json.loads can raise UnicodeDecodeError while decoding raw request bytes, leaving a reachable HTTP 500 path. Files Needing Attention: libs/arcade-serve/arcade_serve/fastapi/worker.py
|
| Filename | Overview |
|---|---|
| libs/arcade-serve/arcade_serve/fastapi/worker.py | Adds protocol error mappings, but malformed request bytes with invalid UTF-8 still bypass the intended HTTP 400 handling. |
| libs/arcade-serve/arcade_serve/core/components.py | Adds typed list/read resource components and omits absent optional fields from responses. |
| libs/arcade-serve/arcade_serve/core/base.py | Registers both resource components in the default worker component set. |
| libs/tests/worker/test_worker_resources_endpoints.py | Covers resource endpoint contracts and common malformed JSON shapes, but not invalid byte encodings. |
| libs/arcade-serve/pyproject.toml | Bumps arcade-serve for the new worker protocol functionality. |
| libs/arcade-mcp-server/pyproject.toml | Bumps arcade-mcp-server and raises its minimum arcade-serve dependency. |
Reviews (3): Last reviewed commit: "feat(serve): serve POST /worker/resource..." | Re-trigger Greptile
eb459e3 to
b4d3d91
Compare
1db7d9e to
e0c6c4e
Compare
b4d3d91 to
504cadf
Compare
e0c6c4e to
43a1d6f
Compare
| method=request.method, | ||
| body_json=body_json, | ||
| ) | ||
| except (json.JSONDecodeError, ValidationError): |
There was a problem hiding this comment.
Invalid encoding escapes mapping
When a request body contains invalid UTF-8 bytes, json.loads raises UnicodeDecodeError, which this handler does not catch, causing an HTTP 500 instead of the intended HTTP 400 invalid-params response.
| except (json.JSONDecodeError, ValidationError): | |
| except (json.JSONDecodeError, UnicodeDecodeError, ValidationError): |
504cadf to
0d952d2
Compare
43a1d6f to
457e3f6
Compare
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
…read [TOO-1936]
The two endpoint components, registered in the default component set. These are
the entire new protocol for this project, and they are thin by design: a
serialization shim over the registry, with no caching and no logic.
The path suffix names the operation, the request body is that operation's params
object, and the response is its result object, all spelled as they go on the
wire so neither end has to translate.
Both are POST because both requests carry params: an optional cursor on the
list, a required uri on the read. RequestData carries only path, method and
body, and its path comes from request.url.path, which Starlette strips of the
query string, so a component cannot read a query parameter at all. A first-page
list sends an empty body, which the router already coerces to {}.
Responses pass response_model_exclude_none through to FastAPI. Its default
emits an explicit null for every unset optional field, so a resource with no
annotations would go out as annotations, size and icons all null, which the
format does not do.
Errors are error objects carried by an HTTP status. An unknown URI is 404 with
code -32002. That overlaps with treating a 404 as "this worker serves no
resource endpoints", so the tolerance rule is endpoint-specific: 404 and 405
mean an empty catalog on the list endpoint only. The mapping lives in the
FastAPI binding rather than in core, which stays framework-agnostic.
The default-components test pinned the three existing routes by name and now
pins five.
0d952d2 to
97f6382
Compare
457e3f6 to
67a78e1
Compare
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
Follows the same reconnection one commit down the stack. Conflicts are all the shape where this side already carries the base's changes plus its own; taking this side keeps both intents and leaves the tree unchanged.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
Summary
The two resource endpoints,
POST /worker/resources/listandPOST /worker/resources/read, registered in the default component set. One rule governs both: the path suffix names the operation, the request body is that operation's params object, and the response is its result object, all spelled the way they go on the wire so neither end has to translate anything.Part 6 of 6. Stacked on #921. This is the whole protocol addition.
Resolves: https://linear.app/arcadedev/issue/TOO-1936
Design decisions
Both are POST
Both requests carry params: an optional
cursoron list, a requiredurion read. AndRequestDatacarries onlypath,methodandbody, withpathcoming fromrequest.url.path, which Starlette has already stripped of the query string. A worker component genuinely cannot read a query parameter. A first-page list sends an empty body, which the router already coerces to{}.response_model_exclude_none=Truegets passed throughFastAPI defaults it to
False, so without it a resource with no annotations goes out carrying"annotations": null, "size": null, "icons": null. Those keys should be absent.An unknown URI is 404 with code
-32002Which overlaps awkwardly with reading a 404 as "this worker has no resource endpoints at all", so the tolerance rule is per-endpoint: 404 and 405 mean an empty catalog on the list endpoint only. On read, a 404 is a typed not-found. That leaves one ambiguous case, an old worker 404ing a read, and it is harmless, because the caller does not get the resource either way.
A malformed body is 400, not 500
The body was parsed and
RequestDatabuilt above the block that maps errors, so a body that is not a JSON object escaped as an unhandled 500.RequestData.body_jsonisdict[str, Any] | None, so[],false,0,""and truncated JSON all took that path. A caller sending nonsense was told the worker had broken, which matters because the engine reads a 500 as a worker fault worth retrying and a 400 as its own bug. The parse moved inside the mapped block.This one is wider than the rest of the PR and worth a reviewer's attention:
_wrap_handleris shared, so/worker/toolsand/worker/tools/invokemove from 500 to 400 on a malformed body too. That behavior is pre-existing and identical onmain. It is fixed here because this PR is what adds theValidationErrorto 400 mapping in this file, and that mapping was dead for the most likely error. A literalnullbody is left reading as empty params, since the router already maps an absent body to{}.The HTTP mapping lives in the FastAPI binding, which keeps
coreframework-agnostic. Components raise typed errors and the binding turns them into an error object carried by a status code.Test plan
Exercised against a real uvicorn worker with a real toolkit installed, nothing in front of it:
The mime type is whatever the toolkit declared. These libraries carry it verbatim and never inspect it.
uv run pytest libs/tests: 3807 passed, 1 skippeduv run mypy .clean in each of the four librariesruff checkandruff format --checkclean on every touched fileRisk note
Adds two routes under
/worker/*, which this repo's PR template lists as sensitive. Both sit behind the samevalidate_engine_requestdependency as the existing endpoints, and no auth path changes. A worker with no declared resources answers list with an empty array.The malformed-body change above touches
_wrap_handler, which every worker endpoint goes through. The blast radius is the status code a caller sees for a body that was already failing: 500 becomes 400 on/worker/toolsand/worker/tools/invokeas well. No well-formed request changes behavior, and there is a test pinning each status.Reviewer note
The default-components test pinned the three existing routes by name and now pins five.
Author checklist
Before moving this PR from Draft to Ready for Review:
make checkandmake testare green locally; CI is expected to passNote
Medium Risk
New authenticated
/worker/*routes plus shared request-wrapper behavior that changes status codes for malformed bodies on all worker endpoints; well-formed traffic should behave as before.Overview
Adds
POST /worker/resources/listandPOST /worker/resources/readto the default worker component set, wiring them tolist_resources/read_resourceon the catalog with params/result shapes fromarcade_core.resource_schema. Responses useresponse_model_exclude_none=Trueso optional resource fields stay off the wire instead of appearing asnull.The FastAPI
_wrap_handlerlayer now maps typed failures to structured JSON errors: unknown URI → 404 with code -32002, bad cursor or Pydantic params → 400 with -32602, and malformed JSON bodies → 400 instead of an unhandled 500. That parsing/error mapping is shared with existing routes (/worker/tools,/worker/tools/invoke), so nonsense bodies on those paths also move from 500 to 400.arcade-serve bumps to 3.6.0; arcade-mcp-server bumps to 1.28.1 and requires arcade-serve>=3.6.0. Contract tests assert wire bytes (mime types, omitted optional fields, auth, pagination).
Reviewed by Cursor Bugbot for commit 5f4a2bb. Bugbot is set up for automated code reviews on this repo. Configure here.