Ship the SR-003 schema bump: jmanifest_version 2
Moves the exchange envelope to version 2 in lockstep with the truth file's schema_version, per SR-003's requirement that breaking changes be batched and ship together rather than piecemeal. The plugin had already moved to schema_version 2; the server declaring 1 while accepting the new fields defeated the point of having a version at all. Flag day, not dual-accept (JR-003): version 1 is now rejected outright. All three components are pre-release, and a v1 read path would be the one nobody exercises, so it is the one that would rot while being dragged through every later change to the reader. A pipeline still emitting v1 is incompatible until updated — stated plainly rather than papered over with a shim nobody tests. scenes become objects carrying belief and route (extraction AR-017) instead of float pairs. Belief is bounded to [0, 1] rather than merely stored: §5a's Threat 1 argument rests on every accepted value being bounded, and an unbounded float is a 64-bit channel however harmless it looks. route is a closed enum, so an invented value cannot be stored. UR-018 is the requirement with the trap in it, and the reason content_id.rs is untouched by this commit: belief is a producer-side estimate that may legitimately differ between pipeline versions for identical timings, so including it in the canonical form would give two servers different ids for the same content — the exact failure mode §9a quantises centiseconds to avoid, reintroduced one field along. It replicates as an attribute, exactly as audio_signature does. The golden vector still passes unchanged, which is the evidence rather than the claim. 191 tests. UR-015..018 move from Planned to Done; coverage 24/32 (75%). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: UR-014, UR-015, UR-016, UR-017, UR-018 | SR-003
This commit is contained in:
+20
-20
@@ -261,10 +261,10 @@ async fn sql_payloads_in_identifier_fields_are_rejected() {
|
||||
|
||||
for payload in SQL_PAYLOADS {
|
||||
let manifest = json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"identity": { "type": "movie", "tmdb_id": payload },
|
||||
"cut": { "runtime_sec": 100.0 },
|
||||
"actors": [ { "tmdb_id": "884", "scenes": [[1.0, 2.0]] } ]
|
||||
"actors": [ { "tmdb_id": "884", "scenes": [{"start":1.0,"end":2.0}] } ]
|
||||
});
|
||||
let (status, body) = s.post("/api/v1/manifests", Some(&token), &manifest).await;
|
||||
assert_eq!(
|
||||
@@ -288,16 +288,16 @@ async fn sql_payloads_in_free_text_fields_are_rejected() {
|
||||
for payload in SQL_PAYLOADS {
|
||||
for manifest in [
|
||||
json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"identity": { "type": "movie", "tmdb_id": "504172", "title": payload },
|
||||
"cut": { "runtime_sec": 100.0 },
|
||||
"actors": [ { "tmdb_id": "884", "scenes": [[1.0, 2.0]] } ]
|
||||
"actors": [ { "tmdb_id": "884", "scenes": [{"start":1.0,"end":2.0}] } ]
|
||||
}),
|
||||
json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"identity": { "type": "movie", "tmdb_id": "504172" },
|
||||
"cut": { "runtime_sec": 100.0 },
|
||||
"actors": [ { "name": payload, "tmdb_id": "884", "scenes": [[1.0, 2.0]] } ]
|
||||
"actors": [ { "name": payload, "tmdb_id": "884", "scenes": [{"start":1.0,"end":2.0}] } ]
|
||||
}),
|
||||
] {
|
||||
let (status, body) = s.post("/api/v1/manifests", Some(&token), &manifest).await;
|
||||
@@ -326,10 +326,10 @@ async fn sql_payloads_in_a_report_note_cannot_escape() {
|
||||
"/api/v1/manifests",
|
||||
Some(&token),
|
||||
&json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"identity": { "type": "movie", "tmdb_id": "504172" },
|
||||
"cut": { "runtime_sec": 100.0 },
|
||||
"actors": [ { "tmdb_id": "884", "scenes": [[1.0, 2.0]] } ]
|
||||
"actors": [ { "tmdb_id": "884", "scenes": [{"start":1.0,"end":2.0}] } ]
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
@@ -375,10 +375,10 @@ async fn sql_payloads_in_a_bearer_token_are_inert() {
|
||||
.header("authorization", format!("Bearer {payload}"))
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"identity": { "type": "movie", "tmdb_id": "504172" },
|
||||
"cut": { "runtime_sec": 100.0 },
|
||||
"actors": [ { "tmdb_id": "884", "scenes": [[1.0, 2.0]] } ]
|
||||
"actors": [ { "tmdb_id": "884", "scenes": [{"start":1.0,"end":2.0}] } ]
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
@@ -422,16 +422,16 @@ async fn json_structure_abuse_is_rejected_cleanly() {
|
||||
let cases: Vec<(&str, String)> = vec![
|
||||
("deep nesting", format!("{}{}", "[".repeat(20_000), "]".repeat(20_000))),
|
||||
("unterminated", "{\"identity\": {\"type\": \"movie\"".to_string()),
|
||||
("duplicate keys", r#"{"jmanifest_version":1,"jmanifest_version":2}"#.to_string()),
|
||||
("duplicate keys", r#"{"jmanifest_version":2,"jmanifest_version":2}"#.to_string()),
|
||||
("null bytes", "{\"jmanifest_version\":\u{0}1}".to_string()),
|
||||
("huge number", format!("{{\"jmanifest_version\":{}}}", "9".repeat(5000))),
|
||||
("nan literal", r#"{"jmanifest_version":1,"cut":{"runtime_sec":NaN}}"#.to_string()),
|
||||
("nan literal", r#"{"jmanifest_version":2,"cut":{"runtime_sec":NaN}}"#.to_string()),
|
||||
("bare array", "[1,2,3]".to_string()),
|
||||
("bare string", "\"just a string\"".to_string()),
|
||||
("empty body", String::new()),
|
||||
(
|
||||
"prototype-style key",
|
||||
r#"{"__proto__":{"admin":true},"jmanifest_version":1}"#.to_string(),
|
||||
r#"{"__proto__":{"admin":true},"jmanifest_version":2}"#.to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -463,7 +463,7 @@ async fn non_finite_scene_times_are_rejected() {
|
||||
// Sent as raw JSON text rather than via `json!`, because rustc refuses an
|
||||
// out-of-range float literal — and the point is to make the *server's* parser
|
||||
// handle it, which is the real attack path.
|
||||
let raw = r#"{"jmanifest_version":1,
|
||||
let raw = r#"{"jmanifest_version":2,
|
||||
"identity":{"type":"movie","tmdb_id":"504172"},
|
||||
"cut":{"runtime_sec":100.0},
|
||||
"actors":[{"tmdb_id":"884","scenes":[[1.0,1e400]]}]}"#;
|
||||
@@ -479,10 +479,10 @@ async fn non_finite_scene_times_are_rejected() {
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST, "{body}");
|
||||
|
||||
// Likewise an overflowing runtime.
|
||||
let raw = r#"{"jmanifest_version":1,
|
||||
let raw = r#"{"jmanifest_version":2,
|
||||
"identity":{"type":"movie","tmdb_id":"504172"},
|
||||
"cut":{"runtime_sec":1e400},
|
||||
"actors":[{"tmdb_id":"884","scenes":[[1.0,2.0]]}]}"#;
|
||||
"actors":[{"tmdb_id":"884","scenes":[{"start":1.0,"end":2.0}]}]}"#;
|
||||
let req = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/v1/manifests")
|
||||
@@ -583,10 +583,10 @@ async fn unicode_tricks_cannot_smuggle_text_past_the_character_class() {
|
||||
"/api/v1/manifests",
|
||||
Some(&token),
|
||||
&json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"identity": { "type": "movie", "tmdb_id": "504172" },
|
||||
"cut": { "runtime_sec": 100.0 },
|
||||
"actors": [ { "name": payload, "tmdb_id": "884", "scenes": [[1.0, 2.0]] } ]
|
||||
"actors": [ { "name": payload, "tmdb_id": "884", "scenes": [{"start":1.0,"end":2.0}] } ]
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
@@ -617,10 +617,10 @@ async fn the_audio_signature_field_cannot_carry_arbitrary_bytes() {
|
||||
"/api/v1/manifests",
|
||||
Some(&token),
|
||||
&json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"identity": { "type": "movie", "tmdb_id": "504172" },
|
||||
"cut": { "runtime_sec": 6420.5, "audio_signature": sig },
|
||||
"actors": [ { "tmdb_id": "884", "scenes": [[1.0, 2.0]] } ]
|
||||
"actors": [ { "tmdb_id": "884", "scenes": [{"start":1.0,"end":2.0}] } ]
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user