Establish Codex transport contract

This commit is contained in:
Meta Vens 2026-08-24 14:14:45 +02:00
commit 0659cdacba
8 changed files with 1307 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/target/
*.cc
*.log

25
AGENTS.md Normal file
View file

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

View file

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

215
Cargo.lock generated Normal file
View file

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

18
Cargo.toml Normal file
View file

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

261
LICENSE Normal file
View file

@ -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
<https://polyformproject.org/licenses/noncommercial/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.

26
README.md Normal file
View file

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

578
src/lib.rs Normal file
View file

@ -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<CodexInputItem>,
pub reasoning_effort: Option<String>,
pub reasoning_summary: Option<String>,
pub service_tier: Option<String>,
pub output_contract_id: Option<String>,
pub previous_response_id: Option<String>,
pub tools: Vec<CodexToolDefinition>,
pub output_schema_json: Option<String>,
pub max_output_tokens: Option<u32>,
pub prompt_cache_key: Option<String>,
}
impl CodexProviderRequest {
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_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<String>,
expires_at_unix_ms: u64,
native_request_sha256: [u8; 32],
request: CodexProviderRequest,
) -> 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 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<CodexTransportEvent>,
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<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 CodexRefusal {
CallerNotAdmitted,
Expired,
IdentitySubstitution,
ProviderDigestSubstitution,
Policy,
ReplayConflict,
Malformed,
}
pub fn canonical_provider_request_bytes(
request: &CodexProviderRequest,
) -> Result<Vec<u8>, 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)
);
}
}