Replace the footer's placeholder usage with a real observation ledger
The footer showed invented percentages. It now shows what two harnesses actually report, or says it does not know. lumbridge-core gains an append-only per-profile UsageLedger and a projection that labels every derived value estimated, withholds a burn rate from a single sample, withholds a window fraction with no reported ceiling, withholds an exhaustion estimate that lands after the reset, and reports an expired window as rolled over rather than freezing its last percentage. A missing fact renders as missing, never as zero. (0012) lumbridge-harness is the impure side: processes, clocks, and untrusted wire text in, observations out. Three adapters: - Codex's account/rateLimits/read over the app-server's JSON-RPC stdio. The client cannot express a request outside a two-variant enum and answers every server-to-client request with -32601, so a harness asking Lumbridge for a credential is refused by construction. (0013) - Claude Code's session transcripts, as a byte-offset tail follower that reports nothing until the backlog is read to EOF — a partially-read backlog is indistinguishable from a burst of spend, and the first run against 20 MB reported forty-six billion tokens an hour. The parser models four counters, so the conversations in those files are not representable. (0014) - Claude Code's five-hour and seven-day subscription windows, via a bridge installed as its statusLine command. 0014 had claimed no such surface existed; it does, and the record is corrected in place rather than quietly edited. Lumbridge does not read the OAuth credential to call the account usage endpoint, which is what comparable tools do — AGENTS.md forbids it, and 0015 says so rather than leaving the gap unexplained. Also in here: a capability-check ordering fix in the workspace reducer, where the applied-request replay table was consulted before the capability check and so answered questions the caller had no right to ask; the GPUI spike wired to the live probes with per-harness gauges and provenance chips; and a launcher that matches its own window by PID, because GPUI sets WM_NAME but not _NET_WM_NAME and a title match never succeeded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7fe84f71e2
commit
ef52aa7ce2
@@ -0,0 +1,324 @@
|
||||
//! A supervised, byte-capped stdio child.
|
||||
//!
|
||||
//! The child is launched with a cleared environment and a literal allowlist.
|
||||
//! It speaks a machine protocol, so it gets pipes rather than a PTY: a TTY
|
||||
//! would invite terminal control sequences in both directions for no benefit.
|
||||
//! Its stderr is discarded rather than captured, because a harness's diagnostic
|
||||
//! output is exactly the kind of text that ends up in a log carrying a path or
|
||||
//! a token fragment.
|
||||
//!
|
||||
//! Reading a pipe blocks. The child therefore splits into three owners: a
|
||||
//! reader that may block on its own thread, a writer, and a killer the
|
||||
//! supervising thread keeps. Shutdown kills first and joins second, so a child
|
||||
//! that has gone quiet — or hostile — can never hold the caller hostage.
|
||||
//!
|
||||
//! The child is put in its own process group and the group is what gets
|
||||
//! killed. A pipe reports end of file only when *every* write end closes, so
|
||||
//! killing the direct child alone is not enough: a launcher that execs the
|
||||
//! real program as a grandchild with inherited stdio — which is how the npm
|
||||
//! distribution of Codex works — leaves that grandchild holding our stdout,
|
||||
//! and the reader would block forever on a pipe nothing will ever write to.
|
||||
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::HarnessError;
|
||||
|
||||
/// Environment a probe child may inherit.
|
||||
///
|
||||
/// Literal names only: never a pattern and never a user-supplied name.
|
||||
///
|
||||
/// This deliberately mirrors but does not reuse the terminal crate's private
|
||||
/// allowlist. AGENTS.md keeps the harness and terminal boundaries separate,
|
||||
/// and the two lists genuinely differ: a probe needs `CODEX_HOME` so it reads
|
||||
/// the same account the user's panes use, and has no use for `SHELL`.
|
||||
/// `CODEX_HOME` is a filesystem path, never a secret.
|
||||
pub const PROBE_ENV_ALLOWLIST: &[&str] = &[
|
||||
"CODEX_HOME",
|
||||
"HOME",
|
||||
"LANG",
|
||||
"LC_ALL",
|
||||
"LC_CTYPE",
|
||||
"LOGNAME",
|
||||
"PATH",
|
||||
"TMPDIR",
|
||||
"USER",
|
||||
"XDG_CONFIG_HOME",
|
||||
"XDG_DATA_HOME",
|
||||
"XDG_RUNTIME_DIR",
|
||||
];
|
||||
|
||||
/// A single frame longer than this is treated as a fault rather than buffered.
|
||||
/// A hostile or broken relay must not be able to grow this process's memory.
|
||||
pub(crate) const MAX_LINE_BYTES: usize = 256 * 1024;
|
||||
|
||||
/// A freshly launched child, before its pipes are handed to their owners.
|
||||
pub(crate) struct ProbeChild {
|
||||
child: Arc<Mutex<Child>>,
|
||||
group: u32,
|
||||
stdin: ChildStdin,
|
||||
stdout: ChildStdout,
|
||||
}
|
||||
|
||||
impl ProbeChild {
|
||||
/// Launches a probe program with a minimal environment.
|
||||
///
|
||||
/// This runs on the caller's thread, following the same pattern as the
|
||||
/// runtime actor: start synchronously so a missing program is an immediate
|
||||
/// error, then transfer the pipes to their threads.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`HarnessError::EmptyProgram`] for a blank program name and
|
||||
/// [`HarnessError::SpawnFailed`] if the process cannot start. The
|
||||
/// underlying [`std::io::Error`] is deliberately reduced to its
|
||||
/// [`std::io::ErrorKind`] so a filesystem path cannot travel in an error.
|
||||
pub(crate) fn spawn(program: &str, arguments: &[&str]) -> Result<Self, HarnessError> {
|
||||
if program.trim().is_empty() {
|
||||
return Err(HarnessError::EmptyProgram);
|
||||
}
|
||||
let mut command = Command::new(program);
|
||||
command
|
||||
.args(arguments)
|
||||
.env_clear()
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null());
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
// The child leads its own group, so its group ID is its process ID
|
||||
// and every descendant it does not deliberately detach joins it.
|
||||
command.process_group(0);
|
||||
}
|
||||
for name in PROBE_ENV_ALLOWLIST {
|
||||
if let Some(value) = std::env::var_os(name) {
|
||||
command.env(name, value);
|
||||
}
|
||||
}
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|error| HarnessError::SpawnFailed(error.kind()))?;
|
||||
let broken = HarnessError::SpawnFailed(std::io::ErrorKind::BrokenPipe);
|
||||
let group = child.id();
|
||||
let stdin = child.stdin.take().ok_or(broken)?;
|
||||
let stdout = child.stdout.take().ok_or(broken)?;
|
||||
Ok(Self {
|
||||
child: Arc::new(Mutex::new(child)),
|
||||
group,
|
||||
stdin,
|
||||
stdout,
|
||||
})
|
||||
}
|
||||
|
||||
/// Splits the child into its three independent owners.
|
||||
pub(crate) fn into_parts(self) -> (ProbeWriter, ProbeReader, ProbeKiller) {
|
||||
(
|
||||
ProbeWriter { stdin: self.stdin },
|
||||
ProbeReader {
|
||||
reader: BufReader::new(self.stdout),
|
||||
},
|
||||
ProbeKiller {
|
||||
child: self.child,
|
||||
group: self.group,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The writing half.
|
||||
pub(crate) struct ProbeWriter {
|
||||
stdin: ChildStdin,
|
||||
}
|
||||
|
||||
impl ProbeWriter {
|
||||
/// Writes one newline-terminated frame.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`HarnessError::Eof`] once the child has closed its input.
|
||||
pub(crate) fn write_line(&mut self, line: &str) -> Result<(), HarnessError> {
|
||||
self.stdin
|
||||
.write_all(line.as_bytes())
|
||||
.and_then(|()| self.stdin.write_all(b"\n"))
|
||||
.and_then(|()| self.stdin.flush())
|
||||
.map_err(|_| HarnessError::Eof)
|
||||
}
|
||||
}
|
||||
|
||||
/// The reading half. Its methods block, so it belongs on its own thread.
|
||||
pub(crate) struct ProbeReader {
|
||||
reader: BufReader<ChildStdout>,
|
||||
}
|
||||
|
||||
impl ProbeReader {
|
||||
/// Reads one frame, refusing an oversized line instead of buffering it.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`HarnessError::Eof`] at end of stream and
|
||||
/// [`HarnessError::OversizedLine`] past [`MAX_LINE_BYTES`].
|
||||
pub(crate) fn read_line(&mut self) -> Result<String, HarnessError> {
|
||||
let mut buffer = Vec::new();
|
||||
let read = self
|
||||
.reader
|
||||
.by_ref()
|
||||
.take(MAX_LINE_BYTES as u64 + 1)
|
||||
.read_until(b'\n', &mut buffer)
|
||||
.map_err(|_| HarnessError::Eof)?;
|
||||
if read == 0 {
|
||||
return Err(HarnessError::Eof);
|
||||
}
|
||||
if read > MAX_LINE_BYTES {
|
||||
return Err(HarnessError::OversizedLine(read));
|
||||
}
|
||||
String::from_utf8(buffer).map_err(|_| HarnessError::Malformed)
|
||||
}
|
||||
}
|
||||
|
||||
/// The half a supervisor keeps so it can always stop the child.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ProbeKiller {
|
||||
child: Arc<Mutex<Child>>,
|
||||
group: u32,
|
||||
}
|
||||
|
||||
impl ProbeKiller {
|
||||
/// Ends the child and everything in its process group, then reaps it.
|
||||
///
|
||||
/// Killing the whole group is what closes every write end of our stdout
|
||||
/// pipe, which is what unblocks the reader. Idempotent: killing an
|
||||
/// already-dead process is not an error worth reporting, and a poisoned
|
||||
/// lock means another thread died mid-kill, which the operating system has
|
||||
/// already resolved.
|
||||
pub(crate) fn kill(&self) {
|
||||
self.kill_group();
|
||||
if let Ok(mut child) = self.child.lock() {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn kill_group(&self) {
|
||||
use nix::sys::signal::{Signal, killpg};
|
||||
use nix::unistd::Pid;
|
||||
|
||||
if let Ok(group) = i32::try_from(self.group) {
|
||||
let _ = killpg(Pid::from_raw(group), Signal::SIGKILL);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn kill_group(&self) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{MAX_LINE_BYTES, PROBE_ENV_ALLOWLIST, ProbeChild};
|
||||
use crate::HarnessError;
|
||||
|
||||
#[test]
|
||||
fn an_empty_program_is_refused_before_spawning() {
|
||||
assert!(matches!(
|
||||
ProbeChild::spawn(" ", &[]).err(),
|
||||
Some(HarnessError::EmptyProgram)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_missing_program_reports_a_kind_and_never_a_path() {
|
||||
let Err(error) = ProbeChild::spawn("lumbridge-no-such-probe-binary", &[]) else {
|
||||
panic!("a missing program cannot spawn");
|
||||
};
|
||||
assert!(matches!(error, HarnessError::SpawnFailed(_)));
|
||||
assert!(
|
||||
!error.to_string().contains("lumbridge-no-such-probe-binary"),
|
||||
"an error must not carry a filesystem path"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_allowlist_is_literal_sorted_and_free_of_secret_bearing_names() {
|
||||
let mut sorted = PROBE_ENV_ALLOWLIST.to_vec();
|
||||
sorted.sort_unstable();
|
||||
assert_eq!(sorted, PROBE_ENV_ALLOWLIST);
|
||||
for name in PROBE_ENV_ALLOWLIST {
|
||||
let upper = name.to_uppercase();
|
||||
assert!(
|
||||
!upper.contains("TOKEN")
|
||||
&& !upper.contains("KEY")
|
||||
&& !upper.contains("SECRET")
|
||||
&& !upper.contains("PASSWORD"),
|
||||
"{name} could carry a credential into a child"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_child_round_trips_one_frame() {
|
||||
let Ok(child) = ProbeChild::spawn("cat", &[]) else {
|
||||
return; // cat is not guaranteed on every supported platform.
|
||||
};
|
||||
let (mut writer, mut reader, killer) = child.into_parts();
|
||||
writer
|
||||
.write_line(r#"{"ok":true}"#)
|
||||
.expect("cat accepts input");
|
||||
assert_eq!(
|
||||
reader.read_line().expect("cat echoes the line"),
|
||||
"{\"ok\":true}\n"
|
||||
);
|
||||
killer.kill();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_oversized_frame_is_refused_rather_than_buffered() {
|
||||
// Generated by the child so the test never deadlocks writing more than
|
||||
// a pipe buffer into a process that is not reading.
|
||||
let script = format!("head -c {} /dev/zero | tr '\\0' 'x'", MAX_LINE_BYTES + 16);
|
||||
let Ok(child) = ProbeChild::spawn("sh", &["-c", &script]) else {
|
||||
return;
|
||||
};
|
||||
let (_writer, mut reader, killer) = child.into_parts();
|
||||
assert!(matches!(
|
||||
reader.read_line(),
|
||||
Err(HarnessError::OversizedLine(_))
|
||||
));
|
||||
killer.kill();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn killing_a_child_unblocks_a_reader_blocked_on_a_grandchild() {
|
||||
// A launcher that backgrounds a helper and waits is the shape of the
|
||||
// npm Codex distribution: the helper inherits our stdout, so killing
|
||||
// only the direct child would leave the reader blocked forever.
|
||||
let Ok(child) = ProbeChild::spawn("sh", &["-c", "sleep 30 & wait"]) else {
|
||||
return;
|
||||
};
|
||||
let (_writer, mut reader, killer) = child.into_parts();
|
||||
let reading = std::thread::spawn(move || reader.read_line());
|
||||
killer.kill();
|
||||
let outcome = reading.join().expect("the reader thread must not panic");
|
||||
assert!(
|
||||
matches!(outcome, Err(HarnessError::Eof)),
|
||||
"killing the group must end the read rather than hang it"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn killing_a_child_unblocks_its_reader() {
|
||||
// `sleep` never writes, so the reader is blocked until the kill lands.
|
||||
let Ok(child) = ProbeChild::spawn("sleep", &["30"]) else {
|
||||
return;
|
||||
};
|
||||
let (_writer, mut reader, killer) = child.into_parts();
|
||||
let reading = std::thread::spawn(move || reader.read_line());
|
||||
killer.kill();
|
||||
let outcome = reading.join().expect("the reader thread must not panic");
|
||||
assert!(
|
||||
matches!(outcome, Err(HarnessError::Eof)),
|
||||
"a killed child must end the read rather than hang it"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user