commit 0659cdacba3b7ad56e24b1ead5561f3e24db91f8 Author: Meta Vens Date: Mon Aug 24 14:14:45 2026 +0200 Establish Codex transport contract diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..33476d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target/ +*.cc +*.log + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1e91533 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,25 @@ +# CodexConnector Instructions + +CodexConnector is the independent credential and transport boundary for +Codex-compatible subscription inference. It is not an agent, scheduler, prompt +owner, tool executor, decision store, or consumer runtime. + +Keep one Cargo package and one production daemon entrypoint. Add another target +only when an independent lifecycle or privilege invariant requires it. + +The daemon may own credential loading and refresh, upstream client identity, +authenticated caller admission, bounded concurrency/replay, provider transport, +typed events, and transport receipts. Consumers own native requests, provider +lowering, prompts, schemas, tool execution, retry between passes, interpretation, +decisions, and state admission. + +Use typed Rust contracts, CultCache state, CultNet transport, and CultMesh/Eve +projection. JSON is permitted only at the upstream provider boundary or for +published schemas. Never log credentials, prompts, model output, or decrypted +request cargo. + +Before broad compilation, name the exact package/target/profile, output root, +current footprint, and retention decision. Focused library checks and tests are +the default. Code and tests must buy consequential behavior; delete mirrors, +wrappers, compatibility paths, and self-certifying status surfaces. + diff --git a/CONTRIBUTOR_LICENSE_AGREEMENT.md b/CONTRIBUTOR_LICENSE_AGREEMENT.md new file mode 100644 index 0000000..2906b20 --- /dev/null +++ b/CONTRIBUTOR_LICENSE_AGREEMENT.md @@ -0,0 +1,180 @@ +CodexConnector Contributor License Agreement +=========================================== + +Version 1.0 + +This Contributor License Agreement ("Agreement") governs Contributions +submitted to CodexConnector. + +This Agreement is designed as a broad contributor license, not a copyright +assignment. You keep ownership of Your Contributions, but You give the Project +Steward enough rights to use, sublicense, and relicense them without returning +to You for later permission. + +This structure follows the same general family of contributor-license +agreements used by projects and organizations such as the Apache Software +Foundation, Project Harmony, and Canonical: contributors retain ownership, while +the project receives a broad inbound license. + +1. Definitions +-------------- + +"You" or "Your" means the individual or legal entity making a Contribution. +For legal entities, "You" includes any entity that controls, is controlled by, +or is under common control with the contributing entity. + +"Project" means CodexConnector and any successor project or distribution that +continues, forks, renames, reorganizes, or commercializes substantially the +same Project-Authored Material. + +"Project Steward" means the copyright holder or holders who maintain +CodexConnector, any legal entity later formed or designated by them to own, +license, or commercialize Project-Authored Material, and any successor or +assignee authorized by them in writing. + +"Contribution" means any original work of authorship, invention, improvement, +modification, patch, documentation, design, media, text, code, configuration, +test, or other material intentionally submitted by You to the Project for +inclusion in, or use with, the Project. + +"Submitted" means communicated to the Project or its maintainers by any +reasonable channel used for Project development, including GitHub pull +requests, patches, commits, issues, discussions, email, chat, or other written +communication. A submission is not a Contribution if You clearly mark it +"Not a Contribution." + +2. Copyright License +-------------------- + +Subject to this Agreement, You grant the Project Steward a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable, transferable, +and sublicensable copyright license to use, reproduce, modify, prepare +derivative works of, publicly display, publicly perform, distribute, +sublicense, relicense, make available, sell, offer for sale, import, and +otherwise exploit Your Contributions, in source or object form, for any +purpose. + +This includes the right to license or relicense Your Contributions, and +derivative works of them, under any license or terms the Project Steward +chooses, including open-source, source-available, noncommercial, commercial, +proprietary, dual-license, or future license terms. + +3. Patent License +----------------- + +Subject to this Agreement, You grant the Project Steward and recipients of +Project distributions a perpetual, worldwide, non-exclusive, no-charge, +royalty-free, irrevocable patent license to make, have made, use, offer to +sell, sell, import, and otherwise transfer the Project and Your Contributions. + +This patent license covers patent claims that You can license and that would +necessarily be infringed by Your Contributions alone or by combination of Your +Contributions with the Project to which they were submitted. + +If any entity brings a written patent infringement claim alleging that Your +Contribution or the Project infringes a patent, any patent license granted to +that entity under this Agreement terminates as of the date that claim is filed. + +4. Ownership +------------ + +You retain ownership of Your Contributions. This Agreement does not prevent You +from using, licensing, selling, or contributing Your Contributions elsewhere, +so long as doing so does not violate another agreement or law. + +5. Representations +------------------ + +You represent that: + +- You are legally entitled to grant the rights in this Agreement. +- Each Contribution is Your original creation, or You have sufficient rights to + submit it under this Agreement. +- If Your employer, client, school, sponsor, or another organization may own or + control rights in Your Contribution, You have permission to submit it under + this Agreement or that organization has provided a separate written + authorization acceptable to the Project Steward. +- Your Contribution does not knowingly violate any agreement, license, trade + secret obligation, confidentiality obligation, or third-party right. +- You will identify any third-party code, data, model output, assets, license + restrictions, patent restrictions, trademark restrictions, or other legal + constraints of which You are aware and that are associated with Your + Contribution. + +6. Third-Party Material +----------------------- + +If You submit material that is not entirely Your original creation, You must +clearly identify its source and applicable license or restriction. + +The Project Steward may reject, remove, rewrite, or separately track that +material if it is incompatible with the Project's licensing or commercial +plans. + +7. Moral Rights +--------------- + +To the maximum extent permitted by law, You waive and agree not to assert any +moral rights, droit moral, rights of integrity, rights of attribution, or +similar rights that would interfere with the Project Steward's exercise of the +rights granted in this Agreement. + +Where those rights cannot be waived, You grant the Project Steward a +perpetual, worldwide, royalty-free license to exercise them and agree not to +assert them against the Project Steward, its sublicensees, distributors, +customers, or users. + +8. No Obligation To Use +----------------------- + +The Project Steward is not required to accept, use, preserve, publish, or +maintain any Contribution. + +9. No Support Or Warranty +------------------------- + +You are not required to provide support for Your Contributions. + +Unless required by law or separately agreed in writing, You provide Your +Contributions on an "AS IS" basis, without warranties or conditions of any +kind, including warranties or conditions of title, non-infringement, +merchantability, or fitness for a particular purpose. + +10. Acceptance +-------------- + +You accept this Agreement by signing it, by submitting a Contribution after +being presented with this Agreement, or by including the following statement in +a pull request, issue, patch, email, or other written submission: + +I agree that my Contributions to CodexConnector are submitted under the +CodexConnector Contributor License Agreement version 1.0. + +The Project Steward may require a separate signed copy or an organization-level +agreement before accepting non-trivial, employer-owned, client-owned, or +commercially significant Contributions. + +11. Entire Agreement For Contributions +-------------------------------------- + +This Agreement governs the rights You grant to the Project Steward for Your +Contributions unless You and the Project Steward sign a different written +agreement. + +If any part of this Agreement is unenforceable, the remaining parts remain in +effect to the maximum extent permitted by law. + +12. Signature Record +-------------------- + +Contributor name: + +Contributor GitHub username or other account: + +Contributor email: + +If signing for an organization, organization name and authority to sign: + +Signature: + +Date: diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1d82556 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,215 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[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 = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "codex-connector" +version = "0.1.0" +dependencies = [ + "rmp-serde", + "serde", + "sha2", + "thiserror", +] + +[[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", + "typenum", +] + +[[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 = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[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 = "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 = "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_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", +] + +[[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 = "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", +] + +[[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 = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bb1a230 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "codex-connector" +version = "0.1.0" +edition = "2024" +publish = false +autobins = false +license-file = "LICENSE" +description = "Credential-isolated typed transport for Codex-compatible subscription inference" +repository = "https://github.com/GameCult/CodexConnector" + +[lib] +path = "src/lib.rs" + +[dependencies] +rmp-serde = "1" +serde = { version = "1", features = ["derive"] } +sha2 = "0.10" +thiserror = "2" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..51833e3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,261 @@ +CodexConnector Repository License Notice +======================================= + +This repository is a multi-license distribution. Different parts of the +repository are available under different terms. + +Nothing in this file narrows, revokes, or relabels rights already granted by +upstream licenses. In particular, nothing in this file should be read as +withdrawing any permission granted by Apache License 2.0 for Apache-covered +material. + +1. Scope +-------- + +For this file: + +- `Project-Authored Material` means original software, documentation, and other + repository content authored for CodexConnector, except where a file or + directory states different terms. +- `Upstream Material` means material copied or adapted from third parties and + distributed under their own licenses. +- `Mixed Apache Files` means files under `vendor/codex/**` that contain + project-authored changes on top of upstream Apache-licensed material. + +2. Upstream And Third-Party Material +------------------------------------ + +- All material under `vendor/codex/**` remains subject to its upstream licenses + and notices, including Apache License 2.0 at `vendor/codex/LICENSE` and any + applicable `NOTICE` or file-level notices. +- Other third-party material distributed with the repository keeps its own + license terms where provided. +- You must continue to comply with those upstream terms when using, + redistributing, or modifying upstream material. + +3. Public License For Project-Authored Material +----------------------------------------------- + +Except where a file or directory says otherwise, Project-Authored Material +outside `vendor/codex/**` is licensed to the public under: + +- PolyForm Noncommercial License 1.0.0 + +The full text of that license appears in Appendix A of this file. + +Required Notice: Copyright 2026 CodexConnector authors and contributors. + +4. Commercial License Option +---------------------------- + +If you want to use Project-Authored Material for any purpose not permitted by +PolyForm Noncommercial License 1.0.0, you must obtain a separate written +commercial license from the applicable copyright holder or another party +expressly authorized in writing to grant that license. + +Commercial rights are not granted by: + +- source availability +- public access to this repository +- silence +- estoppel by implication +- the fact that the licensors are not yet organized as a corporation or LLC + +A natural person can own and license copyright. Until a separate business +entity is formed and designated in writing, any commercial license for +Project-Authored Material may be offered directly by the individual copyright +holder or holders. + +5. Mixed Apache Files Under `vendor/codex/**` +--------------------------------------------- + +Some files under `vendor/codex/**` contain project-authored changes layered on +top of upstream Apache-licensed code. + +For those files: + +- Apache License 2.0 and the applicable upstream notices still apply. +- You must not read this repository notice as converting those files into a + PolyForm-only codebase. +- To the extent the project owns copyright in new additions to those files, the + project may also offer separate commercial terms for its own contributions. +- This repository notice does not claim to erase or retract Apache-2.0 + permissions already granted for Apache-covered material. + +The strongest commercial boundary in this repository therefore applies to +clearly separable Project-Authored Material outside `vendor/codex/**`, +including `src/**` and other non-vendored project files. + +6. Open Source Status +--------------------- + +Because the public license path for Project-Authored Material is PolyForm +Noncommercial License 1.0.0 rather than an OSI-approved license, the +repository as a whole is source-available and dual-licensed. It is not an OSI +Open Source distribution as a whole. + +7. Reference Structures +----------------------- + +This licensing structure intentionally borrows the general dual-license shape +used by established projects such as Qt and MySQL: + +- one public-facing license path +- one separate commercial license path + +This repository does not claim that Qt or MySQL use the same exact public +license terms used here. Those projects are referenced only as structural +examples of dual licensing, not as the operative terms of this repository. + +8. Contributions +---------------- + +External contributions are accepted only under the CodexConnector Contributor +License Agreement in `CONTRIBUTOR_LICENSE_AGREEMENT.md`, or under a separate +written agreement accepted by the Project Steward. + +The contributor agreement is intentionally broad. Contributors keep ownership +of their contributions, but grant the Project Steward the right to sublicense +and relicense those contributions under any license or terms the Project +Steward chooses, including commercial or proprietary terms. + +The project should not accept non-trivial outside contributions unless the +contributor has agreed to that contributor agreement or an equivalent written +agreement. + +Appendix A: PolyForm Noncommercial License 1.0.0 +------------------------------------------------ + +The following text is included as the operative public license for +Project-Authored Material described in Section 3. + +# PolyForm Noncommercial License 1.0.0 + + + +## Acceptance + +In order to get any license under these terms, you must agree +to them as both strict obligations and conditions to all +your licenses. + +## Copyright License + +The licensor grants you a copyright license for the +software to do everything you might do with the software +that would otherwise infringe the licensor's copyright +in it for any permitted purpose. However, you may +only distribute the software according to Distribution +License and make changes or new works based on the software +according to Changes and New Works License. + +## Distribution License + +The licensor grants you an additional copyright license +to distribute copies of the software. Your license +to distribute covers distributing the software with +changes and new works permitted by Changes and New Works +License. + +## Notices + +You must ensure that anyone who gets a copy of any part of +the software from you also gets a copy of these terms or the +URL for them above, as well as copies of any plain-text lines +beginning with `Required Notice:` that the licensor provided +with the software. For example: + +> Required Notice: Copyright Yoyodyne, Inc. (http://example.com) + +## Changes and New Works License + +The licensor grants you an additional copyright license to +make changes and new works based on the software for any +permitted purpose. + +## Patent License + +The licensor grants you a patent license for the software that +covers patent claims the licensor can license, or becomes able +to license, that you would infringe by using the software. + +## Noncommercial Purposes + +Any noncommercial purpose is a permitted purpose. + +## Personal Uses + +Personal use for research, experiment, and testing for +the benefit of public knowledge, personal study, private +entertainment, hobby projects, amateur pursuits, or religious +observance, without any anticipated commercial application, +is use for a permitted purpose. + +## Noncommercial Organizations + +Use by any charitable organization, educational institution, +public research organization, public safety or health +organization, environmental protection organization, +or government institution is use for a permitted purpose +regardless of the source of funding or obligations resulting +from the funding. + +## Fair Use + +You may have "fair use" rights for the software under the +law. These terms do not limit them. + +## No Other Rights + +These terms do not allow you to sublicense or transfer any of +your licenses to anyone else, or prevent the licensor from +granting licenses to anyone else. These terms do not imply +any other licenses. + +## Patent Defense + +If you make any written claim that the software infringes or +contributes to infringement of any patent, your patent license +for the software granted under these terms ends immediately. If +your company makes such a claim, your patent license ends +immediately for work on behalf of your company. + +## Violations + +The first time you are notified in writing that you have +violated any of these terms, or done anything with the software +not covered by your licenses, your licenses can nonetheless +continue if you come into full compliance with these terms, +and take practical steps to correct past violations, within +32 days of receiving notice. Otherwise, all your licenses +end immediately. + +## No Liability + +As far as the law allows, the software comes as is, without +any warranty or condition, and the licensor will not be liable +to you for any damages arising out of these terms or the use +or nature of the software, under any kind of legal claim. + +## Definitions + +The licensor is the individual or entity offering these +terms, and the software is the software the licensor makes +available under these terms. + +You refers to the individual or entity agreeing to these +terms. + +Your company is any legal entity, sole proprietorship, +or other kind of organization that you work for, plus all +organizations that have control over, are under the control of, +or are under common control with that organization. Control +means ownership of substantially all the assets of an entity, +or the power to direct its management and policies by vote, +contract, or otherwise. Control can be direct or indirect. + +Your licenses are all the licenses granted to you for the +software under these terms. + +Use means anything you do with the software requiring one +of your licenses. diff --git a/README.md b/README.md new file mode 100644 index 0000000..137ea92 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# CodexConnector + +CodexConnector is a credential-isolated transport daemon for Codex-compatible +subscription inference. Epiphany and Ghostlight remain separate minds. They +derive their own exact provider requests, execute their own tools, interpret +their own results, and admit their own state. + +The connector owns one writable credential/refresh store, authenticated caller +admission, bounded transport physiology, exact provider-request receipts, and +redacted CultMesh health. It owns no prompt policy or decision authority. + +The current source establishes the v2 typed contract before adding the daemon. +This is deliberate: the existing Yggdrasil connector already proves the +process seam, but its single-caller, fixed-model, stateless, no-tools wire law +cannot become the shared contract by inertia. + +## Contract invariant + +Each consumer internally derives an exact typed Codex provider request. The +connector verifies and transports those exact bytes and returns a terminal +receipt binding their SHA-256. A consumer-native request digest is carried for +audit correlation; the connector does not interpret or rederive consumer-native +cognition. + +Tool calls are typed output. The connector never executes them. + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2367a99 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,578 @@ +use std::collections::HashSet; + +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use thiserror::Error; + +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"; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CodexProviderRequest { + pub schema_id: String, + pub request_id: String, + pub conversation_id: String, + pub model: String, + pub instructions: String, + pub input: Vec, + pub reasoning_effort: Option, + pub reasoning_summary: Option, + pub service_tier: Option, + pub output_contract_id: Option, + pub previous_response_id: Option, + pub tools: Vec, + pub output_schema_json: Option, + pub max_output_tokens: Option, + pub prompt_cache_key: Option, +} + +impl CodexProviderRequest { + pub fn new( + request_id: impl Into, + conversation_id: impl Into, + model: impl Into, + instructions: impl Into, + ) -> 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_contract_id: None, + previous_response_id: None, + tools: Vec::new(), + 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_contract_id.as_deref(), "output_contract_id"), + (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")?; + } + + let mut tool_names = HashSet::new(); + for tool in &self.tools { + require_id(&tool.name, "tool.name")?; + require_content(&tool.description, "tool.description")?; + require_content(&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 { + CodexInputItem::UserText { text } | CodexInputItem::AssistantText { text } => { + require_content(text, "input.text")?; + } + CodexInputItem::ToolCall { + call_id, + name, + arguments, + } => { + require_id(call_id, "input.call_id")?; + require_id(name, "input.tool_name")?; + require_content(arguments, "input.arguments")?; + } + CodexInputItem::ToolResult { call_id, output } => { + require_id(call_id, "input.call_id")?; + require_content(output, "input.output")?; + } + } + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum CodexInputItem { + 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 CodexToolDefinition { + pub name: String, + pub description: String, + pub parameters_json: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CodexTransportInvocation { + 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: CodexProviderRequest, +} + +impl CodexTransportInvocation { + pub fn new( + caller_runtime_id: impl Into, + expires_at_unix_ms: u64, + native_request_sha256: [u8; 32], + request: CodexProviderRequest, + ) -> Result { + 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 enum CodexTransportResult { + Refused { + schema_id: String, + request_id: String, + caller_runtime_id: String, + reason: CodexRefusal, + }, + Transported { + schema_id: String, + request_id: String, + caller_runtime_id: String, + provider_request_sha256: [u8; 32], + events: Vec, + receipt: CodexTransportReceipt, + }, +} + +impl CodexTransportResult { + pub fn validate_against( + &self, + invocation: &CodexTransportInvocation, + ) -> Result<(), ContractError> { + let (schema_id, request_id, caller_runtime_id) = match self { + Self::Refused { + schema_id, + request_id, + caller_runtime_id, + .. + } + | Self::Transported { + schema_id, + request_id, + caller_runtime_id, + .. + } => (schema_id, request_id, caller_runtime_id), + }; + if schema_id != RESULT_SCHEMA_ID { + return Err(ContractError::Schema); + } + if request_id != invocation.request_id() + || caller_runtime_id != &invocation.caller_runtime_id + { + return Err(ContractError::Identity); + } + + if let Self::Transported { + provider_request_sha256, + events, + receipt, + .. + } = self + { + if provider_request_sha256 != &invocation.provider_request_sha256 { + return Err(ContractError::ProviderDigest); + } + 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 CodexTransportEventPayload { + TextDelta { + text: String, + }, + ToolCall { + call_id: String, + name: String, + arguments: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CodexTransportEvent { + pub sequence: u64, + pub payload: CodexTransportEventPayload, +} + +impl CodexTransportEvent { + fn validate(&self) -> Result<(), ContractError> { + match &self.payload { + CodexTransportEventPayload::TextDelta { text } => require_content(text, "event.text"), + CodexTransportEventPayload::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 CodexTransportReceipt { + 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: CodexTransportOutcome, +} + +impl CodexTransportReceipt { + pub fn validate_against( + &self, + invocation: &CodexTransportInvocation, + ) -> 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 { + CodexTransportOutcome::Completed { + provider_response_id: Some(value), + .. + } => require_id(value, "receipt.provider_response_id")?, + CodexTransportOutcome::Failed { + failure_kind, + message, + } => { + require_id(failure_kind, "receipt.failure_kind")?; + require_content(message, "receipt.failure_message")?; + } + CodexTransportOutcome::Completed { + provider_response_id: None, + .. + } => {} + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum CodexTransportOutcome { + Completed { + provider_response_id: Option, + input_tokens: Option, + output_tokens: Option, + reasoning_output_tokens: Option, + cached_input_tokens: Option, + }, + Failed { + failure_kind: String, + message: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum CodexRefusal { + CallerNotAdmitted, + Expired, + IdentitySubstitution, + ProviderDigestSubstitution, + Policy, + ReplayConflict, + Malformed, +} + +pub fn canonical_provider_request_bytes( + request: &CodexProviderRequest, +) -> Result, ContractError> { + request.validate()?; + rmp_serde::to_vec(request).map_err(|_| ContractError::Encoding) +} + +pub fn provider_request_sha256(request: &CodexProviderRequest) -> Result<[u8; 32], ContractError> { + Ok(Sha256::digest(canonical_provider_request_bytes(request)?).into()) +} + +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(()) +} + +#[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::*; + + fn request() -> CodexProviderRequest { + let mut request = CodexProviderRequest::new( + "request-1", + "conversation-1", + "gpt-5.4", + "Return the typed result.", + ); + request.input.push(CodexInputItem::UserText { + text: "Projected state".to_string(), + }); + request.tools.push(CodexToolDefinition { + name: "read_source".to_string(), + description: "Read one admitted source.".to_string(), + parameters_json: r#"{"type":"object"}"#.to_string(), + }); + request + } + + fn invocation() -> CodexTransportInvocation { + CodexTransportInvocation::new("epiphany-yggdrasil", 2_000, [7; 32], request()).unwrap() + } + + fn receipt(invocation: &CodexTransportInvocation) -> CodexTransportReceipt { + CodexTransportReceipt { + 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: "codex-responses-sse".to_string(), + outcome: 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, + }, + } + } + + #[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 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: CodexTransportInvocation = rmp_serde::from_slice(&bytes).unwrap(); + assert_eq!(decoded, invocation); + assert!(matches!( + decoded.request.tools.as_slice(), + [CodexToolDefinition { name, .. }] if name == "read_source" + )); + } + + #[test] + fn transported_result_binds_identity_digest_and_event_order() { + let invocation = invocation(); + let result = CodexTransportResult::Transported { + schema_id: RESULT_SCHEMA_ID.to_string(), + request_id: invocation.request_id().to_string(), + caller_runtime_id: invocation.caller_runtime_id.clone(), + provider_request_sha256: invocation.provider_request_sha256, + events: vec![CodexTransportEvent { + sequence: 0, + payload: CodexTransportEventPayload::ToolCall { + call_id: "call-1".to_string(), + name: "read_source".to_string(), + arguments: "{}".to_string(), + }, + }], + receipt: receipt(&invocation), + }; + assert_eq!(result.validate_against(&invocation), Ok(())); + + let mut reordered = result; + let CodexTransportResult::Transported { events, .. } = &mut reordered 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) + ); + } +}