Compare commits

..
Author SHA1 Message Date
Meta Vens
fd7cec4176 Merge readme-front-door: README front door
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:16:40 +02:00
Meta Vens
5b6ba919b7 README front door: value, problem, entry points, live versus planned
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:10:47 +02:00
Meta Vens
3ddc12e9f3 REC-100/REC-101: drop impossible gitlink requirement and fictitious RUST_LOG passthrough
deployment/idunn/recipe.toml declared required_gitlinks = ["vendor/cultcache-rs",
"vendor/cultnet-rs"], but HEAD (6519289, "Consume the core stack from CultLib
instead of forking it") already deleted both vendor directories and
.gitmodules. Idunn's TargetDeclaration::admit() (F:/Projects/Idunn/src/
deployment.rs:1390-1399) requires the operator's bound gitlink set to match
required_gitlinks exactly, so with no vendor dirs left to bind, this recipe
cannot currently be admitted at all — a live deployment defect, not just
dead text. required_gitlinks is #[serde(default)] (deployment.rs:26-27), so
dropping the line is a valid empty declaration, not a schema violation.

Also drops optional_environment = ["RUST_LOG"]: the recipe declares Idunn
may forward an operator-set RUST_LOG into the child process, but
src/provider_backend.rs:634 unconditionally overwrites it with
.env("RUST_LOG", "warn") right before spawn. The declaration currently
promises a passthrough the code never honors.

Kill attempts that failed to find a live consumer of the removed lines:
- git ls-tree origin/main -- vendor/: empty. No .gitmodules file exists.
- git grep -n "RUST_LOG" over the whole repo: only the two lines touched
  here (the recipe declaration and the hardcoded overwrite).
- Read F:/Projects/Idunn/src/deployment.rs to confirm both fields are
  optional (serde default) before cutting, per the assignment's
  instruction to understand Idunn's parser before touching recipe fields.

Collision check: git diff --stat origin/main...codex/ghostlight-release-binding
and ...codex/ghostlight-model-allowlist over deployment/idunn/recipe.toml
and src/provider_backend.rs: both empty, no in-flight work touches these
lines.

Validated deployment/idunn/recipe.toml still parses as TOML after the edit
(python tomllib load succeeded). Not touched: the RUST_LOG overwrite in
provider_backend.rs itself is a behavior question (should the child honor
an operator override?), not a dead-code cut, so it stays as-is per the
subtraction-only mandate.

REC-100, REC-101

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 14:05:19 +02:00
Meta Vens
65192892f0 Consume the core stack from CultLib instead of forking it
CultCache and CultNet were submodules on two more branches of the retiring
standalone repos. The CultCache fork held nothing CultLib lacks. The CultNet
fork held the pre-migration RuntimePresenceCorrelation as an enum where CultLib
has a struct, but the connector never referenced that type -- it came along with
the vendored library rather than being used -- so the migration costs nothing
here. Builds and its 35 tests pass against the pinned revision with no code
changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 04:23:05 +02:00
Meta Vens
b70ec4d4ba Wire AGENTS.md into Claude Code memory
Claude Code reads CLAUDE.md, not AGENTS.md, so this repo's doctrine was
invisible to it. This one-line import makes the existing AGENTS.md load
without duplicating or renaming it. Codex is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 22:22:28 +02:00
Meta Vens
a7ee296602 Republish Warming while awaiting a write lease 2026-09-03 10:38:41 +02:00
Meta Vens
78b4331ee5 Expose managed Connector route presence 2026-09-03 09:27:00 +02:00
Meta Vens
ede3c303bf Adopt Idunn runtime presence authority 2026-09-03 05:53:42 +02:00
Meta Vens
68fe94b947 Gate Connector traffic by exact runtime admission 2026-09-02 07:18:56 +02:00
Meta Vens
b2e76841bf Admit multiple models per connector caller 2026-08-27 16:03:31 +02:00
Meta Vens
2f29b90eeb Reserve connector capacity for every caller 2026-08-26 06:17:20 +02:00
Meta Vens
be1776ab45 Admit authenticated unicast connector clients 2026-08-25 14:02:22 +02:00
Meta Vens
80de3acc18 Admit additional connector callers 2026-08-25 05:13:56 +02:00
Meta Vens
b6b5102cb9 Give streamed Codex responses their full deadline 2026-08-25 04:18:13 +02:00
Meta Vens
e7ef6a565f Preserve safe SSE failure classes 2026-08-25 04:08:50 +02:00
12 changed files with 2393 additions and 581 deletions

View file

@ -16,12 +16,6 @@ repair the store. Consumers own native requests, provider lowering, prompts,
schemas, tool execution, retry between passes, interpretation, decisions, and schemas, tool execution, retry between passes, interpretation, decisions, and
state admission. state admission.
Keep the feature boundary exact. `--no-default-features` is the contract-only
surface used where a Mind seals provider request bytes. The default `client`
surface adds only authenticated framing and socket transport. `daemon` adds the
service authorities. Contract-only consumers must not compile client crypto or
socket physiology by accident.
Do not link upstream Codex crates into this package. Idunn freezes the official Do not link upstream Codex crates into this package. Idunn freezes the official
Codex binary as an exact package input. The child receives auth RPC only, never Codex binary as an exact package input. The child receives auth RPC only, never
consumer identity, prompts, tools, provider requests, or model output. consumer identity, prompts, tools, provider requests, or model output.

3
CLAUDE.md Normal file
View file

@ -0,0 +1,3 @@
Repository doctrine for this project lives in AGENTS.md, shared with Codex.
@AGENTS.md

56
Cargo.lock generated
View file

@ -145,18 +145,17 @@ dependencies = [
"anyhow", "anyhow",
"base64 0.23.1", "base64 0.23.1",
"chrono", "chrono",
"cultcache-rs 0.1.0 (git+https://github.com/GameCult/cultcache-rs.git?rev=fdbf3bf0a4b3f6b617c224d7439feb8e69d4474c)", "cultcache-rs",
"cultnet-rs", "cultnet-rs",
"ed25519-dalek", "fs2",
"libc",
"rand_core 0.6.4", "rand_core 0.6.4",
"rmp-serde", "rmp-serde",
"serde", "serde",
"serde_bytes",
"serde_json", "serde_json",
"sha2", "sha2",
"thiserror", "thiserror",
"ureq", "ureq",
"uuid",
"zeroize", "zeroize",
] ]
@ -204,28 +203,11 @@ dependencies = [
[[package]] [[package]]
name = "cultcache-rs" name = "cultcache-rs"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/GameCult/CultLib.git?rev=75c180782aeba7cfd22d6412877397708a4ed28f#75c180782aeba7cfd22d6412877397708a4ed28f" source = "git+https://github.com/GameCult/CultLib.git?rev=e171eca32329baa928f4a1d810401a8b4c857029#e171eca32329baa928f4a1d810401a8b4c857029"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"cultcache-rs-derive 0.1.0 (git+https://github.com/GameCult/CultLib.git?rev=75c180782aeba7cfd22d6412877397708a4ed28f)", "cultcache-rs-derive",
"fs2",
"rmp-serde",
"serde",
"serde_bytes",
"serde_json",
"uuid",
"windows-sys 0.59.0",
]
[[package]]
name = "cultcache-rs"
version = "0.1.0"
source = "git+https://github.com/GameCult/cultcache-rs.git?rev=fdbf3bf0a4b3f6b617c224d7439feb8e69d4474c#fdbf3bf0a4b3f6b617c224d7439feb8e69d4474c"
dependencies = [
"anyhow",
"chrono",
"cultcache-rs-derive 0.1.0 (git+https://github.com/GameCult/cultcache-rs.git?rev=fdbf3bf0a4b3f6b617c224d7439feb8e69d4474c)",
"fs2", "fs2",
"redb", "redb",
"rmp-serde", "rmp-serde",
@ -239,17 +221,7 @@ dependencies = [
[[package]] [[package]]
name = "cultcache-rs-derive" name = "cultcache-rs-derive"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/GameCult/CultLib.git?rev=75c180782aeba7cfd22d6412877397708a4ed28f#75c180782aeba7cfd22d6412877397708a4ed28f" source = "git+https://github.com/GameCult/CultLib.git?rev=e171eca32329baa928f4a1d810401a8b4c857029#e171eca32329baa928f4a1d810401a8b4c857029"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "cultcache-rs-derive"
version = "0.1.0"
source = "git+https://github.com/GameCult/cultcache-rs.git?rev=fdbf3bf0a4b3f6b617c224d7439feb8e69d4474c#fdbf3bf0a4b3f6b617c224d7439feb8e69d4474c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -259,13 +231,14 @@ dependencies = [
[[package]] [[package]]
name = "cultnet-rs" name = "cultnet-rs"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/GameCult/CultLib.git?rev=75c180782aeba7cfd22d6412877397708a4ed28f#75c180782aeba7cfd22d6412877397708a4ed28f" source = "git+https://github.com/GameCult/CultLib.git?rev=e171eca32329baa928f4a1d810401a8b4c857029#e171eca32329baa928f4a1d810401a8b4c857029"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"anyhow", "anyhow",
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
"cultcache-rs 0.1.0 (git+https://github.com/GameCult/CultLib.git?rev=75c180782aeba7cfd22d6412877397708a4ed28f)", "cultcache-rs",
"ed25519-dalek",
"hmac", "hmac",
"rand", "rand",
"rmp-serde", "rmp-serde",
@ -276,6 +249,8 @@ dependencies = [
"sha2", "sha2",
"socket2", "socket2",
"uuid", "uuid",
"windows-sys 0.61.2",
"zeroize",
] ]
[[package]] [[package]]
@ -1184,6 +1159,15 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"

View file

@ -17,39 +17,31 @@ path = "src/main.rs"
required-features = ["daemon"] required-features = ["daemon"]
[features] [features]
default = ["client"] default = []
client = [
"dep:aes-gcm",
"dep:rand_core",
"dep:zeroize",
]
daemon = [ daemon = [
"client",
"dep:anyhow", "dep:anyhow",
"dep:chrono", "dep:chrono",
"dep:cultcache-rs", "dep:cultcache-rs",
"dep:cultnet-rs", "dep:cultnet-rs",
"dep:ed25519-dalek", "dep:fs2",
"dep:serde_bytes", "dep:libc",
"dep:ureq", "dep:ureq",
"dep:uuid",
] ]
[dependencies] [dependencies]
aes-gcm = { version = "0.10", optional = true } aes-gcm = "0.10"
anyhow = { version = "1", optional = true } anyhow = { version = "1", optional = true }
base64 = "0.23" base64 = "0.23"
chrono = { version = "0.4", features = ["serde"], optional = true } chrono = { version = "0.4", features = ["serde"], optional = true }
cultcache-rs = { git = "https://github.com/GameCult/cultcache-rs.git", rev = "fdbf3bf0a4b3f6b617c224d7439feb8e69d4474c", optional = true } cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "e171eca32329baa928f4a1d810401a8b4c857029", package = "cultcache-rs", optional = true }
cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "75c180782aeba7cfd22d6412877397708a4ed28f", optional = true } cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "e171eca32329baa928f4a1d810401a8b4c857029", package = "cultnet-rs", optional = true }
ed25519-dalek = { version = "2.2", optional = true } fs2 = { version = "0.4", optional = true }
rand_core = { version = "0.6", features = ["getrandom"], optional = true } libc = { version = "0.2", optional = true }
rand_core = { version = "0.6", features = ["getrandom"] }
rmp-serde = "1" rmp-serde = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_bytes = { version = "0.11", optional = true }
serde_json = "1" serde_json = "1"
sha2 = "0.10" sha2 = "0.10"
thiserror = "2" thiserror = "2"
ureq = { version = "3.4", default-features = false, features = ["rustls"], optional = true } ureq = { version = "3.4", default-features = false, features = ["rustls"], optional = true }
uuid = { version = "1", features = ["v4"], optional = true } zeroize = "1"
zeroize = { version = "1", optional = true }

View file

@ -5,6 +5,14 @@ subscription inference. Epiphany and Ghostlight remain separate minds. They
derive their own exact provider requests, execute their own tools, interpret derive their own exact provider requests, execute their own tools, interpret
their own results, and admit their own state. their own results, and admit their own state.
**Valuable here:** the consumer ABI in `src/lib.rs`. It holds the v2 typed contracts (`CodexProviderRequest`, `CodexTransportInvocation`, `CodexTransportReceipt`), exact Responses lowering (`canonical_responses_body`), AES-256-GCM envelopes (`encrypt_invocation`, `decrypt_result`) and the one-request client (`CodexConnectorClient`). The daemon behind it (`src/daemon.rs`, `src/provider_backend.rs`) is the only process that ever holds the Codex credential.
**Problem it solves:** several minds (Epiphany, Ghostlight) need Codex-compatible subscription inference, but must not each hold or refresh the credential, copy wire law, or hand cognition to a transport service. The connector isolates the credential and transports exact provider requests with receipts, and decides nothing about prompts or tools.
**Entry points:** library `src/lib.rs` (no daemon code, safe to depend on); daemon feature `--features daemon`, binary `codex-connector` (`src/main.rs`); Idunn declaration `deployment/idunn/recipe.toml`, deployed with `idunn up codex-connector`; verification commands are listed below.
**Status:** live as the library plus the daemon and its Idunn recipe, last changed 2026-09-13. This repo records no planned work; changes to the contract below are the open surface.
The service owns authenticated caller admission, bounded transport physiology, The service owns authenticated caller admission, bounded transport physiology,
exact provider-request receipts, and redacted CultMesh health. A private pinned exact provider-request receipts, and redacted CultMesh health. A private pinned
official `codex app-server` child is the sole writer and refresh authority for official `codex app-server` child is the sole writer and refresh authority for
@ -12,13 +20,11 @@ one credential store. The public connector reads the credential only after the
child's private auth RPC completes, then performs the exact Responses request. child's private auth RPC completes, then performs the exact Responses request.
It owns no prompt policy or decision authority. It owns no prompt policy or decision authority.
The library has three explicit compile surfaces. With no features it exposes The default library is the shared consumer ABI: v2 typed contracts, exact
only the v2 provider/result contracts and their deterministic validation. The provider lowering, encryption, bounded direct-pipe framing, one-request socket
default `client` feature adds encryption, bounded direct-pipe framing, and the transport, and exact result validation. It does not compile CultCache, HTTP,
one-request socket client. It still compiles no CultCache, HTTP, TLS, provider TLS, provider auth, replay, or daemon code. Epiphany and Ghostlight consume this
auth, replay, or daemon code. The `daemon` feature includes the client surface surface instead of copying wire law.
and the service. Epiphany and Ghostlight can therefore type and seal their exact
provider requests without importing transport physiology into their Minds.
Connection establishment is bounded independently. Response read/write timeout Connection establishment is bounded independently. Response read/write timeout
is optional so a consumer's typed outer pass may remain the sole deadline owner. is optional so a consumer's typed outer pass may remain the sole deadline owner.
@ -47,21 +53,62 @@ No upstream Codex crate is linked into the package. The official binary is a
pinned deployment input, keeping Codex's application build graph outside both pinned deployment input, keeping Codex's application build graph outside both
consumers and this small daemon. consumers and this small daemon.
Focused verification proves each real build surface independently: Focused verification uses the two real build surfaces:
```text ```text
cargo test --lib --no-default-features
cargo test --lib cargo test --lib
cargo test --lib --features daemon cargo test --lib --features daemon
cargo check --bin codex-connector --features daemon cargo check --bin codex-connector --features daemon
``` ```
The same production binary can initialize a typed single-caller `.cc` The same production binary can initialize a typed single-caller `.cc`
configuration for first deployment, then serve only from that admitted state. configuration for first deployment, then serve from that state. Outside an
The daemon publishes private-free signed health to Idunn from its own runtime Idunn-managed launch the configuration's loopback bind remains available for
identity. Its source revision also owns the exact Codex Linux package and local operation. In a managed launch `GAMECULT_IDUNN_CANDIDATE_BIND` is the
binary hashes in `deployment/codex-linux-x64.manifest`, so changing the provider effective private bind. Idunn admits the candidate and controls route promotion;
toolchain necessarily changes the connector release selected by Idunn. the Connector only binds and serves its private endpoint.
The repo-visible Idunn declaration is
`deployment/idunn/recipe.toml`. A normal deployment is `idunn up
codex-connector`. The declaration pins the Codex package and extracted binary,
builds the one daemon, declares its process-bound replay and Codex credential
state, and requires a compatible Odin rendezvous capability before start.
`CODEX_CONNECTOR_CULTNET_RUDP` is only the current transport address for that
rendezvous. The selected managed dependency must carry an explicit `rudp://`,
`udp://`, or deliberately bare socket endpoint, and both addresses must agree.
Idunn supplies a root-owned `GAMECULT_IDUNN_RUNTIME_BUNDLE` containing the exact
typed Expected and activation records. Systemd opens the root-owned activation
and stable-identity sources and passes exactly two named descriptors,
`gamecult-idunn-runtime-activation-key` and
`gamecult-runtime-presence-identity`, to the Connector parent. Connector marks
both close-on-exec, consumes and closes them before spawning the Codex
app-server, and strips systemd's descriptor environment from that child.
Connector publishes `gamecult.runtime_presence_health.v2` through CultNet with
both the stable provider proof and the activation-scoped proof. Target, plan,
incarnation, release, runtime, endpoint, health contract, state lineage, and
capability claims are bound to Expected rather than reconstructed from service
configuration. The binary pins the canonical two-slot state-contract digest, so
an Expected projection for a different recipe cannot be attested under the same
generation label.
The managed command receives Idunn's admitted state root as an explicit launch
binding. Connector derives `codex-home` and `replay.cc` from that root and
rejects configuration that names a different layout. The managed configuration
and its CultCache lock must themselves be root-custodied read-only files; they
may describe caller admission and fixed runtime bounds, but cannot mutate those
inputs behind an admitted incarnation.
The candidate socket is bound before the initial `warming` presence so the
reported endpoint is an observed service fact. Connector then waits for the
root-owned typed process-write lease that names that exact warming presence
before opening replay state or starting the official child that may refresh
`codex-home/auth.json`. It holds the lease's pre-created sibling lock shared for
the lifetime of both writers, so Idunn stops the process before moving write
authority to another incarnation. Periodic `active` or `degraded` presence is
factual provider observation and carries the exact current lease digest. Odin
derives Present and Ready by correlating these statements; Idunn decides route
admission and lifecycle actuation.
## Contract invariant ## Contract invariant

View file

@ -1,4 +0,0 @@
schema_version=gamecult.codex_connector.codex_toolchain.v1
package_url=https://registry.npmjs.org/@openai/codex/-/codex-0.150.0-alpha.7-linux-x64.tgz
package_sha256=8aca3112aef60127d4ffaf4c6f08e524fa7315279a1d08e7aa9c5e7f1a6943e0
binary_sha256=aca268dc02dcef8b1ea9f528a10173d5071ae76d659a03cce6458bbebe228bee

View file

@ -0,0 +1,122 @@
schema = "gamecult.idunn.target_declaration.v1"
target = "codex-connector"
source_stamp_environment = "CODEX_CONNECTOR_BUILD_COMMIT"
[[steps]]
id = "extract-codex"
phase = "prepare"
runner = "rust-build"
argv = [
"tar",
"--extract",
"--gzip",
"--file",
".idunn-inputs/codex-linux-x64.tgz",
"--strip-components=4",
"package/vendor/x86_64-unknown-linux-musl/bin/codex",
]
[[steps]]
id = "test-contract"
phase = "test"
runner = "rust-build"
argv = ["cargo", "test", "--locked"]
[[steps]]
id = "test-daemon"
phase = "test"
runner = "rust-build"
argv = ["cargo", "test", "--locked", "--features", "daemon"]
[[steps]]
id = "build-daemon"
phase = "build"
runner = "rust-build"
argv = [
"cargo",
"build",
"--locked",
"--release",
"--features",
"daemon",
"--bin",
"codex-connector",
]
[[external_inputs]]
id = "codex-linux-x64"
url = "https://registry.npmjs.org/@openai/codex/-/codex-0.150.0-alpha.7-linux-x64.tgz"
sha256 = "8aca3112aef60127d4ffaf4c6f08e524fa7315279a1d08e7aa9c5e7f1a6943e0"
runner = "rust-build"
destination = ".idunn-inputs/codex-linux-x64.tgz"
[[artifacts]]
id = "codex-connector"
source_kind = "runner-output"
runner = "rust-build"
source = "target/release/codex-connector"
destination = "codex-connector"
executable = true
[[artifacts]]
id = "codex"
source_kind = "runner-output"
runner = "rust-build"
source = "codex"
destination = "codex"
expected_sha256 = "aca268dc02dcef8b1ea9f528a10173d5071ae76d659a03cce6458bbebe228bee"
executable = true
[service]
executable_artifact = "codex-connector"
required_adjacent_artifacts = ["codex"]
arguments = [
{ kind = "literal", value = "--config" },
{ kind = "binding", name = "config_path" },
{ kind = "literal", value = "--state-root" },
{ kind = "binding", name = "state_root" },
]
transport = "tcp"
route_required = true
required_environment = [
"CODEX_CONNECTOR_CULTNET_RUDP",
"GAMECULT_IDUNN_CANDIDATE_BIND",
"GAMECULT_IDUNN_PROCESS_WRITE_LEASE",
"GAMECULT_IDUNN_RUNTIME_BUNDLE",
]
[service.health]
contract = "codex-connector.runtime-health.v1"
[state]
schema_generation = "connector-v1"
[[state.slots]]
id = "replay"
relative_path = "replay.cc"
kind = "cultcache-file"
schema = "gamecult.codex.replay_record.v1"
writer = "process-bound-single-writer"
recovery = "preserve"
startup = "create-or-open-after-write-lease"
[[state.slots]]
id = "codex-auth"
relative_path = "codex-home/auth.json"
kind = "external-file"
schema = "openai.codex.auth-json.v1"
writer = "process-bound-single-writer"
recovery = "preserve"
startup = "create-or-open-after-write-lease"
[[provides]]
capability = "gamecult.codex.subscription-inference"
schema = "gamecult.codex.transport_envelope.v2"
compatibility = "v2"
[[dependencies]]
kind = "bootstrap"
capability = "odin.verse-rendezvous"
schema = "odin.verse-topology.v1"
compatibility = "v1"
startup = "before-start"

View file

@ -7,11 +7,20 @@ use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[cfg(target_os = "linux")]
use std::os::unix::fs::MetadataExt;
use cultcache_rs::{CultCache, DatabaseEntry, SingleFileMessagePackBackingStore}; use cultcache_rs::{CultCache, DatabaseEntry, SingleFileMessagePackBackingStore};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use zeroize::Zeroizing; use zeroize::Zeroizing;
#[cfg(target_os = "linux")]
use cultnet_rs::{
CultNetMessage, CultNetWireContract, GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA,
decode_cultnet_message_from_slice, encode_cultnet_message_to_vec,
};
use crate::{ use crate::{
CodexAppServerConfig, CodexCallerAdmission, CodexProviderBackend, CodexProviderBackendError, CodexAppServerConfig, CodexCallerAdmission, CodexProviderBackend, CodexProviderBackendError,
CodexTransportAdmission, CodexTransportEnvelope, CodexTransportService, ServiceError, CodexTransportAdmission, CodexTransportEnvelope, CodexTransportService, ServiceError,
@ -110,18 +119,24 @@ impl CodexDaemonConfig {
{ {
return Err(CodexDaemonError::InvalidConfig("runtime bounds")); return Err(CodexDaemonError::InvalidConfig("runtime bounds"));
} }
let required_connections = required_connection_capacity(&self.callers)?;
if self.max_connections < required_connections {
return Err(CodexDaemonError::InvalidConfig("connection capacity"));
}
let mut caller_ids = HashSet::new(); let mut caller_ids = HashSet::new();
for caller in &self.callers { for caller in &self.callers {
let mut allowed_models = HashSet::new();
if caller.caller_runtime_id.trim().is_empty() if caller.caller_runtime_id.trim().is_empty()
|| caller.caller_runtime_id.trim() != caller.caller_runtime_id || caller.caller_runtime_id.trim() != caller.caller_runtime_id
|| !caller_ids.insert(caller.caller_runtime_id.as_str()) || !caller_ids.insert(caller.caller_runtime_id.as_str())
|| caller.connection_key_file.as_os_str().is_empty() || caller.connection_key_file.as_os_str().is_empty()
|| caller.connection_key_epoch == 0 || caller.connection_key_epoch == 0
|| caller.allowed_models.is_empty() || caller.allowed_models.is_empty()
|| caller || caller.allowed_models.iter().any(|model| {
.allowed_models model.trim().is_empty()
.iter() || model.trim() != model
.any(|model| model.trim().is_empty() || model.trim() != model) || !allowed_models.insert(model.as_str())
})
|| caller.max_concurrent_requests == 0 || caller.max_concurrent_requests == 0
|| caller.max_payload_bytes < 4096 || caller.max_payload_bytes < 4096
|| caller.max_payload_bytes > self.max_frame_bytes - FRAME_OVERHEAD_BUDGET || caller.max_payload_bytes > self.max_frame_bytes - FRAME_OVERHEAD_BUDGET
@ -132,6 +147,40 @@ impl CodexDaemonConfig {
} }
Ok(bind) Ok(bind)
} }
pub fn admit_caller(&mut self, caller: CodexCallerConfig) -> Result<(), CodexDaemonError> {
let mut candidate = self.clone();
if candidate
.callers
.iter()
.any(|existing| existing.caller_runtime_id == caller.caller_runtime_id)
{
return Err(CodexDaemonError::InvalidConfig("duplicate caller"));
}
candidate.max_frame_bytes = candidate.max_frame_bytes.max(
caller
.max_payload_bytes
.saturating_add(FRAME_OVERHEAD_BUDGET),
);
candidate.callers.push(caller);
candidate.max_connections = required_connection_capacity(&candidate.callers)?;
candidate.validate()?;
*self = candidate;
Ok(())
}
}
fn required_connection_capacity(callers: &[CodexCallerConfig]) -> Result<usize, CodexDaemonError> {
Ok(caller_request_capacity(callers)?.max(8))
}
fn caller_request_capacity(callers: &[CodexCallerConfig]) -> Result<usize, CodexDaemonError> {
callers
.iter()
.try_fold(0_usize, |total, caller| {
total.checked_add(caller.max_concurrent_requests)
})
.ok_or(CodexDaemonError::InvalidConfig("connection capacity"))
} }
pub fn load_daemon_config(path: &Path) -> Result<CodexDaemonConfig, CodexDaemonError> { pub fn load_daemon_config(path: &Path) -> Result<CodexDaemonConfig, CodexDaemonError> {
@ -150,6 +199,30 @@ pub fn load_daemon_config(path: &Path) -> Result<CodexDaemonConfig, CodexDaemonE
Ok(config) Ok(config)
} }
fn load_daemon_config_read_only(path: &Path) -> Result<CodexDaemonConfig, CodexDaemonError> {
let store = SingleFileMessagePackBackingStore::new(path);
let config = store
.with_read_only_shared_snapshot(|entries| {
let [envelope] = entries.as_slice() else {
anyhow::bail!("connector configuration must contain exactly one record");
};
if envelope.key != CONFIG_KEY
|| envelope.r#type != CodexDaemonConfig::TYPE
|| envelope.schema_id.as_deref() != Some(CodexDaemonConfig::TYPE)
{
anyhow::bail!("connector configuration has the wrong typed envelope");
}
let config: CodexDaemonConfig = rmp_serde::from_slice(&envelope.payload)?;
if rmp_serde::to_vec(&config)? != envelope.payload {
anyhow::bail!("connector configuration is not canonical positional MessagePack");
}
Ok(config)
})
.map_err(config_store_error)?;
config.validate()?;
Ok(config)
}
pub fn write_daemon_config( pub fn write_daemon_config(
path: &Path, path: &Path,
config: &CodexDaemonConfig, config: &CodexDaemonConfig,
@ -167,37 +240,109 @@ pub fn write_daemon_config(
Ok(()) Ok(())
} }
pub fn serve(config_path: &Path) -> Result<(), CodexDaemonError> { pub fn serve(
let config = load_daemon_config(config_path)?; config_path: &Path,
let bind = config.validate()?; managed_state_root: Option<&Path>,
let service = Arc::new(Mutex::new(CodexTransportService::open( ) -> Result<(), CodexDaemonError> {
&config.replay_store, #[cfg(target_os = "linux")]
load_admissions(&config)?, if std::env::var_os(crate::idunn_health::IDUNN_RUNTIME_BUNDLE_ENVIRONMENT).is_some() {
config.max_expiry_skew_ms, crate::idunn_health::require_managed_config_store(config_path)
)?)); .map_err(CodexDaemonError::Health)?;
}
let config = load_daemon_config_read_only(config_path)?;
let configured_bind = config.validate()?;
#[cfg(target_os = "linux")]
let bind = effective_bind_from_environment(configured_bind)?;
#[cfg(not(target_os = "linux"))]
let bind = configured_bind;
#[cfg(target_os = "linux")]
let (mut codex_executable, mut codex_home, mut replay_store) =
effective_runtime_paths_from_environment(&config, managed_state_root)?;
#[cfg(not(target_os = "linux"))]
let (codex_executable, codex_home, replay_store) = match managed_state_root {
None => (
config.codex_executable.clone(),
config.codex_home.clone(),
config.replay_store.clone(),
),
Some(_) => {
return Err(CodexDaemonError::InvalidConfig(
"Idunn state root on an unmanaged platform",
));
}
};
let admissions = load_admissions(&config)?;
#[cfg(target_os = "linux")]
// Consume the systemd-owned signer descriptors before opening another
// process descriptor. Their numeric 3/4 contract is valid only at the
// managed process entry boundary.
let mut prepared_presence_publisher = prepare_runtime_presence_publisher(bind, &config)?;
// Binding the candidate socket is service physiology. Idunn owns whether
// that candidate receives traffic, so Connector carries no second route
// or traffic-admission gate.
let listener = TcpListener::bind(bind).map_err(CodexDaemonError::Listen)?;
let bound = listener.local_addr().map_err(CodexDaemonError::Listen)?;
#[cfg(target_os = "linux")]
if let Some(publisher) = prepared_presence_publisher.as_mut() {
if bound != bind {
return Err(CodexDaemonError::InvalidConfig("Idunn candidate bind"));
}
let warming_sha256 = publisher
.publish("warming", "awaiting-process-write-lease")
.map_err(CodexDaemonError::Health)?;
publisher
.acquire_process_write_lease(&warming_sha256, Duration::from_secs(120))
.map_err(CodexDaemonError::Health)?;
// The incumbent is fenced only when Idunn can replace the lease under
// our shared lock. Re-observe every write-capable path after that
// handoff so a pre-lease filesystem shape cannot survive into Active.
(codex_executable, codex_home, replay_store) =
effective_runtime_paths_from_environment(&config, managed_state_root)?;
}
#[cfg(target_os = "linux")]
// Keep the shared lease lock in the main service body as well as the
// publisher thread. A health-thread failure must not release write
// authority while replay or the official credential writer remains live.
let _process_write_lease_guard = prepared_presence_publisher
.as_ref()
.map(|publisher| publisher.process_write_lease_guard())
.transpose()
.map_err(CodexDaemonError::Health)?;
// Opening replay state is the first persistent write-capable actuation.
// Managed launches reach this point only after the exact warming process
// has received Idunn's typed process-write lease.
let backend = Arc::new(CodexProviderBackend::start(CodexAppServerConfig { let backend = Arc::new(CodexProviderBackend::start(CodexAppServerConfig {
executable: config.codex_executable.clone(), executable: codex_executable,
executable_sha256: config.codex_executable_sha256, executable_sha256: config.codex_executable_sha256,
codex_home: config.codex_home.clone(), codex_home,
max_result_bytes: config.max_frame_bytes - FRAME_OVERHEAD_BUDGET, max_result_bytes: config.max_frame_bytes - FRAME_OVERHEAD_BUDGET,
})?); })?);
let readiness = backend.readiness()?; let readiness = backend.readiness()?;
let listener = TcpListener::bind(bind).map_err(CodexDaemonError::Listen)?; let service = Arc::new(Mutex::new(CodexTransportService::open(
&replay_store,
admissions,
config.max_expiry_skew_ms,
)?));
#[cfg(target_os = "linux")]
let runtime_presence_publisher =
prepared_presence_publisher.map(|publisher| Arc::new(Mutex::new(publisher)));
#[cfg(target_os = "linux")]
if let Some(publisher) = runtime_presence_publisher.as_ref() {
start_periodic_runtime_presence_publisher(publisher.clone(), backend.clone())?;
}
let active_connections = Arc::new(AtomicUsize::new(0)); let active_connections = Arc::new(AtomicUsize::new(0));
eprintln!( eprintln!(
"codex-connector ready at {} for {} callers using {:?}", "codex-connector ready at {} for {} callers using {:?}",
listener.local_addr().map_err(CodexDaemonError::Listen)?, bound,
config.callers.len(), config.callers.len(),
readiness.auth_mode readiness.auth_mode
); );
#[cfg(target_os = "linux")]
start_idunn_health_publisher()?;
for accepted in listener.incoming() { loop {
let stream = match accepted { let (stream, _) = listener.accept().map_err(CodexDaemonError::Listen)?;
Ok(stream) => stream,
Err(error) => return Err(CodexDaemonError::Listen(error)),
};
if !stream if !stream
.peer_addr() .peer_addr()
.map_err(CodexDaemonError::Connection)? .map_err(CodexDaemonError::Connection)?
@ -213,49 +358,66 @@ pub fn serve(config_path: &Path) -> Result<(), CodexDaemonError> {
}; };
let service = service.clone(); let service = service.clone();
let backend = backend.clone(); let backend = backend.clone();
#[cfg(target_os = "linux")]
let runtime_presence_publisher = runtime_presence_publisher.clone();
let max_frame_bytes = config.max_frame_bytes; let max_frame_bytes = config.max_frame_bytes;
let timeout = Duration::from_millis(config.socket_timeout_ms); let timeout = Duration::from_millis(config.socket_timeout_ms);
thread::Builder::new() thread::Builder::new()
.name("codex-connector-request".to_string()) .name("codex-connector-request".to_string())
.spawn(move || { .spawn(move || {
let _permit = permit; let _permit = permit;
let _ = serve_connection(stream, &service, &backend, max_frame_bytes, timeout); let _ = serve_connection(
stream,
&service,
&backend,
#[cfg(target_os = "linux")]
runtime_presence_publisher.as_deref(),
max_frame_bytes,
timeout,
);
}) })
.map_err(CodexDaemonError::Thread)?; .map_err(CodexDaemonError::Thread)?;
} }
Err(CodexDaemonError::ListenerClosed)
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn start_idunn_health_publisher() -> Result<(), CodexDaemonError> { fn prepare_runtime_presence_publisher(
let Some(endpoint) = std::env::var_os("CODEX_CONNECTOR_IDUNN_RUDP") else { candidate: SocketAddr,
return Ok(()); config: &CodexDaemonConfig,
}; ) -> Result<Option<crate::idunn_health::RuntimePresencePublisher>, CodexDaemonError> {
let endpoint = endpoint if std::env::var_os(crate::idunn_health::IDUNN_RUNTIME_BUNDLE_ENVIRONMENT).is_none() {
.to_string_lossy() return Ok(None);
.parse::<SocketAddr>() }
.map_err(|_| CodexDaemonError::InvalidConfig("Idunn health endpoint"))?; let capacity = u32::try_from(caller_request_capacity(&config.callers)?)
let daemon_id = required_environment("CODEX_CONNECTOR_IDUNN_DAEMON")?; .map_err(|_| CodexDaemonError::InvalidConfig("runtime capability capacity"))?;
let runtime_id = required_environment("CODEX_CONNECTOR_RUNTIME_ID")?; let publisher =
let contract = std::env::var("CODEX_CONNECTOR_IDUNN_HEALTH_CONTRACT") crate::idunn_health::RuntimePresencePublisher::open_from_environment(candidate, capacity)
.unwrap_or_else(|_| crate::CODEX_CONNECTOR_IDUNN_HEALTH_CONTRACT.to_string());
let identity = PathBuf::from(required_environment(
"CODEX_CONNECTOR_IDUNN_HEALTH_IDENTITY",
)?);
let mut publisher =
crate::ProviderHealthPublisher::open(endpoint, daemon_id, runtime_id, contract, &identity)
.map_err(CodexDaemonError::Health)?; .map_err(CodexDaemonError::Health)?;
Ok(Some(publisher))
}
#[cfg(target_os = "linux")]
fn start_periodic_runtime_presence_publisher(
publisher: Arc<Mutex<crate::idunn_health::RuntimePresencePublisher>>,
backend: Arc<CodexProviderBackend>,
) -> Result<(), CodexDaemonError> {
let (state, detail) = provider_health_observation(&backend);
publisher publisher
.publish("active", "credential-isolated-transport-ready") .lock()
.map_err(|_| CodexDaemonError::RuntimePresencePoisoned)?
.publish(state, detail)
.map_err(CodexDaemonError::Health)?; .map_err(CodexDaemonError::Health)?;
thread::Builder::new() thread::Builder::new()
.name("codex-connector-health".to_string()) .name("codex-connector-health".to_string())
.spawn(move || { .spawn(move || {
loop { loop {
thread::sleep(Duration::from_secs(10)); thread::sleep(Duration::from_secs(10));
if let Err(error) = let (state, detail) = provider_health_observation(&backend);
publisher.publish("active", "credential-isolated-transport-ready") let publication = publisher
{ .lock()
.map_err(|_| anyhow::anyhow!("runtime presence publisher lock was poisoned"))
.and_then(|mut publisher| publisher.publish(state, detail));
if let Err(error) = publication {
eprintln!("codex-connector health publication failed: {error}"); eprintln!("codex-connector health publication failed: {error}");
} }
} }
@ -265,25 +427,153 @@ fn start_idunn_health_publisher() -> Result<(), CodexDaemonError> {
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn required_environment(name: &'static str) -> Result<String, CodexDaemonError> { fn provider_health_observation(backend: &CodexProviderBackend) -> (&'static str, &'static str) {
std::env::var(name) provider_health_state(backend.readiness().is_ok())
.ok() }
.filter(|value| !value.trim().is_empty() && value == value.trim())
.ok_or(CodexDaemonError::InvalidConfig(name)) #[cfg(target_os = "linux")]
fn provider_health_state(ready: bool) -> (&'static str, &'static str) {
if ready {
("active", "credential-isolated-transport-ready")
} else {
("degraded", "provider-backend-unready")
}
}
#[cfg(target_os = "linux")]
fn effective_bind_from_environment(configured: SocketAddr) -> Result<SocketAddr, CodexDaemonError> {
select_effective_bind(
configured,
std::env::var(crate::idunn_health::IDUNN_RUNTIME_BUNDLE_ENVIRONMENT).ok(),
std::env::var(crate::idunn_health::IDUNN_CANDIDATE_BIND_ENVIRONMENT).ok(),
)
}
#[cfg(target_os = "linux")]
fn effective_runtime_paths_from_environment(
config: &CodexDaemonConfig,
managed_state_root: Option<&Path>,
) -> Result<(PathBuf, PathBuf, PathBuf), CodexDaemonError> {
if std::env::var_os(crate::idunn_health::IDUNN_RUNTIME_BUNDLE_ENVIRONMENT).is_none() {
if managed_state_root.is_some() {
return Err(CodexDaemonError::InvalidConfig(
"Idunn state root outside a managed launch",
));
}
return Ok((
config.codex_executable.clone(),
config.codex_home.clone(),
config.replay_store.clone(),
));
}
let managed_state_root =
managed_state_root.ok_or(CodexDaemonError::InvalidConfig("missing Idunn state root"))?;
let running = std::env::current_exe()
.and_then(fs::canonicalize)
.map_err(|_| CodexDaemonError::InvalidConfig("managed Connector executable"))?;
managed_runtime_paths(config, &running, managed_state_root)
}
#[cfg(target_os = "linux")]
fn managed_runtime_paths(
config: &CodexDaemonConfig,
running: &Path,
state_root: &Path,
) -> Result<(PathBuf, PathBuf, PathBuf), CodexDaemonError> {
let adjacent_codex = running
.parent()
.ok_or(CodexDaemonError::InvalidConfig(
"managed Connector executable",
))?
.join("codex");
let adjacent_codex = fs::canonicalize(adjacent_codex)
.map_err(|_| CodexDaemonError::InvalidConfig("managed Codex executable"))?;
let configured_codex = fs::canonicalize(&config.codex_executable)
.map_err(|_| CodexDaemonError::InvalidConfig("managed Codex executable"))?;
if configured_codex != adjacent_codex {
return Err(CodexDaemonError::InvalidConfig("managed Codex executable"));
}
if !state_root.is_absolute() {
return Err(CodexDaemonError::InvalidConfig("managed state root"));
}
let canonical_state_root = fs::canonicalize(state_root)
.map_err(|_| CodexDaemonError::InvalidConfig("managed state root"))?;
let state_root_metadata = fs::symlink_metadata(state_root)
.map_err(|_| CodexDaemonError::InvalidConfig("managed state root"))?;
if canonical_state_root != state_root
|| !state_root_metadata.is_dir()
|| state_root_metadata.file_type().is_symlink()
{
return Err(CodexDaemonError::InvalidConfig("managed state root"));
}
let codex_home = state_root.join("codex-home");
if config.codex_home != codex_home {
return Err(CodexDaemonError::InvalidConfig("managed state layout"));
}
match fs::symlink_metadata(&codex_home) {
Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
_ => return Err(CodexDaemonError::InvalidConfig("managed state layout")),
}
let replay_store = state_root.join("replay.cc");
if config.replay_store != replay_store {
return Err(CodexDaemonError::InvalidConfig("managed state layout"));
}
match fs::symlink_metadata(&replay_store) {
Ok(metadata)
if metadata.is_file()
&& !metadata.file_type().is_symlink()
&& metadata.nlink() == 1 => {}
Ok(_) => return Err(CodexDaemonError::InvalidConfig("managed state layout")),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(_) => return Err(CodexDaemonError::InvalidConfig("managed state layout")),
}
Ok((adjacent_codex, codex_home, replay_store))
}
#[cfg(target_os = "linux")]
fn select_effective_bind(
configured: SocketAddr,
runtime_bundle: Option<String>,
candidate_bind: Option<String>,
) -> Result<SocketAddr, CodexDaemonError> {
match (runtime_bundle, candidate_bind) {
(None, None) => Ok(configured),
(Some(bundle), Some(candidate))
if !bundle.trim().is_empty()
&& bundle.trim() == bundle
&& candidate.trim() == candidate =>
{
let bind = candidate
.parse::<SocketAddr>()
.map_err(|_| CodexDaemonError::InvalidConfig("Idunn candidate bind"))?;
if !bind.ip().is_loopback() {
return Err(CodexDaemonError::InvalidConfig("Idunn candidate bind"));
}
Ok(bind)
}
_ => Err(CodexDaemonError::InvalidConfig(
"partial Idunn managed runtime environment",
)),
}
} }
fn load_admissions( fn load_admissions(
config: &CodexDaemonConfig, config: &CodexDaemonConfig,
) -> Result<Vec<CodexCallerAdmission>, CodexDaemonError> { ) -> Result<Vec<CodexCallerAdmission>, CodexDaemonError> {
config let mut admissions = Vec::with_capacity(config.callers.len());
.callers for caller in &config.callers {
.iter() let raw = Zeroizing::new(
.map(|caller| { fs::read(&caller.connection_key_file).map_err(CodexDaemonError::ConnectionKey)?,
let secret = Zeroizing::new( );
fs::read_to_string(&caller.connection_key_file) let secret = std::str::from_utf8(raw.as_slice())
.map_err(CodexDaemonError::ConnectionKey)?, .map_err(|_| CodexDaemonError::InvalidConfig("caller connection key encoding"))?
); .trim_end_matches(['\r', '\n']);
let secret = secret.trim_end_matches(['\r', '\n']); admissions.push(
CodexCallerAdmission::new( CodexCallerAdmission::new(
caller.caller_runtime_id.clone(), caller.caller_runtime_id.clone(),
secret.to_string(), secret.to_string(),
@ -293,15 +583,19 @@ fn load_admissions(
caller.max_payload_bytes, caller.max_payload_bytes,
caller.max_output_tokens, caller.max_output_tokens,
) )
.map_err(CodexDaemonError::Service) .map_err(CodexDaemonError::Service)?,
}) );
.collect() }
Ok(admissions)
} }
fn serve_connection( fn serve_connection(
mut stream: TcpStream, mut stream: TcpStream,
service: &Mutex<CodexTransportService>, service: &Mutex<CodexTransportService>,
backend: &CodexProviderBackend, backend: &CodexProviderBackend,
#[cfg(target_os = "linux")] runtime_presence_publisher: Option<
&Mutex<crate::idunn_health::RuntimePresencePublisher>,
>,
max_frame_bytes: usize, max_frame_bytes: usize,
timeout: Duration, timeout: Duration,
) -> Result<(), CodexDaemonError> { ) -> Result<(), CodexDaemonError> {
@ -310,8 +604,37 @@ fn serve_connection(
.and_then(|()| stream.set_write_timeout(Some(timeout))) .and_then(|()| stream.set_write_timeout(Some(timeout)))
.map_err(CodexDaemonError::Connection)?; .map_err(CodexDaemonError::Connection)?;
let request = read_transport_frame(&mut stream, max_frame_bytes).map_err(frame_error)?; let request = read_transport_frame(&mut stream, max_frame_bytes).map_err(frame_error)?;
#[cfg(target_os = "linux")]
let response = dispatch_native_frame(
&request,
runtime_presence_publisher.is_some(),
|message_id| {
let publisher =
runtime_presence_publisher.ok_or(CodexDaemonError::RouteObservationRefused)?;
let mut publisher = publisher
.lock()
.map_err(|_| CodexDaemonError::RuntimePresencePoisoned)?;
let backend_ready = backend.readiness().is_ok();
let response = publisher
.route_observation(message_id, backend_ready)
.map_err(CodexDaemonError::Health)?;
encode_cultnet_message_to_vec(&response, CultNetWireContract::CultNetSchemaV0)
.map_err(|_| CodexDaemonError::FrameEncoding)
},
|request| execute_inference_frame(request, service, backend),
)?;
#[cfg(not(target_os = "linux"))]
let response = execute_inference_frame(&request, service, backend)?;
write_transport_frame(&mut stream, &response, max_frame_bytes).map_err(frame_error)
}
fn execute_inference_frame(
request: &[u8],
service: &Mutex<CodexTransportService>,
backend: &CodexProviderBackend,
) -> Result<Vec<u8>, CodexDaemonError> {
let envelope: CodexTransportEnvelope = let envelope: CodexTransportEnvelope =
rmp_serde::from_slice(&request).map_err(|_| CodexDaemonError::FrameEncoding)?; rmp_serde::from_slice(request).map_err(|_| CodexDaemonError::FrameEncoding)?;
let admission = service let admission = service
.lock() .lock()
.map_err(|_| CodexDaemonError::ServicePoisoned)? .map_err(|_| CodexDaemonError::ServicePoisoned)?
@ -326,8 +649,45 @@ fn serve_connection(
.complete(*claim, result)? .complete(*claim, result)?
} }
}; };
let response = rmp_serde::to_vec(&response).map_err(|_| CodexDaemonError::FrameEncoding)?; rmp_serde::to_vec(&response).map_err(|_| CodexDaemonError::FrameEncoding)
write_transport_frame(&mut stream, &response, max_frame_bytes).map_err(frame_error) }
#[cfg(target_os = "linux")]
fn dispatch_native_frame<RouteObservation, Inference>(
request: &[u8],
managed: bool,
route_observation: RouteObservation,
inference: Inference,
) -> Result<Vec<u8>, CodexDaemonError>
where
RouteObservation: FnOnce(&str) -> Result<Vec<u8>, CodexDaemonError>,
Inference: FnOnce(&[u8]) -> Result<Vec<u8>, CodexDaemonError>,
{
let Ok(message) =
decode_cultnet_message_from_slice(request, CultNetWireContract::CultNetSchemaV0)
else {
return inference(request);
};
let CultNetMessage::SnapshotRequest {
message_id,
schema_ids,
record_keys,
} = message
else {
return inference(request);
};
let exact_schema = matches!(
schema_ids.as_deref(),
Some([schema_id]) if schema_id == GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA
);
let exact_record = matches!(
record_keys.as_deref(),
Some([record_key]) if record_key == crate::idunn_health::CONNECTOR_TARGET
);
if !managed || !exact_schema || !exact_record {
return Err(CodexDaemonError::RouteObservationRefused);
}
route_observation(&message_id)
} }
fn frame_error(error: TransportFrameError) -> CodexDaemonError { fn frame_error(error: TransportFrameError) -> CodexDaemonError {
@ -398,11 +758,17 @@ pub enum CodexDaemonError {
#[error("connector request thread failed")] #[error("connector request thread failed")]
Thread(#[source] std::io::Error), Thread(#[source] std::io::Error),
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[error("connector health publication could not start: {0}")] #[error("connector runtime presence failed: {0}")]
Health(#[source] anyhow::Error), Health(#[source] anyhow::Error),
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[error("connector health thread could not start")] #[error("connector health thread could not start")]
HealthThread(#[source] std::io::Error), HealthThread(#[source] std::io::Error),
#[cfg(target_os = "linux")]
#[error("connector runtime presence publisher lock was poisoned")]
RuntimePresencePoisoned,
#[cfg(target_os = "linux")]
#[error("connector route observation request was refused")]
RouteObservationRefused,
#[error("connector connection failed")] #[error("connector connection failed")]
Connection(#[source] std::io::Error), Connection(#[source] std::io::Error),
#[error("connector frame exceeded its bound")] #[error("connector frame exceeded its bound")]
@ -411,13 +777,13 @@ pub enum CodexDaemonError {
FrameEncoding, FrameEncoding,
#[error("system clock is before the Unix epoch")] #[error("system clock is before the Unix epoch")]
Clock, Clock,
#[error("connector listener closed")]
ListenerClosed,
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[cfg(target_os = "linux")]
use std::cell::Cell;
use std::io::Cursor; use std::io::Cursor;
fn config(root: &Path) -> CodexDaemonConfig { fn config(root: &Path) -> CodexDaemonConfig {
@ -444,6 +810,118 @@ mod tests {
} }
} }
#[test]
#[cfg(target_os = "linux")]
fn signed_health_never_reports_an_unready_provider_active() {
assert_eq!(
provider_health_state(true),
("active", "credential-isolated-transport-ready")
);
assert_eq!(
provider_health_state(false),
("degraded", "provider-backend-unready")
);
}
#[test]
#[cfg(target_os = "linux")]
fn exact_managed_route_probe_bypasses_inference_execution() {
let request = encode_cultnet_message_to_vec(
&CultNetMessage::SnapshotRequest {
message_id: "route-challenge-17".into(),
schema_ids: Some(vec![GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA.into()]),
record_keys: Some(vec![crate::idunn_health::CONNECTOR_TARGET.into()]),
},
CultNetWireContract::CultNetSchemaV0,
)
.unwrap();
let inference_calls = Cell::new(0);
let route_calls = Cell::new(0);
let response = dispatch_native_frame(
&request,
true,
|message_id| {
route_calls.set(route_calls.get() + 1);
assert_eq!(message_id, "route-challenge-17");
Ok(b"signed-route-observation".to_vec())
},
|_| {
inference_calls.set(inference_calls.get() + 1);
Ok(b"provider-response".to_vec())
},
)
.unwrap();
assert_eq!(response, b"signed-route-observation");
assert_eq!(route_calls.get(), 1);
assert_eq!(inference_calls.get(), 0);
}
#[test]
#[cfg(target_os = "linux")]
fn route_probe_refuses_broad_foreign_and_unmanaged_requests() {
let requests = [
(
CultNetMessage::SnapshotRequest {
message_id: "broad".into(),
schema_ids: None,
record_keys: None,
},
true,
),
(
CultNetMessage::SnapshotRequest {
message_id: "foreign-schema".into(),
schema_ids: Some(vec!["gamecult.other.v1".into()]),
record_keys: Some(vec![crate::idunn_health::CONNECTOR_TARGET.into()]),
},
true,
),
(
CultNetMessage::SnapshotRequest {
message_id: "foreign-record".into(),
schema_ids: Some(vec![GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA.into()]),
record_keys: Some(vec!["other-target".into()]),
},
true,
),
(
CultNetMessage::SnapshotRequest {
message_id: "unmanaged".into(),
schema_ids: Some(vec![GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA.into()]),
record_keys: Some(vec![crate::idunn_health::CONNECTOR_TARGET.into()]),
},
false,
),
];
for (request, managed) in requests {
let request =
encode_cultnet_message_to_vec(&request, CultNetWireContract::CultNetSchemaV0)
.unwrap();
let route_calls = Cell::new(0);
let inference_calls = Cell::new(0);
assert!(matches!(
dispatch_native_frame(
&request,
managed,
|_| {
route_calls.set(route_calls.get() + 1);
Ok(Vec::new())
},
|_| {
inference_calls.set(inference_calls.get() + 1);
Ok(Vec::new())
},
),
Err(CodexDaemonError::RouteObservationRefused)
));
assert_eq!(route_calls.get(), 0);
assert_eq!(inference_calls.get(), 0);
}
}
#[test] #[test]
fn typed_cultcache_config_round_trips_and_refuses_epoch_substitution() { fn typed_cultcache_config_round_trips_and_refuses_epoch_substitution() {
let root = std::env::temp_dir().join(format!( let root = std::env::temp_dir().join(format!(
@ -456,6 +934,8 @@ mod tests {
let expected = config(&root); let expected = config(&root);
write_daemon_config(&path, &expected).unwrap(); write_daemon_config(&path, &expected).unwrap();
assert_eq!(load_daemon_config(&path).unwrap(), expected); assert_eq!(load_daemon_config(&path).unwrap(), expected);
let loaded = load_daemon_config_read_only(&path).unwrap();
assert_eq!(loaded, expected);
let mut stale = expected; let mut stale = expected;
stale.epoch = 0; stale.epoch = 0;
@ -466,6 +946,74 @@ mod tests {
fs::remove_dir_all(root).unwrap(); fs::remove_dir_all(root).unwrap();
} }
#[test]
fn production_config_snapshot_never_creates_its_authority_lock() {
let root = std::env::temp_dir().join(format!(
"codex-connector-read-only-config-{}-{}",
std::process::id(),
unix_ms().unwrap()
));
fs::create_dir_all(&root).unwrap();
let path = root.join("connector.cc");
write_daemon_config(&path, &config(&root)).unwrap();
let before = fs::read(&path).unwrap();
let lock_path = path.with_file_name("connector.cc.lock");
fs::remove_file(&lock_path).unwrap();
assert!(load_daemon_config_read_only(&path).is_err());
assert!(!lock_path.exists());
assert_eq!(fs::read(&path).unwrap(), before);
fs::remove_dir_all(root).unwrap();
}
#[test]
#[cfg(target_os = "linux")]
fn managed_runtime_refuses_a_mutable_config_authority() {
let root = std::env::temp_dir().join(format!(
"codex-connector-managed-config-{}-{}",
std::process::id(),
unix_ms().unwrap()
));
fs::create_dir_all(&root).unwrap();
let path = root.join("connector.cc");
write_daemon_config(&path, &config(&root)).unwrap();
assert!(crate::idunn_health::require_managed_config_store(&path).is_err());
fs::remove_dir_all(root).unwrap();
}
#[test]
#[cfg(target_os = "linux")]
fn idunn_candidate_bind_replaces_config_only_for_a_complete_managed_launch() {
let configured = "127.0.0.1:4103".parse().unwrap();
let candidate = "127.0.0.1:18831".parse().unwrap();
assert_eq!(
select_effective_bind(configured, None, None).unwrap(),
configured
);
assert_eq!(
select_effective_bind(
configured,
Some("/run/idunn/runtime/instance".into()),
Some("127.0.0.1:18831".into()),
)
.unwrap(),
candidate
);
assert!(
select_effective_bind(configured, Some("/run/idunn/runtime/instance".into()), None,)
.is_err()
);
assert!(
select_effective_bind(
configured,
Some("/run/idunn/runtime/instance".into()),
Some("0.0.0.0:18831".into()),
)
.is_err()
);
}
#[test] #[test]
fn single_caller_initializer_derives_daemon_bounds_without_reowning_policy() { fn single_caller_initializer_derives_daemon_bounds_without_reowning_policy() {
let root = Path::new("/srv/codex-connector"); let root = Path::new("/srv/codex-connector");
@ -494,6 +1042,125 @@ mod tests {
config.validate().unwrap(); config.validate().unwrap();
} }
#[test]
fn typed_config_preserves_a_unique_multi_model_caller_allowlist() {
let root = Path::new("/srv/codex-connector");
let mut config = config(root);
config.callers[0].allowed_models =
vec!["gpt-5.6-luna".to_string(), "gpt-5.6-terra".to_string()];
config.validate().unwrap();
config.callers[0]
.allowed_models
.push("gpt-5.6-luna".to_string());
assert!(matches!(
config.validate(),
Err(CodexDaemonError::InvalidConfig("caller admission"))
));
}
#[test]
fn caller_admission_reserves_every_independently_admitted_request_slot() {
let root = Path::new("/srv/codex-connector");
let first = CodexCallerConfig {
caller_runtime_id: "ghostlight-dungeon-yggdrasil".to_string(),
connection_key_file: root.join("ghostlight.key"),
connection_key_epoch: 1,
allowed_models: vec!["gpt-5.6-luna".to_string()],
max_concurrent_requests: 8,
max_payload_bytes: 1_048_576,
max_output_tokens: 16_384,
};
let mut config = CodexDaemonConfig::single_caller(
"127.0.0.1:4103",
root.join("codex"),
[7; 32],
root.join("codex-home"),
root.join("replay.cc"),
first,
);
config
.admit_caller(CodexCallerConfig {
caller_runtime_id: "epiphany-model-runtime".to_string(),
connection_key_file: root.join("epiphany.key"),
connection_key_epoch: 1,
allowed_models: vec!["gpt-5.6-luna".to_string()],
max_concurrent_requests: 8,
max_payload_bytes: 1_048_576,
max_output_tokens: 16_384,
})
.unwrap();
assert_eq!(config.max_connections, 16);
config.validate().unwrap();
config.max_connections = 15;
assert!(matches!(
config.validate(),
Err(CodexDaemonError::InvalidConfig("connection capacity"))
));
}
#[test]
fn provider_capacity_is_caller_execution_capacity_not_listener_capacity() {
let root = Path::new("/srv/codex-connector");
let mut config = config(root);
config.max_connections = 64;
assert_eq!(caller_request_capacity(&config.callers).unwrap(), 4);
assert_eq!(required_connection_capacity(&config.callers).unwrap(), 8);
}
#[test]
#[cfg(target_os = "linux")]
fn managed_runtime_uses_the_sealed_adjacent_codex_and_declared_state_layout() {
let root = std::env::temp_dir().join(format!(
"codex-connector-layout-{}-{}",
std::process::id(),
unix_ms().unwrap()
));
let release = root.join("release");
let state = root.join("state");
fs::create_dir_all(release.clone()).unwrap();
fs::create_dir_all(&state).unwrap();
fs::write(release.join("codex-connector"), b"connector").unwrap();
fs::write(release.join("codex"), b"codex").unwrap();
let mut config = config(&root);
config.codex_executable = release.join("codex");
config.codex_home = state.join("codex-home");
config.replay_store = state.join("replay.cc");
let (_, codex_home, replay_store) =
managed_runtime_paths(&config, &release.join("codex-connector"), &state).unwrap();
assert_eq!(codex_home, state.join("codex-home"));
assert_eq!(
replay_store,
fs::canonicalize(&state).unwrap().join("replay.cc")
);
fs::write(&replay_store, b"replay").unwrap();
let replay_alias = state.join("replay-alias.cc");
fs::hard_link(&replay_store, &replay_alias).unwrap();
assert!(managed_runtime_paths(&config, &release.join("codex-connector"), &state).is_err());
fs::remove_file(&replay_alias).unwrap();
fs::remove_file(&replay_store).unwrap();
std::os::unix::fs::symlink(state.join("missing-replay.cc"), &replay_store).unwrap();
assert!(managed_runtime_paths(&config, &release.join("codex-connector"), &state).is_err());
fs::remove_file(&replay_store).unwrap();
config.codex_executable = root.join("other-codex");
fs::write(&config.codex_executable, b"other").unwrap();
assert!(managed_runtime_paths(&config, &release.join("codex-connector"), &state).is_err());
config.codex_executable = release.join("codex");
let other_state = root.join("other-state");
fs::create_dir_all(other_state.join("codex-home")).unwrap();
assert!(
managed_runtime_paths(&config, &release.join("codex-connector"), &other_state,)
.is_err()
);
fs::remove_dir_all(root).unwrap();
}
#[test] #[test]
fn direct_pipe_frame_is_big_endian_and_refuses_oversize_before_allocation() { fn direct_pipe_frame_is_big_endian_and_refuses_oversize_before_allocation() {
let mut encoded = Vec::new(); let mut encoded = Vec::new();

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
#[cfg(feature = "client")]
use std::io::{Read, Write}; use std::io::{Read, Write};
#[cfg(feature = "client")]
use std::net::{SocketAddr, TcpStream}; use std::net::{SocketAddr, TcpStream};
#[cfg(feature = "client")]
use std::time::Duration; use std::time::Duration;
#[cfg(feature = "daemon")] #[cfg(feature = "daemon")]
@ -11,18 +8,14 @@ use std::collections::HashMap;
#[cfg(feature = "daemon")] #[cfg(feature = "daemon")]
use std::path::Path; use std::path::Path;
#[cfg(feature = "client")]
use aes_gcm::aead::{Aead, Payload}; use aes_gcm::aead::{Aead, Payload};
#[cfg(feature = "client")]
use aes_gcm::{Aes256Gcm, KeyInit, Nonce}; use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
#[cfg(feature = "daemon")] #[cfg(feature = "daemon")]
use cultcache_rs::{CultCache, DatabaseEntry, OwnedRedbMessagePackBackingStore}; use cultcache_rs::{CultCache, DatabaseEntry, OwnedRedbMessagePackBackingStore};
#[cfg(feature = "client")]
use rand_core::{OsRng, RngCore}; use rand_core::{OsRng, RngCore};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use thiserror::Error; use thiserror::Error;
#[cfg(feature = "client")]
use zeroize::{Zeroize, Zeroizing}; use zeroize::{Zeroize, Zeroizing};
#[cfg(feature = "daemon")] #[cfg(feature = "daemon")]
@ -44,11 +37,6 @@ pub use daemon::load_daemon_config;
pub use daemon::serve; pub use daemon::serve;
#[cfg(feature = "daemon")] #[cfg(feature = "daemon")]
pub use daemon::write_daemon_config; pub use daemon::write_daemon_config;
#[cfg(all(feature = "daemon", target_os = "linux"))]
pub use idunn_health::{
CODEX_CONNECTOR_IDUNN_HEALTH_CONTRACT, ProviderHealthPublisher,
enroll_provider_health_identity, provider_health_public_key_hex,
};
#[cfg(feature = "daemon")] #[cfg(feature = "daemon")]
pub use provider_backend::CodexAppServerConfig; pub use provider_backend::CodexAppServerConfig;
#[cfg(feature = "daemon")] #[cfg(feature = "daemon")]
@ -66,11 +54,9 @@ pub const RESULT_SCHEMA_ID: &str = "gamecult.codex.transport_result.v2";
pub const RECEIPT_SCHEMA_ID: &str = "gamecult.codex.transport_receipt.v2"; pub const RECEIPT_SCHEMA_ID: &str = "gamecult.codex.transport_receipt.v2";
pub const ENVELOPE_SCHEMA_ID: &str = "gamecult.codex.transport_envelope.v2"; pub const ENVELOPE_SCHEMA_ID: &str = "gamecult.codex.transport_envelope.v2";
#[cfg(feature = "client")]
#[derive(Clone, PartialEq, Eq)] #[derive(Clone, PartialEq, Eq)]
pub struct CodexTransportKey([u8; 32]); pub struct CodexTransportKey([u8; 32]);
#[cfg(feature = "client")]
impl CodexTransportKey { impl CodexTransportKey {
pub fn from_connection_secret(secret: &str) -> Result<Self, ServiceError> { pub fn from_connection_secret(secret: &str) -> Result<Self, ServiceError> {
if secret.trim().is_empty() || secret.trim() != secret { if secret.trim().is_empty() || secret.trim() != secret {
@ -80,7 +66,6 @@ impl CodexTransportKey {
} }
} }
#[cfg(feature = "client")]
impl Drop for CodexTransportKey { impl Drop for CodexTransportKey {
fn drop(&mut self) { fn drop(&mut self) {
self.0.zeroize(); self.0.zeroize();
@ -644,14 +629,12 @@ pub fn canonical_invocation_bytes(
rmp_serde::to_vec(invocation).map_err(|_| ContractError::Encoding) rmp_serde::to_vec(invocation).map_err(|_| ContractError::Encoding)
} }
#[cfg(feature = "client")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CodexEnvelopeKind { pub enum CodexEnvelopeKind {
Invocation, Invocation,
Result, Result,
} }
#[cfg(feature = "client")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodexTransportEnvelope { pub struct CodexTransportEnvelope {
pub schema_id: String, pub schema_id: String,
@ -662,7 +645,6 @@ pub struct CodexTransportEnvelope {
pub ciphertext: Vec<u8>, pub ciphertext: Vec<u8>,
} }
#[cfg(feature = "client")]
pub fn encrypt_invocation( pub fn encrypt_invocation(
invocation: &CodexTransportInvocation, invocation: &CodexTransportInvocation,
security: &CodexTransportKey, security: &CodexTransportKey,
@ -677,7 +659,6 @@ pub fn encrypt_invocation(
) )
} }
#[cfg(feature = "client")]
pub fn decrypt_invocation( pub fn decrypt_invocation(
envelope: &CodexTransportEnvelope, envelope: &CodexTransportEnvelope,
security: &CodexTransportKey, security: &CodexTransportKey,
@ -694,7 +675,6 @@ pub fn decrypt_invocation(
Ok(invocation) Ok(invocation)
} }
#[cfg(feature = "client")]
pub fn encrypt_result( pub fn encrypt_result(
result: &CodexTransportResult, result: &CodexTransportResult,
security: &CodexTransportKey, security: &CodexTransportKey,
@ -709,7 +689,6 @@ pub fn encrypt_result(
) )
} }
#[cfg(feature = "client")]
pub fn decrypt_result( pub fn decrypt_result(
envelope: &CodexTransportEnvelope, envelope: &CodexTransportEnvelope,
security: &CodexTransportKey, security: &CodexTransportKey,
@ -728,7 +707,6 @@ pub fn decrypt_result(
Ok(result) Ok(result)
} }
#[cfg(feature = "client")]
#[derive(Clone)] #[derive(Clone)]
pub struct CodexConnectorClient { pub struct CodexConnectorClient {
endpoint: SocketAddr, endpoint: SocketAddr,
@ -737,7 +715,6 @@ pub struct CodexConnectorClient {
response_timeout: Option<Duration>, response_timeout: Option<Duration>,
} }
#[cfg(feature = "client")]
impl CodexConnectorClient { impl CodexConnectorClient {
pub fn new( pub fn new(
endpoint: SocketAddr, endpoint: SocketAddr,
@ -745,8 +722,10 @@ impl CodexConnectorClient {
max_frame_bytes: usize, max_frame_bytes: usize,
response_timeout: Option<Duration>, response_timeout: Option<Duration>,
) -> Result<Self, CodexConnectorClientError> { ) -> Result<Self, CodexConnectorClientError> {
if !endpoint.ip().is_loopback() if endpoint.port() == 0
|| endpoint.port() == 0 || endpoint.ip().is_unspecified()
|| endpoint.ip().is_multicast()
|| endpoint.ip() == std::net::Ipv4Addr::BROADCAST
|| !(4096..=u32::MAX as usize).contains(&max_frame_bytes) || !(4096..=u32::MAX as usize).contains(&max_frame_bytes)
|| response_timeout.is_some_and(|timeout| timeout.is_zero()) || response_timeout.is_some_and(|timeout| timeout.is_zero())
{ {
@ -788,7 +767,45 @@ impl CodexConnectorClient {
} }
} }
#[cfg(feature = "client")] #[cfg(test)]
mod client_endpoint_tests {
use super::*;
#[test]
fn client_admits_explicit_unicast_and_refuses_non_destinations() {
for endpoint in ["127.0.0.1:4103", "172.19.0.1:4103", "[::1]:4103"] {
assert!(
CodexConnectorClient::new(
endpoint.parse().unwrap(),
"bounded-client-endpoint-key",
64 * 1024,
Some(Duration::from_secs(1)),
)
.is_ok(),
"{endpoint} should be an admissible explicit destination"
);
}
for endpoint in [
"0.0.0.0:4103",
"255.255.255.255:4103",
"224.0.0.1:4103",
"[::]:4103",
"[ff02::1]:4103",
"127.0.0.1:0",
] {
assert!(matches!(
CodexConnectorClient::new(
endpoint.parse().unwrap(),
"bounded-client-endpoint-key",
64 * 1024,
Some(Duration::from_secs(1)),
),
Err(CodexConnectorClientError::InvalidConfig)
));
}
}
}
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum CodexConnectorClientError { pub enum CodexConnectorClientError {
#[error("invalid connector client configuration")] #[error("invalid connector client configuration")]
@ -803,14 +820,12 @@ pub enum CodexConnectorClientError {
Transport(#[from] ServiceError), Transport(#[from] ServiceError),
} }
#[cfg(feature = "client")]
#[derive(Debug)] #[derive(Debug)]
enum TransportFrameError { enum TransportFrameError {
Connection(std::io::Error), Connection(std::io::Error),
Size, Size,
} }
#[cfg(feature = "client")]
fn client_frame_error(error: TransportFrameError) -> CodexConnectorClientError { fn client_frame_error(error: TransportFrameError) -> CodexConnectorClientError {
match error { match error {
TransportFrameError::Connection(error) => CodexConnectorClientError::Connection(error), TransportFrameError::Connection(error) => CodexConnectorClientError::Connection(error),
@ -818,7 +833,6 @@ fn client_frame_error(error: TransportFrameError) -> CodexConnectorClientError {
} }
} }
#[cfg(feature = "client")]
fn read_transport_frame( fn read_transport_frame(
reader: &mut impl Read, reader: &mut impl Read,
max_frame_bytes: usize, max_frame_bytes: usize,
@ -838,7 +852,6 @@ fn read_transport_frame(
Ok(payload) Ok(payload)
} }
#[cfg(feature = "client")]
fn write_transport_frame( fn write_transport_frame(
writer: &mut impl Write, writer: &mut impl Write,
payload: &[u8], payload: &[u8],
@ -854,7 +867,6 @@ fn write_transport_frame(
.map_err(TransportFrameError::Connection) .map_err(TransportFrameError::Connection)
} }
#[cfg(feature = "client")]
fn encrypt_message( fn encrypt_message(
caller_runtime_id: &str, caller_runtime_id: &str,
request_id: &str, request_id: &str,
@ -892,7 +904,6 @@ fn encrypt_message(
}) })
} }
#[cfg(feature = "client")]
fn decrypt_message( fn decrypt_message(
envelope: &CodexTransportEnvelope, envelope: &CodexTransportEnvelope,
security: &CodexTransportKey, security: &CodexTransportKey,
@ -915,7 +926,6 @@ fn decrypt_message(
.map_err(|_| ServiceError::Encryption) .map_err(|_| ServiceError::Encryption)
} }
#[cfg(feature = "client")]
fn envelope_aad( fn envelope_aad(
schema_id: &str, schema_id: &str,
caller_runtime_id: &str, caller_runtime_id: &str,
@ -926,7 +936,6 @@ fn envelope_aad(
.map_err(|_| ServiceError::Encoding) .map_err(|_| ServiceError::Encoding)
} }
#[cfg(feature = "client")]
fn validate_envelope( fn validate_envelope(
envelope: &CodexTransportEnvelope, envelope: &CodexTransportEnvelope,
expected_kind: CodexEnvelopeKind, expected_kind: CodexEnvelopeKind,
@ -1670,7 +1679,6 @@ mod tests {
.unwrap() .unwrap()
} }
#[cfg(feature = "client")]
fn security(key: &str) -> CodexTransportKey { fn security(key: &str) -> CodexTransportKey {
CodexTransportKey::from_connection_secret(key).unwrap() CodexTransportKey::from_connection_secret(key).unwrap()
} }
@ -1682,7 +1690,6 @@ mod tests {
CodexTransportInvocation::new(caller_runtime_id, 2_000, [7; 32], request).unwrap() CodexTransportInvocation::new(caller_runtime_id, 2_000, [7; 32], request).unwrap()
} }
#[cfg(feature = "client")]
#[test] #[test]
fn encrypted_invocation_hides_content_and_binds_outer_identity() { fn encrypted_invocation_hides_content_and_binds_outer_identity() {
let invocation = invocation(); let invocation = invocation();
@ -1711,7 +1718,6 @@ mod tests {
); );
} }
#[cfg(feature = "client")]
#[test] #[test]
fn shared_client_transports_one_exact_encrypted_request_and_result() { fn shared_client_transports_one_exact_encrypted_request_and_result() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();

View file

@ -1,35 +1,13 @@
use std::collections::HashSet;
use std::ffi::OsString; use std::ffi::OsString;
use std::path::PathBuf; use std::path::PathBuf;
use codex_connector::{CodexCallerConfig, CodexDaemonConfig}; use codex_connector::{CodexCallerConfig, CodexDaemonConfig};
const USAGE: &str = "usage:\n codex-connector --config PATH.cc\n codex-connector --initialize-single-caller-config PATH.cc BIND CODEX_EXECUTABLE CODEX_SHA256 CODEX_HOME REPLAY_STORE CALLER_RUNTIME_ID CONNECTION_KEY_FILE CONNECTION_KEY_EPOCH ALLOWED_MODEL MAX_CONCURRENT_REQUESTS MAX_PAYLOAD_BYTES MAX_OUTPUT_TOKENS\n codex-connector --enroll-provider-health-identity PATH.cc\n codex-connector --provider-health-public-key PATH.cc"; const USAGE: &str = "usage:\n codex-connector --config PATH.cc\n codex-connector --config PATH.cc --state-root PATH\n codex-connector --initialize-single-caller-config PATH.cc BIND CODEX_EXECUTABLE CODEX_SHA256 CODEX_HOME REPLAY_STORE CALLER_RUNTIME_ID CONNECTION_KEY_FILE CONNECTION_KEY_EPOCH ALLOWED_MODELS_CSV MAX_CONCURRENT_REQUESTS MAX_PAYLOAD_BYTES MAX_OUTPUT_TOKENS\n codex-connector --admit-caller-config SOURCE.cc DESTINATION.cc CALLER_RUNTIME_ID CONNECTION_KEY_FILE CONNECTION_KEY_EPOCH ALLOWED_MODELS_CSV MAX_CONCURRENT_REQUESTS MAX_PAYLOAD_BYTES MAX_OUTPUT_TOKENS";
fn main() { fn main() {
let args = std::env::args_os().skip(1).collect::<Vec<_>>(); let args = std::env::args_os().skip(1).collect::<Vec<_>>();
#[cfg(target_os = "linux")]
if let [flag, path] = args.as_slice() {
if flag == "--enroll-provider-health-identity" {
match codex_connector::enroll_provider_health_identity(&PathBuf::from(path)) {
Ok(public_key) => println!("{public_key}"),
Err(error) => {
eprintln!("codex-connector health identity enrollment failed: {error}");
std::process::exit(2);
}
}
return;
}
if flag == "--provider-health-public-key" {
match codex_connector::provider_health_public_key_hex(&PathBuf::from(path)) {
Ok(public_key) => println!("{public_key}"),
Err(error) => {
eprintln!("codex-connector health identity read failed: {error}");
std::process::exit(2);
}
}
return;
}
}
if args if args
.first() .first()
.is_some_and(|value| value == "--initialize-single-caller-config") .is_some_and(|value| value == "--initialize-single-caller-config")
@ -41,19 +19,68 @@ fn main() {
} }
return; return;
} }
let config = match args.as_slice() { if args
[flag, path] if flag == "--config" => PathBuf::from(path), .first()
.is_some_and(|value| value == "--admit-caller-config")
{
if let Err(error) = admit_caller_config(&args[1..]) {
eprintln!("codex-connector caller admission failed: {error}");
std::process::exit(2);
}
return;
}
let (config, state_root) = match args.as_slice() {
[flag, path] if flag == "--config" => (PathBuf::from(path), None),
[config_flag, config, state_flag, state_root]
if config_flag == "--config" && state_flag == "--state-root" =>
{
(PathBuf::from(config), Some(PathBuf::from(state_root)))
}
_ => { _ => {
eprintln!("{USAGE}"); eprintln!("{USAGE}");
std::process::exit(2); std::process::exit(2);
} }
}; };
if let Err(error) = codex_connector::serve(&config) { if let Err(error) = codex_connector::serve(&config, state_root.as_deref()) {
eprintln!("codex-connector stopped: {error}"); eprintln!("codex-connector stopped: {error}");
std::process::exit(1); std::process::exit(1);
} }
} }
fn admit_caller_config(args: &[OsString]) -> Result<(), String> {
let [
source,
destination,
caller_runtime_id,
connection_key_file,
connection_key_epoch,
allowed_models,
max_concurrent_requests,
max_payload_bytes,
max_output_tokens,
] = args
else {
return Err("expected exactly nine caller-admission values".to_string());
};
let mut config = codex_connector::load_daemon_config(&PathBuf::from(source))
.map_err(|error| error.to_string())?;
let caller_runtime_id = os_text(caller_runtime_id, "caller runtime")?;
let caller = CodexCallerConfig {
caller_runtime_id,
connection_key_file: PathBuf::from(connection_key_file),
connection_key_epoch: parse_number(connection_key_epoch, "connection key epoch")?,
allowed_models: parse_allowed_models(allowed_models)?,
max_concurrent_requests: parse_number(max_concurrent_requests, "max concurrent requests")?,
max_payload_bytes: parse_number(max_payload_bytes, "max payload bytes")?,
max_output_tokens: parse_number(max_output_tokens, "max output tokens")?,
};
config
.admit_caller(caller)
.map_err(|error| error.to_string())?;
codex_connector::write_daemon_config(&PathBuf::from(destination), &config)
.map_err(|error| error.to_string())
}
fn initialize_single_caller_config(args: &[OsString]) -> Result<(), String> { fn initialize_single_caller_config(args: &[OsString]) -> Result<(), String> {
let [ let [
path, path,
@ -65,7 +92,7 @@ fn initialize_single_caller_config(args: &[OsString]) -> Result<(), String> {
caller_runtime_id, caller_runtime_id,
connection_key_file, connection_key_file,
connection_key_epoch, connection_key_epoch,
allowed_model, allowed_models,
max_concurrent_requests, max_concurrent_requests,
max_payload_bytes, max_payload_bytes,
max_output_tokens, max_output_tokens,
@ -83,7 +110,7 @@ fn initialize_single_caller_config(args: &[OsString]) -> Result<(), String> {
caller_runtime_id: os_text(caller_runtime_id, "caller runtime")?, caller_runtime_id: os_text(caller_runtime_id, "caller runtime")?,
connection_key_file: PathBuf::from(connection_key_file), connection_key_file: PathBuf::from(connection_key_file),
connection_key_epoch: parse_number(connection_key_epoch, "connection key epoch")?, connection_key_epoch: parse_number(connection_key_epoch, "connection key epoch")?,
allowed_models: vec![os_text(allowed_model, "allowed model")?], allowed_models: parse_allowed_models(allowed_models)?,
max_concurrent_requests: parse_number( max_concurrent_requests: parse_number(
max_concurrent_requests, max_concurrent_requests,
"max concurrent requests", "max concurrent requests",
@ -104,6 +131,19 @@ fn os_text(value: &OsString, field: &str) -> Result<String, String> {
.ok_or_else(|| format!("{field} must be non-empty UTF-8")) .ok_or_else(|| format!("{field} must be non-empty UTF-8"))
} }
fn parse_allowed_models(value: &OsString) -> Result<Vec<String>, String> {
let raw = os_text(value, "allowed models")?;
let mut seen = HashSet::new();
let mut models = Vec::new();
for model in raw.split(',') {
if model.is_empty() || model.trim() != model || !seen.insert(model) {
return Err("allowed models must be unique non-empty comma-separated IDs".to_string());
}
models.push(model.to_string());
}
Ok(models)
}
fn parse_number<T>(value: &OsString, field: &str) -> Result<T, String> fn parse_number<T>(value: &OsString, field: &str) -> Result<T, String>
where where
T: std::str::FromStr, T: std::str::FromStr,
@ -113,6 +153,27 @@ where
.map_err(|_| format!("{field} is not a valid integer")) .map_err(|_| format!("{field} is not a valid integer"))
} }
#[cfg(test)]
mod tests {
use super::parse_allowed_models;
use std::ffi::OsString;
#[test]
fn model_allowlist_parser_preserves_one_or_many_exact_models() {
assert_eq!(
parse_allowed_models(&OsString::from("gpt-5.6-luna")).unwrap(),
vec!["gpt-5.6-luna"]
);
assert_eq!(
parse_allowed_models(&OsString::from("gpt-5.6-luna,gpt-5.6-terra")).unwrap(),
vec!["gpt-5.6-luna", "gpt-5.6-terra"]
);
assert!(parse_allowed_models(&OsString::from("gpt-5.6-luna,")).is_err());
assert!(parse_allowed_models(&OsString::from("gpt-5.6-luna, gpt-5.6-terra")).is_err());
assert!(parse_allowed_models(&OsString::from("gpt-5.6-luna,gpt-5.6-luna")).is_err());
}
}
fn parse_sha256(value: &OsString) -> Result<[u8; 32], String> { fn parse_sha256(value: &OsString) -> Result<[u8; 32], String> {
let value = os_text(value, "Codex SHA-256")?; let value = os_text(value, "Codex SHA-256")?;
if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) { if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {

View file

@ -72,7 +72,11 @@ impl CodexProviderBackend {
let max_result_bytes = config.max_result_bytes; let max_result_bytes = config.max_result_bytes;
let http: ureq::Agent = ureq::Agent::config_builder() let http: ureq::Agent = ureq::Agent::config_builder()
.timeout_connect(Some(Duration::from_secs(30))) .timeout_connect(Some(Duration::from_secs(30)))
.timeout_recv_response(Some(Duration::from_secs(30))) // ureq carries the receive-response deadline into streamed body
// reads as a preceding phase. Keep both receive phases on the
// same provider window so a 30-second header bound cannot silently
// cap a valid long-running SSE response.
.timeout_recv_response(Some(Duration::from_secs(300)))
.timeout_recv_body(Some(Duration::from_secs(300))) .timeout_recv_body(Some(Duration::from_secs(300)))
.max_redirects(0) .max_redirects(0)
.http_status_as_error(false) .http_status_as_error(false)
@ -631,6 +635,7 @@ impl CodexAppServerAuthority {
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::inherit()); .stderr(Stdio::inherit());
strip_systemd_descriptor_environment(&mut command);
let mut child = command.spawn().map_err(CodexProviderBackendError::Spawn)?; let mut child = command.spawn().map_err(CodexProviderBackendError::Spawn)?;
let stdin = match child.stdin.take() { let stdin = match child.stdin.take() {
Some(stdin) => stdin, Some(stdin) => stdin,
@ -693,6 +698,13 @@ impl CodexAppServerAuthority {
} }
} }
pub(crate) fn strip_systemd_descriptor_environment(command: &mut Command) {
command
.env_remove("LISTEN_PID")
.env_remove("LISTEN_FDS")
.env_remove("LISTEN_FDNAMES");
}
fn codex_cli_version_from_user_agent( fn codex_cli_version_from_user_agent(
user_agent: &str, user_agent: &str,
) -> Result<String, CodexProviderBackendError> { ) -> Result<String, CodexProviderBackendError> {