--- id: ops-json-ops name: the JSON control plane — blobs/stat, blobs/have (+ the token-renewal form), blobs/delete status: completed depends_on: [ops-foundation, ops-call-harness] scope: moderate risk: medium impact: component level: implementation tags: [wave-7, ops, json, acl, tokens] --- ## Description The three JSON control ops (ops-surface.md §Op family), each exported as a spec/handler pair the embedder assembles — riding alkcall's `OperationSpec` + JSON Schema validation (alkcall ADR-016) and its ACL machinery at op entry (convention 6: this module never re-implements an authorization check): - **`blobs/stat`** `{namespace, digest}` → `{len}` — Query; the **read** action; the handler calls `Store::stat` (the fall-through general rule, ADR-012 §6.1); a miss is the typed missing-error schema (not a `{len: null}`). - **`blobs/have`** `{namespace, digests[], token?}` → `{present[]}` — Query; **read-gated** (an existence probe over arbitrary digests is a discovery surface — gated exactly like fetch, not public); `present[]` corresponds to `digests[]` by index; content never traverses this op (hashes only). **The renewal form** (ADR-012 §2): when `token` is present, exactly one digest accompanies it — the handler verifies the token maps to that digest's pin row (the foundation's token layer), calls the crate-internal `pin_renew(key, owner)` via the foundation's store-core access, and answers `present[]` as usual; an expired or foreign row answers `present: false` and no renewal occurs — the putter's backstop is re-put. Without `token`, the pure existence probe (ADR-008's `has` discipline) over the facade's `has`. **The non-fleet renewal form**: the token is the held guard's handle — the probe answers present from the custody (nothing expires; ADR-012 §2's generalization); the custody/conversion lifecycle is the foundation's. - **`blobs/delete`** `{namespace, digests[]}` — Mutation; **`Visibility::Internal`**, evaluated under the internal authority context per alkcall ADR-017 (internal calls switch authority context, never skip ACL): requires the embedder's operator/admin authority context, **not a namespace grant**; the `namespace` field exists for audit/triage scoping, not gating. The handler calls `Store::delete` (the arbitration-then-delete path; a `GcRefuse` refusal surfaces as the typed error schema). External discovery of an Internal op returns NOT_FOUND (alkcall ADR-017's visibility discipline). - **The ACL mapping** (ops-surface.md §ACL mapping; OQ-09's resolution): **namespace = resource** — `resource_type: "blob-namespace"`, `resource_id_path` selecting the payload's `namespace` field; stat/have = the **read** action; **unlisted namespaces deny by default** (closed-by-default: no ACL entry ⇒ no read, no write — a grant per namespace is the deferred cost, deliberately paid over accidental exposure). - **Schemas** (alkcall ADR-016): input/output JSON Schema per op + typed error schemas; the namespace field's syntax (non-empty UTF-8 string, embedder-validated — the minting of namespaces is the embedder's act); the digest wire form is **hex** (the canonical digest hex — the same form `blobs/put`'s response returns; parsed via the key layer's hex ingress, malformed → the typed invalid error, rejects-never-guesses). - **Tests through the harness** (`ops-call-harness`): stat round-trip + the miss shape; have's index correspondence (present/absent mixed); the renewal form's three answers (live row renews → present: true; expired row → present: false, no renewal; foreign token → present: false); delete under the internal authority context (an external caller gets NOT_FOUND; an internal operator-context call deletes); the closed-by-default ACL matrix (an unlisted namespace denies read; a granted identity reads). ## Acceptance Criteria - [x] Three spec/handler pairs exported (stat, have, delete); input/ output schemas + typed error schemas per alkcall ADR-016 - [x] The ACL mapping: namespace-as-resource with the resource_id_path; the read action on stat/have; closed-by-default verified by test (unlisted namespace denies) - [x] delete: `Visibility::Internal`, the internal-authority-context requirement (not namespace-gated), external discovery NOT_FOUND - [x] have's renewal form: one digest + token; verify-mapping → `pin_renew`; expired/foreign → `present: false` with no renewal; the non-fleet form answers from the held custody - [x] The handlers touch only the store facade + the foundation's crate-internal renewal access (no backend side-door — the ADR-001 §4 rule) - [x] Harness tests green; `cargo test` (default) green; `cargo test --features ops` green; clippy/fmt green on both shapes; `cargo doc --no-deps` clean ## References - docs/architecture/ops-surface.md (§Op family — the three JSON ops; §ACL mapping) - docs/architecture/decisions/001-substrate-posture-and-ops-placement.md §2/§4 - docs/architecture/decisions/012-pre-decomposition-consistency-rulings.md §2 (the have payload shape + the renewal form's three-step handler duty) - docs/architecture/decisions/008-trait-size-probe-fleet-gc-and-large-engines.md (the `has` discipline the non-token form preserves) - docs/architecture/open-questions.md OQ-09 (closed-by-default, the risk-asymmetry rationale) - alkcall (upstream checkout): ADR-016 (operation error schemas), ADR-017 (the privilege model — internal switches context, never skips ACL), `src/registry/spec.rs` (`AccessControl`, `resource_id_path`) - tasks/ops-foundation.md (the token layer + the renewal access the have handler rides), tasks/ops-call-harness.md (the harness) ## Notes > Landed in `src/ops.rs` (the ops module's "JSON control plane" > section, appended to the foundation's substrate) + `src/lib.rs` > re-exports (`stat_pair`/`have_pair`/`delete_pair` and the four > domain codes `BLOB_MISSING`/`INVALID_DIGEST`/`INVALID_RENEWAL_FORM`/ > `GC_REFUSED`). Each export is `pub fn X_pair(ctx: Arc) -> > (OperationSpec, Handler)` — the embedder assembles; the harness's > assembly (extended: `assemble_registrations` now carries the > registration-side composition authority and scoped composition env, > ADR-017/019 — the pair-level `assemble` shape is unchanged). > > **Schemas (alkcall ADR-016):** input/output JSON Schema per op — the > namespace field's syntax enforced at the schema (`minLength: 1`, the > non-empty-UTF-8 rule); the digest wire form hex with the canonical > pattern (`^[0-9a-f]{40}$|^[0-9a-f]{64}$`, the key layer's > lowercase-only ingress also enforced at the handlers — > rejects-never-guesses). The have schema carries the renewal form's > arity as `if`/`then` (token ⇒ digests minItems/maxItems 1) — verified > live at the wire (`INVALID_INPUT` on the two-digest+token shape) — > and re-enforced at the handler (`INVALID_RENEWAL_FORM`, the > typed error for non-registry dispatch paths, exercised by direct > handler invocation). Typed error schemas: `BLOB_MISSING` > (stat's miss — details `{digest}`; a `{len: null}` was never an > option), `INVALID_DIGEST` (the ingress), `INVALID_RENEWAL_FORM` > (have's arity), `GC_REFUSED` (delete's `Error::GcRefuse`). > > **The ACL mapping (OQ-09's resolution, verified by test):** > namespace-as-resource — `resource_type: "blob-namespace"`, > `resource_id_path: "$.namespace"`, the read action on stat/have; > closed-by-default proven on the wire (an unlisted namespace denies > the granted identity's probe; a granted namespace reads; the > anonymous call answers the authentication required gate). delete: > `Visibility::Internal`, `AccessControl` left un-gated (not > namespace-gated — the field is audit/triage scoping), the > operator-admin authority context realized as the **composition > authority the embedder declares at registration** (the pair exports > it authority-agnostic); external discovery is the registry's > NOT_FOUND (tested); the internal path proven through a composant op > (`blobs/compose-delete` test stub, scoped env → delete) whose > grantless caller composes — the check ran against the declared > operator authority, the namespace grant not the gate. GcRefuse and > Missing surface as the typed errors (fail-fast; no atomic multi- > delete was specced — the preceding digests' deletions stand, and the > doc says so). > > **The renewal form** (ADR-012 §2's three-step duty, riding the > foundation's `OpsContext::renew` — no new store access, no backend > side-door): the non-fleet arms answer from custody (renew → > present: true; unknown/foreign/mismatched → present: false, never an > error; the malformed-token ingress maps to the unknown-token answer); > the fleet arms ran against the harness server (env-driven > `ALKBLOBS_PG_HOST/...`, skipping cleanly server-less — the fleet > column's SKIP posture adapted in-crate): the live row renews with > the row's expiry observed moving (the independent `gc_pins` > instrument), the expired row answers `present: false` with the > expiry frozen at the last renewal (no renewal occurred — the > never-resurrect discipline), and the peer mint's foreign token > answers `present: false` the same way (the validity domain = the > minting node). The foundation's noted `mint_custodial`-refused-under- > fleet arm is asserted in the same test. The put-side arms (mint the > token from a wire-carried put) ride the put's handler (ops-put). ## Summary > Completed 2026-10-13 (wave 7). The JSON control plane's three > spec/handler pairs landed in `src/ops.rs` under feature `ops`: > `blobs/stat` (Query/read — `Store::stat`, the miss as the typed > `BLOB_MISSING`), `blobs/have` (Query/read — the pure existence probe > over `Store::has` with index-corresponded `present[]`, engine > failures folding to present: false, and the token-renewal form > riding `OpsContext::renew` with the one-digest arity enforced at the > schema (`if`/`then`) and the handler), and `blobs/delete` > (Mutation/`Visibility::Internal` — external discovery NOT_FOUND; the > operator authority context via the registration-time composition > authority; not namespace-gated; `Store::delete` with `GC_REFUSED`/ > `BLOB_MISSING` as the typed errors, fail-fast). ACL: namespace-as- > resource, the read action, closed-by-default (verified by test); > digests wire-form hex parsed via the key layer's ingress > (rejects-never-guesses); domain error codes `BLOB_MISSING`/ > `INVALID_DIGEST`/`INVALID_RENEWAL_FORM`/`GC_REFUSED` declared per op > with their details schemas. Five harness tests added (stat > round-trip/miss/ingress, have's index correspondence + closed-by- > default matrix, the renewal form's arms incl. the arity gate at both > the wire and the handler, delete's NOT_FOUND + internal composition > + refusal arms) plus one fleet harness-server test (the three > renewal answers with the row-expiry instrument; the foreign token; > the custodial-mint refusal under fleet) — env-driven, skipping > cleanly server-less (SKIP posture). Gates green: `cargo test` > (default 73/19/3), `cargo test --features ops` (92/19/3), > `cargo test --all-features` against the harness server (246/125/4 — > one pre-existing fleet-timing row flaked once under full-suite load > and passed on the isolated + full reruns, untouched by this task's > diff: the fleet column reads no ops code), clippy > `--all-targets -- -D warnings` on the default/ops/all-features > shapes plus the pg-pure checks, `cargo fmt --check`, > `cargo doc --no-deps` clean on the default and ops builds.