Compare commits

...
Sign in to create a new pull request.
Author SHA1 Message Date
Meta Vens
67c7849cb6 claude-connector: pin every serve-error mapping to a field with no stored value
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 12:18:50 +01:00
Meta Vens
85ed36d292 claude-connector: rustfmt and clippy on the readiness tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 12:10:35 +01:00
Meta Vens
2f2f7a9db5 claude-connector: pre-spawn failures bill zero, model ids and prices are fuses, check runs serve's readiness, serve errors name fields
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 12:08:29 +01:00
Meta Vens
83c93b2bb8 claude-connector: pin the config envelope, exact bounds and the prompt-dir type check
Mutation survivors of the first pass: each part of the typed envelope (key,
type, schema id, canonical encoding) is refused alone; the exact boundaries of
the child and caller bounds, every serve limit and the bare model prefix are
pinned; a private regular file is not a prompt directory; a short sha256 is
refused.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 11:37:17 +01:00
Meta Vens
b44c5a3b75 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>
2026-10-29 11:26:32 +01:00
Meta Vens
9a062f0846 claude-connector: token from a systemd credential, pin before every spawn, prompts in prompt_dir, door set 401/403/429
Delete EnvLookup and the connector's own environment as a token source; a
TokenSource port reads $CREDENTIALS_DIRECTORY/<token_credential> at each use
(claude-connector-token-credential). binary_matches_pin is the one pin check,
used by ready() and before every spawn. PromptFile::create stages into the
configured prompt_dir instead of temp_dir(). The zero-bill door set is exactly
401, 403 and 429 with explicitly empty since-last usage, pinned by a table over
statuses and usage shapes. Soul s2's stderr-join probe is committed. The three
price types gain serde derives so the price table can live in the config
document.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 11:26:30 +01:00
14 changed files with 2273 additions and 134 deletions

2
Cargo.lock generated
View file

@ -138,6 +138,8 @@ dependencies = [
name = "claude-connector"
version = "0.1.0"
dependencies = [
"anyhow",
"cultcache-rs",
"libc",
"model-connector",
"rmp-serde",

View file

@ -7,7 +7,13 @@ license.workspace = true
repository.workspace = true
publish = false
[[bin]]
name = "claude-connector"
path = "src/main.rs"
[dependencies]
anyhow = "1"
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a4eae9056152335662f1115f97bf35fb00ab6d23", package = "cultcache-rs" }
model-connector = { path = "../model-connector", features = ["daemon"] }
rmp-serde = "1"
serde = { version = "1", features = ["derive"] }

View file

@ -1,5 +1,6 @@
use std::fs::File;
use std::io;
use std::path::PathBuf;
use std::time::Instant;
use model_connector::{
@ -11,12 +12,54 @@ use zeroize::Zeroizing;
use crate::config::{ClaudeCodeConfig, PROVIDER};
use crate::handle::{ClaudeCodeHandle, SessionStore, plan, prefix_digest, stdin_bytes};
use crate::launch::{PromptFile, TOKEN_VAR, argv, child_env};
use crate::launch::{PromptFile, argv, child_env};
use crate::process::{ProcessPort, RunningChild, StdoutRead};
use crate::stream::{Failure, Finished, Step, StreamState};
/// How the backend reads its own environment.
type EnvLookup = Box<dyn Fn(&str) -> Option<String> + Send + Sync>;
/// Where the child's credential comes from, read afresh at every use. The
/// connector's own environment is never a source (ruling
/// claude-connector-token-credential).
pub trait TokenSource: Send + Sync {
/// The token, or `None` when there is none or it is empty.
fn read(&self) -> Option<Zeroizing<String>>;
}
/// A token in a file named by a systemd credential: the file
/// `$CREDENTIALS_DIRECTORY/<name>`, read on every call so a rotated credential
/// is picked up by the next spawn.
pub struct CredentialTokenSource {
directory: Option<PathBuf>,
name: String,
}
impl CredentialTokenSource {
/// The credential `name` under the directory systemd names in
/// `CREDENTIALS_DIRECTORY`; with no such variable the source has no token.
/// The variable names a directory, never a token.
pub fn from_environment(name: impl Into<String>) -> Self {
Self {
directory: std::env::var_os("CREDENTIALS_DIRECTORY").map(PathBuf::from),
name: name.into(),
}
}
pub fn in_directory(directory: impl Into<PathBuf>, name: impl Into<String>) -> Self {
Self {
directory: Some(directory.into()),
name: name.into(),
}
}
}
impl TokenSource for CredentialTokenSource {
fn read(&self) -> Option<Zeroizing<String>> {
let directory = self.directory.as_ref()?;
let mut token = Zeroizing::new(std::fs::read_to_string(directory.join(&self.name)).ok()?);
let trimmed = token.trim_end_matches(['\r', '\n']).len();
token.truncate(trimmed);
(!token.is_empty()).then_some(token)
}
}
/// Where the tail of a child's stderr goes. The backend never puts it in a
/// result; the connector binary wires this to its log.
@ -31,7 +74,7 @@ pub struct ClaudeCodeBackend {
port: Box<dyn ProcessPort>,
sessions: Box<dyn SessionStore>,
sink: Box<dyn StderrSink>,
env_lookup: EnvLookup,
tokens: Box<dyn TokenSource>,
}
impl ClaudeCodeBackend {
@ -42,7 +85,10 @@ impl ClaudeCodeBackend {
let sessions = Box::new(crate::handle::FsSessionStore::new(
config.config_dir.clone(),
));
Self::with_ports(config, port, sessions, sink)
let tokens = Box::new(CredentialTokenSource::from_environment(
config.token_credential.clone(),
));
Self::with_ports(config, port, sessions, sink, tokens)
}
pub fn with_ports(
@ -50,29 +96,21 @@ impl ClaudeCodeBackend {
port: Box<dyn ProcessPort>,
sessions: Box<dyn SessionStore>,
sink: Box<dyn StderrSink>,
tokens: Box<dyn TokenSource>,
) -> Self {
Self {
config,
port,
sessions,
sink,
env_lookup: Box::new(|name| std::env::var(name).ok()),
tokens,
}
}
fn token(&self) -> Option<Zeroizing<String>> {
(self.env_lookup)(TOKEN_VAR)
.filter(|value| !value.is_empty())
.map(Zeroizing::new)
}
/// The one pin check: `ready` and every spawn use it, and nothing caches
/// its answer.
fn binary_matches_pin(&self) -> bool {
let Ok(mut file) = File::open(&self.config.claude_binary) else {
return false;
};
let mut hasher = Sha256::new();
io::copy(&mut file, &mut hasher).is_ok()
&& <[u8; 32]>::from(hasher.finalize()) == self.config.claude_binary_sha256
binary_matches_pin(&self.config)
}
fn run(&self, request: &ProviderRequest) -> (Vec<TransportEvent>, TransportOutcome) {
@ -86,15 +124,21 @@ impl ClaudeCodeBackend {
);
}
};
let Some(token) = self.token() else {
let Some(token) = self.tokens.read() else {
return failed(
FailureKind::Authentication,
"claude-code: the connector has no credential to give the child".to_string(),
Some(zero_usage()),
);
};
let Ok(prompt) = PromptFile::create(&request.instructions) else {
return failure(Failure::unbilled(
if !self.binary_matches_pin() {
return failure(Failure::never_reached(
FailureKind::Provider,
"claude-code: the claude binary does not match its pin",
));
}
let Ok(prompt) = PromptFile::create(&self.config.prompt_dir, &request.instructions) else {
return failure(Failure::never_reached(
FailureKind::Provider,
"claude-code: the system prompt could not be staged",
));
@ -110,7 +154,7 @@ impl ClaudeCodeBackend {
stdin_bytes(plan.blocks()),
);
let Ok(mut child) = spawned else {
return failure(Failure::unbilled(
return failure(Failure::never_reached(
FailureKind::Provider,
"claude-code: the child could not be spawned",
));
@ -175,11 +219,8 @@ impl ClaudeCodeBackend {
}
impl ProviderBackend for ClaudeCodeBackend {
fn ready(&self) -> bool {
self.config.work_dir.is_dir()
&& self.config.config_dir.is_dir()
&& self.token().is_some()
&& self.binary_matches_pin()
fn ready(&self) -> Result<(), &'static str> {
readiness(&self.config, self.tokens.as_ref())
}
fn provider(&self) -> &str {
@ -206,19 +247,19 @@ fn read_stream(
}
}
StdoutRead::Eof => {
return Err(Failure::unbilled(
return Err(Failure::cost_unknown(
FailureKind::Provider,
"claude-code: the stream ended without a result",
));
}
StdoutRead::Timeout => {
return Err(Failure::unbilled(
return Err(Failure::cost_unknown(
FailureKind::Timeout,
"claude-code: the deadline passed before a result",
));
}
StdoutRead::Breach => {
return Err(Failure::unbilled(
return Err(Failure::cost_unknown(
FailureKind::Provider,
"claude-code: the output passed its size cap",
));
@ -230,6 +271,39 @@ fn read_stream(
finished
}
fn binary_matches_pin(config: &ClaudeCodeConfig) -> bool {
let Ok(mut file) = File::open(&config.claude_binary) else {
return false;
};
let mut hasher = Sha256::new();
io::copy(&mut file, &mut hasher).is_ok()
&& <[u8; 32]>::from(hasher.finalize()) == config.claude_binary_sha256
}
/// Whether the backend can take work, naming the first config field that
/// cannot be used. `config check` and `serve` both ask this.
pub(crate) fn readiness(
config: &ClaudeCodeConfig,
tokens: &dyn TokenSource,
) -> Result<(), &'static str> {
if !config.work_dir.is_dir() {
return Err("work_dir");
}
if !config.config_dir.is_dir() {
return Err("config_dir");
}
if !config.prompt_dir.is_dir() {
return Err("prompt_dir");
}
if tokens.read().is_none() {
return Err("token_credential");
}
if !binary_matches_pin(config) {
return Err("claude_binary_sha256");
}
Ok(())
}
fn zero_usage() -> Usage {
Usage {
input_tokens: 0,

View file

@ -13,7 +13,7 @@ use model_connector::{
use super::*;
use crate::config::ClaudeCodeLimits;
use crate::handle::{FsSessionStore, SessionStore};
use crate::launch::ChildEnv;
use crate::launch::{ChildEnv, TOKEN_VAR};
const CANARY_TOKEN: &str = "canary-oauth-token-value";
const MODEL: &str = "claude-haiku-5-5";
@ -243,17 +243,47 @@ impl StderrSink for Sink {
}
}
fn config(instance: &str, limits: ClaudeCodeLimits) -> ClaudeCodeConfig {
/// The bytes of the stand-in `claude` the scripted rigs pin.
const PINNED: &[u8] = b"the pinned claude bytes";
fn pin_of(bytes: &[u8]) -> [u8; 32] {
Sha256::digest(bytes).into()
}
/// A directory holding a pinned `claude` file and an empty `prompts` directory.
fn sandbox() -> TempDir {
let dir = TempDir::new();
std::fs::write(dir.path().join("claude"), PINNED).unwrap();
std::fs::create_dir_all(dir.path().join("prompts")).unwrap();
dir
}
fn config(dir: &Path, instance: &str, limits: ClaudeCodeLimits) -> ClaudeCodeConfig {
ClaudeCodeConfig {
claude_binary: PathBuf::from("/opt/claude"),
claude_binary_sha256: [9; 32],
claude_binary: dir.join("claude"),
claude_binary_sha256: pin_of(PINNED),
work_dir: PathBuf::from("/work"),
config_dir: PathBuf::from("/cfg"),
prompt_dir: dir.join("prompts"),
token_credential: "claude-token".to_string(),
instance: instance.to_string(),
limits,
}
}
/// A token source with a fixed answer.
struct FixedToken(Option<&'static str>);
impl TokenSource for FixedToken {
fn read(&self) -> Option<Zeroizing<String>> {
self.0.map(|token| Zeroizing::new(token.to_string()))
}
}
fn canary_token() -> Box<dyn TokenSource> {
Box::new(FixedToken(Some(CANARY_TOKEN)))
}
fn limits() -> ClaudeCodeLimits {
ClaudeCodeLimits {
deadline: Duration::from_secs(30),
@ -263,12 +293,8 @@ fn limits() -> ClaudeCodeLimits {
}
}
fn with_canary_token(mut backend: ClaudeCodeBackend) -> ClaudeCodeBackend {
backend.env_lookup = Box::new(|name| (name == TOKEN_VAR).then(|| CANARY_TOKEN.to_string()));
backend
}
struct Rig {
dir: TempDir,
backend: ClaudeCodeBackend,
spawns: Arc<AtomicUsize>,
seen: Arc<Mutex<Seen>>,
@ -285,13 +311,16 @@ fn rig_for(instance: &str, scripts: Vec<Script>, sessions: &[&str], stderr: &[u8
seen: seen.clone(),
stderr: stderr.to_vec(),
};
let backend = with_canary_token(ClaudeCodeBackend::with_ports(
config(instance, limits()),
let dir = sandbox();
let backend = ClaudeCodeBackend::with_ports(
config(dir.path(), instance, limits()),
Box::new(port),
Box::new(Sessions(sessions.iter().map(|s| s.to_string()).collect())),
Box::new(sink.clone()),
));
canary_token(),
);
Rig {
dir,
backend,
spawns,
seen,
@ -443,7 +472,7 @@ fn argv_is_the_fixed_hygiene_set() {
.expect("the prompt is passed as a file")
.to_string();
let expected: Vec<String> = [
"/opt/claude",
rig.dir.path().join("claude").to_str().unwrap(),
"-p",
"--output-format",
"stream-json",
@ -569,7 +598,7 @@ fn the_token_never_reaches_a_message_an_event_or_a_debug_form() {
);
let env = child_env(
&config(INSTANCE, limits()),
&config(rig.dir.path(), INSTANCE, limits()),
&request(vec![user("x")]),
Zeroizing::new(CANARY_TOKEN.to_string()),
);
@ -580,59 +609,163 @@ fn the_token_never_reaches_a_message_an_event_or_a_debug_form() {
// ---- ready ----
/// A backend over a scripted port that never has a script: `ready` runs nothing.
fn ready_backend(config: ClaudeCodeConfig, tokens: Box<dyn TokenSource>) -> ClaudeCodeBackend {
ClaudeCodeBackend::with_ports(
config,
Box::new(ScriptedPort {
scripts: Mutex::new(VecDeque::new()),
spawns: Arc::new(AtomicUsize::new(0)),
seen: Arc::new(Mutex::new(Seen::default())),
stderr: Vec::new(),
}),
Box::new(Sessions(vec![])),
Box::new(Sink::default()),
tokens,
)
}
#[test]
fn ready_checks_the_pinned_binary_digest() {
let dir = TempDir::new();
let dir = sandbox();
let (work, cfg) = (dir.path().join("work"), dir.path().join("cfg"));
std::fs::create_dir_all(&work).unwrap();
std::fs::create_dir_all(&cfg).unwrap();
let binary = dir.path().join("claude");
std::fs::write(&binary, b"the pinned bytes").unwrap();
let pin: [u8; 32] = Sha256::digest(b"the pinned bytes").into();
let ready = |sha: [u8; 32], binary: &Path, work: &Path, token: bool| {
let mut config = config(INSTANCE, limits());
let ready_field = |sha: [u8; 32], binary: &Path, work: &Path, prompts: &Path, token: bool| {
let mut config = config(dir.path(), INSTANCE, limits());
config.claude_binary = binary.to_path_buf();
config.claude_binary_sha256 = sha;
config.work_dir = work.to_path_buf();
config.config_dir = cfg.clone();
let rig = rig(vec![]);
let mut backend = ClaudeCodeBackend::with_ports(
config,
Box::new(ScriptedPort {
scripts: Mutex::new(VecDeque::new()),
spawns: rig.spawns.clone(),
seen: rig.seen.clone(),
stderr: Vec::new(),
}),
Box::new(Sessions(vec![])),
Box::new(Sink::default()),
);
backend.env_lookup =
Box::new(move |name| (token && name == TOKEN_VAR).then(|| "t".to_string()));
let answer = backend.ready();
assert_eq!(rig.spawns(), 0, "ready runs nothing");
answer
config.prompt_dir = prompts.to_path_buf();
let tokens = Box::new(FixedToken(token.then_some("t")));
ready_backend(config, tokens).ready()
};
let ready = |sha: [u8; 32], binary: &Path, work: &Path, prompts: &Path, token: bool| {
ready_field(sha, binary, work, prompts, token).is_ok()
};
let prompts = dir.path().join("prompts");
let pin = pin_of(PINNED);
assert_eq!(
ready_field([0; 32], &binary, &work, &prompts, true),
Err("claude_binary_sha256")
);
assert_eq!(
ready_field(pin, &binary, &dir.path().join("absent"), &prompts, true),
Err("work_dir")
);
assert_eq!(
ready_field(pin, &binary, &work, &dir.path().join("absent"), true),
Err("prompt_dir")
);
assert_eq!(
ready_field(pin, &binary, &work, &prompts, false),
Err("token_credential")
);
assert!(ready(pin, &binary, &work, true));
assert!(!ready([0; 32], &binary, &work, true), "other pin");
assert!(ready(pin, &binary, &work, &prompts, true));
assert!(!ready([0; 32], &binary, &work, &prompts, true), "other pin");
assert!(
!ready(pin, &dir.path().join("absent"), &work, true),
!ready(pin, &dir.path().join("absent"), &work, &prompts, true),
"no binary"
);
assert!(
!ready(pin, &binary, &dir.path().join("absent"), true),
!ready(pin, &binary, &dir.path().join("absent"), &prompts, true),
"no work dir"
);
assert!(!ready(pin, &binary, &work, false), "no token");
assert!(
!ready(pin, &binary, &work, &dir.path().join("absent"), true),
"no prompt dir"
);
assert!(!ready(pin, &binary, &work, &prompts, false), "no token");
std::fs::write(&binary, b"other bytes").unwrap();
assert!(
!ready(pin, &binary, &work, true),
!ready(pin, &binary, &work, &prompts, true),
"bytes differ from the pin"
);
}
#[test]
fn token_comes_from_the_credential_file_not_the_environment() {
static CANARY_PARENT: std::sync::Once = std::sync::Once::new();
CANARY_PARENT.call_once(|| {
// SAFETY: set once to a value no code path reads; the test proves none does.
unsafe { std::env::set_var(TOKEN_VAR, "canary-environment-token") };
});
let dir = sandbox();
let credentials = dir.path().join("credentials");
for sub in ["credentials", "work", "cfg"] {
std::fs::create_dir_all(dir.path().join(sub)).unwrap();
}
let seen = Arc::new(Mutex::new(Seen::default()));
let mut config = config(dir.path(), INSTANCE, limits());
config.work_dir = dir.path().join("work");
config.config_dir = dir.path().join("cfg");
let backend = ClaudeCodeBackend::with_ports(
config,
Box::new(ScriptedPort {
scripts: Mutex::new(vec![ok_script(SESSION, "x")].into()),
spawns: Arc::new(AtomicUsize::new(0)),
seen: seen.clone(),
stderr: Vec::new(),
}),
Box::new(Sessions(vec![])),
Box::new(Sink::default()),
Box::new(CredentialTokenSource::in_directory(
&credentials,
"claude-token",
)),
);
let mut door = Door::new();
// The variable is set and the file is absent: nothing to give the child.
assert_eq!(backend.ready(), Err("token_credential"));
let (claim, _) = door.begin(request(vec![user("hi")]));
let (kind, message, usage) = failure_of(&backend.execute(claim.call()));
assert_eq!(kind, FailureKind::Authentication);
assert_eq!(usage, Some(usage_zero()));
assert!(!message.contains("canary-environment-token"));
assert!(seen.lock().unwrap().argv.is_empty(), "no child was spawned");
// The file is read afresh at the call, its trailing newline trimmed, and
// the child's environment carries the file's value.
std::fs::write(
credentials.join("claude-token"),
format!("{CANARY_TOKEN}\n"),
)
.unwrap();
assert_eq!(backend.ready(), Ok(()));
let mut door = Door::new();
let (claim, _) = door.begin(request(vec![user("hi")]));
let result = backend.execute(claim.call());
assert!(matches!(
outcome(&result),
TransportOutcome::Completed { .. }
));
let env = seen.lock().unwrap().env.clone();
assert!(env.contains(&(TOKEN_VAR.to_string(), CANARY_TOKEN.to_string())));
// An empty credential is no token.
std::fs::write(credentials.join("claude-token"), "\n").unwrap();
assert_eq!(backend.ready(), Err("token_credential"));
}
#[test]
fn an_unreadable_credential_is_no_token() {
// A credential that is a directory, or a directory that is absent.
let dir = sandbox();
std::fs::create_dir_all(dir.path().join("claude-token")).unwrap();
let source = CredentialTokenSource::in_directory(dir.path(), "claude-token");
assert!(source.read().is_none());
assert!(
CredentialTokenSource::in_directory(dir.path().join("absent"), "x")
.read()
.is_none()
);
}
// ---- planning ----
#[test]
@ -1224,7 +1357,7 @@ fn one_spawn_per_execute() {
#[test]
fn a_missing_token_fails_before_spawn() {
let mut rig = rig(vec![ok_script(SESSION, "x")]);
rig.backend.env_lookup = Box::new(|_| None);
rig.backend.tokens = Box::new(FixedToken(None));
let (kind, _, usage) = failure_of(&rig.run(request(vec![user("hi")])));
assert_eq!(kind, FailureKind::Authentication);
assert_eq!(usage, Some(usage_zero()));
@ -1245,18 +1378,29 @@ fn a_port_that_cannot_spawn_is_a_provider_failure() {
Err(io::Error::from(io::ErrorKind::NotFound))
}
}
let backend = with_canary_token(ClaudeCodeBackend::with_ports(
config(INSTANCE, limits()),
let dir = sandbox();
let backend = ClaudeCodeBackend::with_ports(
config(dir.path(), INSTANCE, limits()),
Box::new(Refusing),
Box::new(Sessions(vec![])),
Box::new(Sink::default()),
));
canary_token(),
);
let mut door = Door::new();
let (claim, _) = door.begin(request(vec![user("hi")]));
let (kind, message, usage) = failure_of(&backend.execute(claim.call()));
let result = backend.execute(claim.call());
let (kind, message, usage) = failure_of(&result);
assert_eq!(kind, FailureKind::Provider);
assert_eq!(usage, None);
assert!(!message.contains("/opt/claude"), "no path in a message");
assert_eq!(
usage,
Some(usage_zero()),
"a child that never ran bills nothing"
);
assert_eq!(parts(&result).1.cost_micro_usd, 0);
assert!(
!message.contains(dir.path().to_str().unwrap()),
"no path in a message"
);
}
#[test]
@ -1384,28 +1528,60 @@ fn usage_the_result_does_not_report_is_billed_the_reservation() {
}
#[test]
fn a_refusal_at_the_door_is_billed_nothing() {
// Ruling spend-bills-on-identity-not-validity: a bound result is charged
// its true cost, and the provider bills nothing for a 401 or 403.
// The shape is a real claude 2.1.312 capture with a bogus token.
fn door_set_is_exactly_401_403_429() {
// Ruling claude-code-plan-quota-fuse-2 with spend-bills-on-identity-not-
// validity: a refusal at the door is charged nothing, for exactly the
// statuses the provider cannot have billed (401, 403, 429) and only when
// the result says its usage is explicitly empty. Every other shape is
// billed the reservation. The 401 shape is a real claude 2.1.312 capture
// with a bogus token.
let retry = r#"{"type":"system","subtype":"api_retry","error":"authentication_failed"}"#;
let synthetic = r#"{"type":"assistant","api_error_status":401,"error":"authentication_failed","message":{"id":"e48001b7-cf4f-49b1-9810-db8fd7f73c73","model":"<synthetic>","stop_reason":"stop_sequence","content":[{"type":"text","text":"Failed to authenticate. API Error: 401 OAuth access token is invalid."}]}}"#;
for status in [401, 403] {
let result_line = format!(
r#"{{"type":"result","subtype":"success","is_error":true,"api_error_status":{status},"stop_reason":"stop_sequence","result":"Failed to authenticate.","modelUsage":{{}},"model_usage_since_last_result":{{}},"terminal_reason":"api_error"}}"#
);
let result = rig(vec![Script::lines(vec![
init_line(MODEL, SESSION),
retry.to_string(),
retry.to_string(),
synthetic.to_string(),
result_line,
])])
.run(request(vec![user("hi")]));
let (kind, _, usage) = failure_of(&result);
assert_eq!(kind, FailureKind::Authentication, "{status}");
assert_eq!(usage, Some(usage_zero()), "{status}");
assert_eq!(parts(&result).1.cost_micro_usd, 0, "{status}");
let reservation = reservation();
let statuses: [Option<u16>; 9] = [
Some(400),
Some(401),
Some(402),
Some(403),
Some(404),
Some(429),
Some(500),
Some(529),
None,
];
let shapes = [
(Some("{}"), "empty"),
(None, "absent"),
(Some("null"), "null"),
];
for status in statuses {
for (since_last, shape) in shapes {
let status_field = status
.map(|status| format!(r#""api_error_status":{status},"#))
.unwrap_or_default();
let usage_field = since_last
.map(|usage| format!(r#","model_usage_since_last_result":{usage}"#))
.unwrap_or_default();
let result_line = format!(
r#"{{"type":"result","subtype":"success","is_error":true,{status_field}"stop_reason":"stop_sequence","result":"Failed.","modelUsage":{CUMULATIVE}{usage_field},"terminal_reason":"api_error"}}"#
);
let result = rig(vec![Script::lines(vec![
init_line(MODEL, SESSION),
retry.to_string(),
synthetic.to_string(),
result_line,
])])
.run(request(vec![user("hi")]));
let (_, _, usage) = failure_of(&result);
let case = format!("{status:?} {shape}");
if matches!(status, Some(401 | 403 | 429)) && shape == "empty" {
assert_eq!(usage, Some(usage_zero()), "{case}");
assert_eq!(parts(&result).1.cost_micro_usd, 0, "{case}");
} else {
assert_eq!(usage, None, "{case}");
assert_eq!(parts(&result).1.cost_micro_usd, reservation, "{case}");
}
}
}
}
@ -1458,6 +1634,57 @@ fn the_prompt_file_is_removed_when_the_call_fails() {
}
}
#[test]
fn prompt_file_lives_in_prompt_dir_exclusively() {
let rig = rig(vec![ok_script(SESSION, "x")]);
rig.run(request(vec![user("hi")]));
let (path, _) = rig.seen().prompt_file.expect("a prompt file");
assert_eq!(
path.parent(),
Some(rig.dir.path().join("prompts").as_path())
);
// A prompt directory that cannot take the file fails staging: nothing
// reached the provider, so it bills nothing, and no child runs.
let rig = rig_for(INSTANCE, vec![ok_script(SESSION, "x")], &[SESSION], b"");
std::fs::remove_dir_all(rig.dir.path().join("prompts")).unwrap();
let result = rig.run(request(vec![user("hi")]));
let (kind, _, usage) = failure_of(&result);
assert_eq!((kind, usage), (FailureKind::Provider, Some(usage_zero())));
assert_eq!(parts(&result).1.cost_micro_usd, 0);
assert_eq!(rig.spawns(), 0);
}
#[test]
fn pin_is_checked_before_every_spawn() {
let rig = rig(vec![ok_script(SESSION, "x"), ok_script(SESSION, "y")]);
let binary = rig.dir.path().join("claude");
assert!(rig.backend.binary_matches_pin());
rig.run(request(vec![user("hi")]));
assert_eq!(rig.spawns(), 1);
// The binary is swapped after readiness: the next call never spawns.
std::fs::write(&binary, b"swapped bytes").unwrap();
let result = rig.run(request(vec![user("hi")]));
let (kind, message, usage) = failure_of(&result);
assert_eq!((kind, usage), (FailureKind::Provider, Some(usage_zero())));
assert_eq!(
parts(&result).1.cost_micro_usd,
0,
"billed nothing, not the reservation"
);
assert!(!message.contains(rig.dir.path().to_str().unwrap()));
assert_eq!(rig.spawns(), 1, "no child for a binary off its pin");
assert!(!rig.backend.binary_matches_pin());
let staged = std::fs::read_dir(rig.dir.path().join("prompts")).unwrap();
assert_eq!(staged.count(), 0, "no prompt was staged for it");
// Restoring the pinned bytes is enough: nothing remembers the verdict.
std::fs::write(&binary, PINNED).unwrap();
rig.run(request(vec![user("hi")]));
assert_eq!(rig.spawns(), 2);
}
#[test]
fn the_provider_tag_is_claude_code() {
assert_eq!(rig(vec![]).backend.provider(), "claude-code");
@ -1540,12 +1767,21 @@ mod real {
let binary = dir.path().join("claude");
std::fs::write(&binary, format!("#!/bin/sh\n{body}\n")).unwrap();
std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o755)).unwrap();
let mut config = config(INSTANCE, limits);
let prompts = dir.path().join("prompts");
std::fs::create_dir_all(&prompts).unwrap();
let mut config = config(dir.path(), INSTANCE, limits);
config.claude_binary_sha256 = pin_of(&std::fs::read(&binary).unwrap());
config.claude_binary = binary;
config.work_dir = work;
config.config_dir = cfg;
let sink = Sink::default();
let backend = with_canary_token(ClaudeCodeBackend::new(config, Box::new(sink.clone())));
let backend = ClaudeCodeBackend::with_ports(
config.clone(),
Box::new(crate::process::StdProcessPort::new(config.limits)),
Box::new(FsSessionStore::new(config.config_dir.clone())),
Box::new(sink.clone()),
canary_token(),
);
Fake {
_guard: guard,
dir,
@ -1655,7 +1891,7 @@ mod real {
leader.display()
);
let fake = fake(&body, limits());
let config = config(INSTANCE, limits());
let config = config(fake.dir.path(), INSTANCE, limits());
let env = child_env(
&config,
&request(vec![user("x")]),
@ -1801,6 +2037,11 @@ mod real {
"600"
);
let path = std::fs::read_to_string(probe.path().join("path")).unwrap();
assert_eq!(
Path::new(path.trim()).parent(),
Some(fake.dir.path().join("prompts").as_path()),
"staged in the configured prompt directory"
);
assert!(!Path::new(path.trim()).exists(), "removed after the call");
}
@ -1837,6 +2078,25 @@ mod real {
assert!(sunk.trim_end().ends_with("END-OF-STDERR"), "{}", sunk.len());
}
#[test]
fn stderr_join_survives_a_megabyte_after_result() {
// Soul s2's probe: the result line first, then a megabyte of stderr
// before the child exits. The call must wait for the stderr reader to
// finish, or the tail handed to the sink is cut short.
let body = format!(
"{}\nhead -c 1000000 /dev/zero | tr '\\0' x >&2\necho END-OF-STDERR >&2",
printf_lines(&good_lines())
);
let fake = fake(&body, limits());
let result = fake.run(request(vec![user("hi")]));
assert!(matches!(
outcome(&result),
TransportOutcome::Completed { .. }
));
let sunk = String::from_utf8(fake.sink.0.lock().unwrap().clone()).unwrap();
assert!(sunk.trim_end().ends_with("END-OF-STDERR"), "{}", sunk.len());
}
#[test]
fn a_result_is_enough_even_if_the_child_keeps_stdout_open() {
let body = format!("{}\nexec sleep 300", printf_lines(&good_lines()));

View file

@ -19,6 +19,12 @@ pub struct ClaudeCodeConfig {
/// Passed to the child as `CLAUDE_CONFIG_DIR` and `HOME`; it holds the
/// CLI's session store.
pub config_dir: PathBuf,
/// A directory the connector owns (mode 0700), where each call's system
/// prompt is staged for the child.
pub prompt_dir: PathBuf,
/// The name of the systemd credential that holds the child's token, under
/// `$CREDENTIALS_DIRECTORY`.
pub token_credential: String,
/// The connector instance id written into every handle.
pub instance: String,
pub limits: ClaudeCodeLimits,

View file

@ -0,0 +1,571 @@
//! 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::{
CallerAdmission, CallerConfig, MIN_PAYLOAD_BYTES, ModelPrice, ServeError, ServeLimits,
ServiceError, load_admissions, serve,
};
use thiserror::Error;
use crate::backend::readiness;
use crate::{
ClaudeCodeBackend, ClaudeCodeConfig, ClaudeCodeLimits, CredentialTokenSource, 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,
/// Something the document names exists but cannot be used now.
#[error("the configured {0} cannot be used")]
Unusable(&'static str),
/// The serve loop failed after it started.
#[error("the serve loop failed at {0}")]
Serving(&'static str),
}
/// The kit's serve errors carry stored paths, ids and numbers; none of that
/// text is shown. Each maps to the configuration field or stage it names.
impl From<ServeError> for ConfigError {
fn from(error: ServeError) -> Self {
match error {
ServeError::InvalidConfig(field) | ServeError::BackendUnready(field) => {
Self::Unusable(field)
}
ServeError::FrameBound { .. } => Self::Invalid("max_frame_bytes"),
ServeError::ConnectionKey(_) => Self::Unusable("connection_key_file"),
ServeError::Service(error) => match error {
ServiceError::ReplayStore(_) => Self::Unusable("replay_store"),
ServiceError::UnpricedModel(_) => Self::Invalid("prices"),
ServiceError::InvalidAdmission
| ServiceError::DuplicateCaller
| ServiceError::SharedCallerKey => Self::Invalid("callers"),
_ => Self::Serving("service"),
},
ServeError::ServicePoisoned => Self::Serving("service"),
ServeError::Listen(_) => Self::Serving("listener"),
ServeError::Thread(_) | ServeError::Connection(_) => Self::Serving("connection"),
ServeError::FrameSize | ServeError::FrameEncoding => Self::Serving("frame"),
ServeError::Clock => Self::Serving("clock"),
}
}
}
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
.iter()
.all(|(model, price)| is_canonical_model(model) && is_fully_priced(price))
{
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: `claude-<family>-<major>-<minor>`
/// with an optional `-YYYYMMDD` suffix, the family lowercase letters. Aliases
/// and bare prefixes are refused because the stream's `init.model` must equal
/// the requested model.
fn is_canonical_model(model: &str) -> bool {
let Some(rest) = model.strip_prefix(MODEL_PREFIX) else {
return false;
};
let digits = |part: &str, most: usize| {
(1..=most).contains(&part.len()) && part.bytes().all(|byte| byte.is_ascii_digit())
};
let mut parts = rest.split('-');
let family = parts.next().unwrap_or("");
let major = parts.next().unwrap_or("");
let minor = parts.next().unwrap_or("");
let date = parts.next();
!family.is_empty()
&& family.bytes().all(|byte| byte.is_ascii_lowercase())
&& digits(major, 2)
&& digits(minor, 2)
&& date.is_none_or(|date| date.len() == 8 && digits(date, 8))
&& parts.next().is_none()
}
/// A price is a fuse only if every component it bills is nonzero: a zero rate
/// lets a call cost nothing against the caller's ceiling.
fn is_fully_priced(price: &ModelPrice) -> bool {
let long = price.long_context.map(|long| long.rates);
[Some(price.rates), long]
.into_iter()
.flatten()
.all(|rates| {
[
rates.input,
rates.cache_write_short,
rates.cache_write_long,
rates.cache_read,
rates.output,
]
.iter()
.all(|rate| *rate > 0)
})
}
/// 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<W>(std::sync::Mutex<W>);
impl<W: std::io::Write + Send> StderrSink for LogSink<W> {
fn stderr_tail(&self, tail: &[u8]) {
if let Ok(mut log) = self.0.lock() {
let _ = writeln!(log, "claude-connector: child stderr, {} bytes", tail.len());
}
}
}
/// Everything `serve` needs true before it binds, in one place so that
/// `config check` passes exactly what `serve` will start: the prompt
/// directory, the other directories, the credential, the pin, the connection
/// key and the frame and connection bounds. Returns the admitted callers.
fn readiness_of(config: &ClaudeConnectorConfig) -> Result<Vec<CallerAdmission>, ConfigError> {
config.validate()?;
#[cfg(unix)]
check_prompt_dir(&config.prompt_dir)?;
let admissions = load_admissions(&config.callers)?;
config.serve_limits().validate(&admissions)?;
let backend = config.backend_config();
readiness(
&backend,
&CredentialTokenSource::from_environment(backend.token_credential.clone()),
)
.map_err(ConfigError::Unusable)?;
Ok(admissions)
}
/// Whether `serve` would start with this document and this environment.
pub fn check_ready(config: &ClaudeConnectorConfig) -> Result<(), ConfigError> {
readiness_of(config).map(drop)
}
/// 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 admissions = readiness_of(&config)?;
let listen = config.validate()?;
let backend = Arc::new(ClaudeCodeBackend::new(
config.backend_config(),
Box::new(LogSink(std::sync::Mutex::new(std::io::stderr()))),
));
let listener = TcpListener::bind(listen).map_err(|_| ConfigError::Listen)?;
serve(
listener,
config.serve_limits(),
admissions,
config.prices.clone(),
&config.replay_store,
backend,
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use super::*;
/// A log that a test can read back.
#[derive(Clone, Default)]
struct Captured(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for Captured {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[test]
fn serve_errors_name_a_field_or_stage_and_never_a_stored_value() {
const CANARY: &str = "canary-path-8e2a";
let io = || std::io::Error::other(CANARY);
let cases: Vec<(ServeError, &str)> = vec![
(ServeError::InvalidConfig("callers"), "callers"),
(ServeError::BackendUnready("work_dir"), "work_dir"),
(
ServeError::FrameBound {
max_frame_bytes: 4099,
payload_bytes: 4096,
needed_frame_bytes: 9999,
},
"max_frame_bytes",
),
(ServeError::ConnectionKey(io()), "connection_key_file"),
(
ServeError::Service(ServiceError::ReplayStore(format!("{CANARY}/replay.cc"))),
"replay_store",
),
(
ServeError::Service(ServiceError::UnpricedModel(CANARY.to_string())),
"prices",
),
(
ServeError::Service(ServiceError::InvalidAdmission),
"callers",
),
(
ServeError::Service(ServiceError::DuplicateCaller),
"callers",
),
(
ServeError::Service(ServiceError::SharedCallerKey),
"callers",
),
(ServeError::Listen(io()), "listener"),
];
for (error, field) in cases {
let shown = ConfigError::from(error);
let text = format!("{shown} {shown:?}");
assert!(text.contains(field), "{text}");
for stored in [CANARY, "4099", "9999", "replay.cc"] {
assert!(!text.contains(stored), "{text}");
}
}
}
#[test]
fn the_log_gets_the_size_of_the_childs_stderr_and_never_its_bytes() {
let log = Captured::default();
let sink = LogSink(Mutex::new(log.clone()));
sink.stderr_tail(b"canary-stderr-text, 22 bytes");
let written = String::from_utf8(log.0.lock().unwrap().clone()).unwrap();
assert_eq!(
written,
"claude-connector: child stderr, 28 bytes
"
);
assert!(!written.contains("canary"));
}
#[cfg(unix)]
mod prompt_dir {
use std::os::unix::fs::{PermissionsExt, chown, symlink};
use std::path::PathBuf;
use super::super::check_prompt_dir;
struct Dir(PathBuf);
impl Dir {
fn new(name: &str) -> Self {
let path = std::env::temp_dir()
.join(format!("claude-prompt-dir-{}-{name}", std::process::id()));
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path).unwrap();
Self(path)
}
fn mode(&self, mode: u32) {
std::fs::set_permissions(&self.0, std::fs::Permissions::from_mode(mode)).unwrap();
}
}
impl Drop for Dir {
fn drop(&mut self) {
let _ = std::fs::set_permissions(&self.0, std::fs::Permissions::from_mode(0o700));
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn only_mode_0700_passes() {
let dir = Dir::new("modes");
dir.mode(0o700);
assert!(check_prompt_dir(&dir.0).is_ok());
// Each of these leaves the owner able to enter, or adds a bit
// beyond the owner's three; all are refused.
for mode in [
0o500, 0o300, 0o100, 0o600, 0o770, 0o707, 0o750, 0o755, 0o1700, 0o2700, 0o4700,
] {
dir.mode(mode);
assert!(check_prompt_dir(&dir.0).is_err(), "{mode:o}");
}
}
#[test]
fn a_link_to_a_good_directory_is_refused() {
let dir = Dir::new("link-target");
dir.mode(0o700);
let link =
std::env::temp_dir().join(format!("claude-prompt-dir-{}-link", std::process::id()));
let _ = std::fs::remove_file(&link);
symlink(&dir.0, &link).unwrap();
let refused = check_prompt_dir(&link).is_err();
std::fs::remove_file(&link).unwrap();
assert!(refused);
}
#[test]
fn a_directory_owned_by_another_user_is_refused() {
// SAFETY: geteuid has no preconditions and cannot fail.
if unsafe { libc::geteuid() } != 0 {
eprintln!("skipped: only root can give a directory away");
return;
}
let dir = Dir::new("owner");
dir.mode(0o700);
assert!(check_prompt_dir(&dir.0).is_ok());
chown(&dir.0, Some(1234), None).unwrap();
assert!(check_prompt_dir(&dir.0).is_err());
}
}
}

View file

@ -14,8 +14,8 @@ use zeroize::Zeroizing;
use crate::config::ClaudeCodeConfig;
use crate::handle::Plan;
/// The one credential variable, copied by name from the connector's own
/// environment into the child and nowhere else.
/// The one credential variable: the child's environment is the only
/// environment the token ever enters.
pub(crate) const TOKEN_VAR: &str = "CLAUDE_CODE_OAUTH_TOKEN";
const CHILD_PATH: &str = "/usr/local/bin:/usr/bin:/bin";
@ -62,13 +62,18 @@ pub(crate) struct PromptFile(PathBuf);
static PROMPT_FILES: AtomicU64 = AtomicU64::new(0);
impl PromptFile {
pub(crate) fn create(instructions: &str) -> io::Result<Self> {
pub(crate) fn create(dir: &Path, instructions: &str) -> io::Result<Self> {
let name = format!(
"claude-connector-prompt-{}-{}",
std::process::id(),
PROMPT_FILES.fetch_add(1, Ordering::Relaxed)
);
let path = std::env::temp_dir().join(name);
Self::create_at(dir.join(name), instructions)
}
/// Creates `path` and nothing that already exists there: a file, a link
/// (dangling or not) or a directory at the name fails, and is left alone.
fn create_at(path: PathBuf, instructions: &str) -> io::Result<Self> {
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
@ -146,3 +151,57 @@ pub(crate) fn argv(
}
argv
}
#[cfg(all(test, unix))]
mod tests {
use std::os::unix::fs::{PermissionsExt, symlink};
use super::*;
fn scratch(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"claude-connector-launch-{name}-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn a_prompt_is_created_private_and_removed_on_drop() {
let dir = scratch("private");
let prompt = PromptFile::create(&dir, "the prompt").unwrap();
let path = prompt.path().to_path_buf();
assert_eq!(path.parent(), Some(dir.as_path()));
assert_eq!(std::fs::read_to_string(&path).unwrap(), "the prompt");
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600);
drop(prompt);
assert!(!path.exists());
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn a_name_already_taken_is_never_written_or_removed() {
let dir = scratch("taken");
let victim = dir.join("victim");
std::fs::write(&victim, "keep").unwrap();
let file = dir.join("file");
std::fs::write(&file, "mine").unwrap();
let link = dir.join("link");
symlink(&victim, &link).unwrap();
let dangling = dir.join("dangling");
symlink(dir.join("nowhere"), &dangling).unwrap();
for taken in [&file, &link, &dangling] {
assert!(PromptFile::create_at(taken.clone(), "the prompt").is_err());
}
assert_eq!(std::fs::read_to_string(&victim).unwrap(), "keep");
assert_eq!(std::fs::read_to_string(&file).unwrap(), "mine");
assert!(!dir.join("nowhere").exists());
assert!(link.exists() && dangling.symlink_metadata().is_ok());
std::fs::remove_dir_all(dir).unwrap();
}
}

View file

@ -5,13 +5,17 @@
mod backend;
mod config;
mod daemon;
mod handle;
mod launch;
mod process;
mod stream;
pub use backend::{ClaudeCodeBackend, StderrSink};
pub use backend::{ClaudeCodeBackend, CredentialTokenSource, StderrSink, TokenSource};
pub use config::{ClaudeCodeConfig, ClaudeCodeLimits};
pub use daemon::{ClaudeConnectorConfig, ConfigError, check_ready, 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,241 @@
//! 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, check_ready, 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())?;
check_ready(&config)?;
println!("claude connector configuration is valid and ready to serve");
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

@ -8,7 +8,8 @@ use serde::Deserialize;
use serde_json::Value;
/// A call that cannot complete, with the stage that failed. `usage` is `None`
/// unless a result event reported it.
/// (the kit bills its reservation) unless a result event reported it or the
/// call never reached the provider (zero).
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Failure {
pub kind: FailureKind,
@ -17,13 +18,24 @@ pub(crate) struct Failure {
}
impl Failure {
pub(crate) fn unbilled(kind: FailureKind, message: &'static str) -> Self {
/// A failure after the child ran: what the provider billed is unknown, so
/// the kit bills its reservation.
pub(crate) fn cost_unknown(kind: FailureKind, message: &'static str) -> Self {
Self {
kind,
message,
usage: None,
}
}
/// A failure before anything was sent to the provider: it bills nothing.
pub(crate) fn never_reached(kind: FailureKind, message: &'static str) -> Self {
Self {
kind,
message,
usage: Some(sum_usage(std::iter::empty())),
}
}
}
/// What the stream said, once its result event arrived.
@ -181,7 +193,7 @@ impl<'a> StreamState<'a> {
let door_refusal = !success
&& matches!(
line.api_error_status.as_ref().and_then(Value::as_u64),
Some(401 | 403)
Some(401 | 403 | 429)
);
let usage = read_usage(line.model_usage_since_last_result, door_refusal);
if !success {
@ -252,13 +264,13 @@ impl<'a> StreamState<'a> {
}
fn fail(kind: FailureKind, message: &'static str) -> Step {
Step::Done(Box::new(Err(Failure::unbilled(kind, message))))
Step::Done(Box::new(Err(Failure::cost_unknown(kind, message))))
}
/// What this turn consumed, summed over every model it used (harness side
/// calls included), or `None` when the result does not say: the field absent,
/// of another shape, or empty. An empty report is believed only for a refusal
/// at the door (401 or 403), which the provider cannot have billed; elsewhere
/// at the door (401, 403 or 429), which the provider cannot have billed; elsewhere
/// the CLI also zeroes usage on crashes after real work. `None` is billed the
/// reservation by the kit.
fn read_usage(reported: Option<Value>, door_refusal: bool) -> Option<Usage> {

View file

@ -0,0 +1,902 @@
//! 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::{
CacheBackingStore, CultCache, CultCacheEnvelope, DatabaseEntry,
SingleFileMessagePackBackingStore,
};
use model_connector::{CallerConfig, MIN_PAYLOAD_BYTES, ModelPrice, Rates};
const CANARY: &str = "canary-9f3b7c1d";
const OPUS: &str = "claude-opus-5-1";
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)
));
}
fn store_raw(path: &Path, key: &str, ty: &str, schema: Option<&str>, payload: Vec<u8>) {
let mut store = SingleFileMessagePackBackingStore::new(path);
store
.push(&CultCacheEnvelope {
key: key.to_string(),
r#type: ty.to_string(),
payload,
stored_at: "2026-10-29T00:00:00Z".to_string(),
schema_id: schema.map(str::to_string),
})
.unwrap();
}
#[test]
fn the_envelope_must_be_exactly_the_documents_key_type_schema_and_encoding() {
let scratch = Scratch::new();
let canonical = rmp_serde::to_vec(&valid()).unwrap();
let ty = ClaudeConnectorConfig::TYPE;
let mut trailing = canonical.clone();
trailing.push(0xc0);
let cases = vec![
("wrong key", "other", ty, Some(ty), canonical.clone()),
(
"wrong type",
"runtime",
"gamecult.other.v1",
Some(ty),
canonical.clone(),
),
(
"wrong schema",
"runtime",
ty,
Some("gamecult.other.v1"),
canonical.clone(),
),
("not canonical", "runtime", ty, Some(ty), trailing),
];
for (name, key, ty, schema, payload) in cases {
let path = scratch.path(&format!("{}.cc", name.replace(' ', "-")));
store_raw(&path, key, ty, schema, payload);
assert!(
matches!(load_config(&path), Err(ConfigError::Store)),
"{name}"
);
}
// The same bytes with every part right load.
let path = scratch.path("right.cc");
store_raw(&path, "runtime", ty, Some(ty), canonical);
assert_eq!(load_config(&path).unwrap(), valid());
}
#[test]
fn bounds_sit_exactly_where_the_connector_needs_them() {
let mut config = valid();
config.max_total_bytes = config.max_line_bytes;
config.validate().unwrap();
config.callers[0].max_payload_bytes = MIN_PAYLOAD_BYTES;
config.validate().unwrap();
config.callers[0].max_payload_bytes = MIN_PAYLOAD_BYTES - 1;
assert_eq!(refused(&config), "caller bounds");
for set in [
(|c: &mut ClaudeConnectorConfig| c.max_connections = 0) as fn(&mut ClaudeConnectorConfig),
|c| c.socket_timeout_ms = 0,
|c| c.max_expiry_skew_ms = 0,
|c| c.max_frame_bytes = 0,
] {
let mut config = valid();
set(&mut config);
assert_eq!(refused(&config), "serve limits");
}
let mut config = valid();
config.prices.insert("claude-".to_string(), price());
assert_eq!(refused(&config), "prices");
}
#[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()];
}),
),
(
"family only",
Box::new(|c| {
c.callers[0].allowed_models = vec!["claude-haiku".to_string()];
c.prices.insert("claude-haiku".to_string(), price());
}),
),
(
"latest alias",
Box::new(|c| {
c.callers[0].allowed_models = vec!["claude-sonnet-latest".to_string()];
c.prices.insert("claude-sonnet-latest".to_string(), price());
}),
),
(
"no minor",
Box::new(|c| {
c.callers[0].allowed_models = vec!["claude-opus-5".to_string()];
c.prices.insert("claude-opus-5".to_string(), price());
}),
),
(
"uppercase family",
Box::new(|c| {
c.callers[0].allowed_models = vec!["claude-Opus-5-1".to_string()];
c.prices.insert("claude-Opus-5-1".to_string(), price());
}),
),
(
"short date",
Box::new(|c| {
c.callers[0].allowed_models = vec!["claude-opus-5-1-2026".to_string()];
c.prices.insert("claude-opus-5-1-2026".to_string(), price());
}),
),
(
"trailing part",
Box::new(|c| {
c.callers[0].allowed_models = vec!["claude-opus-5-1-20260101-x".to_string()];
c.prices
.insert("claude-opus-5-1-20260101-x".to_string(), price());
}),
),
(
"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",
"allowed_models",
"allowed_models",
"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 a_dated_canonical_id_is_allowed() {
let dated = "claude-opus-5-1-20260101".to_string();
let mut config = valid();
config.callers[0].allowed_models = vec![dated.clone()];
config.prices.insert(dated, price());
config.validate().unwrap();
}
#[test]
fn a_price_with_a_zero_billed_component_is_refused() {
type Zero = fn(&mut Rates);
let zeroed: Vec<(&str, Zero)> = vec![
("input", |r| r.input = 0),
("cache_write_short", |r| r.cache_write_short = 0),
("cache_write_long", |r| r.cache_write_long = 0),
("cache_read", |r| r.cache_read = 0),
("output", |r| r.output = 0),
];
for (name, zero) in &zeroed {
let mut config = valid();
zero(&mut config.prices.get_mut(HAIKU).unwrap().rates);
assert_eq!(refused(&config), "prices", "{name}");
let mut config = valid();
let mut long = price().rates;
zero(&mut long);
config.prices.get_mut(HAIKU).unwrap().long_context = Some(model_connector::LongContext {
above_prompt_tokens: 200_000,
rates: long,
});
assert_eq!(refused(&config), "prices", "long context {name}");
}
// A zero overhead is not a rate.
let mut config = valid();
config.prices.get_mut(HAIKU).unwrap().overhead_tokens = 0;
config.validate().unwrap();
}
#[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_writes_the_flags_into_the_document() {
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]);
}
/// A scratch directory in which `serve` would start: every directory, the
/// pinned binary, the connection key and the credential exist. Returns the
/// `config init` flags and the credential directory.
#[cfg(unix)]
fn prepare(scratch: &Scratch) -> (Vec<String>, PathBuf) {
use sha2::{Digest, Sha256};
use std::os::unix::fs::PermissionsExt;
for name in ["work", "home", "prompts", "credentials"] {
std::fs::create_dir_all(scratch.path(name)).unwrap();
}
std::fs::set_permissions(
scratch.path("prompts"),
std::fs::Permissions::from_mode(0o700),
)
.unwrap();
std::fs::write(scratch.path("credentials/claude-token"), CANARY).unwrap();
std::fs::write(scratch.path("caller.key"), "caller-secret").unwrap();
std::fs::write(scratch.path("claude"), b"pinned claude").unwrap();
let sha: String = Sha256::digest(b"pinned claude")
.iter()
.map(|byte| format!("{byte:02x}"))
.collect();
let mut flags = init_flags(scratch, &[]);
let at = flags
.iter()
.position(|flag| flag == "--claude-sha256")
.unwrap();
flags[at + 1] = sha;
(flags, scratch.path("credentials"))
}
#[cfg(unix)]
fn check_and_serve(config: &Path, credentials: Option<&Path>) -> (Output, Output) {
let envs: Vec<(&str, &Path)> = credentials
.map(|dir| ("CREDENTIALS_DIRECTORY", dir))
.into_iter()
.collect();
let path = config.to_str().unwrap();
(
run(&["config", "check", path], &envs),
run(&["serve", "--config", path], &envs),
)
}
#[cfg(unix)]
#[test]
fn check_passes_a_document_serve_can_start_and_reports_counts_only() {
let scratch = Scratch::new();
let (flags, credentials) = prepare(&scratch);
let made = init(&scratch, &flags);
assert!(made.status.success(), "{}", shown(&made));
let checked = run(
&[
"config",
"check",
scratch.path("connector.cc").to_str().unwrap(),
],
&[("CREDENTIALS_DIRECTORY", &credentials)],
);
assert!(checked.status.success(), "{}", shown(&checked));
let text = shown(&checked);
assert!(text.contains("callers: 1") && text.contains("allowed_models: 1"));
assert!(text.contains("prices: 1"));
for secret in ["persona-a", HAIKU, CANARY, "caller-secret"] {
assert!(!text.contains(secret), "{text}");
}
}
#[cfg(unix)]
#[test]
fn check_refuses_exactly_what_serve_refuses_and_names_the_field() {
use std::os::unix::fs::PermissionsExt;
type Break = fn(&Scratch, &Path);
let cases: Vec<(&str, &str, Break)> = vec![
("open prompt dir", "prompt directory", |s, _| {
std::fs::set_permissions(s.path("prompts"), std::fs::Permissions::from_mode(0o755))
.unwrap();
}),
("no key file", "connection_key_file", |s, _| {
std::fs::remove_file(s.path("caller.key")).unwrap();
}),
("binary off its pin", "claude_binary_sha256", |s, _| {
std::fs::write(s.path("claude"), b"other bytes").unwrap();
}),
("no credential", "token_credential", |_, credentials| {
std::fs::remove_file(credentials.join("claude-token")).unwrap();
}),
("no work dir", "work_dir", |s, _| {
std::fs::remove_dir(s.path("work")).unwrap();
}),
("no config dir", "config_dir", |s, _| {
std::fs::remove_dir(s.path("home")).unwrap();
}),
("frame too small", "max_frame_bytes", |s, _| {
let path = s.path("connector.cc");
let mut config = load_config(&path).unwrap();
config.max_frame_bytes = 4099;
write_config(&path, &config).unwrap();
}),
("too few connections", "connection capacity", |s, _| {
let path = s.path("connector.cc");
let mut config = load_config(&path).unwrap();
config.max_connections = 1;
write_config(&path, &config).unwrap();
}),
];
for (name, field, break_it) in cases {
let scratch = Scratch::new();
let (flags, credentials) = prepare(&scratch);
let made = init(&scratch, &flags);
assert!(made.status.success(), "{name}: {}", shown(&made));
break_it(&scratch, &credentials);
let (checked, served) = check_and_serve(&scratch.path("connector.cc"), Some(&credentials));
assert!(!checked.status.success(), "{name} check passed");
assert!(!served.status.success(), "{name} serve started");
assert!(
shown(&checked).contains(field),
"{name}: {}",
shown(&checked)
);
assert_eq!(shown(&checked), shown(&served), "{name}");
for secret in [CANARY, "caller-secret", "other bytes"] {
assert!(!shown(&checked).contains(secret), "{name}");
}
}
// No credential directory at all is the same refusal.
let scratch = Scratch::new();
let (flags, _) = prepare(&scratch);
assert!(init(&scratch, &flags).status.success());
let (checked, served) = check_and_serve(&scratch.path("connector.cc"), None);
assert!(!checked.status.success() && !served.status.success());
assert!(shown(&checked).contains("token_credential"));
assert_eq!(shown(&checked), shown(&served));
}
#[cfg(unix)]
#[test]
fn serve_names_a_corrupt_replay_store_without_its_path() {
let scratch = Scratch::new();
let (mut flags, credentials) = prepare(&scratch);
let store = scratch.path(&format!("{CANARY}-store/replay.cc"));
std::fs::create_dir_all(store.parent().unwrap()).unwrap();
std::fs::write(&store, b"not a cultcache store").unwrap();
let at = flags
.iter()
.position(|flag| flag == "--replay-store")
.unwrap();
flags[at + 1] = store.to_string_lossy().into_owned();
assert!(init(&scratch, &flags).status.success());
let (_, served) = check_and_serve(&scratch.path("connector.cc"), Some(&credentials));
assert!(!served.status.success());
let text = shown(&served);
assert!(text.contains("replay_store"), "{text}");
assert!(
!text.contains(CANARY) && !text.contains("replay.cc"),
"{text}"
);
}
#[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),
("--claude-sha256", "abcd"),
("--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}"
);
}
}
#[cfg(unix)]
#[test]
fn a_private_regular_file_is_not_a_prompt_dir() {
use std::os::unix::fs::PermissionsExt;
let scratch = Scratch::new();
let file = scratch.path("file");
std::fs::write(&file, "x").unwrap();
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o700)).unwrap();
assert!(claude_connector::check_prompt_dir(&file).is_err());
assert!(claude_connector::check_prompt_dir(&scratch.path("absent")).is_err());
}

View file

@ -23,10 +23,11 @@ use crate::service::{
/// The only provider seam of the connector kit. A connector binary implements
/// it over one model provider; the kit names no provider.
pub trait ProviderBackend: Send + Sync {
/// Whether the provider can take work. `serve` consults it once, before it
/// opens the replay store or accepts a connection; the request path does
/// not consult it.
fn ready(&self) -> bool;
/// Whether the provider can take work; `Err` names the backend field that
/// is not ready, never a value. `serve` consults it once, before it opens
/// the replay store or accepts a connection; the request path does not
/// consult it.
fn ready(&self) -> Result<(), &'static str>;
/// The tag of the provider this backend fronts. The service owns it from
/// `serve` on: it names the provider on every receipt and decides which
/// reasoning state is foreign.
@ -59,7 +60,7 @@ impl ServeLimits {
/// largest whose sealed frame, at its worst-case encoding, fits the frame.
/// An admitted payload therefore always fits, and `complete` is given the
/// same frame bound to fit every result in.
fn validate(&self, admissions: &[CallerAdmission]) -> Result<(), ServeError> {
pub fn validate(&self, admissions: &[CallerAdmission]) -> Result<(), ServeError> {
if self.max_frame_bytes == 0
|| self.max_frame_bytes > u32::MAX as usize
|| self.max_connections == 0
@ -171,9 +172,7 @@ pub fn serve(
{
return Err(ServeError::InvalidConfig("listener address"));
}
if !backend.ready() {
return Err(ServeError::BackendUnready);
}
backend.ready().map_err(ServeError::BackendUnready)?;
let service = Arc::new(Mutex::new(TransportService::open(
replay_store,
backend.provider(),
@ -322,8 +321,8 @@ pub enum ServeError {
ConnectionKey(#[source] std::io::Error),
#[error(transparent)]
Service(#[from] ServiceError),
#[error("provider backend is not ready")]
BackendUnready,
#[error("provider backend is not ready: {0}")]
BackendUnready(&'static str),
#[error("connector service lock was poisoned")]
ServicePoisoned,
#[error("connector listener failed")]
@ -379,9 +378,9 @@ mod tests {
}
impl ProviderBackend for CountingBackend {
fn ready(&self) -> bool {
fn ready(&self) -> Result<(), &'static str> {
self.ready_calls.fetch_add(1, Ordering::SeqCst);
self.ready
self.ready.then_some(()).ok_or("fixture")
}
fn provider(&self) -> &str {
@ -710,7 +709,10 @@ mod tests {
&store.path,
backend.clone(),
);
assert!(matches!(result, Some(Err(ServeError::BackendUnready))));
assert!(matches!(
result,
Some(Err(ServeError::BackendUnready("fixture")))
));
assert_eq!(backend.executions.load(Ordering::SeqCst), 0);
assert!(
!store.path.exists(),
@ -1016,8 +1018,8 @@ mod tests {
}
impl ProviderBackend for SizedBackend {
fn ready(&self) -> bool {
true
fn ready(&self) -> Result<(), &'static str> {
Ok(())
}
fn provider(&self) -> &str {

View file

@ -119,7 +119,7 @@ impl CallerAdmission {
}
/// What one million tokens of each class cost, in micro-USD.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Rates {
pub input: u64,
pub cache_write_short: u64,
@ -129,7 +129,7 @@ pub struct Rates {
}
/// The rates a prompt strictly above `above_prompt_tokens` is priced at.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct LongContext {
pub above_prompt_tokens: u64,
pub rates: Rates,
@ -138,7 +138,7 @@ pub struct LongContext {
/// One model's price. Explicit zero rates are admitted (a local model is
/// free); a missing price is not. `overhead_tokens` is added to the byte
/// bound of a request when its worst case is reserved.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelPrice {
pub rates: Rates,
pub long_context: Option<LongContext>,

View file

@ -38,8 +38,8 @@ impl RecordingBackend {
}
impl ProviderBackend for RecordingBackend {
fn ready(&self) -> bool {
true
fn ready(&self) -> Result<(), &'static str> {
Ok(())
}
fn provider(&self) -> &str {
@ -291,8 +291,8 @@ struct TamperingBackend {
}
impl ProviderBackend for TamperingBackend {
fn ready(&self) -> bool {
true
fn ready(&self) -> Result<(), &'static str> {
Ok(())
}
fn provider(&self) -> &str {