Establish Codex transport contract
This commit is contained in:
commit
0659cdacba
8 changed files with 1307 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
/target/
|
||||||
|
*.cc
|
||||||
|
*.log
|
||||||
|
|
||||||
25
AGENTS.md
Normal file
25
AGENTS.md
Normal 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.
|
||||||
|
|
||||||
180
CONTRIBUTOR_LICENSE_AGREEMENT.md
Normal file
180
CONTRIBUTOR_LICENSE_AGREEMENT.md
Normal 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
215
Cargo.lock
generated
Normal 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
18
Cargo.toml
Normal 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
261
LICENSE
Normal 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
26
README.md
Normal 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
578
src/lib.rs
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue