Files
lumbridge-code/crates/lumbridge-harness/src/jsonrpc.rs
T
Metal AgentandClaude Opus 5 ef52aa7ce2 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>
2026-08-31 21:47:11 -07:00

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"));
}
}