Make standalone connector config deployable
This commit is contained in:
parent
54d8bc2525
commit
548ee73b01
3 changed files with 150 additions and 8 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,31 @@ pub struct CodexDaemonConfig {
|
|||
}
|
||||
|
||||
impl CodexDaemonConfig {
|
||||
pub fn single_caller(
|
||||
bind: impl Into<String>,
|
||||
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<SocketAddr, CodexDaemonError> {
|
||||
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();
|
||||
|
|
|
|||
96
src/main.rs
96
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::<Vec<_>>();
|
||||
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<String, String> {
|
||||
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<T>(value: &OsString, field: &str) -> Result<T, String>
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue