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>
279 lines
9.6 KiB
Rust
279 lines
9.6 KiB
Rust
//! A deliberately deaf JSON-RPC client.
|
|
//!
|
|
//! The Codex app-server can send requests *to* its client, including
|
|
//! `account/chatgptAuthTokens/refresh`, which returns a bare access token.
|
|
//! AGENTS.md forbids Lumbridge from scraping a harness's private credentials.
|
|
//! Choosing not to call that method would be a convention; this module makes
|
|
//! it unrepresentable instead.
|
|
//!
|
|
//! There is no free-form method string anywhere in this crate. Outbound
|
|
//! requests come from a two-variant enum, and every inbound server request is
|
|
//! answered with `-32601 method not found` regardless of what it asks for.
|
|
//! Classification exists only so a refusal can be counted and named, which
|
|
//! makes "we were asked for a credential and refused" an auditable event
|
|
//! rather than an absence.
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
/// JSON-RPC's "method not found". The only reply this client ever sends.
|
|
const METHOD_NOT_FOUND: i64 = -32601;
|
|
/// Codex returns this for a request that needs an account it does not have.
|
|
pub(crate) const INVALID_REQUEST: i64 = -32600;
|
|
|
|
/// Every request this crate is able to send.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub(crate) enum OutboundRequest {
|
|
Initialize,
|
|
ReadAccountRateLimits,
|
|
}
|
|
|
|
impl OutboundRequest {
|
|
pub(crate) const fn method(self) -> &'static str {
|
|
match self {
|
|
Self::Initialize => "initialize",
|
|
Self::ReadAccountRateLimits => "account/rateLimits/read",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The notifications this client accepts. Anything else is ignored.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub(crate) enum AcceptedNotification {
|
|
AccountRateLimitsUpdated,
|
|
}
|
|
|
|
impl AcceptedNotification {
|
|
pub(crate) fn from_method(method: &str) -> Option<Self> {
|
|
match method {
|
|
"account/rateLimits/updated" => Some(Self::AccountRateLimitsUpdated),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// What a server-to-client request was asking for.
|
|
///
|
|
/// Purely descriptive. Every class is refused identically; the distinction
|
|
/// exists so a probe can report that it declined a credential request.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum ServerRequestClass {
|
|
/// A request that would hand Lumbridge a credential. Always refused.
|
|
CredentialRefresh,
|
|
/// A request to run something on the harness's behalf.
|
|
Execution,
|
|
/// A request for a human approval decision.
|
|
Approval,
|
|
/// A request for ambient information such as the current time.
|
|
Ambient,
|
|
/// Anything this client does not recognise.
|
|
Unrecognised,
|
|
}
|
|
|
|
impl ServerRequestClass {
|
|
pub(crate) fn classify(method: &str) -> Self {
|
|
match method {
|
|
"account/chatgptAuthTokens/refresh" => Self::CredentialRefresh,
|
|
"item/tool/call" | "attestation/generate" => Self::Execution,
|
|
"item/permissions/requestApproval" => Self::Approval,
|
|
"currentTime/read" => Self::Ambient,
|
|
_ => Self::Unrecognised,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn label(self) -> &'static str {
|
|
match self {
|
|
Self::CredentialRefresh => "credential refresh",
|
|
Self::Execution => "execution",
|
|
Self::Approval => "approval",
|
|
Self::Ambient => "ambient information",
|
|
Self::Unrecognised => "unrecognised request",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A classified inbound line.
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) enum Inbound {
|
|
/// A successful response to one of our two requests.
|
|
Result { id: u64, result: Value },
|
|
/// A failed response to one of our two requests.
|
|
Failure { id: u64, code: i64 },
|
|
/// An accepted push notification.
|
|
Notification {
|
|
kind: AcceptedNotification,
|
|
params: Value,
|
|
},
|
|
/// The server asked us for something. It will be refused.
|
|
ServerRequest {
|
|
id: Value,
|
|
class: ServerRequestClass,
|
|
},
|
|
/// A well-formed frame with nothing for us in it.
|
|
Ignored,
|
|
}
|
|
|
|
/// Encodes one of the two permitted requests.
|
|
pub(crate) fn encode_request(id: u64, request: OutboundRequest, params: &Value) -> String {
|
|
json!({
|
|
"jsonrpc": "2.0",
|
|
"id": id,
|
|
"method": request.method(),
|
|
"params": params,
|
|
})
|
|
.to_string()
|
|
}
|
|
|
|
pub(crate) fn encode_notification(method: &'static str, params: &Value) -> String {
|
|
json!({ "jsonrpc": "2.0", "method": method, "params": params }).to_string()
|
|
}
|
|
|
|
/// The refusal sent for every server-to-client request.
|
|
pub(crate) fn encode_refusal(id: &Value) -> String {
|
|
json!({
|
|
"jsonrpc": "2.0",
|
|
"id": id,
|
|
"error": { "code": METHOD_NOT_FOUND, "message": "method not found" },
|
|
})
|
|
.to_string()
|
|
}
|
|
|
|
/// Classifies one line of JSON-RPC without interpreting its payload.
|
|
///
|
|
/// Returns `None` when the line is not a JSON object at all, which the caller
|
|
/// reports as [`crate::HarnessError::Malformed`].
|
|
pub(crate) fn classify(line: &str) -> Option<Inbound> {
|
|
let frame: Value = serde_json::from_str(line).ok()?;
|
|
let object = frame.as_object()?;
|
|
let method = object.get("method").and_then(Value::as_str);
|
|
let id = object.get("id");
|
|
|
|
match (method, id) {
|
|
// A method with an id is the server asking us for something.
|
|
(Some(method), Some(id)) => Some(Inbound::ServerRequest {
|
|
id: id.clone(),
|
|
class: ServerRequestClass::classify(method),
|
|
}),
|
|
// A method without an id is a notification.
|
|
(Some(method), None) => Some(AcceptedNotification::from_method(method).map_or(
|
|
Inbound::Ignored,
|
|
|kind| Inbound::Notification {
|
|
kind,
|
|
params: object.get("params").cloned().unwrap_or(Value::Null),
|
|
},
|
|
)),
|
|
// An id without a method is a response to one of ours.
|
|
(None, Some(id)) => {
|
|
let id = id.as_u64()?;
|
|
if let Some(error) = object.get("error") {
|
|
let code = error.get("code").and_then(Value::as_i64).unwrap_or(0);
|
|
return Some(Inbound::Failure { id, code });
|
|
}
|
|
Some(Inbound::Result {
|
|
id,
|
|
result: object.get("result").cloned().unwrap_or(Value::Null),
|
|
})
|
|
}
|
|
(None, None) => Some(Inbound::Ignored),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{
|
|
AcceptedNotification, Inbound, OutboundRequest, ServerRequestClass, classify,
|
|
encode_refusal, encode_request,
|
|
};
|
|
use serde_json::{Value, json};
|
|
|
|
#[test]
|
|
fn only_two_methods_can_be_sent() {
|
|
assert_eq!(OutboundRequest::Initialize.method(), "initialize");
|
|
assert_eq!(
|
|
OutboundRequest::ReadAccountRateLimits.method(),
|
|
"account/rateLimits/read"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_credential_refresh_request_is_classified_and_refused() {
|
|
let line = r#"{"jsonrpc":"2.0","id":9,"method":"account/chatgptAuthTokens/refresh"}"#;
|
|
let Some(Inbound::ServerRequest { id, class }) = classify(line) else {
|
|
panic!("a method with an id is a server request");
|
|
};
|
|
assert_eq!(class, ServerRequestClass::CredentialRefresh);
|
|
let refusal: Value =
|
|
serde_json::from_str(&encode_refusal(&id)).expect("the refusal is valid JSON");
|
|
assert_eq!(refusal["error"]["code"], json!(-32601));
|
|
assert!(
|
|
refusal.get("result").is_none(),
|
|
"a refusal must never carry a result"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn every_server_request_class_is_refused_the_same_way() {
|
|
for method in [
|
|
"account/chatgptAuthTokens/refresh",
|
|
"item/tool/call",
|
|
"item/permissions/requestApproval",
|
|
"currentTime/read",
|
|
"something/entirely/new",
|
|
] {
|
|
let line = format!(r#"{{"jsonrpc":"2.0","id":1,"method":"{method}"}}"#);
|
|
let Some(Inbound::ServerRequest { id, .. }) = classify(&line) else {
|
|
panic!("{method} must classify as a server request");
|
|
};
|
|
let refusal: Value = serde_json::from_str(&encode_refusal(&id)).expect("valid JSON");
|
|
assert_eq!(refusal["error"]["code"], json!(-32601));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn only_the_rate_limit_notification_is_accepted() {
|
|
let accepted = classify(
|
|
r#"{"jsonrpc":"2.0","method":"account/rateLimits/updated","params":{"rateLimits":{}}}"#,
|
|
);
|
|
assert!(matches!(
|
|
accepted,
|
|
Some(Inbound::Notification {
|
|
kind: AcceptedNotification::AccountRateLimitsUpdated,
|
|
..
|
|
})
|
|
));
|
|
let ignored =
|
|
classify(r#"{"jsonrpc":"2.0","method":"thread/tokenUsage/updated","params":{}}"#);
|
|
assert!(matches!(ignored, Some(Inbound::Ignored)));
|
|
}
|
|
|
|
#[test]
|
|
fn responses_and_failures_are_separated() {
|
|
assert!(matches!(
|
|
classify(r#"{"jsonrpc":"2.0","id":7,"result":{"rateLimits":{}}}"#),
|
|
Some(Inbound::Result { id: 7, .. })
|
|
));
|
|
assert!(matches!(
|
|
classify(r#"{"jsonrpc":"2.0","id":7,"error":{"code":-32600,"message":"nope"}}"#),
|
|
Some(Inbound::Failure {
|
|
id: 7,
|
|
code: -32600
|
|
})
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_and_non_object_lines_are_rejected() {
|
|
for line in ["", "not json", "[1,2,3]", "\"a string\"", "{"] {
|
|
assert!(classify(line).is_none(), "{line:?} must not classify");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_request_encodes_without_a_free_form_method() {
|
|
let encoded = encode_request(1, OutboundRequest::ReadAccountRateLimits, &json!({}));
|
|
let frame: Value = serde_json::from_str(&encoded).expect("valid JSON");
|
|
assert_eq!(frame["method"], json!("account/rateLimits/read"));
|
|
assert_eq!(frame["jsonrpc"], json!("2.0"));
|
|
}
|
|
}
|