The tier table states the rule without qualification: "every receiver (and every federated peer) compares the DerivativeManifest.format value against this list, and an unknown value is a structural rejection". #436 implemented that check and wired it on the producer side only.
What exists after #436
capsule_core::derivative_format::verify_still_format is the check. It was moved out of capsule-core::media into an unconditional crate-root module precisely so a receiver can link it: capsule-server and capsule-wasm both build capsule-core with default-features = false, and the media feature is implied only by native. A closed set only its producer can evaluate is not a closed set.
It is called at capsule_core::lifecycle::upload::derivative_blobs — the client refusing to ship a derivative whose format is outside the set. Nothing calls it on receipt.
The exact function that must call it
capsule_server::upload::envelope::check_envelope — the function that already runs capsule_core::validation::check_manifest_envelope(&core, &ctx) (see capsule-server/src/upload/envelope.rs:326, :353, :420, and the shared envelope_context at :465). That is the single chokepoint every POST /upload body passes through, and it is where a structurally invalid envelope is already rejected.
The check to add, for a blob whose declared role is thumbnail or preview:
// `Err(format)` is an unrecognised still format: a structural rejection, not a future codec.
if let Err(format) = capsule_core::derivative_format::verify_still_format(&manifest) {
return Err(EnvelopeReject::/* the unrecognised-format arm */);
}
Two shape questions the implementer has to settle, neither of them decided here:
- The server holds an envelope, not a signed
DerivativeManifest. verify_still_format takes &DerivativeManifest. Either the derivative envelope needs to carry enough to reconstruct the (role, format) pair for a key-free check, or the check needs a second entry point taking (DerivativeRole, &str). The latter is probably right — the server is key-free and must not need a manifest it cannot verify.
- A new
EnvelopeReject arm and its wire error code, which is a capsule-i18n error.* catalog key, not just a Rust variant.
Why it was not done in #436
capsule-server/** is outside that lane's manifest, and the orchestrator's decision 21 explicitly excluded it. The move to an unconditional module was the half that had to land first — without it, the server could not call the check at all, so filing this before that move would have described impossible work.
Scope note
Federated peers are the second half of the same rule and are post-v1 (capsule-server::federation is still in planned-modules.txt); this issue is the upload path only.
Contract: Thumbnails and Previews, Threat Model — Schema Rules (the closed-enum rule). Slices S-B1, S-C1.
The tier table states the rule without qualification: "every receiver (and every federated peer) compares the
DerivativeManifest.formatvalue against this list, and an unknown value is a structural rejection".#436implemented that check and wired it on the producer side only.What exists after #436
capsule_core::derivative_format::verify_still_formatis the check. It was moved out ofcapsule-core::mediainto an unconditional crate-root module precisely so a receiver can link it:capsule-serverandcapsule-wasmboth buildcapsule-corewithdefault-features = false, and themediafeature is implied only bynative. A closed set only its producer can evaluate is not a closed set.It is called at
capsule_core::lifecycle::upload::derivative_blobs— the client refusing to ship a derivative whose format is outside the set. Nothing calls it on receipt.The exact function that must call it
capsule_server::upload::envelope::check_envelope— the function that already runscapsule_core::validation::check_manifest_envelope(&core, &ctx)(seecapsule-server/src/upload/envelope.rs:326,:353,:420, and the sharedenvelope_contextat:465). That is the single chokepoint everyPOST /uploadbody passes through, and it is where a structurally invalid envelope is already rejected.The check to add, for a blob whose declared role is
thumbnailorpreview:Two shape questions the implementer has to settle, neither of them decided here:
DerivativeManifest.verify_still_formattakes&DerivativeManifest. Either the derivative envelope needs to carry enough to reconstruct the(role, format)pair for a key-free check, or the check needs a second entry point taking(DerivativeRole, &str). The latter is probably right — the server is key-free and must not need a manifest it cannot verify.EnvelopeRejectarm and its wire error code, which is acapsule-i18nerror.*catalog key, not just a Rust variant.Why it was not done in #436
capsule-server/**is outside that lane's manifest, and the orchestrator's decision 21 explicitly excluded it. The move to an unconditional module was the half that had to land first — without it, the server could not call the check at all, so filing this before that move would have described impossible work.Scope note
Federated peers are the second half of the same rule and are post-v1 (
capsule-server::federationis still inplanned-modules.txt); this issue is the upload path only.Contract: Thumbnails and Previews, Threat Model — Schema Rules (the closed-enum rule). Slices
S-B1,S-C1.