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>
This commit is contained in:
Meta Vens 2026-10-29 11:26:30 +01:00
commit 9a062f0846
9 changed files with 442 additions and 95 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,10 @@ license.workspace = true
repository.workspace = true
publish = false
[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,22 +96,19 @@ 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;
@ -86,14 +129,20 @@ 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 {
if !self.binary_matches_pin() {
return failure(Failure::unbilled(
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::unbilled(
FailureKind::Provider,
"claude-code: the system prompt could not be staged",
@ -178,7 +227,8 @@ 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.config.prompt_dir.is_dir()
&& self.tokens.read().is_some()
&& self.binary_matches_pin()
}

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,144 @@ 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 = |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 prompts = dir.path().join("prompts");
let pin = pin_of(PINNED);
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!(!backend.ready());
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!(backend.ready());
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!(!backend.ready());
}
#[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 +1338,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 +1359,23 @@ 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()));
assert_eq!(kind, FailureKind::Provider);
assert_eq!(usage, None);
assert!(!message.contains("/opt/claude"), "no path in a message");
assert!(
!message.contains(dir.path().to_str().unwrap()),
"no path in a message"
);
}
#[test]
@ -1384,28 +1503,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 +1609,49 @@ 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: the failure
// is the unbilled constructor's, 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 (kind, _, usage) = failure_of(&rig.run(request(vec![user("hi")])));
assert_eq!((kind, usage), (FailureKind::Provider, None));
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 (kind, message, usage) = failure_of(&rig.run(request(vec![user("hi")])));
assert_eq!((kind, usage), (FailureKind::Provider, None));
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 +1734,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 +1858,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 +2004,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 +2045,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

@ -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

@ -10,7 +10,7 @@ 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 handle::{FsSessionStore, SessionStore};
pub use launch::ChildEnv;

View file

@ -181,7 +181,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 {
@ -258,7 +258,7 @@ fn fail(kind: FailureKind, message: &'static str) -> Step {
/// 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

@ -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>,