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:
parent
af99b3423f
commit
ae28dbedfd
27 changed files with 461 additions and 268 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
crates/model-connector/tests/fixtures/wire_v2/.gitattributes
vendored
Normal file
2
crates/model-connector/tests/fixtures/wire_v2/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*.mp binary
|
||||
*.bin binary
|
||||
20
crates/model-connector/tests/fixtures/wire_v2/README.md
vendored
Normal file
20
crates/model-connector/tests/fixtures/wire_v2/README.md
vendored
Normal 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.
|
||||
BIN
crates/model-connector/tests/fixtures/wire_v2/canonical_invocation.bin
vendored
Normal file
BIN
crates/model-connector/tests/fixtures/wire_v2/canonical_invocation.bin
vendored
Normal file
Binary file not shown.
BIN
crates/model-connector/tests/fixtures/wire_v2/canonical_provider_request.bin
vendored
Normal file
BIN
crates/model-connector/tests/fixtures/wire_v2/canonical_provider_request.bin
vendored
Normal file
Binary file not shown.
6
crates/model-connector/tests/fixtures/wire_v2/envelope_invocation.mp
vendored
Normal file
6
crates/model-connector/tests/fixtures/wire_v2/envelope_invocation.mp
vendored
Normal 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汤km'+绦挞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屉
|
||||
7
crates/model-connector/tests/fixtures/wire_v2/envelope_result_failed.mp
vendored
Normal file
7
crates/model-connector/tests/fixtures/wire_v2/envelope_result_failed.mp
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
–Ù$gamecult.codex.transport_envelope.v2parity-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.Ẁ̧&Ìå̶̑[
|
||||
BIN
crates/model-connector/tests/fixtures/wire_v2/envelope_result_refused_replay_conflict.mp
vendored
Normal file
BIN
crates/model-connector/tests/fixtures/wire_v2/envelope_result_refused_replay_conflict.mp
vendored
Normal file
Binary file not shown.
BIN
crates/model-connector/tests/fixtures/wire_v2/envelope_result_transported.mp
vendored
Normal file
BIN
crates/model-connector/tests/fixtures/wire_v2/envelope_result_transported.mp
vendored
Normal file
Binary file not shown.
BIN
crates/model-connector/tests/fixtures/wire_v2/invocation_auto.mp
vendored
Normal file
BIN
crates/model-connector/tests/fixtures/wire_v2/invocation_auto.mp
vendored
Normal file
Binary file not shown.
BIN
crates/model-connector/tests/fixtures/wire_v2/invocation_required.mp
vendored
Normal file
BIN
crates/model-connector/tests/fixtures/wire_v2/invocation_required.mp
vendored
Normal file
Binary file not shown.
BIN
crates/model-connector/tests/fixtures/wire_v2/provider_request_sha256.bin
vendored
Normal file
BIN
crates/model-connector/tests/fixtures/wire_v2/provider_request_sha256.bin
vendored
Normal file
Binary file not shown.
BIN
crates/model-connector/tests/fixtures/wire_v2/result_failed.mp
vendored
Normal file
BIN
crates/model-connector/tests/fixtures/wire_v2/result_failed.mp
vendored
Normal file
Binary file not shown.
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_capacity.mp
vendored
Normal file
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_capacity.mp
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
”Ù"gamecult.codex.transport_result.v2©request-1parity-caller<65>§Refused¨Capacity
|
||||
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_expired.mp
vendored
Normal file
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_expired.mp
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
”Ù"gamecult.codex.transport_result.v2©request-1parity-caller<65>§Refused§Expired
|
||||
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_identity_substitution.mp
vendored
Normal file
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_identity_substitution.mp
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
”Ù"gamecult.codex.transport_result.v2©request-1parity-caller<65>§Refused´IdentitySubstitution
|
||||
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_in_flight.mp
vendored
Normal file
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_in_flight.mp
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
”Ù"gamecult.codex.transport_result.v2©request-1parity-caller<65>§Refused¨InFlight
|
||||
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_indeterminate.mp
vendored
Normal file
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_indeterminate.mp
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
”Ù"gamecult.codex.transport_result.v2©request-1parity-caller<65>§RefusedIndeterminate
|
||||
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_malformed.mp
vendored
Normal file
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_malformed.mp
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
”Ù"gamecult.codex.transport_result.v2©request-1parity-caller<65>§Refused©Malformed
|
||||
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_policy.mp
vendored
Normal file
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_policy.mp
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
”Ù"gamecult.codex.transport_result.v2©request-1parity-caller<65>§Refused¦Policy
|
||||
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_provider_digest_substitution.mp
vendored
Normal file
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_provider_digest_substitution.mp
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
”Ù"gamecult.codex.transport_result.v2©request-1parity-caller<65>§RefusedºProviderDigestSubstitution
|
||||
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_replay_conflict.mp
vendored
Normal file
1
crates/model-connector/tests/fixtures/wire_v2/result_refused_replay_conflict.mp
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
”Ù"gamecult.codex.transport_result.v2©request-1parity-caller<65>§Refused®ReplayConflict
|
||||
BIN
crates/model-connector/tests/fixtures/wire_v2/result_transported.mp
vendored
Normal file
BIN
crates/model-connector/tests/fixtures/wire_v2/result_transported.mp
vendored
Normal file
Binary file not shown.
301
crates/model-connector/tests/wire_golden_v2.rs
Normal file
301
crates/model-connector/tests/wire_golden_v2.rs
Normal 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());
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
Loading…
Reference in a new issue