diff --git a/README.md b/README.md index 253236d..425b54d 100644 --- a/README.md +++ b/README.md @@ -53,10 +53,11 @@ cargo test --lib --features daemon cargo check --bin codex-connector --features daemon ``` -This is not yet a deployable replacement for the live connector. Redacted -CultMesh/Odin publication, shared consumer client cuts, and the independent -Idunn target remain open. The old daemon remains the sole live credential -writer until those proofs land. +The same production binary can initialize a typed single-caller `.cc` +configuration for first deployment, then serve only from that admitted state. +Redacted CultMesh/Odin publication and the independent Idunn target remain +open. The old daemon remains the sole live credential writer until that +deployment transaction lands. ## Contract invariant diff --git a/src/daemon.rs b/src/daemon.rs index 571df50..8b402f6 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -61,6 +61,31 @@ pub struct CodexDaemonConfig { } impl CodexDaemonConfig { + pub fn single_caller( + bind: impl Into, + codex_executable: PathBuf, + codex_executable_sha256: [u8; 32], + codex_home: PathBuf, + replay_store: PathBuf, + caller: CodexCallerConfig, + ) -> Self { + Self { + epoch: CONFIG_EPOCH, + bind: bind.into(), + codex_executable, + codex_executable_sha256, + codex_home, + max_frame_bytes: caller + .max_payload_bytes + .saturating_add(FRAME_OVERHEAD_BUDGET), + max_connections: caller.max_concurrent_requests.max(8), + socket_timeout_ms: 10_000, + max_expiry_skew_ms: 300_000, + callers: vec![caller], + replay_store, + } + } + pub fn validate(&self) -> Result { if self.epoch != CONFIG_EPOCH { return Err(CodexDaemonError::InvalidConfig("epoch")); @@ -387,6 +412,34 @@ mod tests { fs::remove_dir_all(root).unwrap(); } + #[test] + fn single_caller_initializer_derives_daemon_bounds_without_reowning_policy() { + let root = Path::new("/srv/codex-connector"); + let caller = CodexCallerConfig { + caller_runtime_id: "ghostlight-dungeon-yggdrasil".to_string(), + connection_key_file: root.join("ghostlight.key"), + connection_key_epoch: 3, + allowed_models: vec!["gpt-5.4".to_string()], + max_concurrent_requests: 12, + max_payload_bytes: 1_048_576, + max_output_tokens: 32_768, + }; + let config = CodexDaemonConfig::single_caller( + "127.0.0.1:4103", + root.join("codex"), + [7; 32], + root.join("codex-home"), + root.join("replay.cc"), + caller.clone(), + ); + + assert_eq!(config.epoch, CONFIG_EPOCH); + assert_eq!(config.max_frame_bytes, 1_052_672); + assert_eq!(config.max_connections, 12); + assert_eq!(config.callers, vec![caller]); + config.validate().unwrap(); + } + #[test] fn direct_pipe_frame_is_big_endian_and_refuses_oversize_before_allocation() { let mut encoded = Vec::new(); diff --git a/src/main.rs b/src/main.rs index 179b922..e3a9a11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,27 @@ +use std::ffi::OsString; use std::path::PathBuf; +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"; + fn main() { - let mut args = std::env::args_os().skip(1); - let config = match (args.next(), args.next(), args.next()) { - (Some(flag), Some(path), None) if flag == "--config" => PathBuf::from(path), + let args = std::env::args_os().skip(1).collect::>(); + if args + .first() + .is_some_and(|value| value == "--initialize-single-caller-config") + { + if let Err(error) = initialize_single_caller_config(&args[1..]) { + eprintln!("codex-connector config initialization failed: {error}"); + eprintln!("{USAGE}"); + std::process::exit(2); + } + return; + } + let config = match args.as_slice() { + [flag, path] if flag == "--config" => PathBuf::from(path), _ => { - eprintln!("usage: codex-connector --config PATH.cc"); + eprintln!("{USAGE}"); std::process::exit(2); } }; @@ -14,3 +30,75 @@ fn main() { std::process::exit(1); } } + +fn initialize_single_caller_config(args: &[OsString]) -> Result<(), String> { + let [ + path, + bind, + executable, + executable_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, + ] = args + else { + return Err("expected exactly thirteen configuration values".to_string()); + }; + let config = CodexDaemonConfig::single_caller( + os_text(bind, "bind")?, + PathBuf::from(executable), + parse_sha256(executable_sha256)?, + PathBuf::from(codex_home), + PathBuf::from(replay_store), + CodexCallerConfig { + caller_runtime_id: os_text(caller_runtime_id, "caller runtime")?, + connection_key_file: PathBuf::from(connection_key_file), + connection_key_epoch: parse_number(connection_key_epoch, "connection key epoch")?, + allowed_models: vec![os_text(allowed_model, "allowed model")?], + 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")?, + }, + ); + codex_connector::write_daemon_config(&PathBuf::from(path), &config) + .map_err(|error| error.to_string()) +} + +fn os_text(value: &OsString, field: &str) -> Result { + value + .to_str() + .filter(|value| !value.is_empty()) + .map(str::to_string) + .ok_or_else(|| format!("{field} must be non-empty UTF-8")) +} + +fn parse_number(value: &OsString, field: &str) -> Result +where + T: std::str::FromStr, +{ + os_text(value, field)? + .parse() + .map_err(|_| format!("{field} is not a valid integer")) +} + +fn parse_sha256(value: &OsString) -> Result<[u8; 32], String> { + let value = os_text(value, "Codex SHA-256")?; + if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err("Codex SHA-256 must be exactly 64 hexadecimal characters".to_string()); + } + let mut digest = [0_u8; 32]; + for (index, output) in digest.iter_mut().enumerate() { + *output = u8::from_str_radix(&value[index * 2..index * 2 + 2], 16) + .map_err(|_| "Codex SHA-256 is malformed".to_string())?; + } + Ok(digest) +}