From ae28dbedfdb485929272fb17e7a932fa31bdfcfc Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 28 Oct 2026 05:08:56 +0100 Subject: [PATCH] Golden v2 wire fixtures replace the CodexConnector dev-dependency; pin envelope validation and frame bounds wire_parity_v2 linked CodexConnector (PolyForm Noncommercial) into the kit's test build. The v2 bytes it compared against are now committed, generated once from fd7cec4 (generator and command beside them): invocations for both tool-choice values, a completed, a failed and a refused result for every refusal, canonical bytes, the provider digest and v2-sealed envelopes the kit must decrypt. envelope.rs tests pin the outer-identity match on each field, envelope validation, and the inclusive, never-empty frame bounds. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 23 -- crates/model-connector/Cargo.toml | 3 - crates/model-connector/src/envelope.rs | 116 +++++++ .../tests/fixtures/wire_v2/.gitattributes | 2 + .../tests/fixtures/wire_v2/README.md | 20 ++ .../fixtures/wire_v2/canonical_invocation.bin | Bin 0 -> 413 bytes .../wire_v2/canonical_provider_request.bin | Bin 0 -> 267 bytes .../fixtures/wire_v2/envelope_invocation.mp | 6 + .../wire_v2/envelope_result_failed.mp | 7 + ...envelope_result_refused_replay_conflict.mp | Bin 0 -> 252 bytes .../wire_v2/envelope_result_transported.mp | Bin 0 -> 609 bytes .../tests/fixtures/wire_v2/invocation_auto.mp | Bin 0 -> 408 bytes .../fixtures/wire_v2/invocation_required.mp | Bin 0 -> 413 bytes .../wire_v2/provider_request_sha256.bin | Bin 0 -> 32 bytes .../tests/fixtures/wire_v2/result_failed.mp | Bin 0 -> 307 bytes .../wire_v2/result_refused_capacity.mp | 1 + .../wire_v2/result_refused_expired.mp | 1 + .../result_refused_identity_substitution.mp | 1 + .../wire_v2/result_refused_in_flight.mp | 1 + .../wire_v2/result_refused_indeterminate.mp | 1 + .../wire_v2/result_refused_malformed.mp | 1 + .../fixtures/wire_v2/result_refused_policy.mp | 1 + ...lt_refused_provider_digest_substitution.mp | 1 + .../wire_v2/result_refused_replay_conflict.mp | 1 + .../fixtures/wire_v2/result_transported.mp | Bin 0 -> 327 bytes .../model-connector/tests/wire_golden_v2.rs | 301 ++++++++++++++++++ .../model-connector/tests/wire_parity_v2.rs | 242 -------------- 27 files changed, 461 insertions(+), 268 deletions(-) create mode 100644 crates/model-connector/tests/fixtures/wire_v2/.gitattributes create mode 100644 crates/model-connector/tests/fixtures/wire_v2/README.md create mode 100644 crates/model-connector/tests/fixtures/wire_v2/canonical_invocation.bin create mode 100644 crates/model-connector/tests/fixtures/wire_v2/canonical_provider_request.bin create mode 100644 crates/model-connector/tests/fixtures/wire_v2/envelope_invocation.mp create mode 100644 crates/model-connector/tests/fixtures/wire_v2/envelope_result_failed.mp create mode 100644 crates/model-connector/tests/fixtures/wire_v2/envelope_result_refused_replay_conflict.mp create mode 100644 crates/model-connector/tests/fixtures/wire_v2/envelope_result_transported.mp create mode 100644 crates/model-connector/tests/fixtures/wire_v2/invocation_auto.mp create mode 100644 crates/model-connector/tests/fixtures/wire_v2/invocation_required.mp create mode 100644 crates/model-connector/tests/fixtures/wire_v2/provider_request_sha256.bin create mode 100644 crates/model-connector/tests/fixtures/wire_v2/result_failed.mp create mode 100644 crates/model-connector/tests/fixtures/wire_v2/result_refused_capacity.mp create mode 100644 crates/model-connector/tests/fixtures/wire_v2/result_refused_expired.mp create mode 100644 crates/model-connector/tests/fixtures/wire_v2/result_refused_identity_substitution.mp create mode 100644 crates/model-connector/tests/fixtures/wire_v2/result_refused_in_flight.mp create mode 100644 crates/model-connector/tests/fixtures/wire_v2/result_refused_indeterminate.mp create mode 100644 crates/model-connector/tests/fixtures/wire_v2/result_refused_malformed.mp create mode 100644 crates/model-connector/tests/fixtures/wire_v2/result_refused_policy.mp create mode 100644 crates/model-connector/tests/fixtures/wire_v2/result_refused_provider_digest_substitution.mp create mode 100644 crates/model-connector/tests/fixtures/wire_v2/result_refused_replay_conflict.mp create mode 100644 crates/model-connector/tests/fixtures/wire_v2/result_transported.mp create mode 100644 crates/model-connector/tests/wire_golden_v2.rs delete mode 100644 crates/model-connector/tests/wire_parity_v2.rs diff --git a/Cargo.lock b/Cargo.lock index a3dfca5..01f3693 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,12 +58,6 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "base64" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" - [[package]] name = "block-buffer" version = "0.10.4" @@ -119,22 +113,6 @@ dependencies = [ "inout", ] -[[package]] -name = "codex-connector" -version = "0.1.0" -source = "git+https://github.com/GameCult/CodexConnector.git?rev=fd7cec4176f4e3f9a5aaa75e77ba10ce5e1522f8#fd7cec4176f4e3f9a5aaa75e77ba10ce5e1522f8" -dependencies = [ - "aes-gcm", - "base64", - "rand_core", - "rmp-serde", - "serde", - "serde_json", - "sha2", - "thiserror", - "zeroize", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -363,7 +341,6 @@ name = "model-connector" version = "0.1.0" dependencies = [ "aes-gcm", - "codex-connector", "cultcache-rs", "rand_core", "rmp-serde", diff --git a/crates/model-connector/Cargo.toml b/crates/model-connector/Cargo.toml index cb30f96..49f83db 100644 --- a/crates/model-connector/Cargo.toml +++ b/crates/model-connector/Cargo.toml @@ -21,6 +21,3 @@ serde_json = "1" sha2 = "0.10" thiserror = "2" zeroize = "1" - -[dev-dependencies] -codex-connector = { git = "https://github.com/GameCult/CodexConnector.git", rev = "fd7cec4176f4e3f9a5aaa75e77ba10ce5e1522f8", default-features = false } diff --git a/crates/model-connector/src/envelope.rs b/crates/model-connector/src/envelope.rs index 091452f..abebbb5 100644 --- a/crates/model-connector/src/envelope.rs +++ b/crates/model-connector/src/envelope.rs @@ -279,4 +279,120 @@ mod tests { Err(ServiceError::Encryption) ); } + + fn sealed_invocation(caller: &str, request_id: &str, kind: EnvelopeKind) -> TransportEnvelope { + let invocation = invocation(); + let plaintext = canonical_invocation_bytes(&invocation).unwrap(); + encrypt_message( + caller, + request_id, + kind, + &plaintext, + &security("epiphany-distinct-test-key"), + ) + .unwrap() + } + + #[test] + fn the_outer_identity_must_match_the_sealed_invocation_on_each_field() { + let key = security("epiphany-distinct-test-key"); + let invocation = invocation(); + let caller = invocation.caller_runtime_id.clone(); + let request_id = invocation.request_id().to_string(); + let matching = sealed_invocation(&caller, &request_id, EnvelopeKind::Invocation); + assert_eq!(decrypt_invocation(&matching, &key).unwrap(), invocation); + for envelope in [ + sealed_invocation("other-caller", &request_id, EnvelopeKind::Invocation), + sealed_invocation(&caller, "other-request", EnvelopeKind::Invocation), + ] { + assert_eq!( + decrypt_invocation(&envelope, &key), + Err(ServiceError::OuterIdentity) + ); + } + } + + #[test] + fn the_outer_identity_must_match_the_sealed_result_on_each_field() { + let key = security("epiphany-distinct-test-key"); + let invocation = invocation(); + let result = TransportResult::transported(&invocation, Vec::new(), receipt(&invocation)); + let plaintext = rmp_serde::to_vec(&result).unwrap(); + let seal = |caller: &str, request_id: &str| { + encrypt_message(caller, request_id, EnvelopeKind::Result, &plaintext, &key).unwrap() + }; + let caller = result.caller_runtime_id.clone(); + let request_id = result.request_id.clone(); + assert_eq!( + decrypt_result(&seal(&caller, &request_id), &key, &invocation).unwrap(), + result + ); + for envelope in [ + seal("other-caller", &request_id), + seal(&caller, "other-request"), + ] { + assert_eq!( + decrypt_result(&envelope, &key, &invocation), + Err(ServiceError::OuterIdentity) + ); + } + } + + #[test] + fn a_malformed_envelope_is_refused_before_any_decryption() { + let key = security("epiphany-distinct-test-key"); + let invocation = invocation(); + let good = encrypt_invocation(&invocation, &key).unwrap(); + + let mut schema = good.clone(); + schema.schema_id = "gamecult.codex.transport_envelope.v1".to_string(); + let wrong_kind = sealed_invocation("epiphany-yggdrasil", "request-1", EnvelopeKind::Result); + let mut empty = good.clone(); + empty.ciphertext.clear(); + for envelope in [schema, wrong_kind, empty] { + assert_eq!( + decrypt_invocation(&envelope, &key), + Err(ServiceError::Envelope) + ); + } + for field in ["caller_runtime_id", "request_id"] { + let mut blank = good.clone(); + if field == "caller_runtime_id" { + blank.caller_runtime_id = " ".to_string(); + } else { + blank.request_id = String::new(); + } + assert_eq!( + decrypt_invocation(&blank, &key), + Err(ServiceError::Contract(ContractError::Invalid(field))) + ); + } + } + + #[test] + fn a_frame_is_bounded_inclusively_and_never_empty() { + use std::io::Cursor; + let read = |length: u32, body: usize, max: usize| { + let mut frame = length.to_be_bytes().to_vec(); + frame.extend(vec![1; body]); + read_transport_frame(&mut Cursor::new(frame), max) + }; + assert_eq!(read(16, 16, 16).unwrap(), vec![1; 16]); + assert!(matches!(read(17, 17, 16), Err(TransportFrameError::Size))); + assert!(matches!(read(0, 0, 16), Err(TransportFrameError::Size))); + // A bound above the length never turns an empty frame into an empty payload. + assert!(matches!(read(0, 0, 1_000), Err(TransportFrameError::Size))); + + let write = |payload: &[u8], max: usize| { + let mut out = Vec::new(); + write_transport_frame(&mut out, payload, max).map(|()| out) + }; + assert_eq!(write(&[1; 16], 16).unwrap().len(), 20); + assert!(matches!( + write(&[1; 17], 16), + Err(TransportFrameError::Size) + )); + assert!(matches!(write(&[], 16), Err(TransportFrameError::Size))); + assert!(matches!(write(&[], 1_000), Err(TransportFrameError::Size))); + } } diff --git a/crates/model-connector/tests/fixtures/wire_v2/.gitattributes b/crates/model-connector/tests/fixtures/wire_v2/.gitattributes new file mode 100644 index 0000000..bf62110 --- /dev/null +++ b/crates/model-connector/tests/fixtures/wire_v2/.gitattributes @@ -0,0 +1,2 @@ +*.mp binary +*.bin binary diff --git a/crates/model-connector/tests/fixtures/wire_v2/README.md b/crates/model-connector/tests/fixtures/wire_v2/README.md new file mode 100644 index 0000000..b9fa9e5 --- /dev/null +++ b/crates/model-connector/tests/fixtures/wire_v2/README.md @@ -0,0 +1,20 @@ +# Golden v2 wire fixtures + +Bytes of the v2 connector wire, produced once by CodexConnector at commit +`fd7cec4176f4e3f9a5aaa75e77ba10ce5e1522f8` (the library Ghostlight pins), +and pinned by `tests/wire_golden_v2.rs` so that the kit cannot drift from +them. No CodexConnector code or dependency is part of this repository. + +Generator: `generator.rs` (not compiled here). Command, run in a clone of +CodexConnector at that commit: + + cp /crates/model-connector/tests/fixtures/wire_v2/generator.rs examples/gen_wire_v2.rs + cargo run --locked -q --example gen_wire_v2 + +It prints `FIXTURE ` lines; each file is the hex decoded. It was +run on Yggdrasil through the verify container with codex-connector at that +commit as a dev-dependency of the kit at d1d28f0 (same source). The `.mp` +files are rmp-serde output; `envelope_*` files carry random nonces and +AES-256-GCM ciphertext sealed under the connection key +`parity-distinct-test-key`. Regenerating changes the envelopes' bytes; the +plaintext fixtures must regenerate byte-identically. diff --git a/crates/model-connector/tests/fixtures/wire_v2/canonical_invocation.bin b/crates/model-connector/tests/fixtures/wire_v2/canonical_invocation.bin new file mode 100644 index 0000000000000000000000000000000000000000..727edf6cf9555f9c02825cc276855e2a1ffeb884 GIT binary patch literal 413 zcmah_u}T9$6bv@0Z7i&gE7HiFoW#aX1wSCfI&AhH$?E0y+uq!1gY8Xhx`nYo%}N~{9B>Y>f03pE=R^K~E<7K9&4&QM6- z4tItDQv&N2XPyUYlAg{}M*1Hl5}MK*&?h}-={-fGqYdgvy57tXQA`86*rs#(P?aL} zw&<0<=pjE(t#i841Mt2KA z4s^2CP!4L|aDs1QihXX}II>8El<4Id;K zSMYR|mBcT^`T80O8mUM;wcRkl575y3C6FYvA6FrmXbak>9;KbM-M~3}BduM$s;E4D z_mFxNT;&y((^>nr1NsQ@vd06mbobA>$?{E{`Qo_n-H+wO6-cBW8zSgrze#*e8BI#C bOXhCxpoq=7-8aTV|7^A=y_J$G2T%V2OD}Km literal 0 HcmV?d00001 diff --git a/crates/model-connector/tests/fixtures/wire_v2/envelope_invocation.mp b/crates/model-connector/tests/fixtures/wire_v2/envelope_invocation.mp new file mode 100644 index 0000000..3f95738 --- /dev/null +++ b/crates/model-connector/tests/fixtures/wire_v2/envelope_invocation.mp @@ -0,0 +1,6 @@ +–Ù$gamecult.codex.transport_envelope.v2­parity-caller©request-1ªInvocationœÌ£VÌÁḩ̃̌xÌ©Y̽Ü­hÌ·ÌýÌâÌ—Ì…3ÌÕ ÌÇÌþÌ‘ÌÜḩ̨Ìé[̪ÌÙ!AÌë̉ÌðÌúÌÛ;ÌëÌú| ̪ÌÉ`!ÌúÌú̧̚Ì� +Ì�kÌÞ'vCZÌà̖̌̈ 8J̧ÌÀḍ̗NÌ¿FÌ¿zm6Ìÿ +Ì÷ÌíÌð\\NMqÌ–Ì×Ì— Ìù1ÌæÌ¦Ì¯Ìè 4*{ÌË̥̱ +ÌÖ̦eÌÇ̨Ì� +̰̆ḭG*SÌ�̳̼ÌÒ̥̮ÌÜ!NÌÀÌÑ̠̫̙̂7Ì�ÌÇ̰Ìì̃ÌÀ̬c̦ÌË ÌÒ̲1kÌá~hÌß3$ÌÚÌ KÌß̰ÌùÌÜ4{ÌÓÌß̼̩U.)ÌÌ̭̤rÌ ÌýÌìÌÆ^bbÏy^̳ÌßÌÌÌ�ÌÉ:Ì Ìš-ÌåYXoÌÓÌÒ[Qy̘̫ +XÌÓI:3̽ÌÔ̰D&V'-ÌòufÌŒX1ÌøÌÒ.Ì�|4cÌãÌ‹EÌåAÌßÌæÌŸÌÍQÌÝLÌ‚-Ìáw̰uÌ´3̹>Ìï$QzÌÀk m'+ÌÐÌ¢G Ì´sÌ®00 f)Ì·C̯̙*̆Ì’ÌÛy~ÌùHzÌøÌåÌý"ÌÇÌ¢*%ÌôoÌÉ̯ÌÙxÌŽ}Ì‹ÌÏÌÝYÌÈCÌîÌ´Ì¿uÌü̸x/gÌëÌñ̘X-ÌÙÌÂ̮̆NÌìRI):HÌñÌÉÌ’ÌÀ̗̉fÌÛÌ '\s̨ Ì�s̉ÌÄGVaÌÓ̶̲C\\`=p ÌžwÌŽHÌÖ4ÌÆÌØ>lfLÌ£ÌþÌäÌÖvÌ¿ÌÞJÌë \ No newline at end of file diff --git a/crates/model-connector/tests/fixtures/wire_v2/envelope_result_failed.mp b/crates/model-connector/tests/fixtures/wire_v2/envelope_result_failed.mp new file mode 100644 index 0000000..4b94559 --- /dev/null +++ b/crates/model-connector/tests/fixtures/wire_v2/envelope_result_failed.mp @@ -0,0 +1,7 @@ +–Ù$gamecult.codex.transport_envelope.v2­parity-caller©request-1¦ResultœÌ…Ì¢ÌÇ"̇Ì�GÌ©ÜCÌôÌË Ì�ÌéHÌßÌ—ÌÿÌÐÌ´ÌÝÌá\Ìö]2/ÌÎe"̘ +Ì¿ÌáÌìÌü{GÌÚṴ̧^b3Ìûj̢̗̟ÌÃÌ’sÌ•*Ì» ÌøÌä= ̸̄<ÌÁ{ÌýÌã.ÌØ8~̫̓CÌâ +̱̘ÌË +[Ì‚ÌÉoÌ–(47ÌÌBML̲Ìó̢̳Ì�hp,ÌîÌ®wÌê6DHÌðbÌÏÌÌb̹@a̧ÌÑ̵̎Ì´Ì�kÌäÌ’ÌáÌóÌ€ÌÑN+ÌüMÌÀjG %Ìß/Ìô +ÌÁÌò̸%Ì»Ì�@sT`ÌâÌ’}P̈ÌÒ ÌëÌÊÌÞ Ì«Ìâ:ÌÚ̟̖̔Ìû^Ìô>ÌûX.x +r]F8Ì�ÌÛÌ‹Ì÷Ì�5ÌÚGÌÆÌºÌÃÌ®C@(ÌóÌÓ̾{̾ÌÙMÌÓ~)Ìà 7ÌÐÌòI+'v ÌøUnP̈$U(NÌêÌçÌüsÌ¢T^[ÌÊYÌæÌ�zÌâ.wÌÆeÌ™C̦̿̄"U^trÌöÌ�ÌÙ̦Ìï̯̬ÌçÌÜÌž+ÌÑ/b̯̭̼Ìî*$Ì× ÌŠÌ²̘ÌÕ\K5̧f +Ìò̃b}̦̬o.Ẁ̧&Ìå̶̑[ \ No newline at end of file diff --git a/crates/model-connector/tests/fixtures/wire_v2/envelope_result_refused_replay_conflict.mp b/crates/model-connector/tests/fixtures/wire_v2/envelope_result_refused_replay_conflict.mp new file mode 100644 index 0000000000000000000000000000000000000000..10e3726559fa374cd41b9b35c34f97bac50dd09f GIT binary patch literal 252 zcmVow+yG@3%-hVD z%)-o`e9W^F%(2YrHq4O`%&g4Y%-PK1%>53`kY&ujNzCoc-OSp|w9K_c%%Ty@ifzpJ zJj}?<;6FXg+01~<_{@#Wrp)EciF;?v?9B4a_H4|q%)!jT%Ql; zi5fzshECDCNW}`R(A1wvVW6QFW|7*^Bqk9?Zye6md7krpxMw0vhEmB`Zg$2Pt!Jdl z7^mG}aoDVOXQ5=Nkjz$_6jKq~Wi!}|omEPs!EBc7?RKfGTyi*-syn4ED39bBK5pXM&8R@Qxs6ZBQ#IeUjNv`M)xrji^lP%uo>cK@ zpG#LH@t3dkFp#9qSJTFNW*NN&eq43gXiHVn%D9*x+}8vNls7if$wYD>wQ{Btz)US` z$LVD*I5aOxXCia)mWb&tvm&pzzeF0o!EU`K=h0mjI1=2l>fRR&Ep?86Mn4F7akFlQ~vTr?D8%#2&3uBfQo!!-ARxZ31hwSed*K zQOXzQbPNfxk(|OqxYNUU!^$>>h504HWSxP0R`Dpb^z%o-6{c7Y=Q&S!C$@_b>O`zM z8Vg0CgS=uY!GcdOy6oX{%_%nYagKC0&har!oHxY`pWq>%=se6yG4;n}ypE$aN`xM- m;cgo3J5|v_`|+*}=K1Yk!F9bL`tp+sO65GD`6!Dt9Qg;?kz>{X literal 0 HcmV?d00001 diff --git a/crates/model-connector/tests/fixtures/wire_v2/invocation_auto.mp b/crates/model-connector/tests/fixtures/wire_v2/invocation_auto.mp new file mode 100644 index 0000000000000000000000000000000000000000..e505ca2bba33dc48ec8cfe6a607ba0f19a5c34eb GIT binary patch literal 408 zcmah_Jxjw-6b&w2Ttsj+WY9qph$w=a3XTq?4o>y?a%rP!UVQH+w3H6{1431t9HdnH zAr&0OrRv~v3msevot<3t7ns@(;^LVOmvb)XoQuIijun819$H*DP_$4oUj;&9*7YO7 z8M?x^qUBD&q>Ck!GtUFrOZA7Ep85xgga-SZzLuy>C)D4jXCiu{*F}1#H-j$M_ULm? zqpMk)hV(H_kMlc2ZDwHni4;f?xemyzgyRqtCUII5YT9>P_z_6;6V$XDxd6>oYo)ab zIusd4>pX7mDv%i{V)a@fLx?*YO1TO3aeS2!c%`_R5yIO_`kmd3TAKE)1mx`?7(h0E~4*92^2i Tqf%!fikEJ0N^5Y&^I+QF(pIMz literal 0 HcmV?d00001 diff --git a/crates/model-connector/tests/fixtures/wire_v2/invocation_required.mp b/crates/model-connector/tests/fixtures/wire_v2/invocation_required.mp new file mode 100644 index 0000000000000000000000000000000000000000..727edf6cf9555f9c02825cc276855e2a1ffeb884 GIT binary patch literal 413 zcmah_u}T9$6bv@0Z7i&gE7HiFoW#aX1wSCfI&AhH$?E0y+uq!1gY8Xhx`nYo%}N~{9B>Y>f03pE=R^K~E<7K9&4&QM6- z4tItDQv&N2XPyUYlAg{}M*1Hl5}MK*&?h}-={-fGqYdgvy57tXQA`86*rs#(P?aL} zw&<0<=pjE(t#i841Mt2KA z4s^2CwXig`xJ1`*Z9!sD zW=W-Pa$-(SYEk3r5SZfBlt~k2+*HP6R&r`)0Y1a-FetDS06-;YE}XfWdgkYueI{of zFq~QECU#~@{+U@)YG=-zS#f6NnRfj%&1W7JElA5BBSyHT!R9TX$ Kkd$AVmjVEMQG6Ky literal 0 HcmV?d00001 diff --git a/crates/model-connector/tests/fixtures/wire_v2/result_refused_capacity.mp b/crates/model-connector/tests/fixtures/wire_v2/result_refused_capacity.mp new file mode 100644 index 0000000..4425e6a --- /dev/null +++ b/crates/model-connector/tests/fixtures/wire_v2/result_refused_capacity.mp @@ -0,0 +1 @@ +”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller�§Refused¨Capacity \ No newline at end of file diff --git a/crates/model-connector/tests/fixtures/wire_v2/result_refused_expired.mp b/crates/model-connector/tests/fixtures/wire_v2/result_refused_expired.mp new file mode 100644 index 0000000..47fff91 --- /dev/null +++ b/crates/model-connector/tests/fixtures/wire_v2/result_refused_expired.mp @@ -0,0 +1 @@ +”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller�§Refused§Expired \ No newline at end of file diff --git a/crates/model-connector/tests/fixtures/wire_v2/result_refused_identity_substitution.mp b/crates/model-connector/tests/fixtures/wire_v2/result_refused_identity_substitution.mp new file mode 100644 index 0000000..8da919c --- /dev/null +++ b/crates/model-connector/tests/fixtures/wire_v2/result_refused_identity_substitution.mp @@ -0,0 +1 @@ +”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller�§Refused´IdentitySubstitution \ No newline at end of file diff --git a/crates/model-connector/tests/fixtures/wire_v2/result_refused_in_flight.mp b/crates/model-connector/tests/fixtures/wire_v2/result_refused_in_flight.mp new file mode 100644 index 0000000..0c9f9d3 --- /dev/null +++ b/crates/model-connector/tests/fixtures/wire_v2/result_refused_in_flight.mp @@ -0,0 +1 @@ +”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller�§Refused¨InFlight \ No newline at end of file diff --git a/crates/model-connector/tests/fixtures/wire_v2/result_refused_indeterminate.mp b/crates/model-connector/tests/fixtures/wire_v2/result_refused_indeterminate.mp new file mode 100644 index 0000000..068bed3 --- /dev/null +++ b/crates/model-connector/tests/fixtures/wire_v2/result_refused_indeterminate.mp @@ -0,0 +1 @@ +”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller�§Refused­Indeterminate \ No newline at end of file diff --git a/crates/model-connector/tests/fixtures/wire_v2/result_refused_malformed.mp b/crates/model-connector/tests/fixtures/wire_v2/result_refused_malformed.mp new file mode 100644 index 0000000..f106b9f --- /dev/null +++ b/crates/model-connector/tests/fixtures/wire_v2/result_refused_malformed.mp @@ -0,0 +1 @@ +”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller�§Refused©Malformed \ No newline at end of file diff --git a/crates/model-connector/tests/fixtures/wire_v2/result_refused_policy.mp b/crates/model-connector/tests/fixtures/wire_v2/result_refused_policy.mp new file mode 100644 index 0000000..28712db --- /dev/null +++ b/crates/model-connector/tests/fixtures/wire_v2/result_refused_policy.mp @@ -0,0 +1 @@ +”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller�§Refused¦Policy \ No newline at end of file diff --git a/crates/model-connector/tests/fixtures/wire_v2/result_refused_provider_digest_substitution.mp b/crates/model-connector/tests/fixtures/wire_v2/result_refused_provider_digest_substitution.mp new file mode 100644 index 0000000..76b2169 --- /dev/null +++ b/crates/model-connector/tests/fixtures/wire_v2/result_refused_provider_digest_substitution.mp @@ -0,0 +1 @@ +”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller�§RefusedºProviderDigestSubstitution \ No newline at end of file diff --git a/crates/model-connector/tests/fixtures/wire_v2/result_refused_replay_conflict.mp b/crates/model-connector/tests/fixtures/wire_v2/result_refused_replay_conflict.mp new file mode 100644 index 0000000..9f621de --- /dev/null +++ b/crates/model-connector/tests/fixtures/wire_v2/result_refused_replay_conflict.mp @@ -0,0 +1 @@ +”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller�§Refused®ReplayConflict \ No newline at end of file diff --git a/crates/model-connector/tests/fixtures/wire_v2/result_transported.mp b/crates/model-connector/tests/fixtures/wire_v2/result_transported.mp new file mode 100644 index 0000000000000000000000000000000000000000..f4fedf862d0f0cd231090f0848610542f5a65cfa GIT binary patch literal 327 zcmbPoQz<<$H#NC5r$jF~KP9z7ucRn3uecz;s3g89wHPE^X0);>wXig`xJ1`*Z9!sD zW=W-Pa$-(SYEk3r5SZfBlu45&F*L3WNv$YxNzExqoVYY2H76&35@X|vko^1{XP}13 z%Rt(6jaC<>CZ@y}=a&{Gr!K0lopDncj}wwpGYjy!;SPfWI{^Sxa^}LByQyb>p4n$| z<^jW*b#7v3rsSWQC8c)e%$XHuR-S3sKhu2XQBiKvnPu8%PM!I4W=+WQ^nwyyQ$3Sa kP#+f;r#7y1&d)8#Ni9iDnYs!XI0gB6#i_c6T+ECt0JtTD5dZ)H literal 0 HcmV?d00001 diff --git a/crates/model-connector/tests/wire_golden_v2.rs b/crates/model-connector/tests/wire_golden_v2.rs new file mode 100644 index 0000000..198b731 --- /dev/null +++ b/crates/model-connector/tests/wire_golden_v2.rs @@ -0,0 +1,301 @@ +//! The kit speaks the v2 wire of CodexConnector fd7cec4, the library Ghostlight pins. +//! The golden bytes in `fixtures/wire_v2` were produced by that library (see its README); +//! nothing of it is linked here. Deleted with the fixtures by cut connector-contract-v3, +//! which changes the wire. + +use model_connector as kit; +use serde::Serialize; +use serde::de::DeserializeOwned; + +const KEY: &str = "parity-distinct-test-key"; +const CALLER: &str = "parity-caller"; + +macro_rules! golden { + ($name:literal) => { + include_bytes!(concat!("fixtures/wire_v2/", $name)) as &[u8] + }; +} + +fn bytes(value: &T) -> Vec { + rmp_serde::to_vec(value).unwrap() +} + +/// The value serializes to the golden bytes and the golden bytes decode back to it. +fn assert_wire( + name: &str, + value: &T, + golden: &[u8], +) { + assert_eq!(bytes(value), golden, "{name}: serialized bytes"); + let decoded: T = rmp_serde::from_slice(golden).unwrap(); + assert_eq!(&decoded, value, "{name}: decoded value"); +} + +fn request(choice: kit::ToolChoice) -> kit::ProviderRequest { + let mut request = + kit::ProviderRequest::new("request-1", "conversation-1", "gpt-5.4", "Be exact."); + request.input = vec![ + kit::InputItem::UserText { + text: "state".to_string(), + }, + kit::InputItem::AssistantText { + text: "reply".to_string(), + }, + kit::InputItem::ToolCall { + call_id: "call-1".to_string(), + name: "read_source".to_string(), + arguments: "{}".to_string(), + }, + kit::InputItem::ToolResult { + call_id: "call-1".to_string(), + output: "ok".to_string(), + }, + ]; + request.tools = vec![kit::ToolDefinition { + name: "read_source".to_string(), + description: "Read one admitted source.".to_string(), + parameters_json: r#"{"type":"object"}"#.to_string(), + }]; + request.tool_choice = choice; + request.reasoning_effort = Some("high".to_string()); + request.parallel_tool_calls = true; + request.max_output_tokens = Some(512); + request.prompt_cache_key = Some("cache-1".to_string()); + request +} + +fn invocation(choice: kit::ToolChoice) -> kit::TransportInvocation { + kit::TransportInvocation::new(CALLER, 2_000, [7; 32], request(choice)).unwrap() +} + +fn receipt( + invocation: &kit::TransportInvocation, + outcome: kit::TransportOutcome, +) -> kit::TransportReceipt { + kit::TransportReceipt { + schema_id: kit::RECEIPT_SCHEMA_ID.to_string(), + request_id: invocation.request_id().to_string(), + caller_runtime_id: invocation.caller_runtime_id.clone(), + native_request_sha256: invocation.native_request_sha256, + provider_request_sha256: invocation.provider_request_sha256, + model: invocation.request.model.clone(), + transport: "parity-sse".to_string(), + outcome, + } +} + +fn completed(invocation: &kit::TransportInvocation) -> kit::TransportResult { + kit::TransportResult::transported( + invocation, + vec![ + kit::TransportEvent { + sequence: 0, + payload: kit::TransportEventPayload::TextDelta { + text: "hello".to_string(), + }, + }, + kit::TransportEvent { + sequence: 1, + payload: kit::TransportEventPayload::ToolCall { + call_id: "call-2".to_string(), + name: "read_source".to_string(), + arguments: "{}".to_string(), + }, + }, + ], + receipt( + invocation, + kit::TransportOutcome::Completed { + provider_response_id: Some("response-1".to_string()), + input_tokens: Some(10), + output_tokens: Some(3), + reasoning_output_tokens: Some(1), + cached_input_tokens: Some(4), + }, + ), + ) +} + +fn failed(invocation: &kit::TransportInvocation) -> kit::TransportResult { + kit::TransportResult::transported( + invocation, + Vec::new(), + receipt( + invocation, + kit::TransportOutcome::Failed { + failure_kind: "result_size".to_string(), + message: "provider result exceeded its byte bound".to_string(), + }, + ), + ) +} + +fn key() -> kit::TransportKey { + kit::TransportKey::from_connection_secret(KEY).unwrap() +} + +fn refusals() -> [(&'static str, kit::Refusal, &'static [u8]); 9] { + use kit::Refusal as R; + [ + ("expired", R::Expired, golden!("result_refused_expired.mp")), + ( + "identity_substitution", + R::IdentitySubstitution, + golden!("result_refused_identity_substitution.mp"), + ), + ( + "provider_digest_substitution", + R::ProviderDigestSubstitution, + golden!("result_refused_provider_digest_substitution.mp"), + ), + ("policy", R::Policy, golden!("result_refused_policy.mp")), + ( + "capacity", + R::Capacity, + golden!("result_refused_capacity.mp"), + ), + ( + "in_flight", + R::InFlight, + golden!("result_refused_in_flight.mp"), + ), + ( + "indeterminate", + R::Indeterminate, + golden!("result_refused_indeterminate.mp"), + ), + ( + "replay_conflict", + R::ReplayConflict, + golden!("result_refused_replay_conflict.mp"), + ), + ( + "malformed", + R::Malformed, + golden!("result_refused_malformed.mp"), + ), + ] +} + +#[test] +fn schema_ids_are_the_v2_ids() { + assert_eq!( + kit::PROVIDER_REQUEST_SCHEMA_ID, + "gamecult.codex.provider_request.v2" + ); + assert_eq!( + kit::INVOCATION_SCHEMA_ID, + "gamecult.codex.transport_invocation.v2" + ); + assert_eq!(kit::RESULT_SCHEMA_ID, "gamecult.codex.transport_result.v2"); + assert_eq!( + kit::RECEIPT_SCHEMA_ID, + "gamecult.codex.transport_receipt.v2" + ); + assert_eq!( + kit::ENVELOPE_SCHEMA_ID, + "gamecult.codex.transport_envelope.v2" + ); +} + +#[test] +fn invocations_carry_the_v2_bytes_for_each_tool_choice() { + assert_wire( + "auto", + &invocation(kit::ToolChoice::Auto), + golden!("invocation_auto.mp"), + ); + assert_wire( + "required", + &invocation(kit::ToolChoice::Required), + golden!("invocation_required.mp"), + ); +} + +#[test] +fn results_carry_the_v2_bytes_for_success_and_failure() { + let invocation = invocation(kit::ToolChoice::Required); + assert_wire( + "transported", + &completed(&invocation), + golden!("result_transported.mp"), + ); + assert_wire("failed", &failed(&invocation), golden!("result_failed.mp")); +} + +#[test] +fn every_refusal_carries_its_v2_bytes() { + let invocation = invocation(kit::ToolChoice::Required); + for (name, refusal, golden) in refusals() { + assert_wire( + name, + &kit::TransportResult::refused(&invocation, refusal), + golden, + ); + } +} + +#[test] +fn canonical_bytes_and_provider_digest_are_the_v2_bytes() { + let invocation = invocation(kit::ToolChoice::Required); + assert_eq!( + kit::canonical_invocation_bytes(&invocation).unwrap(), + golden!("canonical_invocation.bin") + ); + assert_eq!( + kit::canonical_provider_request_bytes(&invocation.request).unwrap(), + golden!("canonical_provider_request.bin") + ); + assert_eq!( + kit::provider_request_sha256(&invocation.request) + .unwrap() + .as_slice(), + golden!("provider_request_sha256.bin") + ); +} + +#[test] +fn v2_sealed_envelopes_decrypt_and_validate_under_the_kit() { + let invocation = invocation(kit::ToolChoice::Required); + let envelope: kit::TransportEnvelope = + rmp_serde::from_slice(golden!("envelope_invocation.mp")).unwrap(); + let received = kit::decrypt_invocation(&envelope, &key()).unwrap(); + assert_eq!(bytes(&received), bytes(&invocation)); + + let results = [ + ( + golden!("envelope_result_transported.mp"), + completed(&invocation), + ), + (golden!("envelope_result_failed.mp"), failed(&invocation)), + ( + golden!("envelope_result_refused_replay_conflict.mp"), + kit::TransportResult::refused(&invocation, kit::Refusal::ReplayConflict), + ), + ]; + for (sealed, expected) in results { + let envelope: kit::TransportEnvelope = rmp_serde::from_slice(sealed).unwrap(); + let received = kit::decrypt_result(&envelope, &key(), &invocation).unwrap(); + assert_eq!(bytes(&received), bytes(&expected)); + } +} + +#[test] +fn kit_sealed_envelopes_have_the_v2_shape() { + let invocation = invocation(kit::ToolChoice::Required); + let golden: kit::TransportEnvelope = + rmp_serde::from_slice(golden!("envelope_invocation.mp")).unwrap(); + let sealed = kit::encrypt_invocation(&invocation, &key()).unwrap(); + assert_eq!(sealed.schema_id, golden.schema_id); + assert_eq!(sealed.caller_runtime_id, golden.caller_runtime_id); + assert_eq!(sealed.request_id, golden.request_id); + assert_eq!(sealed.message_kind, golden.message_kind); + assert_eq!(sealed.ciphertext.len(), golden.ciphertext.len()); + assert_ne!(sealed.nonce, golden.nonce, "nonces are fresh per seal"); + + let golden: kit::TransportEnvelope = + rmp_serde::from_slice(golden!("envelope_result_transported.mp")).unwrap(); + let sealed = kit::encrypt_result(&completed(&invocation), &key()).unwrap(); + assert_eq!(sealed.message_kind, golden.message_kind); + assert_eq!(sealed.ciphertext.len(), golden.ciphertext.len()); +} diff --git a/crates/model-connector/tests/wire_parity_v2.rs b/crates/model-connector/tests/wire_parity_v2.rs deleted file mode 100644 index fa2d3ad..0000000 --- a/crates/model-connector/tests/wire_parity_v2.rs +++ /dev/null @@ -1,242 +0,0 @@ -//! The lift is wire-identical to CodexConnector fd7cec4, the v2 library -//! Ghostlight pins. Deleted by cut connector-contract-v3, which changes the wire. - -use codex_connector as v2; -use model_connector as kit; -use serde::Serialize; -use serde::de::DeserializeOwned; - -const KEY: &str = "parity-distinct-test-key"; -const CALLER: &str = "parity-caller"; - -/// Moves a value across the two crates by its MessagePack bytes, the shape the wire carries. -fn cross(value: &T) -> U { - rmp_serde::from_slice(&rmp_serde::to_vec(value).unwrap()).unwrap() -} - -fn bytes(value: &T) -> Vec { - rmp_serde::to_vec(value).unwrap() -} - -fn kit_request() -> kit::ProviderRequest { - let mut request = - kit::ProviderRequest::new("request-1", "conversation-1", "gpt-5.4", "Be exact."); - request.input = vec![ - kit::InputItem::UserText { - text: "state".to_string(), - }, - kit::InputItem::AssistantText { - text: "reply".to_string(), - }, - kit::InputItem::ToolCall { - call_id: "call-1".to_string(), - name: "read_source".to_string(), - arguments: "{}".to_string(), - }, - kit::InputItem::ToolResult { - call_id: "call-1".to_string(), - output: "ok".to_string(), - }, - ]; - request.tools = vec![kit::ToolDefinition { - name: "read_source".to_string(), - description: "Read one admitted source.".to_string(), - parameters_json: r#"{"type":"object"}"#.to_string(), - }]; - request.tool_choice = kit::ToolChoice::Required; - request.reasoning_effort = Some("high".to_string()); - request.parallel_tool_calls = true; - request.max_output_tokens = Some(512); - request.prompt_cache_key = Some("cache-1".to_string()); - request -} - -fn v2_request() -> v2::CodexProviderRequest { - let mut request = - v2::CodexProviderRequest::new("request-1", "conversation-1", "gpt-5.4", "Be exact."); - request.input = vec![ - v2::CodexInputItem::UserText { - text: "state".to_string(), - }, - v2::CodexInputItem::AssistantText { - text: "reply".to_string(), - }, - v2::CodexInputItem::ToolCall { - call_id: "call-1".to_string(), - name: "read_source".to_string(), - arguments: "{}".to_string(), - }, - v2::CodexInputItem::ToolResult { - call_id: "call-1".to_string(), - output: "ok".to_string(), - }, - ]; - request.tools = vec![v2::CodexToolDefinition { - name: "read_source".to_string(), - description: "Read one admitted source.".to_string(), - parameters_json: r#"{"type":"object"}"#.to_string(), - }]; - request.tool_choice = v2::CodexToolChoice::Required; - request.reasoning_effort = Some("high".to_string()); - request.parallel_tool_calls = true; - request.max_output_tokens = Some(512); - request.prompt_cache_key = Some("cache-1".to_string()); - request -} - -fn kit_invocation() -> kit::TransportInvocation { - kit::TransportInvocation::new(CALLER, 2_000, [7; 32], kit_request()).unwrap() -} - -fn v2_invocation() -> v2::CodexTransportInvocation { - v2::CodexTransportInvocation::new(CALLER, 2_000, [7; 32], v2_request()).unwrap() -} - -fn kit_result(invocation: &kit::TransportInvocation) -> kit::TransportResult { - let receipt = kit::TransportReceipt { - schema_id: kit::RECEIPT_SCHEMA_ID.to_string(), - request_id: invocation.request_id().to_string(), - caller_runtime_id: invocation.caller_runtime_id.clone(), - native_request_sha256: invocation.native_request_sha256, - provider_request_sha256: invocation.provider_request_sha256, - model: invocation.request.model.clone(), - transport: "parity-sse".to_string(), - outcome: kit::TransportOutcome::Completed { - provider_response_id: Some("response-1".to_string()), - input_tokens: Some(10), - output_tokens: Some(3), - reasoning_output_tokens: Some(1), - cached_input_tokens: None, - }, - }; - kit::TransportResult::transported( - invocation, - vec![ - kit::TransportEvent { - sequence: 0, - payload: kit::TransportEventPayload::TextDelta { - text: "hello".to_string(), - }, - }, - kit::TransportEvent { - sequence: 1, - payload: kit::TransportEventPayload::ToolCall { - call_id: "call-2".to_string(), - name: "read_source".to_string(), - arguments: "{}".to_string(), - }, - }, - ], - receipt, - ) -} - -fn v2_result(invocation: &v2::CodexTransportInvocation) -> v2::CodexTransportResult { - let receipt = v2::CodexTransportReceipt { - schema_id: v2::RECEIPT_SCHEMA_ID.to_string(), - request_id: invocation.request_id().to_string(), - caller_runtime_id: invocation.caller_runtime_id.clone(), - native_request_sha256: invocation.native_request_sha256, - provider_request_sha256: invocation.provider_request_sha256, - model: invocation.request.model.clone(), - transport: "parity-sse".to_string(), - outcome: v2::CodexTransportOutcome::Completed { - provider_response_id: Some("response-1".to_string()), - input_tokens: Some(10), - output_tokens: Some(3), - reasoning_output_tokens: Some(1), - cached_input_tokens: None, - }, - }; - v2::CodexTransportResult::transported( - invocation, - vec![ - v2::CodexTransportEvent { - sequence: 0, - payload: v2::CodexTransportEventPayload::TextDelta { - text: "hello".to_string(), - }, - }, - v2::CodexTransportEvent { - sequence: 1, - payload: v2::CodexTransportEventPayload::ToolCall { - call_id: "call-2".to_string(), - name: "read_source".to_string(), - arguments: "{}".to_string(), - }, - }, - ], - receipt, - ) -} - -fn kit_key() -> kit::TransportKey { - kit::TransportKey::from_connection_secret(KEY).unwrap() -} - -fn v2_key() -> v2::CodexTransportKey { - v2::CodexTransportKey::from_connection_secret(KEY).unwrap() -} - -#[test] -fn schema_ids_are_the_v2_ids() { - assert_eq!( - kit::PROVIDER_REQUEST_SCHEMA_ID, - v2::PROVIDER_REQUEST_SCHEMA_ID - ); - assert_eq!(kit::INVOCATION_SCHEMA_ID, v2::INVOCATION_SCHEMA_ID); - assert_eq!(kit::RESULT_SCHEMA_ID, v2::RESULT_SCHEMA_ID); - assert_eq!(kit::RECEIPT_SCHEMA_ID, v2::RECEIPT_SCHEMA_ID); - assert_eq!(kit::ENVELOPE_SCHEMA_ID, v2::ENVELOPE_SCHEMA_ID); -} - -#[test] -fn canonical_bytes_and_provider_digest_match_v2() { - let from_kit = kit_invocation(); - let from_v2 = v2_invocation(); - assert_eq!(bytes(&from_kit), bytes(&from_v2)); - assert_eq!( - kit::canonical_invocation_bytes(&from_kit).unwrap(), - v2::canonical_invocation_bytes(&from_v2).unwrap() - ); - assert_eq!( - kit::canonical_provider_request_bytes(&from_kit.request).unwrap(), - v2::canonical_provider_request_bytes(&from_v2.request).unwrap() - ); - assert_eq!( - kit::provider_request_sha256(&from_kit.request).unwrap(), - v2::provider_request_sha256(&from_v2.request).unwrap() - ); -} - -#[test] -fn kit_invocation_decrypts_and_validates_under_v2() { - let sent = kit_invocation(); - let envelope = kit::encrypt_invocation(&sent, &kit_key()).unwrap(); - let received = v2::decrypt_invocation(&cross(&envelope), &v2_key()).unwrap(); - assert_eq!(bytes(&received), bytes(&sent)); -} - -#[test] -fn v2_invocation_decrypts_and_validates_under_kit() { - let sent = v2_invocation(); - let envelope = v2::encrypt_invocation(&sent, &v2_key()).unwrap(); - let received = kit::decrypt_invocation(&cross(&envelope), &kit_key()).unwrap(); - assert_eq!(bytes(&received), bytes(&sent)); -} - -#[test] -fn kit_result_decrypts_and_validates_under_v2() { - let sent = kit_result(&kit_invocation()); - let envelope = kit::encrypt_result(&sent, &kit_key()).unwrap(); - let received = v2::decrypt_result(&cross(&envelope), &v2_key(), &v2_invocation()).unwrap(); - assert_eq!(bytes(&received), bytes(&sent)); -} - -#[test] -fn v2_result_decrypts_and_validates_under_kit() { - let sent = v2_result(&v2_invocation()); - let envelope = v2::encrypt_result(&sent, &v2_key()).unwrap(); - let received = kit::decrypt_result(&cross(&envelope), &kit_key(), &kit_invocation()).unwrap(); - assert_eq!(bytes(&received), bytes(&sent)); -}