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 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-28 05:08:56 +01:00
commit ae28dbedfd
27 changed files with 461 additions and 268 deletions

23
Cargo.lock generated
View file

@ -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",

View file

@ -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 }

View file

@ -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)));
}
}

View file

@ -0,0 +1,2 @@
*.mp binary
*.bin binary

View file

@ -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 <ModelConnector>/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 <name> <hex>` 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.

View file

@ -0,0 +1,6 @@
栙$gamecult.codex.transport_envelope.v2璸arity-caller﹔equest-1狪nvocation溙<6E>塘h虄虒抬x泰Y探<1B>環谭听题虠虆3陶 糖烃虘誊h抬台涕[酞藤!A屉虊甜铁疼;屉铁| 酞躺`!铁铁虤抬虗
虂k剔'vCZ锑號虒虉 8J抬汤d蹋虠N炭F炭zm6<6D><36>
眺添甜\\NMq號套虠 贴1替苔摊惕 4*{趟胎瘫
讨苔e糖台虖
贪虇i贪G*S虂坛碳桃胎坍誊!N汤萄虪虃太虣7虂糖贪天虄汤态c苔趟 桃滩1k提~h踢3$腾虪K踢贪贴誊4{逃踢碳泰U.)烫汰踏r虪听天唐^bbI虉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屉

View file

@ -0,0 +1,7 @@
–Ù$gamecult.codex.transport_envelope.v2­parity-caller©request-1¦ResultœÌ…Ì¢ÌÇ"̇Ì<E280A1>GÌ©ÜCÌôÌË Ì<>ÌéHÌßÌ—ÌÿÌÐÌ´ÌÝÌá\Ìö]2/ÌÎe"̘
Ì¿ÌáÌìÌü{GÌÚṴ̧^b3Ìûj̢̗̟ÌÃÌ’sÌ•*Ì» ÌøÌä= ̸̄<ÌÁ{ÌýÌã.ÌØ8~̫̓CÌâ
̱̘ÌË
[Ì‚ÌÉoÌ–(47ÌÌBML̲Ìó̢̳Ì<C2A2>hp,ÌîÌ®wÌê6DHÌðbÌÏÌÌb̹@a̧ÌÑ̵̎Ì´Ì<>kÌäÌ’ÌáÌóÌ€ÌÑN+ÌüMÌÀjG %Ìß/Ìô
ÌÁÌò̸%Ì»Ì<C2BB>@sT`ÌâÌ’}P̈ÌÒ ÌëÌÊÌÞ Ì«Ìâ:ÌÚ̟̖̔Ìû^Ìô>ÌûX.x
r]F8Ì<38>ÌÛÌ‹Ì÷Ì<C3B7>5ÌÚGÌÆÌºÌÃÌ®C@(ÌóÌÓ̾{̾ÌÙMÌÓ~)Ìà 7ÌÐÌòI+'v ÌøUnP̈$U(NÌêÌçÌüsÌ¢T^[ÌÊYÌæÌ<C3A6>zÌâ.wÌÆeÌ™C̦̿̄"U^trÌöÌ<C3B6>ÌÙ̦Ìï̯̬ÌçÌÜÌž+ÌÑ/b̯̭̼Ìî*$Ì× ÌŠÌ²̘ÌÕ\K5̧f
Ìò̃b}̦̬o.Ẁ̧&Ìå̶̑[

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1 @@
”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller<65>§Refused¨Capacity

View file

@ -0,0 +1 @@
”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller<65>§Refused§Expired

View file

@ -0,0 +1 @@
”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller<65>§Refused´IdentitySubstitution

View file

@ -0,0 +1 @@
”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller<65>§Refused¨InFlight

View file

@ -0,0 +1 @@
”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller<65>§Refused­Indeterminate

View file

@ -0,0 +1 @@
”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller<65>§Refused©Malformed

View file

@ -0,0 +1 @@
”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller<65>§Refused¦Policy

View file

@ -0,0 +1 @@
”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller<65>§RefusedºProviderDigestSubstitution

View file

@ -0,0 +1 @@
”Ù"gamecult.codex.transport_result.v2©request-1­parity-caller<65>§Refused®ReplayConflict

Binary file not shown.

View file

@ -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<T: Serialize>(value: &T) -> Vec<u8> {
rmp_serde::to_vec(value).unwrap()
}
/// The value serializes to the golden bytes and the golden bytes decode back to it.
fn assert_wire<T: Serialize + DeserializeOwned + PartialEq + std::fmt::Debug>(
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());
}

View file

@ -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<T: Serialize, U: DeserializeOwned>(value: &T) -> U {
rmp_serde::from_slice(&rmp_serde::to_vec(value).unwrap()).unwrap()
}
fn bytes<T: Serialize>(value: &T) -> Vec<u8> {
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));
}