Lease socket answers a usage readout #1

Merged
eureka merged 5 commits from eureka-body/lease-usage-readout into main 2026-11-06 22:55:18 +00:00
8 changed files with 398 additions and 21 deletions

View file

@ -5,7 +5,7 @@ between a caller that needs a model turn and a daemon that holds the provider
credential, so no project wires its own provider calls.
The connector meters nothing in dollars. It holds the subscription token, lends it, and
publishes the plan's usage windows (`usage_readout`, and `plan_quota` on every lease grant) for the
publishes the plan's usage windows (`usage_readout`, `claude_usage_readout` on the lease socket, and `plan_quota` on every lease grant) for the
minds to read and decide their own burn. The one usage-derived refusal is the provider's own
(`ProviderRejected`), repeated until its reset.

View file

@ -10,6 +10,10 @@
//! provider's own, repeated until its own reset. Nothing the run says about
//! itself decides who it is or what the plan allows.
//!
//! The same socket also answers a usage readout to any peer that can reach it
//! (the claude-lease group): no lease, no caller check, no token read and no
//! wake of the meter. Usage is state every Mind reads.
//!
//! The token exists in this process only as the value the token source returns
//! and in the pipe. It is not in an environment, an argument, a file, a log or
//! a reply frame. The connector spawns nothing here: the Self run's `claude`
@ -27,7 +31,7 @@ use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use model_connector::{
LeaseAdmission, LeaseGrant, LeaseRefusal, LeaseTurn, PlanQuota, TransportService,
LeaseAdmission, LeaseGrant, LeaseRefusal, LeaseTurn, PlanQuota, TransportService, UsageReadout,
};
use serde::{Deserialize, Serialize};
@ -48,6 +52,10 @@ pub enum LeaseMessage {
/// holds the token, its write end closed.
ClaudeLeaseGrant(LeaseGranted),
ClaudeLeaseRefusal(LeaseRefused),
/// Asks for the plan's usage windows. Opens no lease.
ClaudeUsageReadoutRequest(LeaseUsageReadoutRequest),
/// The answer to a readout request: the cell's windows as of now, no fd.
ClaudeUsageReadout(UsageReadout),
}
/// A request names only when the lease ends. Who asks is the connector's to know.
@ -56,6 +64,10 @@ pub struct LeaseRequest {
pub expires_at_unix_ms: u64,
}
/// A readout request names nothing: the answer is the same for every asker.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LeaseUsageReadoutRequest {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LeaseGranted {
pub lease_id: String,
@ -383,10 +395,22 @@ struct Lender {
quota: Arc<QuotaCell>,
tokens: Arc<dyn TokenSource>,
units: Arc<dyn PeerUnits>,
/// Connections holding or waiting on a lease, at most `max_connections`.
leases: Arc<AtomicUsize>,
/// Connections being answered a readout, at most `READOUT_CONNECTIONS`.
readouts: Arc<AtomicUsize>,
}
/// Serves leases on `listener` until it fails. Each connection is one lease:
/// request, then grant or refusal, then (after a grant) the hold. A caller is
/// How many usage readouts are served at once, beside the leases. A readout
/// holds nothing, so a few suffice, and a flood of them cannot take a lease's
/// slot: the accept loop admits `max_connections` plus this many.
const READOUT_CONNECTIONS: usize = 4;
/// Serves leases on `listener` until it fails. Each connection is one lease or
/// one usage readout. A lease: request, then grant or refusal, then (after a
/// grant) the hold. A readout: request, then one frame, then close; its reach
/// is the socket's group, and it reads the plan's windows without waking the
/// meter. A caller is
/// the account whose uid the kernel reports for the connection, and only a
/// process in that caller's unit may lease, once per unit invocation for a Self
/// run and once per turn for a Persona; no
@ -407,11 +431,14 @@ pub fn serve_leases(
quota,
tokens,
units,
leases: Arc::new(AtomicUsize::new(0)),
readouts: Arc::new(AtomicUsize::new(0)),
});
let active = Arc::new(AtomicUsize::new(0));
loop {
let (stream, _) = listener.accept()?;
let Some(permit) = Permit::acquire(&active, limits.max_connections) else {
let admitted = limits.max_connections.saturating_add(READOUT_CONNECTIONS);
let Some(permit) = Permit::acquire(&active, admitted) else {
continue;
};
let lender = lender.clone();
@ -421,21 +448,45 @@ pub fn serve_leases(
let _permit = permit;
// A connection that fails is dropped; the lease it may have
// held is ended inside `lend`.
let _ = lender.lend(stream);
let _ = lender.serve_one(stream);
})?;
}
}
impl Lender {
fn lend(&self, mut stream: UnixStream) -> io::Result<()> {
fn serve_one(&self, mut stream: UnixStream) -> io::Result<()> {
stream.set_write_timeout(Some(self.limits.socket_timeout))?;
let peer = peer_credentials(&stream)?;
let request_deadline = Instant::now() + self.limits.socket_timeout;
let LeaseMessage::ClaudeLeaseRequest(request) =
read_message(&mut stream, request_deadline)?
else {
return Err(io::Error::from(io::ErrorKind::InvalidData));
};
match read_message(&mut stream, request_deadline)? {
LeaseMessage::ClaudeLeaseRequest(request) => {
// A lease over the limit is dropped unanswered; readouts
// never count against it.
let Some(_slot) = Permit::acquire(&self.leases, self.limits.max_connections) else {
return Ok(());
};
let peer = peer_credentials(&stream)?;
self.lend(stream, peer, request)
}
LeaseMessage::ClaudeUsageReadoutRequest(_) => {
let Some(_slot) = Permit::acquire(&self.readouts, READOUT_CONNECTIONS) else {
return refuse(
&mut stream,
LeaseRefusalKind::Busy,
"too many usage readouts at once",
);
};
stream.write_all(&encode_frame(&readout(&self.quota, unix_ms())))
}
_ => Err(io::Error::from(io::ErrorKind::InvalidData)),
}
}
fn lend(
&self,
mut stream: UnixStream,
peer: PeerCredentials,
request: LeaseRequest,
) -> io::Result<()> {
let Some(caller) = self.callers.iter().find(|caller| caller.uid == peer.uid) else {
return refuse(
&mut stream,
@ -609,6 +660,12 @@ impl Lender {
}
}
/// The plan's windows as the cell holds them at `now_ms`. It takes the cell by
/// reference so it cannot note a request, and never waits for a first read.
fn readout(quota: &QuotaCell, now_ms: u64) -> LeaseMessage {
LeaseMessage::ClaudeUsageReadout(UsageReadout::new(now_ms, quota.snapshot()))
}
/// The longest a lease waits for the meter's first read of the week. A call
/// takes a few seconds; the holder's own wait for the grant is far longer.
const FIRST_READ_WAIT: Duration = Duration::from_secs(10);
@ -629,6 +686,210 @@ fn refuse(stream: &mut UnixStream, kind: LeaseRefusalKind, detail: &str) -> io::
mod tests {
use super::*;
struct NoTokens;
impl TokenSource for NoTokens {
fn read(&self) -> Option<zeroize::Zeroizing<String>> {
panic!("a readout read the token")
}
}
struct NoUnits;
impl PeerUnits for NoUnits {
fn invocation_in(&self, _: BorrowedFd<'_>, _: &str) -> Option<PeerUnit> {
panic!("a readout looked up the peer's unit")
}
}
fn lender(
quota: &Arc<QuotaCell>,
dir: &std::path::Path,
max_connections: usize,
) -> (Arc<Mutex<TransportService>>, Lender) {
let service = Arc::new(Mutex::new(
TransportService::open(
&dir.join("replay.cc"),
"claude-code",
[model_connector::CallerAdmission::new(
"lease-test-caller",
"lease-test-connection-key",
1,
["model".to_string()],
1,
65_536,
32_768,
)
.unwrap()],
300_000,
)
.unwrap(),
));
let lender = Lender {
callers: Vec::new(),
limits: LeaseLimits {
socket_timeout: Duration::from_secs(2),
max_lease_ms: 3_600_000,
max_connections,
},
service: service.clone(),
quota: quota.clone(),
tokens: Arc::new(NoTokens),
units: Arc::new(NoUnits),
leases: Arc::new(AtomicUsize::new(0)),
readouts: Arc::new(AtomicUsize::new(0)),
};
(service, lender)
}
/// Serves `lender`'s parts on a socket in `dir`; the path comes back.
fn serve(lender: Lender, dir: &std::path::Path) -> std::path::PathBuf {
let socket = dir.join("lease.sock");
let listener = UnixListener::bind(&socket).unwrap();
let (service, quota) = (lender.service.clone(), lender.quota.clone());
thread::spawn(move || {
let _ = serve_leases(
&listener,
lender.callers,
lender.limits,
service,
quota,
lender.tokens,
lender.units,
);
});
socket
}
fn ask(socket: &std::path::Path, message: &LeaseMessage) -> Option<LeaseMessage> {
let mut stream = UnixStream::connect(socket).ok()?;
stream
.set_read_timeout(Some(Duration::from_secs(3)))
.unwrap();
stream.write_all(&encode_frame(message)).ok()?;
let mut raw = Vec::new();
let _ = stream.read_to_end(&mut raw);
decode_frame(&raw)
}
fn readout_request() -> LeaseMessage {
LeaseMessage::ClaudeUsageReadoutRequest(LeaseUsageReadoutRequest {})
}
fn scratch(name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("{name}-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn a_socket_readout_never_asks_the_meter_waits_for_a_week_or_takes_the_service_lock() {
let dir = scratch("readout-arm");
let quota = Arc::new(QuotaCell::new());
quota.attach_meter();
let (service, lender) = lender(&quota, &dir, 8);
let socket = serve(lender, &dir);
// Held throughout: a readout that takes it hangs past the read timeout.
let guard = service.lock().unwrap();
for _ in 0..20 {
let started = Instant::now();
let Some(LeaseMessage::ClaudeUsageReadout(answer)) = ask(&socket, &readout_request())
else {
panic!("no readout");
};
assert!(started.elapsed() < Duration::from_secs(1));
assert!(answer.plan_quota.is_empty());
}
drop(guard);
assert_eq!(quota.asks_so_far(), 0);
assert!(!quota.wanted_at(unix_ms(), Duration::from_secs(15 * 60)));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_flood_of_readouts_at_their_limit_leaves_every_lease_request_answered() {
let dir = scratch("readout-flood");
let quota = Arc::new(QuotaCell::new());
// Fewer lease slots than flooding readers: the readers must not need them.
let (_service, lender) = lender(&quota, &dir, 2);
let socket = serve(lender, &dir);
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let flood: Vec<_> = (0..READOUT_CONNECTIONS)
.map(|_| {
let (socket, stop) = (socket.clone(), stop.clone());
thread::spawn(move || {
while !stop.load(Ordering::Acquire) {
let _ = ask(&socket, &readout_request());
}
})
})
.collect();
// No caller is configured, so each lease is answered UnknownCaller:
// an answer is all that is asked.
let lease = LeaseMessage::ClaudeLeaseRequest(LeaseRequest {
expires_at_unix_ms: unix_ms() + 60_000,
});
let unanswered = (0..40)
.filter(|_| {
!matches!(
ask(&socket, &lease),
Some(LeaseMessage::ClaudeLeaseRefusal(LeaseRefused {
kind: LeaseRefusalKind::UnknownCaller,
..
}))
)
})
.count();
stop.store(true, Ordering::Release);
for client in flood {
client.join().unwrap();
}
assert_eq!(unanswered, 0);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_readout_over_the_readout_limit_is_refused_busy_not_dropped() {
let dir = scratch("readout-limit");
let quota = Arc::new(QuotaCell::new());
let (_service, lender) = lender(&quota, &dir, 8);
let held: Vec<_> = (0..READOUT_CONNECTIONS)
.map(|_| Permit::acquire(&lender.readouts, READOUT_CONNECTIONS).unwrap())
.collect();
let (mut client, server) = UnixStream::pair().unwrap();
client.write_all(&encode_frame(&readout_request())).unwrap();
lender.serve_one(server).unwrap();
let mut raw = Vec::new();
client.read_to_end(&mut raw).unwrap();
let Some(LeaseMessage::ClaudeLeaseRefusal(refusal)) = decode_frame(&raw) else {
panic!("no refusal");
};
assert_eq!(refusal.kind, LeaseRefusalKind::Busy);
drop(held);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_readout_of_an_empty_metered_cell_does_not_wait() {
let quota = QuotaCell::new();
quota.attach_meter();
let started = Instant::now();
let LeaseMessage::ClaudeUsageReadout(answer) = readout(&quota, 77) else {
panic!("a readout answers with a readout");
};
assert!(started.elapsed() < Duration::from_secs(1));
assert!(answer.plan_quota.is_empty());
assert_eq!(answer.at_unix_ms, 77);
}
#[test]
fn a_readout_is_no_sign_of_life() {
let quota = Arc::new(QuotaCell::new());
let within = Duration::from_secs(15 * 60);
let now = unix_ms();
let _ = readout(&quota, now);
assert!(!quota.wanted_at(now, within));
let _asking = quota.asking();
assert!(quota.wanted_at(now, within));
}
/// A refusal frame whose body is exactly `body_bytes` long.
fn frame_of(body_bytes: usize) -> Vec<u8> {
let frame_with = |pad: usize| {

View file

@ -29,8 +29,8 @@ pub use launch::ChildEnv;
#[cfg(target_os = "linux")]
pub use lease::{
LeaseCaller, LeaseGranted, LeaseLimits, LeaseMessage, LeaseRefusalKind, LeaseRefused,
LeaseRequest, MAX_LEASE_FRAME_BYTES, PeerCredentials, PeerUnit, PeerUnits, decode_frame,
encode_frame, peer_credentials, serve_leases, user_uid,
LeaseRequest, LeaseUsageReadoutRequest, MAX_LEASE_FRAME_BYTES, PeerCredentials, PeerUnit,
PeerUnits, decode_frame, encode_frame, peer_credentials, serve_leases, user_uid,
};
#[cfg(unix)]
pub use process::StdProcessPort;

View file

@ -11,6 +11,10 @@ big-endian length, then the named-MessagePack map `{kind, value}`).
`seven_day` windows (utilization_permille, resets_at_unix_ms, status, observed_at_unix_ms).
On the wire the grant travels with one SCM_RIGHTS fd; the file holds the frame only.
- `usage_readout_request.bin`: `claude_usage_readout_request`, an empty map; opens no lease.
- `usage_readout.bin`: `claude_usage_readout`, the kit's `UsageReadout` (`schema_id`, `at_unix_ms`,
`plan_quota` with `five_hour` and `seven_day` windows); no fd travels with it.
Huginn's lease client vendors these files from this repository and tests against the same
bytes. Generator: `tests/lease_wire_golden.rs`; run it with `LEASE_V2_REGENERATE=1`, then
review the diff: a changed byte is a wire change.

Binary file not shown.

View file

@ -15,10 +15,10 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use claude_connector::{
LeaseCaller, LeaseGranted, LeaseLimits, LeaseMessage, LeaseRefusalKind, LeaseRefused,
LeaseRequest, PeerUnit, PeerUnits, QuotaCell, TokenSource, decode_frame, encode_frame,
serve_leases,
LeaseRequest, LeaseUsageReadoutRequest, PeerUnit, PeerUnits, QuotaCell, TokenSource,
decode_frame, encode_frame, serve_leases,
};
use model_connector::{CallerAdmission, LeaseTurn, PlanQuota, TransportService};
use model_connector::{CallerAdmission, LeaseTurn, PlanQuota, TransportService, UsageReadout};
use zeroize::Zeroizing;
const CALLER: &str = "eureka";
@ -649,3 +649,80 @@ fn a_request_trickled_past_the_socket_timeout_is_dropped_without_a_lease() {
assert!(started.elapsed() < Duration::from_secs(6));
assert_eq!(harness.tokens.reads.load(Ordering::SeqCst), 0);
}
/// What a usage readout request came back with.
fn read_usage(harness: &Harness) -> Answer {
let mut stream = UnixStream::connect(&harness.socket).unwrap();
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
stream
.write_all(&encode_frame(&LeaseMessage::ClaudeUsageReadoutRequest(
LeaseUsageReadoutRequest {},
)))
.unwrap();
let (raw, fds) = receive(&stream);
let message = decode_frame(&raw);
Answer {
_stream: stream,
raw,
message,
fds,
}
}
fn usage_of(answer: &Answer) -> UsageReadout {
match &answer.message {
Some(LeaseMessage::ClaudeUsageReadout(readout)) => readout.clone(),
other => panic!("expected a usage readout, got {other:?}"),
}
}
#[test]
fn a_readout_answers_the_cells_windows_and_lends_nothing() {
let harness = start(setup());
let reset = now_ms() + 3_600_000;
harness.quota.observe([
window("five_hour", 420, "allowed", reset),
window("seven_day", 999, "allowed_warning", reset + 86_400_000),
]);
let before = now_ms();
let answer = read_usage(&harness);
let readout = usage_of(&answer);
assert!(answer.fds.is_empty(), "a readout lends no descriptor");
assert_eq!(readout.plan_quota, harness.quota.snapshot().windows);
assert_eq!(readout.plan_quota.len(), 2);
assert!(readout.at_unix_ms >= before && readout.at_unix_ms <= now_ms());
assert_eq!(harness.tokens.reads.load(Ordering::SeqCst), 0);
// No lease was opened: the next request is granted.
assert!(granted_ok(&ask(&harness, 60_000)));
}
#[test]
fn a_peer_outside_the_lease_callers_still_reads_usage() {
let harness = start(Setup {
uid: own_uid().wrapping_add(1),
..setup()
});
harness
.quota
.observe([window("five_hour", 420, "allowed", now_ms() + 3_600_000)]);
let refused = ask(&harness, 60_000);
assert_eq!(refusal_of(&refused), Some(LeaseRefusalKind::UnknownCaller));
let readout = usage_of(&read_usage(&harness));
assert_eq!(readout.plan_quota.len(), 1);
assert_eq!(readout.plan_quota[0].utilization_permille, Some(420));
assert_eq!(harness.tokens.reads.load(Ordering::SeqCst), 0);
}
#[test]
fn a_readout_beside_a_held_lease_leaves_it_held() {
let harness = start(setup());
let held = ask(&harness, 60_000);
assert!(granted_ok(&held));
assert_eq!(usage_of(&read_usage(&harness)).plan_quota.len(), 0);
harness.enter(2);
let second = ask(&harness, 60_000);
assert_eq!(refusal_of(&second), Some(LeaseRefusalKind::Busy));
assert!(granted_ok(&held));
}

View file

@ -8,10 +8,10 @@
use std::path::{Path, PathBuf};
use claude_connector::{
LeaseGranted, LeaseMessage, LeaseRefusalKind, LeaseRefused, LeaseRequest, decode_frame,
encode_frame,
LeaseGranted, LeaseMessage, LeaseRefusalKind, LeaseRefused, LeaseRequest,
LeaseUsageReadoutRequest, decode_frame, encode_frame,
};
use model_connector::PlanQuota;
use model_connector::{PlanQuota, PlanQuotaSnapshot, UsageReadout};
const REGENERATE: &str = "LEASE_V2_REGENERATE";
@ -97,3 +97,38 @@ fn a_grant_with_both_usage_windows_is_pinned() {
}),
);
}
#[test]
fn the_usage_readout_frames_are_pinned() {
let window = |name: &str, permille, status: &str, resets| PlanQuota {
window: name.to_string(),
utilization_permille: Some(permille),
resets_at_unix_ms: resets,
status: status.to_string(),
observed_at_unix_ms: 1_793_300_000_000,
};
let request = LeaseMessage::ClaudeUsageReadoutRequest(LeaseUsageReadoutRequest {});
let answer = LeaseMessage::ClaudeUsageReadout(UsageReadout::new(
1_793_300_100_000,
PlanQuotaSnapshot {
windows: vec![
window("five_hour", 420, "allowed", 1_793_307_000_000),
window("seven_day", 999, "allowed_warning", 1_793_800_000_000),
],
},
));
assert_frame("usage_readout_request.bin", &request);
assert_frame("usage_readout.bin", &answer);
for (message, spelled) in [
(&request, "claude_usage_readout_request"),
(&answer, "claude_usage_readout"),
] {
let bytes = encode_frame(message);
let value: rmpv::Value = rmp_serde::from_slice(&bytes[4..]).unwrap();
let rmpv::Value::Map(outer) = value else {
panic!("a frame is a map")
};
assert_eq!(outer[0].0.as_str(), Some("kind"));
assert_eq!(outer[0].1.as_str(), Some(spelled));
}
}