Repository navigation
Route writes: the route grant, quotas and provenance (#1717) - #1754
michielbdejong wants to merge 1 commit into
Conversation
At `--plugin-routes read-write`, a route verdict's intents are applied
into the route's declared write targets, signed by the installation's
agent, through the shared planner and applier, before the response is
sent.
- Route grant: `{"route-writes": [write targets]}` in the Installation's
grants. Without it writes answer 403; activation refuses a release whose
targets the grant does not list, so a widened upgrade needs a new review.
- Creates only directly under a target's parent with its classes; sets,
removes and destroys only on resources the installation created (genesis
signer), still inside a target. Parent, class, rights and provenance
properties are never writable through a route.
- Quotas per installation: creates per remote caller per hour (100), creates
per day (10,000), bytes per day (64 MiB), configurable; 429 with
Retry-After and nothing written.
- `routeProvenance` on every created or changed resource: installation,
route, request id, verified caller.
- Enqueues stay refused (501) until the delivery queue; read-only still
refuses every write.
Fixture `testdata/plugin-routes/inbox/`; unit tests through the app and the
`plugin_routes` integration test POST and read the item back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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 Gated. The only change in default builds: Note: Design note, not a blocker: the grant adds the plugin agent to 🤖 Generated with Claude Code |
|
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 #1717. Stacked on #1752 (well-known), which is on #1751 (route execution), #1749 (registry), #1732 (manifest v3) and #1726 (gates).
Design: atomic-plugins
docs/design/server-plugin-routes.md(ontola/atomic-plugins#108), section 2.6 and decision D4.What it does
Everything that applies writes is behind
#[cfg(feature = "plugin-routes")]and only runs at--plugin-routes read-write. Atread-write, the501 route-writes-unavailablefrom #1751 is gone for intents. Code: newserver/src/plugins/route_writes.rs, called fromroute_exec.rs.Order of a writing request
502, and nothing is written.plan_verdict).apply_planwithStoreApplyHost), signed by the installation's agent.2xxmeans the write was stored.Route grant
The route grant is part of the Installation's
grants:{"route-writes": [write targets]}.["storage", {"route-writes": [...]}]) or a key of the object form.check_grantsaccepts it in every build that can install plugins, next to the capability names. It is not a capability.writeTargetsthe grant does not list unchanged. So an upgrade that widens the targets needs a new review, and the old release keeps serving. A narrower release passes.403 route-write-not-granted.What a route may write
config:<key>from the Installation's config, or a URL.createdByis never trusted.parent,isA,read,write,append,genesis,createdByandrouteProvenancecan never be written through a route.502 route-write-refused, and nothing is written. An intent that doesn't plan is502 route-write-invalid.Quotas (per installation;
0turns one off)--plugin-route-creates-per-caller-hour/ATOMIC_PLUGIN_ROUTE_CREATES_PER_CALLER_HOUR--plugin-route-creates-per-day/ATOMIC_PLUGIN_ROUTE_CREATES_PER_DAY--plugin-route-bytes-per-day/ATOMIC_PLUGIN_ROUTE_BYTES_PER_DAY429 route-quota-exceededwithRetry-After(seconds until the window ends), and nothing is written.Provenance
Every created or changed resource is signed by the installation's agent and gets a new JSON property,
https://atomicdata.dev/properties/routeProvenance(added tolib/defaults/plugins.json):{ "installation": "<Installation>", "route": "inbox", "request": "http:<ulid>", "caller": null, "receivedAt": 1790276444383 }requestis the trigger id the handler saw.calleris the verified remote principal. It isnullforauth: none, the only auth this server verifies until Host crypto: installation keys, HTTP signatures, tokens and consent page #1718.Unchanged
read-only: every intent or enqueue is refused (502), and a release with write targets doesn't install.501 route-enqueue-unavailable) until the delivery queue (Durable delivery queue for route-enqueued operations #1719), even when the verdict also has intents.integrationWaits: refused (502).Fixture
testdata/plugin-routes/inbox/is a v3drive-prefixplugin with one write target,inbox-items(parentconfig:inbox, classPlainText). It is exposed astest_fixture::inbox_release(). Its routes:POST /inboxcreates an item;GET /itemslists the items as the public agent;PUT/DELETE /itemchange or delete an item.For the tests, the body may name another parent, class or subject.
Tests
route_writes::http_tests(real execution through the app,read-write):502), and nothing is written or changed;429+Retry-After, and exactly 2 items exist;403, nothing written;read-only, the writing release doesn't install.route_writes::tests: the ledger's windows (per caller and hour, per installation and day, bytes), a whole write refused, refunds,0meaning off, and the design defaults.release::tests::the_route_grant_must_cover_every_write_target: both grant forms, no grant, a narrower grant refused, malformed grants refused.route_exec(existing): a route that doesn't declare writes is still refused atread-onlyandread-write.tests/it/plugin_routes.rs: a new server atread-write. The installer creates the inbox, installs with the route grant and config, and lets the plugin's agent write to the inbox. An anonymous POST gets202. The item reads back throughGET /items(with provenance) and through the client from the store. A POST outside the target gets502.Runs:
cargo test -p atomic-server --lib: 347 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: passes (3 tests).cargo test -p atomic_lib --features db-redb --lib populate: passes (new default property).Interpretations
writeTargetsneeds a new review" enforceable at activation.installReleasestill sendsgrants: string[], so installs from the UI get no route grant, and their writes answer403.StoreApplyHost, which checks the agent's rights. So the installer has to give that agent write rights on the target parent, as the tests do. Nothing grants it automatically.caller: null.403for a missing grant;502for writes outside the targets, or intents that don't plan;500 route-write-failedif the store fails partway through (the only case where part of a write may be stored);501for enqueues.problemsdon't block the write. Onlyintentsare planned.problemsstay in the run log.🤖 Generated with Claude Code