connector-kit-lift: the provider-neutral connector kit, lifted from CodexConnector fd7cec4 (squash of eureka-body/connector-kit-lift 1cf879f..ba1f7eb)

The v2 contract, encrypted envelope and framing, client, caller admission
and replay, and the serve loop move into the model-connector crate behind
one ProviderBackend trait, relicensed AGPL-3.0-only by the sole copyright
holder (ruling connector-kit-relicense-agpl). Same schema ids and wire bytes
as v2, pinned by golden fixtures generated from fd7cec4. The Codex backend
is not ported (ruling codex-connector-park).

Serve owns the frame bounds: it measures the sealed envelope, refuses
configurations that cannot deliver their payload bound, answers over-bound
payloads and over-long ids with v2 refusals, and never records or replays
an undeliverable result.

Spec eureka-body:cut_spec:cut-connector-kit-lift.r1; reports h1-h5;
Soul s5 holds (verdict eureka-body:verdict:cut-connector-kit-lift.s5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-28 07:12:42 +01:00
commit ec0519d9ff
36 changed files with 5348 additions and 15 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

838
Cargo.lock generated Normal file
View file

@ -0,0 +1,838 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aes-gcm"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"ghash",
"subtle",
]
[[package]]
name = "android_system_properties"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "cc"
version = "1.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"rand_core",
"typenum",
]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]]
name = "cultcache-rs"
version = "0.1.0"
source = "git+https://github.com/GameCult/CultLib.git?rev=e171eca32329baa928f4a1d810401a8b4c857029#e171eca32329baa928f4a1d810401a8b4c857029"
dependencies = [
"anyhow",
"chrono",
"cultcache-rs-derive",
"fs2",
"redb",
"rmp-serde",
"serde",
"serde_bytes",
"serde_json",
"uuid",
"windows-sys",
]
[[package]]
name = "cultcache-rs-derive"
version = "0.1.0"
source = "git+https://github.com/GameCult/CultLib.git?rev=e171eca32329baa928f4a1d810401a8b4c857029#e171eca32329baa928f4a1d810401a8b4c857029"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
[[package]]
name = "fs2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "ghash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "log"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "model-connector"
version = "0.1.0"
dependencies = [
"aes-gcm",
"cultcache-rs",
"rand_core",
"rmp-serde",
"serde",
"serde_json",
"sha2",
"thiserror",
"zeroize",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "polyval"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "redb"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de6c3b63e007e90ce536ec2ae4690826136a20ec8dbbbb400daef1bb999d2e36"
dependencies = [
"libc",
]
[[package]]
name = "rmp"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
dependencies = [
"num-traits",
]
[[package]]
name = "rmp-serde"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
dependencies = [
"rmp",
"serde",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_bytes"
version = "0.11.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "uuid"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc"
dependencies = [
"getrandom 0.4.3",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasm-bindgen"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.119",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
dependencies = [
"unicode-ident",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"

9
Cargo.toml Normal file
View file

@ -0,0 +1,9 @@
[workspace]
members = ["crates/*"]
resolver = "3"
[workspace.package]
version = "0.1.0"
edition = "2024"
license = "AGPL-3.0-only"
repository = "https://eureka.gamecult.org/forge/GameCult/ModelConnector"

View file

@ -1,26 +1,55 @@
# ModelConnector
Status: planned. Nothing is implemented in this repository yet.
The connector kit for GameCult minds: one typed, encrypted, replay-safe contract
between a caller that needs a model turn and a daemon that holds the provider
credential, so no project wires its own provider calls.
## What it will hold
Status: the `model-connector` crate is live. No connector binary exists yet
(the Claude connector is first, then an OpenAI-compatible one).
The connector kit for GameCult minds:
## Entry points
- the connector contract, a Rust crate defining the typed boundary every model
turn of a GameCult mind goes through;
- connector daemons that implement the contract against a model provider,
Claude first, then an OpenAI-compatible connector.
Crate `crates/model-connector`:
The problem it solves: a mind should reach any model through one inspectable,
typed contract, rather than each project wiring its own provider calls.
- `src/contract.rs` - the v2 wire contract: `ProviderRequest`, `TransportInvocation`,
`TransportResult`, `TransportReceipt`, `Refusal`, and the canonical byte and digest
functions that bind a result to its request.
- `src/envelope.rs` - the AES-256-GCM `TransportEnvelope`, its identity-binding AAD, and
the length-prefixed framing.
- `src/client.rs` - `ConnectorClient`, the one-request caller side.
- `src/service.rs` (feature `daemon`) - `TransportService`: caller admission, durable
replay (a retried request is answered byte-identically and the provider runs once),
in-flight and capacity refusals.
- `src/serve.rs` (feature `daemon`) - `serve()`, the loopback accept loop, over the
`ProviderBackend` trait. A connector binary implements `ProviderBackend` for its
provider and calls `serve`; the kit names no provider.
## Where it comes from
Frame arithmetic is public in `src/envelope.rs`: `frame_bytes_for_payload(n)` is the frame a
caller payload bound of `n` needs, `payload_bytes_for_frame(frame)` is the largest payload bound
a frame allows, and `MAX_IDENTITY_BYTES` (256) bounds caller ids, request ids and model names.
`serve` refuses a frame/payload pair that violates them (`ServeError::FrameBound` states the frame
needed); the smallest legal frame is `frame_bytes_for_payload(MIN_PAYLOAD_BYTES)`. A request id past
the bound is refused on the wire as `Refusal::Malformed`, the reply echoing the id (the connection is dropped only if that reply cannot fit the frame); `ConnectorClient` refuses such an id locally with `RequestIdTooLong`.
[CodexConnector](https://github.com/GameCult/CodexConnector)
(GameCult/CodexConnector) is the existing connector this kit is lifted from.
The lift is the first piece of work here; until it lands there are no entry
points to run.
The default feature set compiles contract, envelope and client only. Schema ids are
still `gamecult.codex.*.v2`; renaming them changes the wire and belongs to a later
contract version.
## License
## Verify
```
cargo test --locked --workspace
cargo test --locked --workspace --features model-connector/daemon
```
`crates/model-connector/tests/wire_golden_v2.rs` pins the kit byte-for-byte to the golden v2
fixtures in `crates/model-connector/tests/fixtures/wire_v2`.
## Provenance and license
AGPL-3.0-only. See `LICENSE`.
The contract, envelope, client, replay service and serve loop were lifted from
[CodexConnector](https://github.com/GameCult/CodexConnector) at fd7cec4, formerly
PolyForm Noncommercial. Its sole copyright holder relicensed the lifted material as
AGPL-3.0-only for this repository.

View file

@ -0,0 +1,23 @@
[package]
name = "model-connector"
description = "Provider-neutral connector contract, envelope, client, replay service and serve loop for GameCult minds"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
publish = false
[features]
default = []
daemon = ["dep:cultcache-rs"]
[dependencies]
aes-gcm = "0.10"
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "e171eca32329baa928f4a1d810401a8b4c857029", package = "cultcache-rs", optional = true }
rand_core = { version = "0.6", features = ["getrandom"] }
rmp-serde = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
thiserror = "2"
zeroize = "1"

View file

@ -0,0 +1,206 @@
use std::net::{SocketAddr, TcpStream};
use std::time::Duration;
use thiserror::Error;
use zeroize::Zeroizing;
use crate::contract::{TransportInvocation, TransportKey, TransportResult};
use crate::envelope::{
MAX_IDENTITY_BYTES, ServiceError, TransportFrameError, decrypt_result, encrypt_invocation,
read_transport_frame, write_transport_frame,
};
#[derive(Clone)]
pub struct ConnectorClient {
endpoint: SocketAddr,
security: TransportKey,
max_frame_bytes: usize,
response_timeout: Option<Duration>,
}
impl ConnectorClient {
pub fn new(
endpoint: SocketAddr,
connection_key: impl Into<String>,
max_frame_bytes: usize,
response_timeout: Option<Duration>,
) -> Result<Self, ConnectorClientError> {
if endpoint.port() == 0
|| endpoint.ip().is_unspecified()
|| endpoint.ip().is_multicast()
|| endpoint.ip() == std::net::Ipv4Addr::BROADCAST
|| !(4096..=u32::MAX as usize).contains(&max_frame_bytes)
|| response_timeout.is_some_and(|timeout| timeout.is_zero())
{
return Err(ConnectorClientError::InvalidConfig);
}
let connection_key = Zeroizing::new(connection_key.into());
let security = TransportKey::from_connection_secret(connection_key.as_str())?;
Ok(Self {
endpoint,
security,
max_frame_bytes,
response_timeout,
})
}
pub fn execute(
&self,
invocation: &TransportInvocation,
) -> Result<TransportResult, ConnectorClientError> {
if invocation.request_id().len() > MAX_IDENTITY_BYTES {
return Err(ConnectorClientError::RequestIdTooLong);
}
let request = rmp_serde::to_vec(&encrypt_invocation(invocation, &self.security)?)
.map_err(|_| ConnectorClientError::Encoding)?;
let connect_timeout = self
.response_timeout
.unwrap_or(Duration::from_secs(10))
.min(Duration::from_secs(10));
let mut stream = TcpStream::connect_timeout(&self.endpoint, connect_timeout)
.map_err(ConnectorClientError::Connection)?;
stream
.set_read_timeout(self.response_timeout)
.and_then(|()| stream.set_write_timeout(self.response_timeout))
.map_err(ConnectorClientError::Connection)?;
write_transport_frame(&mut stream, &request, self.max_frame_bytes)
.map_err(client_frame_error)?;
let response =
read_transport_frame(&mut stream, self.max_frame_bytes).map_err(client_frame_error)?;
let envelope =
rmp_serde::from_slice(&response).map_err(|_| ConnectorClientError::Encoding)?;
decrypt_result(&envelope, &self.security, invocation).map_err(Into::into)
}
}
#[cfg(test)]
mod client_endpoint_tests {
use super::*;
#[test]
fn client_admits_explicit_unicast_and_refuses_non_destinations() {
for endpoint in ["127.0.0.1:4103", "172.19.0.1:4103", "[::1]:4103"] {
assert!(
ConnectorClient::new(
endpoint.parse().unwrap(),
"bounded-client-endpoint-key",
64 * 1024,
Some(Duration::from_secs(1)),
)
.is_ok(),
"{endpoint} should be an admissible explicit destination"
);
}
for endpoint in [
"0.0.0.0:4103",
"255.255.255.255:4103",
"224.0.0.1:4103",
"[::]:4103",
"[ff02::1]:4103",
"127.0.0.1:0",
] {
assert!(matches!(
ConnectorClient::new(
endpoint.parse().unwrap(),
"bounded-client-endpoint-key",
64 * 1024,
Some(Duration::from_secs(1)),
),
Err(ConnectorClientError::InvalidConfig)
));
}
}
}
#[derive(Debug, Error)]
pub enum ConnectorClientError {
#[error("invalid connector client configuration")]
InvalidConfig,
#[error("connector client connection failed")]
Connection(#[source] std::io::Error),
#[error("connector frame exceeded its bound")]
FrameSize,
#[error("connector MessagePack encoding failed")]
Encoding,
#[error("request id is longer than the connector identity bound")]
RequestIdTooLong,
#[error(transparent)]
Transport(#[from] ServiceError),
}
fn client_frame_error(error: TransportFrameError) -> ConnectorClientError {
match error {
TransportFrameError::Connection(error) => ConnectorClientError::Connection(error),
TransportFrameError::Size => ConnectorClientError::FrameSize,
}
}
#[cfg(test)]
mod fixture_tests {
use super::*;
use crate::fixtures::*;
use crate::{contract::*, envelope::*};
#[test]
fn shared_client_transports_one_exact_encrypted_request_and_result() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let endpoint = listener.local_addr().unwrap();
let invocation = invocation();
let expected = invocation.clone();
let security = security("epiphany-distinct-test-key");
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let request = read_transport_frame(&mut stream, 64 * 1024).unwrap();
let envelope: TransportEnvelope = rmp_serde::from_slice(&request).unwrap();
assert_eq!(decrypt_invocation(&envelope, &security).unwrap(), expected);
let response = encrypt_result(
&TransportResult::refused(&expected, Refusal::Policy),
&security,
)
.unwrap();
write_transport_frame(
&mut stream,
&rmp_serde::to_vec(&response).unwrap(),
64 * 1024,
)
.unwrap();
});
let client = ConnectorClient::new(
endpoint,
"epiphany-distinct-test-key",
64 * 1024,
Some(Duration::from_secs(1)),
)
.unwrap();
assert_eq!(
client.execute(&invocation).unwrap().disposition,
TransportDisposition::Refused(Refusal::Policy)
);
server.join().unwrap();
}
#[test]
fn an_over_long_request_id_is_refused_locally_before_any_byte_is_sent() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
listener.set_nonblocking(true).unwrap();
let client = ConnectorClient::new(
listener.local_addr().unwrap(),
"epiphany-distinct-test-key",
64 * 1024,
Some(Duration::from_secs(1)),
)
.unwrap();
let mut long = invocation();
long.request.request_id = "r".repeat(MAX_IDENTITY_BYTES + 1);
assert!(matches!(
client.execute(&long),
Err(ConnectorClientError::RequestIdTooLong)
));
assert_eq!(
listener.accept().unwrap_err().kind(),
std::io::ErrorKind::WouldBlock,
"the client must not even connect"
);
}
}

View file

@ -0,0 +1,641 @@
use std::collections::HashSet;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error;
use zeroize::Zeroize;
use crate::envelope::ServiceError;
pub const PROVIDER_REQUEST_SCHEMA_ID: &str = "gamecult.codex.provider_request.v2";
pub const INVOCATION_SCHEMA_ID: &str = "gamecult.codex.transport_invocation.v2";
pub const RESULT_SCHEMA_ID: &str = "gamecult.codex.transport_result.v2";
pub const RECEIPT_SCHEMA_ID: &str = "gamecult.codex.transport_receipt.v2";
pub const ENVELOPE_SCHEMA_ID: &str = "gamecult.codex.transport_envelope.v2";
#[derive(Clone, PartialEq, Eq)]
pub struct TransportKey(pub(crate) [u8; 32]);
impl TransportKey {
pub fn from_connection_secret(secret: &str) -> Result<Self, ServiceError> {
if secret.trim().is_empty() || secret.trim() != secret {
return Err(ServiceError::InvalidAdmission);
}
Ok(Self(Sha256::digest(secret.as_bytes()).into()))
}
}
impl Drop for TransportKey {
fn drop(&mut self) {
self.0.zeroize();
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderRequest {
pub schema_id: String,
pub request_id: String,
pub conversation_id: String,
pub model: String,
pub instructions: String,
pub input: Vec<InputItem>,
pub reasoning_effort: Option<String>,
pub reasoning_summary: Option<String>,
pub service_tier: Option<String>,
pub output_format_name: Option<String>,
pub previous_response_id: Option<String>,
pub tools: Vec<ToolDefinition>,
pub tool_choice: ToolChoice,
pub parallel_tool_calls: bool,
pub output_schema_json: Option<String>,
pub max_output_tokens: Option<u32>,
pub prompt_cache_key: Option<String>,
}
impl ProviderRequest {
pub fn new(
request_id: impl Into<String>,
conversation_id: impl Into<String>,
model: impl Into<String>,
instructions: impl Into<String>,
) -> Self {
Self {
schema_id: PROVIDER_REQUEST_SCHEMA_ID.to_string(),
request_id: request_id.into(),
conversation_id: conversation_id.into(),
model: model.into(),
instructions: instructions.into(),
input: Vec::new(),
reasoning_effort: None,
reasoning_summary: None,
service_tier: None,
output_format_name: None,
previous_response_id: None,
tools: Vec::new(),
tool_choice: ToolChoice::Auto,
parallel_tool_calls: false,
output_schema_json: None,
max_output_tokens: None,
prompt_cache_key: None,
}
}
pub fn validate(&self) -> Result<(), ContractError> {
if self.schema_id != PROVIDER_REQUEST_SCHEMA_ID {
return Err(ContractError::Schema);
}
require_id(&self.request_id, "request_id")?;
require_id(&self.conversation_id, "conversation_id")?;
require_id(&self.model, "model")?;
require_content(&self.instructions, "instructions")?;
if matches!(self.max_output_tokens, Some(0)) {
return Err(ContractError::Invalid("max_output_tokens"));
}
if self
.prompt_cache_key
.as_deref()
.is_some_and(|value| value.trim().is_empty())
{
return Err(ContractError::Invalid("prompt_cache_key"));
}
for (value, field) in [
(self.reasoning_effort.as_deref(), "reasoning_effort"),
(self.reasoning_summary.as_deref(), "reasoning_summary"),
(self.service_tier.as_deref(), "service_tier"),
(self.output_format_name.as_deref(), "output_format_name"),
(self.previous_response_id.as_deref(), "previous_response_id"),
] {
if let Some(value) = value {
require_id(value, field)?;
}
}
if let Some(schema) = &self.output_schema_json {
require_content(schema, "output_schema_json")?;
require_json_object(schema, "output_schema_json")?;
require_provider_name(
self.output_format_name
.as_deref()
.ok_or(ContractError::Invalid("output_format_name"))?,
"output_format_name",
)?;
} else if self.output_format_name.is_some() {
return Err(ContractError::Invalid("output_schema_json"));
}
if self.tools.is_empty() && self.tool_choice == ToolChoice::Required {
return Err(ContractError::Invalid("tool_choice"));
}
let mut tool_names = HashSet::new();
for tool in &self.tools {
require_provider_name(&tool.name, "tool.name")?;
require_content(&tool.description, "tool.description")?;
require_content(&tool.parameters_json, "tool.parameters_json")?;
require_json_object(&tool.parameters_json, "tool.parameters_json")?;
if !tool_names.insert(tool.name.as_str()) {
return Err(ContractError::DuplicateTool(tool.name.clone()));
}
}
for item in &self.input {
match item {
InputItem::UserText { text } | InputItem::AssistantText { text } => {
require_content(text, "input.text")?;
}
InputItem::ToolCall {
call_id,
name,
arguments,
} => {
require_provider_call_id(call_id, "input.call_id")?;
require_provider_name(name, "input.tool_name")?;
require_content(arguments, "input.arguments")?;
}
InputItem::ToolResult { call_id, output } => {
require_provider_call_id(call_id, "input.call_id")?;
require_content(output, "input.output")?;
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolChoice {
Auto,
Required,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum InputItem {
UserText {
text: String,
},
AssistantText {
text: String,
},
ToolCall {
call_id: String,
name: String,
arguments: String,
},
ToolResult {
call_id: String,
output: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolDefinition {
pub name: String,
pub description: String,
pub parameters_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TransportInvocation {
pub schema_id: String,
pub caller_runtime_id: String,
pub expires_at_unix_ms: u64,
pub native_request_sha256: [u8; 32],
pub provider_request_sha256: [u8; 32],
pub request: ProviderRequest,
}
impl TransportInvocation {
pub fn new(
caller_runtime_id: impl Into<String>,
expires_at_unix_ms: u64,
native_request_sha256: [u8; 32],
request: ProviderRequest,
) -> Result<Self, ContractError> {
let provider_request_sha256 = provider_request_sha256(&request)?;
Ok(Self {
schema_id: INVOCATION_SCHEMA_ID.to_string(),
caller_runtime_id: caller_runtime_id.into(),
expires_at_unix_ms,
native_request_sha256,
provider_request_sha256,
request,
})
}
pub fn validate(&self, now_unix_ms: u64, max_expiry_skew_ms: u64) -> Result<(), ContractError> {
if self.schema_id != INVOCATION_SCHEMA_ID {
return Err(ContractError::Schema);
}
require_id(&self.caller_runtime_id, "caller_runtime_id")?;
if self.native_request_sha256 == [0; 32] {
return Err(ContractError::Invalid("native_request_sha256"));
}
self.request.validate()?;
if self.provider_request_sha256 != provider_request_sha256(&self.request)? {
return Err(ContractError::ProviderDigest);
}
if self.expires_at_unix_ms < now_unix_ms
|| self.expires_at_unix_ms > now_unix_ms.saturating_add(max_expiry_skew_ms)
{
return Err(ContractError::Expiry);
}
Ok(())
}
pub fn request_id(&self) -> &str {
&self.request.request_id
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TransportResult {
pub schema_id: String,
pub request_id: String,
pub caller_runtime_id: String,
pub disposition: TransportDisposition,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransportDisposition {
Refused(Refusal),
Transported {
events: Vec<TransportEvent>,
receipt: Box<TransportReceipt>,
},
}
impl TransportResult {
pub fn refused(invocation: &TransportInvocation, reason: Refusal) -> Self {
Self {
schema_id: RESULT_SCHEMA_ID.to_string(),
request_id: invocation.request_id().to_string(),
caller_runtime_id: invocation.caller_runtime_id.clone(),
disposition: TransportDisposition::Refused(reason),
}
}
pub fn transported(
invocation: &TransportInvocation,
events: Vec<TransportEvent>,
receipt: TransportReceipt,
) -> Self {
Self {
schema_id: RESULT_SCHEMA_ID.to_string(),
request_id: invocation.request_id().to_string(),
caller_runtime_id: invocation.caller_runtime_id.clone(),
disposition: TransportDisposition::Transported {
events,
receipt: Box::new(receipt),
},
}
}
pub fn validate_against(&self, invocation: &TransportInvocation) -> Result<(), ContractError> {
if self.schema_id != RESULT_SCHEMA_ID {
return Err(ContractError::Schema);
}
if self.request_id != invocation.request_id()
|| self.caller_runtime_id != invocation.caller_runtime_id
{
return Err(ContractError::Identity);
}
if let TransportDisposition::Transported { events, receipt } = &self.disposition {
for (expected, event) in events.iter().enumerate() {
if event.sequence != expected as u64 {
return Err(ContractError::EventSequence);
}
event.validate()?;
}
receipt.validate_against(invocation)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransportEventPayload {
TextDelta {
text: String,
},
ToolCall {
call_id: String,
name: String,
arguments: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TransportEvent {
pub sequence: u64,
pub payload: TransportEventPayload,
}
impl TransportEvent {
fn validate(&self) -> Result<(), ContractError> {
match &self.payload {
TransportEventPayload::TextDelta { text } => require_content(text, "event.text"),
TransportEventPayload::ToolCall {
call_id,
name,
arguments,
} => {
require_id(call_id, "event.call_id")?;
require_id(name, "event.tool_name")?;
require_content(arguments, "event.arguments")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TransportReceipt {
pub schema_id: String,
pub request_id: String,
pub caller_runtime_id: String,
pub native_request_sha256: [u8; 32],
pub provider_request_sha256: [u8; 32],
pub model: String,
pub transport: String,
pub outcome: TransportOutcome,
}
impl TransportReceipt {
pub fn validate_against(&self, invocation: &TransportInvocation) -> Result<(), ContractError> {
if self.schema_id != RECEIPT_SCHEMA_ID {
return Err(ContractError::Schema);
}
if self.request_id != invocation.request_id()
|| self.caller_runtime_id != invocation.caller_runtime_id
|| self.model != invocation.request.model
{
return Err(ContractError::Identity);
}
if self.provider_request_sha256 != invocation.provider_request_sha256 {
return Err(ContractError::ProviderDigest);
}
if self.native_request_sha256 != invocation.native_request_sha256 {
return Err(ContractError::NativeDigest);
}
require_id(&self.transport, "receipt.transport")?;
match &self.outcome {
TransportOutcome::Completed {
provider_response_id: Some(value),
..
} => require_id(value, "receipt.provider_response_id")?,
TransportOutcome::Failed {
failure_kind,
message,
} => {
require_id(failure_kind, "receipt.failure_kind")?;
require_content(message, "receipt.failure_message")?;
}
TransportOutcome::Completed {
provider_response_id: None,
..
} => {}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransportOutcome {
Completed {
provider_response_id: Option<String>,
input_tokens: Option<u64>,
output_tokens: Option<u64>,
reasoning_output_tokens: Option<u64>,
cached_input_tokens: Option<u64>,
},
Failed {
failure_kind: String,
message: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Refusal {
Expired,
IdentitySubstitution,
ProviderDigestSubstitution,
Policy,
Capacity,
InFlight,
Indeterminate,
ReplayConflict,
Malformed,
}
pub fn canonical_provider_request_bytes(
request: &ProviderRequest,
) -> Result<Vec<u8>, ContractError> {
request.validate()?;
rmp_serde::to_vec(request).map_err(|_| ContractError::Encoding)
}
pub fn provider_request_sha256(request: &ProviderRequest) -> Result<[u8; 32], ContractError> {
Ok(Sha256::digest(canonical_provider_request_bytes(request)?).into())
}
pub fn canonical_invocation_bytes(
invocation: &TransportInvocation,
) -> Result<Vec<u8>, ContractError> {
invocation.request.validate()?;
if invocation.provider_request_sha256 != provider_request_sha256(&invocation.request)? {
return Err(ContractError::ProviderDigest);
}
rmp_serde::to_vec(invocation).map_err(|_| ContractError::Encoding)
}
pub(crate) fn require_id(value: &str, field: &'static str) -> Result<(), ContractError> {
if value.trim().is_empty() || value.trim() != value {
return Err(ContractError::Invalid(field));
}
Ok(())
}
fn require_content(value: &str, field: &'static str) -> Result<(), ContractError> {
if value.is_empty() {
return Err(ContractError::Invalid(field));
}
Ok(())
}
fn require_json_object(value: &str, field: &'static str) -> Result<(), ContractError> {
if !matches!(
serde_json::from_str::<serde_json::Value>(value),
Ok(serde_json::Value::Object(_))
) {
return Err(ContractError::Invalid(field));
}
Ok(())
}
fn require_provider_name(value: &str, field: &'static str) -> Result<(), ContractError> {
if value.is_empty()
|| value.len() > 64
|| !value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
{
return Err(ContractError::Invalid(field));
}
Ok(())
}
fn require_provider_call_id(value: &str, field: &'static str) -> Result<(), ContractError> {
if value.is_empty() || value.len() > 64 || !value.is_ascii() {
return Err(ContractError::Invalid(field));
}
Ok(())
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum ContractError {
#[error("unexpected schema")]
Schema,
#[error("invalid {0}")]
Invalid(&'static str),
#[error("duplicate tool {0:?}")]
DuplicateTool(String),
#[error("request identity substitution")]
Identity,
#[error("provider request digest substitution")]
ProviderDigest,
#[error("native request digest substitution")]
NativeDigest,
#[error("invocation expiry is outside the admitted horizon")]
Expiry,
#[error("transport event sequence is not contiguous")]
EventSequence,
#[error("canonical MessagePack encoding failed")]
Encoding,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fixtures::*;
#[test]
fn provider_digest_is_stable_and_content_addressed() {
let original = request();
let same = original.clone();
assert_eq!(
provider_request_sha256(&original).unwrap(),
provider_request_sha256(&same).unwrap()
);
let mut changed = original.clone();
changed.instructions.push_str(" Changed.");
assert_ne!(
provider_request_sha256(&original).unwrap(),
provider_request_sha256(&changed).unwrap()
);
}
#[test]
fn provider_contract_refuses_transport_side_fixup_pressure() {
let mut request = request();
request.output_format_name = Some("epiphany.role.result".to_string());
request.output_schema_json = Some(r#"{"type":"object"}"#.to_string());
assert_eq!(
request.validate(),
Err(ContractError::Invalid("output_format_name"))
);
request.output_format_name = None;
request.output_schema_json = None;
request.input.push(InputItem::ToolResult {
call_id: "x".repeat(65),
output: "evidence".to_string(),
});
assert_eq!(
request.validate(),
Err(ContractError::Invalid("input.call_id"))
);
}
#[test]
fn invocation_refuses_provider_request_substitution() {
let mut invocation = invocation();
invocation.request.model = "substituted".to_string();
assert_eq!(
invocation.validate(1_000, 2_000),
Err(ContractError::ProviderDigest)
);
}
#[test]
fn invocation_refuses_expired_or_blank_native_provenance() {
let mut invocation = invocation();
assert_eq!(
invocation.validate(2_001, 2_000),
Err(ContractError::Expiry)
);
invocation.expires_at_unix_ms = 2_500;
invocation.native_request_sha256 = [0; 32];
assert_eq!(
invocation.validate(2_001, 2_000),
Err(ContractError::Invalid("native_request_sha256"))
);
}
#[test]
fn typed_tool_round_survives_messagepack() {
let invocation = invocation();
let bytes = rmp_serde::to_vec(&invocation).unwrap();
let decoded: TransportInvocation = rmp_serde::from_slice(&bytes).unwrap();
assert_eq!(decoded, invocation);
assert!(matches!(
decoded.request.tools.as_slice(),
[ToolDefinition { name, .. }] if name == "read_source"
));
}
#[test]
fn transported_result_binds_identity_digest_and_event_order() {
let invocation = invocation();
let result = TransportResult::transported(
&invocation,
vec![TransportEvent {
sequence: 0,
payload: TransportEventPayload::ToolCall {
call_id: "call-1".to_string(),
name: "read_source".to_string(),
arguments: "{}".to_string(),
},
}],
receipt(&invocation),
);
assert_eq!(result.validate_against(&invocation), Ok(()));
let mut reordered = result;
let TransportDisposition::Transported { events, .. } = &mut reordered.disposition else {
unreachable!()
};
events[0].sequence = 1;
assert_eq!(
reordered.validate_against(&invocation),
Err(ContractError::EventSequence)
);
}
#[test]
fn receipt_refuses_caller_substitution() {
let invocation = invocation();
let mut substituted_caller = receipt(&invocation);
substituted_caller.caller_runtime_id = "ghostlight-dungeon-yggdrasil".to_string();
assert_eq!(
substituted_caller.validate_against(&invocation),
Err(ContractError::Identity)
);
let mut substituted_native_basis = receipt(&invocation);
substituted_native_basis.native_request_sha256 = [8; 32];
assert_eq!(
substituted_native_basis.validate_against(&invocation),
Err(ContractError::NativeDigest)
);
}
}

View file

@ -0,0 +1,514 @@
use std::io::{Read, Write};
use aes_gcm::aead::{Aead, Payload};
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
use rand_core::{OsRng, RngCore};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::contract::{
ContractError, ENVELOPE_SCHEMA_ID, TransportInvocation, TransportKey, TransportResult,
canonical_invocation_bytes, require_id,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EnvelopeKind {
Invocation,
Result,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TransportEnvelope {
pub schema_id: String,
pub caller_runtime_id: String,
pub request_id: String,
pub message_kind: EnvelopeKind,
pub nonce: [u8; 12],
pub ciphertext: Vec<u8>,
}
/// The longest caller id, request id or model name a service admits. The
/// frame arithmetic below depends on it. A request whose id is longer is
/// refused on the wire as malformed.
pub const MAX_IDENTITY_BYTES: usize = 256;
/// The most bytes a serialized envelope holding `ciphertext_len` ciphertext
/// bytes can occupy, with ids at their longest: the frame a caller payload
/// bound (`CallerAdmission`'s `max_payload_bytes`, a bound on ciphertext
/// length) needs. The v2 wire serializes the ciphertext as a MessagePack array
/// of integers: a byte of 128 or more takes two bytes, so the worst case is
/// twice the ciphertext, plus the array header and the envelope's other
/// fields at their longest. Measured from the real encoding, not assumed.
pub fn frame_bytes_for_payload(ciphertext_len: usize) -> usize {
let shell = rmp_serde::to_vec(&TransportEnvelope {
schema_id: ENVELOPE_SCHEMA_ID.to_string(),
caller_runtime_id: "x".repeat(MAX_IDENTITY_BYTES),
request_id: "x".repeat(MAX_IDENTITY_BYTES),
message_kind: EnvelopeKind::Invocation,
nonce: [0xff; 12],
ciphertext: Vec::new(),
})
.map_or(usize::MAX, |bytes| bytes.len());
// The shell holds an empty array header of one byte.
let header = match ciphertext_len {
0..=15 => 1,
16..=65_535 => 3,
_ => 5,
};
shell
.saturating_sub(1)
.saturating_add(header)
.saturating_add(ciphertext_len.saturating_mul(2))
}
/// The largest payload bound whose worst-case frame fits `max_frame_bytes`:
/// the inverse of [`frame_bytes_for_payload`]. `None` when the frame cannot
/// carry even an empty envelope.
pub fn payload_bytes_for_frame(max_frame_bytes: usize) -> Option<usize> {
// frame_bytes_for_payload(0) is the shell plus a one-byte array header.
let fixed = frame_bytes_for_payload(0) - 1;
let room = max_frame_bytes.checked_sub(fixed)?;
// Each array header width bounds the payloads it can describe; the
// widest payload any width admits is the answer.
let by_header = |header: usize, cap: usize| room.checked_sub(header).map(|r| (r / 2).min(cap));
[
by_header(5, usize::MAX),
by_header(3, 65_535),
by_header(1, 15),
]
.into_iter()
.flatten()
.max()
}
pub fn encrypt_invocation(
invocation: &TransportInvocation,
security: &TransportKey,
) -> Result<TransportEnvelope, ServiceError> {
let plaintext = canonical_invocation_bytes(invocation)?;
encrypt_message(
&invocation.caller_runtime_id,
invocation.request_id(),
EnvelopeKind::Invocation,
&plaintext,
security,
)
}
pub fn decrypt_invocation(
envelope: &TransportEnvelope,
security: &TransportKey,
) -> Result<TransportInvocation, ServiceError> {
validate_envelope(envelope, EnvelopeKind::Invocation)?;
let plaintext = decrypt_message(envelope, security)?;
let invocation: TransportInvocation =
rmp_serde::from_slice(&plaintext).map_err(|_| ServiceError::Encoding)?;
if invocation.caller_runtime_id != envelope.caller_runtime_id
|| invocation.request_id() != envelope.request_id
{
return Err(ServiceError::OuterIdentity);
}
Ok(invocation)
}
pub fn encrypt_result(
result: &TransportResult,
security: &TransportKey,
) -> Result<TransportEnvelope, ServiceError> {
let plaintext = rmp_serde::to_vec(result).map_err(|_| ServiceError::Encoding)?;
encrypt_message(
&result.caller_runtime_id,
&result.request_id,
EnvelopeKind::Result,
&plaintext,
security,
)
}
pub fn decrypt_result(
envelope: &TransportEnvelope,
security: &TransportKey,
invocation: &TransportInvocation,
) -> Result<TransportResult, ServiceError> {
validate_envelope(envelope, EnvelopeKind::Result)?;
let plaintext = decrypt_message(envelope, security)?;
let result: TransportResult =
rmp_serde::from_slice(&plaintext).map_err(|_| ServiceError::Encoding)?;
if result.caller_runtime_id != envelope.caller_runtime_id
|| result.request_id != envelope.request_id
{
return Err(ServiceError::OuterIdentity);
}
result.validate_against(invocation)?;
Ok(result)
}
#[derive(Debug)]
pub(crate) enum TransportFrameError {
Connection(std::io::Error),
Size,
}
pub(crate) fn read_transport_frame(
reader: &mut impl Read,
max_frame_bytes: usize,
) -> Result<Vec<u8>, TransportFrameError> {
let mut length = [0_u8; 4];
reader
.read_exact(&mut length)
.map_err(TransportFrameError::Connection)?;
let length = u32::from_be_bytes(length) as usize;
if length == 0 || length > max_frame_bytes {
return Err(TransportFrameError::Size);
}
let mut payload = vec![0_u8; length];
reader
.read_exact(&mut payload)
.map_err(TransportFrameError::Connection)?;
Ok(payload)
}
pub(crate) fn write_transport_frame(
writer: &mut impl Write,
payload: &[u8],
max_frame_bytes: usize,
) -> Result<(), TransportFrameError> {
if payload.is_empty() || payload.len() > max_frame_bytes || payload.len() > u32::MAX as usize {
return Err(TransportFrameError::Size);
}
writer
.write_all(&(payload.len() as u32).to_be_bytes())
.and_then(|()| writer.write_all(payload))
.and_then(|()| writer.flush())
.map_err(TransportFrameError::Connection)
}
pub(crate) fn encrypt_message(
caller_runtime_id: &str,
request_id: &str,
message_kind: EnvelopeKind,
plaintext: &[u8],
security: &TransportKey,
) -> Result<TransportEnvelope, ServiceError> {
require_id(caller_runtime_id, "caller_runtime_id")?;
require_id(request_id, "request_id")?;
let mut nonce = [0; 12];
OsRng.fill_bytes(&mut nonce);
let cipher = Aes256Gcm::new_from_slice(&security.0).map_err(|_| ServiceError::Encryption)?;
let aad = envelope_aad(
ENVELOPE_SCHEMA_ID,
caller_runtime_id,
request_id,
message_kind,
)?;
let ciphertext = cipher
.encrypt(
Nonce::from_slice(&nonce),
Payload {
msg: plaintext,
aad: &aad,
},
)
.map_err(|_| ServiceError::Encryption)?;
Ok(TransportEnvelope {
schema_id: ENVELOPE_SCHEMA_ID.to_string(),
caller_runtime_id: caller_runtime_id.to_string(),
request_id: request_id.to_string(),
message_kind,
nonce,
ciphertext,
})
}
pub(crate) fn decrypt_message(
envelope: &TransportEnvelope,
security: &TransportKey,
) -> Result<Vec<u8>, ServiceError> {
let cipher = Aes256Gcm::new_from_slice(&security.0).map_err(|_| ServiceError::Encryption)?;
let aad = envelope_aad(
&envelope.schema_id,
&envelope.caller_runtime_id,
&envelope.request_id,
envelope.message_kind,
)?;
cipher
.decrypt(
Nonce::from_slice(&envelope.nonce),
Payload {
msg: &envelope.ciphertext,
aad: &aad,
},
)
.map_err(|_| ServiceError::Encryption)
}
pub(crate) fn envelope_aad(
schema_id: &str,
caller_runtime_id: &str,
request_id: &str,
message_kind: EnvelopeKind,
) -> Result<Vec<u8>, ServiceError> {
rmp_serde::to_vec(&(schema_id, caller_runtime_id, request_id, message_kind))
.map_err(|_| ServiceError::Encoding)
}
pub(crate) fn validate_envelope(
envelope: &TransportEnvelope,
expected_kind: EnvelopeKind,
) -> Result<(), ServiceError> {
if envelope.schema_id != ENVELOPE_SCHEMA_ID || envelope.message_kind != expected_kind {
return Err(ServiceError::Envelope);
}
require_id(&envelope.caller_runtime_id, "caller_runtime_id")?;
require_id(&envelope.request_id, "request_id")?;
if envelope.ciphertext.is_empty() {
return Err(ServiceError::Envelope);
}
Ok(())
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum ServiceError {
#[error(transparent)]
Contract(#[from] ContractError),
#[error("invalid caller admission")]
InvalidAdmission,
#[error("caller runtime was admitted twice")]
DuplicateCaller,
#[error("callers must not share transport keys")]
SharedCallerKey,
#[error("connector replay store failed: {0}")]
ReplayStore(String),
#[error("connector replay record is malformed")]
InvalidReplayRecord,
#[error("caller key changed while durable replay records still exist")]
ReplayCallerKeyMismatch,
#[error("caller runtime is not admitted")]
CallerNotAdmitted,
#[error("unexpected transport envelope")]
Envelope,
#[error("transport envelope identity substitution")]
OuterIdentity,
#[error("transport encryption or authentication failed")]
Encryption,
#[error("transport MessagePack encoding failed")]
Encoding,
#[error("completion did not carry a transported provider outcome")]
InvalidCompletion,
#[error("completion has no active request")]
NoActiveRequest,
#[error("completion does not match the active request")]
ActiveRequestMismatch,
#[error("sealed result does not fit the frame bound")]
FrameBound,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fixtures::*;
#[test]
fn encrypted_invocation_hides_content_and_binds_outer_identity() {
let invocation = invocation();
let epiphany_key = security("epiphany-distinct-test-key");
let envelope = encrypt_invocation(&invocation, &epiphany_key).unwrap();
assert!(
!envelope
.ciphertext
.windows("Projected state".len())
.any(|window| window == b"Projected state")
);
assert_eq!(
decrypt_invocation(&envelope, &epiphany_key).unwrap(),
invocation
);
assert_eq!(
decrypt_invocation(&envelope, &security("wrong-caller-key")),
Err(ServiceError::Encryption)
);
let mut substituted = envelope;
substituted.request_id = "request-2".to_string();
assert_eq!(
decrypt_invocation(&substituted, &epiphany_key),
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)));
}
#[test]
fn the_worst_frame_is_the_real_encoding_of_an_all_high_ciphertext() {
let long = "x".repeat(MAX_IDENTITY_BYTES);
for length in [0, 1, 15, 16, 17, 255, 256, 65_535, 65_536, 70_000] {
let envelope = TransportEnvelope {
schema_id: ENVELOPE_SCHEMA_ID.to_string(),
caller_runtime_id: long.clone(),
request_id: long.clone(),
message_kind: EnvelopeKind::Invocation,
nonce: [0xff; 12],
ciphertext: vec![0xff; length],
};
assert_eq!(
rmp_serde::to_vec(&envelope).unwrap().len(),
frame_bytes_for_payload(length),
"{length}"
);
}
}
#[test]
fn a_sealed_envelope_never_exceeds_the_worst_frame_for_its_ciphertext() {
let key = security("epiphany-distinct-test-key");
for id in ["a".to_string(), "i".repeat(MAX_IDENTITY_BYTES)] {
for plaintext in [1, 100, 5_000, 70_000] {
let sealed =
encrypt_message(&id, &id, EnvelopeKind::Result, &vec![0x41; plaintext], &key)
.unwrap();
let real = rmp_serde::to_vec(&sealed).unwrap().len();
let worst = frame_bytes_for_payload(sealed.ciphertext.len());
assert!(real <= worst, "{real} > {worst}");
// Real ciphertext is random, so about half its bytes take two
// bytes: the worst case is not wildly above what is written.
if id.len() == MAX_IDENTITY_BYTES {
assert!(
worst <= real + sealed.ciphertext.len() + 64,
"{worst} {real}"
);
}
}
}
}
#[test]
fn the_payload_bound_a_frame_allows_inverts_the_frame_a_payload_needs() {
assert_eq!(
payload_bytes_for_frame(frame_bytes_for_payload(0) - 1),
None
);
assert_eq!(payload_bytes_for_frame(frame_bytes_for_payload(0)), Some(0));
for bytes in [1, 15, 16, 4096, 65_535, 65_536, 100_000, 2_000_000] {
let frame = frame_bytes_for_payload(bytes);
assert_eq!(payload_bytes_for_frame(frame), Some(bytes), "{bytes}");
assert!(
payload_bytes_for_frame(frame - 1).unwrap() < bytes,
"{bytes}"
);
}
}
}

View file

@ -0,0 +1,128 @@
//! Shared fixtures for the carried contract, envelope, client and service tests.
use crate::contract::*;
#[cfg(feature = "daemon")]
use crate::service::*;
#[cfg(feature = "daemon")]
use std::sync::atomic::{AtomicUsize, Ordering};
#[cfg(feature = "daemon")]
static TEST_STORE_SEQUENCE: AtomicUsize = AtomicUsize::new(0);
#[cfg(feature = "daemon")]
pub(crate) struct TestStore {
pub(crate) root: std::path::PathBuf,
pub(crate) path: std::path::PathBuf,
}
#[cfg(feature = "daemon")]
impl TestStore {
pub(crate) fn new() -> Self {
let sequence = TEST_STORE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let root = std::env::temp_dir().join(format!(
"model-connector-replay-{}-{sequence}",
std::process::id()
));
std::fs::create_dir_all(&root).unwrap();
let path = root.join("replay.cc");
Self { root, path }
}
}
#[cfg(feature = "daemon")]
impl Drop for TestStore {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
pub(crate) fn request() -> ProviderRequest {
let mut request = ProviderRequest::new(
"request-1",
"conversation-1",
"gpt-5.4",
"Return the typed result.",
);
request.input.push(InputItem::UserText {
text: "Projected state".to_string(),
});
request.tools.push(ToolDefinition {
name: "read_source".to_string(),
description: "Read one admitted source.".to_string(),
parameters_json: r#"{"type":"object"}"#.to_string(),
});
request
}
pub(crate) fn invocation() -> TransportInvocation {
TransportInvocation::new("epiphany-yggdrasil", 2_000, [7; 32], request()).unwrap()
}
pub(crate) fn receipt(invocation: &TransportInvocation) -> TransportReceipt {
TransportReceipt {
schema_id: 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: "fixture-sse".to_string(),
outcome: 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,
},
}
}
#[cfg(feature = "daemon")]
pub(crate) fn caller(
caller_runtime_id: &str,
key: &str,
max_concurrent_requests: usize,
) -> CallerAdmission {
CallerAdmission::new(
caller_runtime_id,
key,
1,
["gpt-5.4".to_string()],
max_concurrent_requests,
64 * 1024,
32_768,
)
.unwrap()
}
#[cfg(feature = "daemon")]
pub(crate) fn open_service(store: &TestStore, max_concurrent_requests: usize) -> TransportService {
TransportService::open(
&store.path,
[
caller(
"epiphany-yggdrasil",
"epiphany-distinct-test-key",
max_concurrent_requests,
),
caller(
"ghostlight-yggdrasil",
"ghostlight-distinct-test-key",
max_concurrent_requests,
),
],
5_000,
)
.unwrap()
}
pub(crate) fn security(key: &str) -> TransportKey {
TransportKey::from_connection_secret(key).unwrap()
}
#[cfg(feature = "daemon")]
pub(crate) fn invocation_for(caller_runtime_id: &str, request_id: &str) -> TransportInvocation {
let mut request = request();
request.request_id = request_id.to_string();
TransportInvocation::new(caller_runtime_id, 2_000, [7; 32], request).unwrap()
}

View file

@ -0,0 +1,20 @@
mod client;
mod contract;
mod envelope;
#[cfg(test)]
mod fixtures;
#[cfg(feature = "daemon")]
mod serve;
#[cfg(feature = "daemon")]
mod service;
pub use client::*;
pub use contract::*;
pub use envelope::*;
#[cfg(feature = "daemon")]
pub use serve::*;
#[cfg(feature = "daemon")]
pub use service::*;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

@ -0,0 +1,21 @@
# Golden v2 wire fixtures
Bytes of the v2 connector wire, produced once by CodexConnector at commit
`fd7cec4176f4e3f9a5aaa75e77ba10ce5e1522f8`. Ghostlight pins CodexConnector
`68fe94b`, not that commit; `src/lib.rs`, the whole v2 wire, is identical in
both, so these bytes are Ghostlight's wire. They are pinned by `tests/wire_golden_v2.rs`, so 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.Ẁ̧&Ìå̶̑[

View file

@ -0,0 +1,171 @@
//! Generator of the golden v2 wire fixtures beside it. It is not compiled by this
//! workspace: it was run once, as an example of CodexConnector at commit
//! fd7cec4176f4e3f9a5aaa75e77ba10ce5e1522f8 (see README.md here for the command).
//! It prints one `FIXTURE <name> <hex>` line per file.
use codex_connector as v2;
use serde::Serialize;
const KEY: &str = "parity-distinct-test-key";
const CALLER: &str = "parity-caller";
fn bytes<T: Serialize>(value: &T) -> Vec<u8> {
rmp_serde::to_vec(value).unwrap()
}
fn emit(name: &str, data: &[u8]) {
let hex: String = data.iter().map(|byte| format!("{byte:02x}")).collect();
println!("FIXTURE {name} {hex}");
}
fn request(choice: v2::CodexToolChoice) -> 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 = 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: v2::CodexToolChoice) -> v2::CodexTransportInvocation {
v2::CodexTransportInvocation::new(CALLER, 2_000, [7; 32], request(choice)).unwrap()
}
fn receipt(
invocation: &v2::CodexTransportInvocation,
outcome: v2::CodexTransportOutcome,
) -> v2::CodexTransportReceipt {
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,
}
}
fn main() {
let key = v2::CodexTransportKey::from_connection_secret(KEY).unwrap();
let auto = invocation(v2::CodexToolChoice::Auto);
let required = invocation(v2::CodexToolChoice::Required);
emit("invocation_auto.mp", &bytes(&auto));
emit("invocation_required.mp", &bytes(&required));
emit(
"canonical_invocation.bin",
&v2::canonical_invocation_bytes(&required).unwrap(),
);
emit(
"canonical_provider_request.bin",
&v2::canonical_provider_request_bytes(&required.request).unwrap(),
);
emit(
"provider_request_sha256.bin",
&v2::provider_request_sha256(&required.request).unwrap(),
);
emit(
"envelope_invocation.mp",
&bytes(&v2::encrypt_invocation(&required, &key).unwrap()),
);
let completed = v2::CodexTransportResult::transported(
&required,
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(
&required,
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: Some(4),
},
),
);
emit("result_transported.mp", &bytes(&completed));
emit(
"envelope_result_transported.mp",
&bytes(&v2::encrypt_result(&completed, &key).unwrap()),
);
let failed = v2::CodexTransportResult::transported(
&required,
Vec::new(),
receipt(
&required,
v2::CodexTransportOutcome::Failed {
failure_kind: "result_size".to_string(),
message: "provider result exceeded its byte bound".to_string(),
},
),
);
emit("result_failed.mp", &bytes(&failed));
emit(
"envelope_result_failed.mp",
&bytes(&v2::encrypt_result(&failed, &key).unwrap()),
);
use v2::CodexRefusal as R;
let refusals = [
("expired", R::Expired),
("identity_substitution", R::IdentitySubstitution),
("provider_digest_substitution", R::ProviderDigestSubstitution),
("policy", R::Policy),
("capacity", R::Capacity),
("in_flight", R::InFlight),
("indeterminate", R::Indeterminate),
("replay_conflict", R::ReplayConflict),
("malformed", R::Malformed),
];
for (name, refusal) in refusals {
let refused = v2::CodexTransportResult::refused(&required, refusal);
emit(&format!("result_refused_{name}.mp"), &bytes(&refused));
if name == "replay_conflict" {
emit(
"envelope_result_refused_replay_conflict.mp",
&bytes(&v2::encrypt_result(&refused, &key).unwrap()),
);
}
}
}

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