use std::path::{Path, PathBuf};
use trusty_mpm::core::agent_cost::{AgentCostConfig, BudgetStatus, evaluate_cost};
const MAX_TRANSCRIPT_TAIL: u64 = 64 * 1024;
const RETRY_TRANSCRIPT_TAIL: u64 = 1024 * 1024;
const EVAL_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(200);
fn agent_transcript_candidate(payload: &serde_json::Value) -> Option<PathBuf> {
let field = |k: &str| {
payload
.get(k)
.and_then(serde_json::Value::as_str)
.filter(|s| !s.is_empty())
};
if let Some(p) = field("agent_transcript_path") {
let path = PathBuf::from(p);
if path.is_file() {
return Some(path);
}
}
let parent = field("transcript_path")?;
let parent = Path::new(parent);
if parent
.parent()
.is_some_and(|d| d.file_name().is_some_and(|n| n == "subagents"))
&& parent.is_file()
{
return Some(parent.to_path_buf());
}
let agent_id = field("agent_id")?;
let stem = parent.file_stem()?;
let derived = parent
.parent()?
.join(stem)
.join("subagents")
.join(format!("agent-{agent_id}.jsonl"));
derived.is_file().then_some(derived)
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum AgentTranscript {
Contained(PathBuf),
Refused(PathBuf),
Absent,
}
impl AgentTranscript {
fn contained(&self) -> Option<&Path> {
match self {
Self::Contained(p) => Some(p),
Self::Refused(_) | Self::Absent => None,
}
}
fn candidate(&self) -> Option<&Path> {
match self {
Self::Contained(p) | Self::Refused(p) => Some(p),
Self::Absent => None,
}
}
}
pub(crate) fn resolve_agent_transcript(payload: &serde_json::Value) -> AgentTranscript {
resolve_agent_transcript_in(
payload,
trusty_mpm::core::session_record::claude_config_dir().as_deref(),
)
}
pub(crate) fn resolve_agent_transcript_in(
payload: &serde_json::Value,
claude_config_dir: Option<&Path>,
) -> AgentTranscript {
let Some(candidate) = agent_transcript_candidate(payload) else {
return AgentTranscript::Absent;
};
let Some(root) = claude_config_dir else {
return AgentTranscript::Refused(candidate);
};
match trusty_mpm::core::session_record::contained_transcript_file(root, &candidate) {
Some(canonical) => AgentTranscript::Contained(canonical),
None => AgentTranscript::Refused(candidate),
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct AgentCost {
pub(crate) status: BudgetStatus,
pub(crate) tokens: u64,
pub(crate) refused_transcript: Option<PathBuf>,
}
impl AgentCost {
fn allow() -> Self {
Self {
status: BudgetStatus::Ok,
tokens: 0,
refused_transcript: None,
}
}
}
pub(crate) fn warn_if_transcript_refused(session_id: &str, cost: &AgentCost) {
if let Some(path) = &cost.refused_transcript {
eprintln!(
"trusty-mpm: AGENT-COST TRANSCRIPT REFUSED (#7278) session={session_id} \
path={} — the payload named a transcript outside the Claude config \
directory; the guard ALLOWED this call without measuring its context.",
path.display()
);
}
}
pub(crate) async fn evaluate_agent_cost(
payload: &serde_json::Value,
config: &AgentCostConfig,
) -> AgentCost {
evaluate_agent_cost_in(
payload,
config,
trusty_mpm::core::session_record::claude_config_dir().as_deref(),
EVAL_TIMEOUT,
)
.await
}
async fn evaluate_agent_cost_in(
payload: &serde_json::Value,
config: &AgentCostConfig,
claude_config_dir: Option<&Path>,
budget: std::time::Duration,
) -> AgentCost {
if !config.enabled {
return AgentCost::allow();
}
let resolved = resolve_agent_transcript_in(payload, claude_config_dir);
let Some(path) = resolved.contained() else {
return AgentCost {
refused_transcript: resolved.candidate().map(Path::to_path_buf),
..AgentCost::allow()
};
};
let measured = tokio::time::timeout(budget, read_latest_context(path))
.await
.ok()
.flatten();
verdict(measured, config)
}
fn verdict(measured: Option<u64>, config: &AgentCostConfig) -> AgentCost {
match measured {
Some(tokens) => AgentCost {
status: evaluate_cost(tokens, config),
tokens,
refused_transcript: None,
},
None => AgentCost::allow(),
}
}
async fn read_latest_context(path: &Path) -> Option<u64> {
use trusty_mpm::core::agent_cost::latest_context_tokens;
for bytes in [MAX_TRANSCRIPT_TAIL, RETRY_TRANSCRIPT_TAIL] {
let jsonl = super::misc::read_transcript_tail(path, bytes).await?;
if let Some(tokens) = latest_context_tokens(&jsonl) {
return Some(tokens);
}
if (jsonl.len() as u64) < bytes {
return None;
}
}
None
}
pub(crate) fn is_persistence_escape(
tool_name: &str,
tool_input: Option<&serde_json::Value>,
) -> bool {
use trusty_mpm::core::agent_cost::is_persistence_tool;
if is_persistence_tool(tool_name) {
return true;
}
if tool_name != "Bash" {
return false;
}
let command = tool_input
.and_then(|v| v.get("command"))
.and_then(serde_json::Value::as_str)
.unwrap_or_default();
super::pm_guard_bash::command_is_persistence_only(command)
}
pub(crate) fn claim_warn_notice(payload: &serde_json::Value) -> bool {
let Some(key) = warn_notice_key(payload) else {
return true;
};
let dir = std::env::temp_dir().join("trusty-mpm-agent-cost");
if std::fs::create_dir_all(&dir).is_err() {
return true;
}
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(dir.join(key))
{
Ok(_) => true,
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => false,
Err(_) => true,
}
}
fn warn_notice_key(payload: &serde_json::Value) -> Option<String> {
let field = |k: &str| {
payload
.get(k)
.and_then(serde_json::Value::as_str)
.filter(|s| !s.is_empty())
.map(str::to_string)
};
let raw = field("agent_id")
.or_else(|| {
resolve_agent_transcript(payload)
.candidate()
.and_then(|p| p.file_stem().map(|s| s.to_string_lossy().into_owned()))
})
.or_else(|| field("session_id"))?;
let key: String = raw
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-')
.take(64)
.collect();
(!key.is_empty()).then_some(key)
}
#[cfg(test)]
mod tests {
use super::*;
use trusty_mpm::core::agent_cost::stop_reason;
const TEST_EVAL_BUDGET: std::time::Duration = std::time::Duration::from_secs(30);
fn transcript_tree(dir: &Path, agent_id: &str, context_tokens: u64) -> (PathBuf, PathBuf) {
let parent = dir.join("5b4e60d2-d5d9-4ec5-927f-4fb9198a296d.jsonl");
std::fs::write(&parent, "{}\n").expect("write parent");
let sub_dir = dir
.join("5b4e60d2-d5d9-4ec5-927f-4fb9198a296d")
.join("subagents");
std::fs::create_dir_all(&sub_dir).expect("mkdir");
let sub = sub_dir.join(format!("agent-{agent_id}.jsonl"));
let line = serde_json::json!({
"type": "assistant",
"message": { "usage": {
"input_tokens": 8,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": context_tokens - 8,
"output_tokens": 100
}}
});
std::fs::write(&sub, format!("{line}\n")).expect("write sub");
(parent, sub)
}
fn contained(path: &Path) -> AgentTranscript {
AgentTranscript::Contained(path.canonicalize().expect("canonicalize fixture"))
}
#[test]
fn derives_the_subagent_path_from_agent_id() {
let tmp = crate::test_support::hermetic_temp_dir();
let (parent, sub) = transcript_tree(tmp.path(), "a1d57cf5a7f59b877", 100_000);
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_id": "a1d57cf5a7f59b877",
});
assert_eq!(
resolve_agent_transcript_in(&payload, Some(tmp.path())),
contained(&sub)
);
}
#[test]
fn resolves_explicit_agent_transcript_path() {
let tmp = crate::test_support::hermetic_temp_dir();
let (parent, sub) = transcript_tree(tmp.path(), "abc123", 100_000);
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_transcript_path": sub.to_str().expect("utf8"),
"agent_id": "abc123",
});
assert_eq!(
resolve_agent_transcript_in(&payload, Some(tmp.path())),
contained(&sub)
);
}
#[test]
fn resolves_a_transcript_path_already_under_subagents() {
let tmp = crate::test_support::hermetic_temp_dir();
let (_, sub) = transcript_tree(tmp.path(), "abc123", 100_000);
let payload = serde_json::json!({
"transcript_path": sub.to_str().expect("utf8"),
});
assert_eq!(
resolve_agent_transcript_in(&payload, Some(tmp.path())),
contained(&sub)
);
}
#[test]
fn fails_open_without_any_transcript_field() {
for payload in [
serde_json::json!({}),
serde_json::json!({"tool_name": "Bash"}),
serde_json::json!({"transcript_path": ""}),
serde_json::json!({"transcript_path": "/tmp/nope/parent.jsonl"}),
serde_json::json!({
"transcript_path": "/tmp/nope/parent.jsonl",
"agent_id": "ghost",
}),
] {
let tmp = crate::test_support::hermetic_temp_dir();
assert_eq!(
resolve_agent_transcript_in(&payload, Some(tmp.path())),
AgentTranscript::Absent,
"expected fail-open for {payload}"
);
}
}
#[test]
fn refuses_a_transcript_outside_the_config_dir() {
let config = crate::test_support::hermetic_temp_dir();
let elsewhere = crate::test_support::hermetic_temp_dir();
let (_, sub) = transcript_tree(elsewhere.path(), "outside", 100_000);
let payload = serde_json::json!({
"transcript_path": sub.to_str().expect("utf8"),
});
assert_eq!(
resolve_agent_transcript_in(&payload, Some(config.path())),
AgentTranscript::Refused(sub),
"a transcript outside the config directory must be refused, not read"
);
}
#[test]
fn refuses_a_traversing_transcript_path() {
let config = crate::test_support::hermetic_temp_dir();
let elsewhere = crate::test_support::hermetic_temp_dir();
let (_, sub) = transcript_tree(elsewhere.path(), "traverse", 100_000);
let traversing = config
.path()
.join("..")
.join(elsewhere.path().file_name().expect("temp dir name"))
.join(sub.strip_prefix(elsewhere.path()).expect("under fixture"));
let payload = serde_json::json!({
"transcript_path": traversing.to_str().expect("utf8"),
});
assert_eq!(
resolve_agent_transcript_in(&payload, Some(config.path())),
AgentTranscript::Refused(traversing),
"a `..` component must be refused"
);
}
#[test]
fn refuses_every_transcript_without_a_config_dir() {
let tmp = crate::test_support::hermetic_temp_dir();
let (_, sub) = transcript_tree(tmp.path(), "nodir", 100_000);
let payload = serde_json::json!({
"transcript_path": sub.to_str().expect("utf8"),
});
assert_eq!(
resolve_agent_transcript_in(&payload, None),
AgentTranscript::Refused(sub),
"no boundary means no read, even for a path that would have passed"
);
}
#[tokio::test]
async fn refusal_is_surfaced_not_silently_allowed() {
let config = crate::test_support::hermetic_temp_dir();
let elsewhere = crate::test_support::hermetic_temp_dir();
let (parent, sub) = transcript_tree(elsewhere.path(), "loud", 622_200);
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_id": "loud",
});
let refused = evaluate_agent_cost_in(
&payload,
&opted_in_stop(),
Some(config.path()),
TEST_EVAL_BUDGET,
)
.await;
assert_eq!(
refused,
AgentCost {
status: BudgetStatus::Ok,
tokens: 0,
refused_transcript: Some(sub),
},
"an out-of-boundary transcript must allow, and say that it was refused"
);
let measured = evaluate_agent_cost_in(
&payload,
&opted_in_stop(),
Some(elsewhere.path()),
TEST_EVAL_BUDGET,
)
.await;
assert_eq!(measured.tokens, 622_200);
assert_eq!(measured.status, BudgetStatus::Exceeded);
assert_eq!(measured.refused_transcript, None);
}
#[tokio::test]
async fn fails_open_when_the_transcript_is_missing() {
let config = crate::test_support::hermetic_temp_dir();
let payload = serde_json::json!({
"transcript_path": "/tmp/definitely-not-here/p.jsonl",
"agent_id": "ghost",
});
assert_eq!(
evaluate_agent_cost_in(
&payload,
&AgentCostConfig::default(),
Some(config.path()),
TEST_EVAL_BUDGET
)
.await,
AgentCost::allow(),
"a missing transcript is Absent, not Refused — nothing to report"
);
}
fn opted_in_stop() -> AgentCostConfig {
AgentCostConfig {
enabled: true,
max_tokens: 400_000,
..Default::default()
}
}
#[tokio::test]
async fn reports_exceeded_for_an_over_ceiling_transcript() {
let tmp = crate::test_support::hermetic_temp_dir();
let (parent, _) = transcript_tree(tmp.path(), "big", 622_200);
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_id": "big",
});
let AgentCost { status, tokens, .. } = evaluate_agent_cost_in(
&payload,
&opted_in_stop(),
Some(tmp.path()),
TEST_EVAL_BUDGET,
)
.await;
assert_eq!(status, BudgetStatus::Exceeded);
assert_eq!(tokens, 622_200);
assert!(stop_reason(tokens, 400_000).contains("622200"));
}
#[tokio::test]
async fn default_config_only_warns_on_the_same_transcript() {
let tmp = crate::test_support::hermetic_temp_dir();
let (parent, _) = transcript_tree(tmp.path(), "big", 622_200);
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_id": "big",
});
let AgentCost { status, tokens, .. } = evaluate_agent_cost_in(
&payload,
&AgentCostConfig::default(),
Some(tmp.path()),
TEST_EVAL_BUDGET,
)
.await;
assert_eq!(status, BudgetStatus::Warning);
assert_eq!(tokens, 622_200);
}
#[tokio::test]
async fn respects_a_disabled_config() {
let tmp = crate::test_support::hermetic_temp_dir();
let (parent, _) = transcript_tree(tmp.path(), "big", 900_000);
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_id": "big",
});
let disabled = AgentCostConfig {
enabled: false,
..Default::default()
};
assert_eq!(
evaluate_agent_cost_in(&payload, &disabled, Some(tmp.path()), TEST_EVAL_BUDGET).await,
AgentCost::allow()
);
}
#[tokio::test]
async fn allows_a_healthy_agent() {
let tmp = crate::test_support::hermetic_temp_dir();
let (parent, _) = transcript_tree(tmp.path(), "small", 71_540);
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_id": "small",
});
let measured = evaluate_agent_cost_in(
&payload,
&AgentCostConfig::default(),
Some(tmp.path()),
TEST_EVAL_BUDGET,
)
.await;
assert_eq!(measured.refused_transcript, None);
assert_eq!(measured.status, BudgetStatus::Ok);
assert_eq!(measured.tokens, 71_540);
}
#[test]
fn an_unmeasured_read_never_denies() {
assert_eq!(verdict(None, &opted_in_stop()), AgentCost::allow());
assert_eq!(
verdict(Some(622_200), &opted_in_stop()),
AgentCost {
status: BudgetStatus::Exceeded,
tokens: 622_200,
refused_transcript: None,
}
);
}
#[test]
fn the_production_budget_stays_far_inside_the_pretooluse_hook_timeout() {
assert!(EVAL_TIMEOUT > std::time::Duration::ZERO);
assert!(EVAL_TIMEOUT <= std::time::Duration::from_millis(500));
}
fn filler_line(bytes: usize) -> String {
format!(
"{}\n",
serde_json::json!({"type": "user", "pad": "x".repeat(bytes)})
)
}
#[tokio::test]
async fn retries_with_a_larger_tail_when_64k_holds_no_usage_record() {
let tmp = crate::test_support::hermetic_temp_dir();
let (parent, sub) = transcript_tree(tmp.path(), "huge", 500_000);
let mut jsonl = std::fs::read_to_string(&sub).expect("read");
jsonl.push_str(&filler_line(100 * 1024));
std::fs::write(&sub, &jsonl).expect("write");
assert!(
jsonl.len() as u64 > MAX_TRANSCRIPT_TAIL,
"the fixture must actually exceed the first window"
);
let first = super::super::misc::read_transcript_tail(&sub, MAX_TRANSCRIPT_TAIL)
.await
.expect("tail read");
assert_eq!(
trusty_mpm::core::agent_cost::latest_context_tokens(&first),
None,
"fixture invalid: 64 KiB must NOT contain a usage record"
);
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_id": "huge",
});
let AgentCost { status, tokens, .. } = evaluate_agent_cost_in(
&payload,
&opted_in_stop(),
Some(tmp.path()),
TEST_EVAL_BUDGET,
)
.await;
assert_eq!(tokens, 500_000, "the larger window must find the record");
assert_eq!(status, BudgetStatus::Exceeded);
}
#[tokio::test]
async fn still_fails_open_when_even_the_larger_tail_has_no_record() {
let tmp = crate::test_support::hermetic_temp_dir();
let (parent, sub) = transcript_tree(tmp.path(), "norec", 500_000);
std::fs::write(&sub, filler_line(100 * 1024)).expect("write");
let payload = serde_json::json!({
"transcript_path": parent.to_str().expect("utf8"),
"agent_id": "norec",
});
assert_eq!(
evaluate_agent_cost_in(
&payload,
&opted_in_stop(),
Some(tmp.path()),
TEST_EVAL_BUDGET
)
.await,
AgentCost::allow()
);
}
#[test]
fn escape_hatch_permits_send_message_and_git_persistence() {
assert!(is_persistence_escape("SendMessage", None));
for command in [
"git add -A",
"git commit -m 'fix: the thing'",
"git push origin HEAD",
"git -C /repo/wt commit -m x",
"git add -A && git commit -m x && git push",
] {
assert!(
is_persistence_escape("Bash", Some(&serde_json::json!({"command": command}))),
"a stopped agent must still be able to run {command:?}"
);
}
}
#[test]
fn escape_hatch_denies_work_tools() {
for tool in ["Write", "Edit", "Read", "Grep", "WebFetch", "Task", "Agent"] {
assert!(
!is_persistence_escape(tool, Some(&serde_json::json!({}))),
"{tool} must stay denied past the ceiling"
);
}
assert!(!is_persistence_escape("Bash", None));
for command in [
"cargo test",
"git commit -m x && cargo test",
"git checkout main",
] {
assert!(
!is_persistence_escape("Bash", Some(&serde_json::json!({"command": command}))),
"{command:?} must stay denied past the ceiling"
);
}
}
#[test]
fn escape_hatch_denies_exec_capable_git() {
for command in [
"git -c diff.external='cargo test' diff",
"git -c protocol.ext.allow=always push ext::sh -c 'cargo test' HEAD",
"git push --receive-pack='cargo test' /tmp/repo HEAD",
"git diff <(cargo test)",
] {
assert!(
!is_persistence_escape("Bash", Some(&serde_json::json!({"command": command}))),
"{command:?} executes a program and must not count as persistence"
);
}
}
#[test]
fn warn_notice_is_claimed_per_sibling_not_per_parent_session() {
let tmp = crate::test_support::hermetic_temp_dir();
let session = format!("parent-{}", std::process::id());
let ids = [
format!("sib-a-{}", std::process::id()),
format!("sib-b-{}", std::process::id()),
];
let subs: Vec<PathBuf> = ids
.iter()
.map(|id| transcript_tree(tmp.path(), id, 300_000).1)
.collect();
let markers: Vec<PathBuf> = ids
.iter()
.map(|id| {
std::env::temp_dir()
.join("trusty-mpm-agent-cost")
.join(format!("agent-{id}"))
})
.collect();
for m in &markers {
let _ = std::fs::remove_file(m);
}
for sub in &subs {
let payload = serde_json::json!({
"transcript_path": sub.to_str().expect("utf8"),
"session_id": session,
});
assert!(
claim_warn_notice(&payload),
"each sibling must get its own notice, not share the parent's"
);
assert!(!claim_warn_notice(&payload), "…and only one each");
}
for m in &markers {
let _ = std::fs::remove_file(m);
}
}
#[test]
fn warn_notice_fails_open_when_no_key_can_be_derived() {
for payload in [
serde_json::json!({"agent_id": "!!!"}),
serde_json::json!({"agent_id": "", "session_id": ""}),
serde_json::json!({}),
] {
assert!(warn_notice_key(&payload).is_none(), "{payload}");
for _ in 0..3 {
assert!(
claim_warn_notice(&payload),
"an underivable key must keep informing the agent: {payload}"
);
}
}
}
#[test]
fn warn_notice_is_claimed_once_per_agent() {
let id = format!("test-{}", std::process::id());
let payload = serde_json::json!({"agent_id": id});
let marker = std::env::temp_dir().join("trusty-mpm-agent-cost").join(&id);
let _ = std::fs::remove_file(&marker);
assert!(claim_warn_notice(&payload), "first call must claim");
assert!(!claim_warn_notice(&payload), "second call must not");
assert!(!claim_warn_notice(&payload), "and neither must the third");
let other = serde_json::json!({"agent_id": format!("{id}-other")});
assert!(claim_warn_notice(&other));
let _ = std::fs::remove_file(&marker);
let _ = std::fs::remove_file(
std::env::temp_dir()
.join("trusty-mpm-agent-cost")
.join(format!("{id}-other")),
);
}
}