From b2e76841bfd43afcd0d92546e1cf6dd822129ef1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 27 Aug 2026 16:03:31 +0200 Subject: [PATCH] Admit multiple models per connector caller --- src/daemon.rs | 27 +++++++++++++++++++++++---- src/main.rs | 45 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index ed48823..4cd4829 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -116,16 +116,18 @@ impl CodexDaemonConfig { } let mut caller_ids = HashSet::new(); for caller in &self.callers { + let mut allowed_models = HashSet::new(); if caller.caller_runtime_id.trim().is_empty() || caller.caller_runtime_id.trim() != caller.caller_runtime_id || !caller_ids.insert(caller.caller_runtime_id.as_str()) || caller.connection_key_file.as_os_str().is_empty() || caller.connection_key_epoch == 0 || caller.allowed_models.is_empty() - || caller - .allowed_models - .iter() - .any(|model| model.trim().is_empty() || model.trim() != model) + || caller.allowed_models.iter().any(|model| { + model.trim().is_empty() + || model.trim() != model + || !allowed_models.insert(model.as_str()) + }) || caller.max_concurrent_requests == 0 || caller.max_payload_bytes < 4096 || caller.max_payload_bytes > self.max_frame_bytes - FRAME_OVERHEAD_BUDGET @@ -529,6 +531,23 @@ mod tests { 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"); diff --git a/src/main.rs b/src/main.rs index 8ef76b6..670dfea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,10 @@ +use std::collections::HashSet; 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\n codex-connector --admit-caller-config SOURCE.cc DESTINATION.cc 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 --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\n codex-connector --enroll-provider-health-identity PATH.cc\n codex-connector --provider-health-public-key PATH.cc"; fn main() { let args = std::env::args_os().skip(1).collect::>(); @@ -71,7 +72,7 @@ fn admit_caller_config(args: &[OsString]) -> Result<(), String> { caller_runtime_id, connection_key_file, connection_key_epoch, - allowed_model, + allowed_models, max_concurrent_requests, max_payload_bytes, max_output_tokens, @@ -86,7 +87,7 @@ fn admit_caller_config(args: &[OsString]) -> Result<(), String> { caller_runtime_id, 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")?], + 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")?, @@ -109,7 +110,7 @@ fn initialize_single_caller_config(args: &[OsString]) -> Result<(), String> { caller_runtime_id, connection_key_file, connection_key_epoch, - allowed_model, + allowed_models, max_concurrent_requests, max_payload_bytes, max_output_tokens, @@ -127,7 +128,7 @@ fn initialize_single_caller_config(args: &[OsString]) -> Result<(), String> { 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")?], + allowed_models: parse_allowed_models(allowed_models)?, max_concurrent_requests: parse_number( max_concurrent_requests, "max concurrent requests", @@ -148,6 +149,19 @@ fn os_text(value: &OsString, field: &str) -> Result { .ok_or_else(|| format!("{field} must be non-empty UTF-8")) } +fn parse_allowed_models(value: &OsString) -> Result, 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(value: &OsString, field: &str) -> Result where T: std::str::FromStr, @@ -157,6 +171,27 @@ where .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> { let value = os_text(value, "Codex SHA-256")?; if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {