claude-connector: the binary over one typed CultCache config document

gamecult.model_connector.claude_connector_config.v1 owns every bound and path
the backend and serve loop run with: read as an exact single-record canonical
snapshot, validated once for config init, config check and serve (loopback
listen, absolute paths, one caller, canonical priced claude- models). Refusals
name a field and never echo a value. serve refuses a prompt_dir that is not a
directory owned by the process uid with mode 0700.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-29 11:26:32 +01:00
commit b44c5a3b75
5 changed files with 1134 additions and 0 deletions

View file

@ -7,6 +7,9 @@ license.workspace = true
repository.workspace = true
publish = false
[[bin]]
name = "claude-connector"
path = "src/main.rs"
[dependencies]
anyhow = "1"

View file

@ -0,0 +1,318 @@
//! The claude-connector's configuration document and its serve verb. One typed
//! CultCache record owns every bound and path the backend and the serve loop
//! run with; nothing in the connector's environment, flags or JSON can change
//! them. Validation errors name a field and never echo a value.
use std::collections::{BTreeMap, BTreeSet};
use std::net::{SocketAddr, TcpListener};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use cultcache_rs::{CultCache, DatabaseEntry, SingleFileMessagePackBackingStore};
use model_connector::{
CallerConfig, MIN_PAYLOAD_BYTES, ModelPrice, ServeError, ServeLimits, load_admissions, serve,
};
use thiserror::Error;
use crate::{ClaudeCodeBackend, ClaudeCodeConfig, ClaudeCodeLimits, StderrSink};
const CONFIG_KEY: &str = "runtime";
const MODEL_PREFIX: &str = "claude-";
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "gamecult.model_connector.claude_connector_config.v1")]
pub struct ClaudeConnectorConfig {
/// A loopback socket address.
#[cultcache(key = 0)]
pub listen: String,
#[cultcache(key = 1)]
pub replay_store: PathBuf,
#[cultcache(key = 2)]
pub claude_binary: PathBuf,
#[cultcache(key = 3)]
pub claude_binary_sha256: [u8; 32],
#[cultcache(key = 4)]
pub work_dir: PathBuf,
#[cultcache(key = 5)]
pub config_dir: PathBuf,
/// Where each call's system prompt is staged: a directory the connector
/// owns, mode 0700.
#[cultcache(key = 6)]
pub prompt_dir: PathBuf,
#[cultcache(key = 7)]
pub instance: String,
/// The name of the systemd credential that holds the child's token.
#[cultcache(key = 8)]
pub token_credential: String,
#[cultcache(key = 9)]
pub deadline_ms: u64,
#[cultcache(key = 10)]
pub max_line_bytes: usize,
#[cultcache(key = 11)]
pub max_total_bytes: usize,
#[cultcache(key = 12)]
pub stderr_tail_bytes: usize,
#[cultcache(key = 13)]
pub max_frame_bytes: usize,
#[cultcache(key = 14)]
pub max_connections: usize,
#[cultcache(key = 15)]
pub socket_timeout_ms: u64,
#[cultcache(key = 16)]
pub max_expiry_skew_ms: u64,
/// Exactly one caller until claude-handle-caller-binding lands.
#[cultcache(key = 17)]
pub callers: Vec<CallerConfig>,
/// What each allowed model costs; keys are canonical model ids.
#[cultcache(key = 18)]
pub prices: BTreeMap<String, ModelPrice>,
}
/// A refusal. It names the field or stage that failed and nothing the
/// operator or the file typed.
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("invalid configuration field {0}")]
Invalid(&'static str),
#[error("the configuration store could not be read as a claude connector document")]
Store,
#[error("the prompt directory is not a directory owned by this process with mode 0700")]
PromptDir,
#[error("the listener could not be bound")]
Listen,
#[error(transparent)]
Serve(#[from] ServeError),
}
impl ClaudeConnectorConfig {
/// The one validation: `config init`, `config check` and `serve` all pass
/// through it. Returns the listen address.
pub fn validate(&self) -> Result<SocketAddr, ConfigError> {
let listen = self
.listen
.parse::<SocketAddr>()
.ok()
.filter(|address| address.ip().is_loopback())
.ok_or(ConfigError::Invalid("listen"))?;
let paths = [
("replay_store", &self.replay_store),
("claude_binary", &self.claude_binary),
("work_dir", &self.work_dir),
("config_dir", &self.config_dir),
("prompt_dir", &self.prompt_dir),
];
for (field, path) in paths {
if !path.is_absolute() {
return Err(ConfigError::Invalid(field));
}
}
if self.claude_binary_sha256 == [0; 32] {
return Err(ConfigError::Invalid("claude_binary_sha256"));
}
if !is_trimmed_nonempty(&self.instance) {
return Err(ConfigError::Invalid("instance"));
}
if !is_credential_name(&self.token_credential) {
return Err(ConfigError::Invalid("token_credential"));
}
if self.deadline_ms == 0
|| self.max_line_bytes == 0
|| self.max_total_bytes < self.max_line_bytes
|| self.stderr_tail_bytes == 0
{
return Err(ConfigError::Invalid("child limits"));
}
if self.max_frame_bytes == 0
|| self.max_connections == 0
|| self.socket_timeout_ms == 0
|| self.max_expiry_skew_ms == 0
{
return Err(ConfigError::Invalid("serve limits"));
}
let [caller] = self.callers.as_slice() else {
return Err(ConfigError::Invalid("callers"));
};
if !is_trimmed_nonempty(&caller.caller_runtime_id) {
return Err(ConfigError::Invalid("caller_runtime_id"));
}
if !caller.connection_key_file.is_absolute() {
return Err(ConfigError::Invalid("connection_key_file"));
}
if caller.connection_key_epoch == 0 {
return Err(ConfigError::Invalid("connection_key_epoch"));
}
if caller.max_concurrent_requests == 0
|| caller.max_payload_bytes < MIN_PAYLOAD_BYTES
|| caller.max_output_tokens == 0
|| caller.spend_ceiling_micro_usd == 0
|| caller.spend_window_ms == 0
{
return Err(ConfigError::Invalid("caller bounds"));
}
if caller.allowed_models.is_empty() {
return Err(ConfigError::Invalid("allowed_models"));
}
let mut seen = BTreeSet::new();
for model in &caller.allowed_models {
if !is_canonical_model(model)
|| !seen.insert(model.as_str())
|| !self.prices.contains_key(model)
{
return Err(ConfigError::Invalid("allowed_models"));
}
}
if !self.prices.keys().all(|model| is_canonical_model(model)) {
return Err(ConfigError::Invalid("prices"));
}
Ok(listen)
}
fn backend_config(&self) -> ClaudeCodeConfig {
ClaudeCodeConfig {
claude_binary: self.claude_binary.clone(),
claude_binary_sha256: self.claude_binary_sha256,
work_dir: self.work_dir.clone(),
config_dir: self.config_dir.clone(),
prompt_dir: self.prompt_dir.clone(),
token_credential: self.token_credential.clone(),
instance: self.instance.clone(),
limits: ClaudeCodeLimits {
deadline: Duration::from_millis(self.deadline_ms),
max_line_bytes: self.max_line_bytes,
max_total_bytes: self.max_total_bytes,
stderr_tail_bytes: self.stderr_tail_bytes,
},
}
}
fn serve_limits(&self) -> ServeLimits {
ServeLimits {
max_frame_bytes: self.max_frame_bytes,
max_connections: self.max_connections,
socket_timeout_ms: self.socket_timeout_ms,
max_expiry_skew_ms: self.max_expiry_skew_ms,
}
}
}
fn is_trimmed_nonempty(text: &str) -> bool {
!text.is_empty() && text.trim() == text
}
/// A model id the CLI echoes back unchanged: aliases are refused because the
/// stream's `init.model` must equal the requested model.
fn is_canonical_model(model: &str) -> bool {
model.starts_with(MODEL_PREFIX)
&& model.len() > MODEL_PREFIX.len()
&& model
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_'))
}
/// One file name under `$CREDENTIALS_DIRECTORY`, never a path.
fn is_credential_name(name: &str) -> bool {
!name.is_empty()
&& name != "."
&& name != ".."
&& name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_'))
}
/// Reads the document without writing: a shared snapshot of exactly one
/// record, with the exact key, type and schema id, in canonical positional
/// MessagePack, then validated.
pub fn load_config(path: &Path) -> Result<ClaudeConnectorConfig, ConfigError> {
let store = SingleFileMessagePackBackingStore::new(path);
let config = store
.with_read_only_shared_snapshot(|entries| {
let [envelope] = entries.as_slice() else {
anyhow::bail!("not exactly one record");
};
if envelope.key != CONFIG_KEY
|| envelope.r#type != ClaudeConnectorConfig::TYPE
|| envelope.schema_id.as_deref() != Some(ClaudeConnectorConfig::TYPE)
{
anyhow::bail!("wrong typed envelope");
}
let config: ClaudeConnectorConfig = rmp_serde::from_slice(&envelope.payload)?;
if rmp_serde::to_vec(&config)? != envelope.payload {
anyhow::bail!("not canonical positional MessagePack");
}
Ok(config)
})
.map_err(|_| ConfigError::Store)?;
config.validate()?;
Ok(config)
}
/// Validates, then writes the document as the store's only record.
pub fn write_config(path: &Path, config: &ClaudeConnectorConfig) -> Result<(), ConfigError> {
config.validate()?;
let mut cache = CultCache::new();
cache
.register_entry_type::<ClaudeConnectorConfig>()
.map_err(|_| ConfigError::Store)?;
cache
.add_generic_backing_store(SingleFileMessagePackBackingStore::new(path))
.map_err(|_| ConfigError::Store)?;
cache
.pull_all_backing_stores()
.map_err(|_| ConfigError::Store)?;
cache
.put(CONFIG_KEY, config)
.map_err(|_| ConfigError::Store)?;
Ok(())
}
/// The prompt directory holds every system prompt in flight: it must be a
/// real directory (not a link) that this process owns and nobody else can
/// enter.
#[cfg(unix)]
pub fn check_prompt_dir(dir: &Path) -> Result<(), ConfigError> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let metadata = std::fs::symlink_metadata(dir).map_err(|_| ConfigError::PromptDir)?;
// SAFETY: geteuid has no preconditions and cannot fail.
let uid = unsafe { libc::geteuid() };
if !metadata.is_dir()
|| metadata.uid() != uid
|| metadata.permissions().mode() & 0o7777 != 0o700
{
return Err(ConfigError::PromptDir);
}
Ok(())
}
/// The child's stderr is the log's business by size only: its bytes are never
/// written anywhere the connector controls.
struct LogSink;
impl StderrSink for LogSink {
fn stderr_tail(&self, tail: &[u8]) {
eprintln!("claude-connector: child stderr, {} bytes", tail.len());
}
}
/// Loads the document and serves until the listener fails.
#[cfg(unix)]
pub fn serve_config(path: &Path) -> Result<(), ConfigError> {
let config = load_config(path)?;
let listen = config.validate()?;
check_prompt_dir(&config.prompt_dir)?;
let admissions = load_admissions(&config.callers)?;
let backend = Arc::new(ClaudeCodeBackend::new(
config.backend_config(),
Box::new(LogSink),
));
let listener = TcpListener::bind(listen).map_err(|_| ConfigError::Listen)?;
serve(
listener,
config.serve_limits(),
admissions,
config.prices.clone(),
&config.replay_store,
backend,
)?;
Ok(())
}

View file

@ -5,6 +5,7 @@
mod backend;
mod config;
mod daemon;
mod handle;
mod launch;
mod process;
@ -12,6 +13,9 @@ mod stream;
pub use backend::{ClaudeCodeBackend, CredentialTokenSource, StderrSink, TokenSource};
pub use config::{ClaudeCodeConfig, ClaudeCodeLimits};
pub use daemon::{ClaudeConnectorConfig, ConfigError, load_config, write_config};
#[cfg(unix)]
pub use daemon::{check_prompt_dir, serve_config};
pub use handle::{FsSessionStore, SessionStore};
pub use launch::ChildEnv;
#[cfg(unix)]

View file

@ -0,0 +1,238 @@
//! The claude-connector binary.
//!
//! claude-connector serve --config PATH.cc
//! claude-connector config init PATH.cc --flag value ...
//! claude-connector config check PATH.cc
//!
//! `serve` takes its configuration from the typed document alone. The flags of
//! `config init` exist only to author that document. No message here echoes a
//! value that was typed or stored; refusals name a field or a flag.
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::process::ExitCode;
use claude_connector::{ClaudeConnectorConfig, ConfigError, load_config, write_config};
use model_connector::{CallerConfig, ModelPrice, Rates, frame_bytes_for_payload};
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
let args: Vec<&str> = args.iter().map(String::as_str).collect();
let result = match args.as_slice() {
["serve", "--config", path] => serve(path),
["config", "init", path, flags @ ..] => init(path, flags),
["config", "check", path] => check(path),
_ => Err(Failure::Usage),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(failure) => {
eprintln!("claude-connector: {failure}");
ExitCode::FAILURE
}
}
}
#[derive(Debug, thiserror::Error)]
enum Failure {
#[error("usage: serve --config PATH | config init PATH --flag value ... | config check PATH")]
Usage,
#[error(transparent)]
Config(#[from] ConfigError),
#[error("flag {0} is missing")]
Missing(&'static str),
#[error("flag {0} is malformed")]
Malformed(&'static str),
#[error("a flag is unknown, repeated or has no value")]
Flags,
}
#[cfg(unix)]
fn serve(path: &str) -> Result<(), Failure> {
Ok(claude_connector::serve_config(path.as_ref())?)
}
#[cfg(not(unix))]
fn serve(_: &str) -> Result<(), Failure> {
Err(Failure::Usage)
}
fn check(path: &str) -> Result<(), Failure> {
let config = load_config(path.as_ref())?;
println!("claude connector configuration is valid");
println!("callers: {}", config.callers.len());
println!("allowed_models: {}", config.callers[0].allowed_models.len());
println!("prices: {}", config.prices.len());
Ok(())
}
/// The flags of `config init`, each given at most once except the two lists.
struct Flags {
single: BTreeMap<&'static str, String>,
models: Vec<String>,
prices: Vec<String>,
}
const SINGLE: [&str; 22] = [
"--listen",
"--replay-store",
"--claude-binary",
"--claude-sha256",
"--work-dir",
"--config-dir",
"--prompt-dir",
"--instance",
"--token-credential",
"--caller-id",
"--connection-key-file",
"--connection-key-epoch",
"--spend-ceiling-micro-usd",
"--spend-window-ms",
"--deadline-ms",
"--max-line-bytes",
"--max-total-bytes",
"--stderr-tail-bytes",
"--max-concurrent-requests",
"--max-payload-bytes",
"--max-output-tokens",
"--socket-timeout-ms",
];
impl Flags {
fn parse(args: &[&str]) -> Result<Self, Failure> {
let mut flags = Flags {
single: BTreeMap::new(),
models: Vec::new(),
prices: Vec::new(),
};
let mut args = args.iter();
while let Some(&name) = args.next() {
let Some(&value) = args.next() else {
return Err(Failure::Flags);
};
match name {
"--allowed-model" => flags.models.push(value.to_string()),
"--price" => flags.prices.push(value.to_string()),
_ => {
let Some(known) = SINGLE.iter().find(|known| **known == name) else {
return Err(Failure::Flags);
};
if flags.single.insert(*known, value.to_string()).is_some() {
return Err(Failure::Flags);
}
}
}
}
Ok(flags)
}
fn text(&self, name: &'static str) -> Result<String, Failure> {
self.single.get(name).cloned().ok_or(Failure::Missing(name))
}
fn path(&self, name: &'static str) -> Result<PathBuf, Failure> {
self.text(name).map(PathBuf::from)
}
fn number<T: std::str::FromStr>(&self, name: &'static str) -> Result<T, Failure> {
self.text(name)?
.parse()
.map_err(|_| Failure::Malformed(name))
}
fn number_or<T: std::str::FromStr>(
&self,
name: &'static str,
default: T,
) -> Result<T, Failure> {
match self.single.get(name) {
None => Ok(default),
Some(text) => text.parse().map_err(|_| Failure::Malformed(name)),
}
}
}
fn sha256(hex: &str) -> Option<[u8; 32]> {
if hex.len() != 64 || !hex.is_ascii() {
return None;
}
let mut digest = [0_u8; 32];
for (byte, pair) in digest.iter_mut().zip(hex.as_bytes().chunks(2)) {
*byte = u8::from_str_radix(std::str::from_utf8(pair).ok()?, 16).ok()?;
}
Some(digest)
}
/// MODEL:INPUT:OUTPUT:CACHE_WRITE:CACHE_READ:OVERHEAD, rates in micro-USD per
/// million tokens. The one cache-write rate prices both retentions.
fn price(text: &str) -> Option<(String, ModelPrice)> {
let mut parts = text.split(':');
let model = parts.next()?.to_string();
let mut next = || parts.next()?.parse::<u64>().ok();
let (input, output, cache_write, cache_read, overhead_tokens) =
(next()?, next()?, next()?, next()?, next()?);
if parts.next().is_some() {
return None;
}
Some((
model,
ModelPrice {
rates: Rates {
input,
cache_write_short: cache_write,
cache_write_long: cache_write,
cache_read,
output,
},
long_context: None,
overhead_tokens,
},
))
}
fn init(path: &str, args: &[&str]) -> Result<(), Failure> {
let flags = Flags::parse(args)?;
let max_payload_bytes: usize = flags.number_or("--max-payload-bytes", 1 << 20)?;
let max_concurrent_requests: usize = flags.number_or("--max-concurrent-requests", 4)?;
let mut prices = BTreeMap::new();
for text in &flags.prices {
let (model, price) = price(text).ok_or(Failure::Malformed("--price"))?;
if prices.insert(model, price).is_some() {
return Err(Failure::Malformed("--price"));
}
}
let config = ClaudeConnectorConfig {
listen: flags.text("--listen")?,
replay_store: flags.path("--replay-store")?,
claude_binary: flags.path("--claude-binary")?,
claude_binary_sha256: sha256(&flags.text("--claude-sha256")?)
.ok_or(Failure::Malformed("--claude-sha256"))?,
work_dir: flags.path("--work-dir")?,
config_dir: flags.path("--config-dir")?,
prompt_dir: flags.path("--prompt-dir")?,
instance: flags.text("--instance")?,
token_credential: flags.text("--token-credential")?,
deadline_ms: flags.number_or("--deadline-ms", 600_000)?,
max_line_bytes: flags.number_or("--max-line-bytes", 4 << 20)?,
max_total_bytes: flags.number_or("--max-total-bytes", 64 << 20)?,
stderr_tail_bytes: flags.number_or("--stderr-tail-bytes", 8192)?,
max_frame_bytes: frame_bytes_for_payload(max_payload_bytes),
max_connections: max_concurrent_requests.max(8),
socket_timeout_ms: flags.number_or("--socket-timeout-ms", 10_000)?,
max_expiry_skew_ms: 300_000,
callers: vec![CallerConfig {
caller_runtime_id: flags.text("--caller-id")?,
connection_key_file: flags.path("--connection-key-file")?,
connection_key_epoch: flags.number("--connection-key-epoch")?,
allowed_models: flags.models.clone(),
max_concurrent_requests,
max_payload_bytes,
max_output_tokens: flags.number_or("--max-output-tokens", 32_768)?,
spend_ceiling_micro_usd: flags.number("--spend-ceiling-micro-usd")?,
spend_window_ms: flags.number("--spend-window-ms")?,
}],
prices,
};
write_config(path.as_ref(), &config)?;
Ok(())
}

View file

@ -0,0 +1,571 @@
//! The claude-connector's configuration document and binary: what the
//! document accepts, what it refuses, and that no refusal or output echoes a
//! byte of a value that was typed, stored or read from a credential.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicUsize, Ordering};
use claude_connector::{ClaudeConnectorConfig, ConfigError, load_config, write_config};
use cultcache_rs::{CultCache, DatabaseEntry, SingleFileMessagePackBackingStore};
use model_connector::{CallerConfig, ModelPrice, Rates};
const CANARY: &str = "canary-9f3b7c1d";
const OPUS: &str = "claude-opus-5";
const HAIKU: &str = "claude-haiku-5-5";
/// An edit that should make the valid document refuse.
type Change = Box<dyn Fn(&mut ClaudeConnectorConfig)>;
static SEQUENCE: AtomicUsize = AtomicUsize::new(0);
struct Scratch(PathBuf);
impl Scratch {
fn new() -> Self {
let dir = std::env::temp_dir().join(format!(
"claude-connector-config-{}-{}",
std::process::id(),
SEQUENCE.fetch_add(1, Ordering::SeqCst)
));
std::fs::create_dir_all(&dir).unwrap();
Self(dir)
}
fn path(&self, name: &str) -> PathBuf {
self.0.join(name)
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn price() -> ModelPrice {
ModelPrice {
rates: Rates {
input: 3_000_000,
cache_write_short: 3_750_000,
cache_write_long: 3_750_000,
cache_read: 300_000,
output: 15_000_000,
},
long_context: None,
overhead_tokens: 100,
}
}
fn valid() -> ClaudeConnectorConfig {
ClaudeConnectorConfig {
listen: "127.0.0.1:7443".to_string(),
replay_store: "/var/lib/claude-connector/replay.cc".into(),
claude_binary: "/opt/claude/bin/claude".into(),
claude_binary_sha256: [7; 32],
work_dir: "/var/lib/claude-connector/work".into(),
config_dir: "/var/lib/claude-connector/home".into(),
prompt_dir: "/var/lib/claude-connector/prompts".into(),
instance: "persona-a".to_string(),
token_credential: "claude-token".to_string(),
deadline_ms: 600_000,
max_line_bytes: 4 << 20,
max_total_bytes: 64 << 20,
stderr_tail_bytes: 8192,
max_frame_bytes: 2 << 20,
max_connections: 8,
socket_timeout_ms: 10_000,
max_expiry_skew_ms: 300_000,
callers: vec![CallerConfig {
caller_runtime_id: "persona-a-caller".to_string(),
connection_key_file: "/etc/claude-connector/caller.key".into(),
connection_key_epoch: 1,
allowed_models: vec![HAIKU.to_string(), OPUS.to_string()],
max_concurrent_requests: 4,
max_payload_bytes: 1 << 20,
max_output_tokens: 32_768,
spend_ceiling_micro_usd: 50_000_000,
spend_window_ms: 3_600_000,
}],
prices: BTreeMap::from([(HAIKU.to_string(), price()), (OPUS.to_string(), price())]),
}
}
fn refused(config: &ClaudeConnectorConfig) -> &'static str {
match config.validate() {
Err(ConfigError::Invalid(field)) => field,
other => panic!("expected a refusal naming a field, got {other:?}"),
}
}
#[test]
fn config_round_trips_through_cultcache() {
let scratch = Scratch::new();
let path = scratch.path("connector.cc");
let config = valid();
write_config(&path, &config).unwrap();
assert_eq!(load_config(&path).unwrap(), config);
}
#[test]
fn a_second_record_or_the_wrong_type_is_refused() {
let scratch = Scratch::new();
let path = scratch.path("two.cc");
let config = valid();
write_config(&path, &config).unwrap();
let mut cache = CultCache::new();
cache
.register_entry_type::<ClaudeConnectorConfig>()
.unwrap();
cache
.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&path))
.unwrap();
cache.pull_all_backing_stores().unwrap();
cache.put("extra", &config).unwrap();
assert!(matches!(load_config(&path), Err(ConfigError::Store)));
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "gamecult.other.document.v1")]
struct Other {
#[cultcache(key = 0)]
value: u32,
}
let other = scratch.path("other.cc");
let mut cache = CultCache::new();
cache.register_entry_type::<Other>().unwrap();
cache
.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&other))
.unwrap();
cache.pull_all_backing_stores().unwrap();
cache.put("runtime", &Other { value: 1 }).unwrap();
assert!(matches!(load_config(&other), Err(ConfigError::Store)));
assert!(matches!(
load_config(&scratch.path("absent.cc")),
Err(ConfigError::Store)
));
}
#[test]
fn a_write_refuses_an_invalid_document_and_leaves_no_file() {
let scratch = Scratch::new();
let path = scratch.path("bad.cc");
let mut config = valid();
config.callers[0].allowed_models = vec!["opus".to_string()];
assert!(write_config(&path, &config).is_err());
assert!(!path.exists());
}
#[test]
fn config_refuses_aliases_unpriced_models_and_second_callers() {
let valid_config = valid();
assert_eq!(valid_config.validate().unwrap().port(), 7443);
let cases: Vec<(&str, Change)> = vec![
(
"alias allowed",
Box::new(|c| {
c.callers[0].allowed_models = vec!["opus".to_string()];
}),
),
(
"alias price key",
Box::new(|c| {
c.prices.insert("sonnet".to_string(), price());
}),
),
(
"bare prefix",
Box::new(|c| {
c.callers[0].allowed_models = vec!["claude-".to_string()];
}),
),
(
"unpriced allowed",
Box::new(|c| {
c.prices.remove(OPUS);
}),
),
(
"duplicate allowed",
Box::new(|c| {
c.callers[0].allowed_models = vec![HAIKU.to_string(), HAIKU.to_string()];
}),
),
(
"no allowed",
Box::new(|c| c.callers[0].allowed_models.clear()),
),
(
"second caller",
Box::new(|c| {
let mut second = c.callers[0].clone();
second.caller_runtime_id = "other".to_string();
c.callers.push(second);
}),
),
("no caller", Box::new(|c| c.callers.clear())),
];
let fields = [
"allowed_models",
"prices",
"allowed_models",
"allowed_models",
"allowed_models",
"allowed_models",
"callers",
"callers",
];
for ((name, change), field) in cases.iter().zip(fields) {
let mut config = valid();
change(&mut config);
assert_eq!(refused(&config), field, "{name}");
}
}
#[test]
fn config_refuses_exposed_listeners_relative_paths_and_empty_bounds() {
let cases: Vec<(&str, &str, Change)> = vec![
(
"wildcard listen",
"listen",
Box::new(|c| c.listen = "0.0.0.0:7443".into()),
),
(
"lan listen",
"listen",
Box::new(|c| c.listen = "192.168.1.9:7443".into()),
),
(
"hostname listen",
"listen",
Box::new(|c| c.listen = "localhost:7443".into()),
),
(
"replay",
"replay_store",
Box::new(|c| c.replay_store = "replay.cc".into()),
),
(
"binary",
"claude_binary",
Box::new(|c| c.claude_binary = "claude".into()),
),
("work", "work_dir", Box::new(|c| c.work_dir = "work".into())),
(
"config",
"config_dir",
Box::new(|c| c.config_dir = "cfg".into()),
),
(
"prompts",
"prompt_dir",
Box::new(|c| c.prompt_dir = "prompts".into()),
),
(
"key file",
"connection_key_file",
Box::new(|c| {
c.callers[0].connection_key_file = "caller.key".into();
}),
),
(
"zero pin",
"claude_binary_sha256",
Box::new(|c| c.claude_binary_sha256 = [0; 32]),
),
(
"blank instance",
"instance",
Box::new(|c| c.instance = " ".into()),
),
(
"credential path",
"token_credential",
Box::new(|c| c.token_credential = "a/b".into()),
),
(
"credential parent",
"token_credential",
Box::new(|c| c.token_credential = "..".into()),
),
(
"credential empty",
"token_credential",
Box::new(|c| c.token_credential = String::new()),
),
(
"zero deadline",
"child limits",
Box::new(|c| c.deadline_ms = 0),
),
(
"total below line",
"child limits",
Box::new(|c| c.max_total_bytes = c.max_line_bytes - 1),
),
(
"zero frame",
"serve limits",
Box::new(|c| c.max_frame_bytes = 0),
),
(
"zero epoch",
"connection_key_epoch",
Box::new(|c| c.callers[0].connection_key_epoch = 0),
),
(
"tiny payload",
"caller bounds",
Box::new(|c| c.callers[0].max_payload_bytes = 16),
),
(
"no ceiling",
"caller bounds",
Box::new(|c| c.callers[0].spend_ceiling_micro_usd = 0),
),
];
for (name, field, change) in cases {
let mut config = valid();
change(&mut config);
assert_eq!(refused(&config), field, "{name}");
}
}
#[test]
fn no_refusal_echoes_a_value() {
let canary_path: PathBuf = format!("{CANARY}/relative").into();
let canaried: Vec<Change> = vec![
Box::new(|c| c.listen = format!("{CANARY}:1")),
Box::new(|c| c.instance = format!(" {CANARY} ")),
Box::new(|c| c.token_credential = format!("{CANARY}/x")),
Box::new(|c| c.callers[0].allowed_models = vec![format!("{CANARY}-model")]),
Box::new(|c| {
c.prices.insert(format!("{CANARY}-alias"), price());
}),
Box::new(|c| c.callers[0].caller_runtime_id = format!(" {CANARY}")),
Box::new({
let path = canary_path.clone();
move |c| c.replay_store = path.clone()
}),
Box::new({
let path = canary_path.clone();
move |c| c.callers[0].connection_key_file = path.clone()
}),
Box::new(|c| {
let mut second = c.callers[0].clone();
second.caller_runtime_id = CANARY.to_string();
c.callers.push(second);
}),
];
for change in canaried {
let mut config = valid();
change(&mut config);
let error = config.validate().unwrap_err();
for shown in [error.to_string(), format!("{error:?}")] {
assert!(!shown.contains(CANARY), "{shown}");
}
let scratch = Scratch::new();
let write = write_config(&scratch.path("x.cc"), &config).unwrap_err();
assert!(!format!("{write} {write:?}").contains(CANARY));
}
}
// ---- the binary ----
fn run(args: &[&str], envs: &[(&str, &Path)]) -> Output {
let mut command = Command::new(env!("CARGO_BIN_EXE_claude-connector"));
command.args(args).env_clear();
for (name, value) in envs {
command.env(name, value);
}
command.output().unwrap()
}
fn shown(output: &Output) -> String {
format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
}
fn init_flags<'a>(scratch: &'a Scratch, extra: &[&'a str]) -> Vec<String> {
let path = |name: &str| scratch.path(name).to_string_lossy().into_owned();
let mut flags: Vec<String> = [
"--listen",
"127.0.0.1:0",
"--replay-store",
&path("replay.cc"),
"--claude-binary",
&path("claude"),
"--claude-sha256",
&"ab".repeat(32),
"--work-dir",
&path("work"),
"--config-dir",
&path("home"),
"--prompt-dir",
&path("prompts"),
"--instance",
"persona-a",
"--token-credential",
"claude-token",
"--caller-id",
"persona-a-caller",
"--connection-key-file",
&path("caller.key"),
"--connection-key-epoch",
"1",
"--spend-ceiling-micro-usd",
"50000000",
"--spend-window-ms",
"3600000",
"--allowed-model",
HAIKU,
"--price",
&format!("{HAIKU}:1000000:5000000:1250000:100000:100"),
]
.map(String::from)
.to_vec();
flags.extend(extra.iter().map(|flag| flag.to_string()));
flags
}
fn init(scratch: &Scratch, flags: &[String]) -> Output {
let config = scratch.path("connector.cc");
let mut args = vec!["config", "init", config.to_str().unwrap()];
args.extend(flags.iter().map(String::as_str));
run(&args, &[])
}
#[test]
fn config_init_then_check_reports_counts_only() {
let scratch = Scratch::new();
let made = init(&scratch, &init_flags(&scratch, &[]));
assert!(made.status.success(), "{}", shown(&made));
let config = load_config(&scratch.path("connector.cc")).unwrap();
assert_eq!(config.callers[0].allowed_models, [HAIKU]);
assert_eq!(config.prices[HAIKU].rates.output, 5_000_000);
assert_eq!(config.prices[HAIKU].rates.cache_write_long, 1_250_000);
assert_eq!(config.prices[HAIKU].overhead_tokens, 100);
assert_eq!(config.claude_binary_sha256, [0xab; 32]);
let checked = run(
&[
"config",
"check",
scratch.path("connector.cc").to_str().unwrap(),
],
&[],
);
assert!(checked.status.success());
let text = shown(&checked);
assert!(text.contains("callers: 1") && text.contains("allowed_models: 1"));
assert!(text.contains("prices: 1"));
assert!(!text.contains("persona-a") && !text.contains(HAIKU));
}
#[test]
fn the_binary_refuses_without_echoing_what_it_was_given() {
let spaced = format!(" {CANARY}");
// Each pair replaces the value of an existing flag; the others are appended.
let replaced: Vec<(&str, &str)> = vec![
("--listen", CANARY),
("--claude-sha256", CANARY),
("--price", CANARY),
("--instance", &spaced),
("--connection-key-epoch", CANARY),
("--allowed-model", CANARY),
];
let appended: Vec<Vec<&str>> = vec![
vec![CANARY, CANARY],
vec!["--listen"],
vec!["--listen", "127.0.0.1:1"],
];
let mut attempts: Vec<Vec<String>> = Vec::new();
for (name, value) in replaced {
let scratch = Scratch::new();
let mut flags = init_flags(&scratch, &[]);
let at = flags.iter().position(|flag| flag == name).unwrap();
flags[at + 1] = value.to_string();
attempts.push(flags);
}
for extra in appended {
let scratch = Scratch::new();
attempts.push(init_flags(&scratch, &extra));
}
for flags in attempts {
let scratch = Scratch::new();
let made = init(&scratch, &flags);
assert!(!made.status.success(), "{flags:?}");
assert!(!shown(&made).contains(CANARY), "{}", shown(&made));
assert!(!scratch.path("connector.cc").exists());
}
// A missing flag is refused by name.
let scratch = Scratch::new();
let mut flags = init_flags(&scratch, &[]);
let at = flags.iter().position(|f| f == "--instance").unwrap();
flags.drain(at..at + 2);
let made = init(&scratch, &flags);
assert!(!made.status.success());
assert!(shown(&made).contains("--instance"));
}
#[cfg(unix)]
#[test]
fn serve_refuses_an_unsafe_prompt_dir_and_prints_no_credential() {
use std::os::unix::fs::{PermissionsExt, symlink};
let scratch = Scratch::new();
let credentials = scratch.path("credentials");
std::fs::create_dir_all(&credentials).unwrap();
std::fs::write(credentials.join("claude-token"), CANARY).unwrap();
std::fs::write(scratch.path("caller.key"), "caller-secret").unwrap();
let made = init(&scratch, &init_flags(&scratch, &[]));
assert!(made.status.success(), "{}", shown(&made));
let config = scratch.path("connector.cc");
let serve = |config: &Path| {
run(
&["serve", "--config", config.to_str().unwrap()],
&[("CREDENTIALS_DIRECTORY", &credentials)],
)
};
// No prompt directory, then one others can enter, then a link to a good one.
let prompts = scratch.path("prompts");
let refused = serve(&config);
assert!(!refused.status.success());
assert!(
shown(&refused).contains("prompt directory"),
"{}",
shown(&refused)
);
std::fs::create_dir_all(&prompts).unwrap();
std::fs::set_permissions(&prompts, std::fs::Permissions::from_mode(0o755)).unwrap();
let open = serve(&config);
assert!(
shown(&open).contains("prompt directory"),
"{}",
shown(&open)
);
assert!(claude_connector::check_prompt_dir(&prompts).is_err());
std::fs::set_permissions(&prompts, std::fs::Permissions::from_mode(0o700)).unwrap();
assert!(claude_connector::check_prompt_dir(&prompts).is_ok());
let linked = scratch.path("linked");
symlink(&prompts, &linked).unwrap();
assert!(claude_connector::check_prompt_dir(&linked).is_err());
// A safe directory but no claude binary: the backend is not ready, and
// nothing the credential holds is printed.
let not_ready = serve(&config);
assert!(!not_ready.status.success());
for output in [&refused, &open, &not_ready] {
let text = shown(output);
assert!(
!text.contains(CANARY) && !text.contains("caller-secret"),
"{text}"
);
}
}