usage-meter-poll: the serve test asks a loopback fake, never the provider (finding serve-test-asks-real-provider)
serve_config is the public wrapper over a crate-private serve_config_with that takes the usage client; the test runs it in-process against a fake and asserts the fake received GET /api/oauth/usage with the bearer. The endpoint is not a document field or env, so whoever writes the config cannot redirect the token. The subprocess test that reached api.anthropic.com is deleted. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f242290ca5
commit
8fb8b5a71d
2 changed files with 105 additions and 2 deletions
|
|
@ -22,6 +22,8 @@ use crate::backend::readiness;
|
|||
use crate::lease::{LeaseCaller, LeaseLimits, serve_leases, user_uid};
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::unit::SystemdUnits;
|
||||
#[cfg(unix)]
|
||||
use crate::usage_poll::UsageClient;
|
||||
use crate::{
|
||||
ClaudeCodeBackend, ClaudeCodeConfig, ClaudeCodeLimits, CredentialTokenSource, StderrSink,
|
||||
};
|
||||
|
|
@ -535,6 +537,13 @@ pub fn check_ready(config: &ClaudeConnectorConfig) -> Result<(), ConfigError> {
|
|||
/// Loads the document and serves until the listener fails.
|
||||
#[cfg(unix)]
|
||||
pub fn serve_config(path: &Path) -> Result<(), ConfigError> {
|
||||
serve_config_with(path, UsageClient::production())
|
||||
}
|
||||
|
||||
/// [`serve_config`] with the usage poll asking `usage`: the endpoint is not a
|
||||
/// setting of the document, so a test reaches it only through here.
|
||||
#[cfg(unix)]
|
||||
fn serve_config_with(path: &Path, usage: UsageClient) -> Result<(), ConfigError> {
|
||||
let config = load_config(path)?;
|
||||
let admissions = readiness_of(&config)?;
|
||||
let listen = config.validate()?;
|
||||
|
|
@ -561,6 +570,7 @@ pub fn serve_config(path: &Path) -> Result<(), ConfigError> {
|
|||
Box::new(CredentialTokenSource::from_environment(
|
||||
config.token_credential.clone(),
|
||||
)),
|
||||
usage,
|
||||
)
|
||||
.map_err(|_| ConfigError::Serving("usage poll"))?;
|
||||
serve_service(listener, config.serve_limits(), service, backend)?;
|
||||
|
|
@ -571,6 +581,8 @@ pub fn serve_config(path: &Path) -> Result<(), ConfigError> {
|
|||
mod tests {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use sha2::Digest;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A log that a test can read back.
|
||||
|
|
@ -587,6 +599,96 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// `serve` starts the usage poll beside the listener, and the poll asks
|
||||
/// where it was told: a fake on loopback receives its request, bearer
|
||||
/// token and all, and nothing reaches the provider.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn serve_config_starts_the_usage_poll_and_stays_up() {
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
const TOKEN: &str = "sk-ant-oat01-fake-token-for-the-serve-test";
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("claude-connector-serve-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let at = |name: &str| dir.join(name);
|
||||
for sub in ["work", "home", "prompts", "credentials"] {
|
||||
std::fs::create_dir_all(at(sub)).unwrap();
|
||||
}
|
||||
std::fs::set_permissions(at("prompts"), std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||
std::fs::write(at("claude"), b"not a real claude binary").unwrap();
|
||||
std::fs::write(at("caller.key"), "k".repeat(48)).unwrap();
|
||||
std::fs::write(at("credentials/claude-token"), TOKEN).unwrap();
|
||||
std::fs::write(at("credentials/claude-handle-secret"), "s".repeat(48)).unwrap();
|
||||
let config = ClaudeConnectorConfig {
|
||||
listen: "127.0.0.1:0".to_string(),
|
||||
replay_store: at("replay.cc"),
|
||||
claude_binary: at("claude"),
|
||||
claude_binary_sha256: sha2::Sha256::digest(b"not a real claude binary").into(),
|
||||
work_dir: at("work"),
|
||||
config_dir: at("home"),
|
||||
prompt_dir: at("prompts"),
|
||||
instance: "persona-a".to_string(),
|
||||
token_credential: "claude-token".to_string(),
|
||||
handle_secret_credential: "claude-handle-secret".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: at("caller.key"),
|
||||
connection_key_epoch: 1,
|
||||
allowed_models: vec!["claude-haiku-5-5".to_string()],
|
||||
max_concurrent_requests: 4,
|
||||
max_payload_bytes: 1 << 20,
|
||||
max_output_tokens: 32_768,
|
||||
}],
|
||||
lease_socket: None,
|
||||
lease_callers: Vec::new(),
|
||||
lease_max_ms: 3_600_000,
|
||||
};
|
||||
write_config(&at("connector.cc"), &config).unwrap();
|
||||
// SAFETY: set once, to a directory only this test reads, before the
|
||||
// serve thread it starts.
|
||||
unsafe { std::env::set_var("CREDENTIALS_DIRECTORY", at("credentials")) };
|
||||
|
||||
let fake = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let url = format!("http://{}/api/oauth/usage", fake.local_addr().unwrap());
|
||||
let (asked, request) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let (mut stream, _) = fake.accept().unwrap();
|
||||
let mut text = vec![0u8; 4096];
|
||||
let n = stream.read(&mut text).unwrap();
|
||||
let _ = asked.send(String::from_utf8_lossy(&text[..n]).into_owned());
|
||||
let _ = stream.write_all(
|
||||
b"HTTP/1.1 429 Too Many Requests
|
||||
Content-Length: 0
|
||||
Connection: close
|
||||
|
||||
",
|
||||
);
|
||||
});
|
||||
let path = at("connector.cc");
|
||||
let served = std::thread::spawn(move || serve_config_with(&path, UsageClient::new(url)));
|
||||
|
||||
let request = request
|
||||
.recv_timeout(Duration::from_secs(60))
|
||||
.expect("the usage poll never asked the fake endpoint");
|
||||
assert!(request.starts_with("GET /api/oauth/usage "), "{request}");
|
||||
assert!(request.contains(&format!("Bearer {TOKEN}")), "{request}");
|
||||
assert!(
|
||||
!served.is_finished(),
|
||||
"serve returned after starting the poll"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serve_errors_name_a_field_or_stage_and_never_a_stored_value() {
|
||||
const CANARY: &str = "canary-path-8e2a";
|
||||
|
|
|
|||
|
|
@ -393,14 +393,15 @@ fn unix_ms() -> u64 {
|
|||
|
||||
/// Starts the poll thread. It reads the token afresh at each poll, writes
|
||||
/// only to `cell`, and logs one line to stderr when its state changes.
|
||||
pub fn start(
|
||||
pub(crate) fn start(
|
||||
cell: Arc<QuotaCell>,
|
||||
tokens: Box<dyn TokenSource>,
|
||||
client: UsageClient,
|
||||
) -> std::io::Result<std::thread::JoinHandle<()>> {
|
||||
let poller = Poller::new(
|
||||
cell,
|
||||
tokens,
|
||||
UsageClient::production(),
|
||||
client,
|
||||
Box::new(|line| eprintln!("claude-connector: {line}")),
|
||||
);
|
||||
std::thread::Builder::new()
|
||||
|
|
|
|||
Loading…
Reference in a new issue