Repository navigation
Blob request and response bodies for plugin routes (#1720) - #1764
michielbdejong wants to merge 1 commit into
Conversation
|
Merge-risk triage (2026-09-28). This is a read-only review of this PR's own diff. The rule it applies: nothing that works today may break for existing users or clients, and new behaviour is added alongside the old. The stack is being rebased onto Verdict: SAFE (conflict is keep-both-sides) Blob responses get Note: The only conflict with its base (and on top of #1871) is 🤖 Generated with Claude Code |
A `body: blob` route no longer answers 501. The host reads the body
(413 past the route's maxBodyBytes, default 16 MiB, capped by the new
--plugin-route-max-blob-bytes; 429 past the bytes-per-day quota), hashes
it with BLAKE3 and stores it in the blob store before the sandbox runs.
The handler gets request.blob = { hash, size, type, subject }, never the
bytes. Only at read-write, and only under a route grant for one of the
route's write targets.
A handler may answer with response.blob. The host sends the bytes with
the handler's (or the uploaded) content type, ETag "<hash>", nosniff and
the drive-prefix rules (no HTML/SVG, sandbox CSP); HEAD gets the length.
A route may only serve, or reference in a route write, a blob its
installation stored or one its approved write targets hold.
The host answers If-Match / If-None-Match against the served blob, or
against response.current (what the target held before a write): 304 or
412, and nothing the verdict asks for is stored.
Fixture testdata/plugin-routes/files/ (remoteStorage-like PUT/GET), unit
tests in route_blobs_test.rs, and a 2 MiB PUT/GET round trip in
tests/it/plugin_routes.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aca62f9 to
ec428a5
Compare
|
Closing as part of the plugin-host stack cleanup (Q-101): not needed by atomic-plugins now. See the inventory at #1726 (comment). The branch is kept, and pin candidates are unaffected. Closed by Claude (VPS coordinator) on behalf of Michiel. |
Closes #1720. Stacked on #1760 (delivery queue), which is on #1759, #1754, #1752, #1751, #1749, #1732 and #1726.
Design: atomic-plugins
docs/design/server-plugin-routes.md(ontola/atomic-plugins#108), sections 2.6 and 2.8.What it does
Everything is behind
#[cfg(feature = "plugin-routes")]. Storing a body only happens at--plugin-routes read-write. The501 route-blob-unavailablefrom #1751 is gone. Code: newserver/src/plugins/route_blobs.rs, called fromroute_exec.rsandroute_writes.rs.Request bodies (
body: "blob")The checks run in this order, and nothing is stored if one fails:
read-writethe answer is403 route-blob-refused. A release with a blob route can't install there anyway.403 route-write-not-granted. Storing a body is a write.maxBodyBytes, default 16 MiB, capped by the operator's new--plugin-route-max-blob-bytes/ATOMIC_PLUGIN_ROUTE_MAX_BLOB_BYTES, default 16 MiB.Content-Length, then each chunk while it streams in, is checked against the limit (413 route-body-too-large) and against the bytes the installation has left today (429 route-quota-exceededwithRetry-After).auth: http-signatureis verified over the received bytes.bytes-per-dayledger and stored content-addressed (BLAKE3).Tree::PluginMetarecords that this installation stored them.request.blob = { hash, size, type, subject }, andrequest.bodyisnull.subjectisatomic:blob:<hash>, what a File'sblobproperty takes. The bytes never enter the sandbox.Blob responses
response.blob(a hex hash or anatomic:blob:/did:ad:blob:subject) instead ofbody. Setting both is502 route-handler-failed.502 route-blob-refused. The same rule covers blob references in route writes: a route can't write a foreign hash into its folder and then serve it from there (502 route-write-refused).Content-Typeis the handler's, else the type the blob was uploaded with, elseapplication/octet-stream. Run plugin routes: the http trigger and route execution (AS-05) #1751's rules still apply: no HTML, XHTML or SVG ondrive-prefix, the sandbox CSP there, andnosniffalways.ETag: "<hash>"is set by the host and replaces anyetagthe handler set.HEAD, only the size is read, andContent-Lengthis the blob's.502 route-blob-missing. That is checked before any intents are applied.Conditional requests (host-handled)
If-Match(strong comparison) andIf-None-Match(weak comparison,*supported) are compared with the served blob, or withresponse.current. That field is the blob (ornull) the handler says the target held before a write.GET/HEAD:304withETag, or412.412 route-precondition-failed.current, the host doesn't evaluate the preconditions. Theif-*headers still reach the handler, as before.2xxresponse without anetaggets the new blob'sETag.Lifecycle
When an installation is revoked or destroyed, its stored-blob records are erased along with its keys and tokens. The bytes stay: they are content-addressed, and a File may hold them.
Limits
maxBodyBytes--plugin-route-max-blob-bytes)--plugin-route-bytes-per-day(64 MiB default from #1754)Fixture
testdata/plugin-routes/files/is a remoteStorage-likedrive-prefixplugin. It has one write target,files(parentconfig:folder, classFile), and it is exposed astest_fixture::files_release().PUT /files/{*path}(body: blob) creates a File, or points the existing one at the new blob. It returnscurrentand echoes what it saw.GET|HEAD /files/{*path}answers with the File's blob.PUT /small/{*path}does the same as/fileswithmaxBodyBytes: 1024.GET /raw/{hash}?type=answers with any hash.POST /link/{*path}writes any hash into a File. Both exist for the refusal tests.Tests
route_blobs_test.rs(through the app,read-write):ETag,nosniff, CSP,Cache-Controland length, also forHEAD;GETs answer304and412;PUTwithIf-None-Match: *on an existing file, or with a staleIf-Match, answers412and nothing changes; a matchingIf-Matchupdates;maxBodyBytes) answer413, and the blob isn't in the store;429+Retry-After, and isn't stored;drive-prefix;403, nothing stored;read-onlythe blob release doesn't install, while a GET-only release still serves a blob its installation stored.route_blobs.rsunit tests: hash parsing, the limit cap, precondition rules, and finding blob references.tests/it/plugin_routes.rs(real server): PUT 2 MiB, then GET it back byte for byte withETag, content type andContent-Length. AlsoHEAD,304,412, and413for 16 MiB + 1.install_inboxis generalised intoinstall_writer.Runs (
ATOMICSERVER_SKIP_JS_BUILD=true):cargo test -p atomic-server --lib: 397 passed with--features plugin-routes, 299 without.cargo clippy -p atomic-server --all-targets -- -D warnings: clean both ways.cargo test -p atomic-server --features plugin-routes --test it plugin_routes: 5 passed.Interpretations
put_blob) takes whole values, so the body is buffered in memory while it is hashed, bounded by the limit, then stored. Limits are enforced per chunk as it arrives, so an oversized body is refused before storage. A streaming backend API would be a separate change.response.current. Without it, preconditions stay with the handler.Rangesupport: the issue doesn't ask for it.403 route-blob-refusedbelowread-write;502 route-blob-refused/route-blob-missing;412 route-precondition-failed;500 route-blob-store-failed.No UI changes.
🤖 Generated with Claude Code