use car_inference::tasks::generate::Message;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
pub const CHECKPOINT_REGISTRY_KIND: &str = "assistant-checkpoint";
pub const ACTION_REGISTRY_KIND: &str = "assistant-action";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepositoryScope {
root: PathBuf,
}
impl RepositoryScope {
pub fn explicit(path: Option<&Path>) -> Result<Self, String> {
let path = path.ok_or("governed host execution requires an explicit --dir")?;
if !path.is_dir() {
return Err(format!(
"repository root '{}' is not a directory",
path.display()
));
}
let root = path
.canonicalize()
.map_err(|e| format!("cannot resolve repository root '{}': {e}", path.display()))?;
if root.parent().is_none() {
return Err("repository root cannot be the filesystem root".to_string());
}
if let Some(home) = home_dir().and_then(|p| p.canonicalize().ok()) {
if root == home {
return Err("repository root cannot be the user's home directory".to_string());
}
}
if !root.join(".git").exists() {
return Err(format!("'{}' is not a Git repository root", root.display()));
}
Ok(Self { root })
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn existing_path(&self, path: &Path) -> Result<PathBuf, String> {
let candidate = if path.is_absolute() {
path.to_path_buf()
} else {
self.root.join(path)
};
let resolved = candidate
.canonicalize()
.map_err(|e| format!("cannot resolve '{}': {e}", candidate.display()))?;
if !resolved.starts_with(&self.root) {
return Err(format!(
"path '{}' escapes repository scope",
path.display()
));
}
Ok(resolved)
}
pub fn write_path(&self, path: &Path) -> Result<PathBuf, String> {
let candidate = if path.is_absolute() {
path.to_path_buf()
} else {
self.root.join(path)
};
let mut ancestor = candidate.as_path();
while !ancestor.exists() {
ancestor = ancestor
.parent()
.ok_or_else(|| format!("path '{}' has no existing ancestor", path.display()))?;
}
let resolved_ancestor = ancestor
.canonicalize()
.map_err(|e| format!("cannot resolve '{}': {e}", ancestor.display()))?;
if !resolved_ancestor.starts_with(&self.root) {
return Err(format!(
"path '{}' escapes repository scope",
path.display()
));
}
let suffix = candidate
.strip_prefix(ancestor)
.map_err(|_| format!("cannot scope '{}'", candidate.display()))?;
Ok(resolved_ancestor.join(suffix))
}
}
fn home_dir() -> Option<PathBuf> {
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from)
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct CredentialCapability(pub String);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ActionScope {
pub tool: String,
pub parameters: Value,
pub repository_root: PathBuf,
pub target: String,
pub environment: String,
#[serde(default)]
pub credential_capabilities: Vec<CredentialCapability>,
}
impl ActionScope {
pub fn canonicalize(mut self) -> Self {
self.credential_capabilities.sort();
self.credential_capabilities.dedup();
self
}
pub fn action_id(&self, session_id: &str, call_id: &str) -> String {
let scope = self.clone().canonicalize();
let value = serde_json::to_value(&scope).expect("ActionScope serializes");
let mut h = Sha256::new();
h.update(b"car-supervised-action-v1\x1f");
h.update(session_id.as_bytes());
h.update(b"\x1f");
h.update(call_id.as_bytes());
h.update(b"\x1f");
h.update(car_sync::canonical_json(&value).as_bytes());
format!("action-{:x}", h.finalize())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ActionState {
Proposed,
Approved,
Denied,
Dispatched,
Completed,
Failed,
Indeterminate,
}
impl ActionState {
pub fn is_terminal(self) -> bool {
matches!(
self,
Self::Denied | Self::Completed | Self::Failed | Self::Indeterminate
)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SupervisedActionRecord {
pub id: String,
pub session_id: String,
pub call_id: String,
pub scope: ActionScope,
pub state: ActionState,
#[serde(default)]
pub receipt: Option<Value>,
}
impl SupervisedActionRecord {
pub fn propose(session_id: &str, call_id: &str, scope: ActionScope) -> Self {
let scope = scope.canonicalize();
Self {
id: scope.action_id(session_id, call_id),
session_id: session_id.to_string(),
call_id: call_id.to_string(),
scope,
state: ActionState::Proposed,
receipt: None,
}
}
pub fn transition(&mut self, next: ActionState, receipt: Option<Value>) -> Result<(), String> {
let valid = matches!(
(self.state, next),
(
ActionState::Proposed,
ActionState::Approved | ActionState::Denied
) | (ActionState::Approved, ActionState::Dispatched)
| (
ActionState::Dispatched,
ActionState::Completed | ActionState::Failed | ActionState::Indeterminate
)
);
if !valid {
return Err(format!(
"invalid supervised action transition {:?} -> {:?}",
self.state, next
));
}
self.state = next;
self.receipt = receipt;
Ok(())
}
pub fn resume_directive(&self) -> ResumeDirective {
match self.state {
ActionState::Proposed => ResumeDirective::AwaitApproval,
ActionState::Approved => ResumeDirective::Dispatch,
ActionState::Dispatched => ResumeDirective::MarkIndeterminate,
ActionState::Denied
| ActionState::Completed
| ActionState::Failed
| ActionState::Indeterminate => ResumeDirective::DoNotDispatch,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResumeDirective {
AwaitApproval,
Dispatch,
MarkIndeterminate,
DoNotDispatch,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ActionGrant {
pub action_id: String,
pub scope: ActionScope,
pub approved: bool,
}
impl ActionGrant {
pub fn authorizes(&self, action: &SupervisedActionRecord) -> bool {
self.approved
&& self.action_id == action.id
&& self.scope.clone().canonicalize() == action.scope
&& action.state == ActionState::Proposed
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompletionMatrix {
pub local_verification: Option<String>,
pub remote_main: Option<String>,
pub ci_cd: Option<String>,
pub deployment: Option<String>,
pub health: Option<String>,
pub production_browser_proof: Option<String>,
}
pub const CHAT_TOOL_RESULT_EXCERPT_BYTES: usize = 2 * 1024;
const CHAT_EVIDENCE_STRING_BYTES: usize = 512;
const CHAT_EVIDENCE_ITEMS: usize = 20;
const CHAT_TOOL_RECEIPTS: usize = 100;
const CHAT_DESKTOP_ACTIONS: usize = 100;
const CHAT_TOOL_RECEIPT_REPORT_BYTES: usize = 64 * 1024;
pub const CHAT_RECEIPT_REPORT_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct BoundedToolResult {
pub tool: String,
pub ok: bool,
pub excerpt: String,
pub evidence: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct DesktopActionEvidence {
pub action: String,
pub target: Option<String>,
pub identifier: Option<String>,
pub verified: bool,
pub evidence: Value,
}
pub fn bounded_tool_result(
tool: &str,
ok: bool,
content: &str,
params: Option<&Value>,
) -> BoundedToolResult {
let parsed = serde_json::from_str::<Value>(content).ok().map(|value| {
car_feedback_core::redact::redact_json(car_feedback_core::redact::strip_env_maps(value))
});
let redacted_excerpt = parsed
.as_ref()
.map(Value::to_string)
.unwrap_or_else(|| car_feedback_core::redact::redact_text(content));
let excerpt = bound_string(&redacted_excerpt, CHAT_TOOL_RESULT_EXCERPT_BYTES);
BoundedToolResult {
tool: bound_string(tool, CHAT_EVIDENCE_STRING_BYTES),
ok,
excerpt,
evidence: extract_tool_evidence(parsed.as_ref(), params),
}
}
pub fn tool_receipts_for_wire(
receipts: &[super::agent_loop::AssistantToolReceipt],
) -> (Vec<Value>, usize) {
let mut rows = Vec::new();
let mut serialized_bytes = 2usize;
for receipt in receipts.iter().take(CHAT_TOOL_RECEIPTS) {
let result = bounded_tool_result(
&receipt.tool,
receipt.ok,
receipt.result.as_deref().unwrap_or_default(),
Some(&receipt.params),
);
let row = serde_json::json!({
"tool": result.tool,
"call_id": receipt.call_id.as_deref().map(|v| bound_string(v, CHAT_EVIDENCE_STRING_BYTES)),
"sequence": receipt.sequence,
"ok": receipt.ok,
"via": receipt.via.as_deref().map(|v| bound_string(v, CHAT_EVIDENCE_STRING_BYTES)),
"excerpt": result.excerpt,
"evidence": result.evidence,
});
let row_bytes = serde_json::to_vec(&row).map_or(0, |encoded| encoded.len());
let delimiter = usize::from(!rows.is_empty());
if serialized_bytes
.saturating_add(delimiter)
.saturating_add(row_bytes)
> CHAT_TOOL_RECEIPT_REPORT_BYTES
{
break;
}
serialized_bytes += delimiter + row_bytes;
rows.push(row);
}
let omitted = receipts.len().saturating_sub(rows.len());
(rows, omitted)
}
pub fn completion_matrix_for_wire(matrix: &CompletionMatrix) -> Value {
let mut value = serde_json::to_value(matrix).unwrap_or(Value::Null);
if let Some(object) = value.as_object_mut() {
for item in object.values_mut() {
if let Some(text) = item.as_str() {
*item = Value::String(bound_string(text, CHAT_TOOL_RESULT_EXCERPT_BYTES));
}
}
}
value
}
pub fn bound_receipt_report(mut frame: Value) -> Value {
let encoded_len = |value: &Value| serde_json::to_vec(value).map_or(usize::MAX, |v| v.len());
while encoded_len(&frame) > CHAT_RECEIPT_REPORT_BYTES {
let Some(object) = frame.as_object_mut() else {
break;
};
let removed_tool = object
.get_mut("tool_receipts")
.and_then(Value::as_array_mut)
.is_some_and(|rows| rows.pop().is_some());
if removed_tool {
let omitted = object
.get("tool_receipts_omitted")
.and_then(Value::as_u64)
.unwrap_or(0)
.saturating_add(1);
object.insert("tool_receipts_omitted".into(), Value::from(omitted));
continue;
}
let removed_action = object
.get_mut("desktop_actions")
.and_then(Value::as_array_mut)
.is_some_and(|rows| rows.pop().is_some());
if removed_action {
let omitted = object
.get("desktop_actions_omitted")
.and_then(Value::as_u64)
.unwrap_or(0)
.saturating_add(1);
object.insert("desktop_actions_omitted".into(), Value::from(omitted));
continue;
}
let removed_claim = object
.get_mut("ungrounded_claims")
.and_then(Value::as_array_mut)
.is_some_and(|rows| rows.pop().is_some());
if removed_claim {
object.insert("ungrounded_claims_omitted".into(), Value::Bool(true));
continue;
}
let kind = object.get("kind").cloned().unwrap_or(Value::Null);
let session_id = object.get("session_id").cloned().unwrap_or(Value::Null);
let tool_receipts_omitted = object
.get("tool_receipts_omitted")
.and_then(Value::as_u64)
.unwrap_or(0);
frame = serde_json::json!({
"kind": kind,
"session_id": session_id,
"receipt_report_truncated": true,
"tool_receipts_omitted": tool_receipts_omitted,
});
break;
}
frame
}
pub fn desktop_actions_from_tool_receipts(
receipts: &[super::agent_loop::AssistantToolReceipt],
mutating: &std::collections::HashSet<String>,
) -> (Vec<DesktopActionEvidence>, usize) {
let projected: Vec<BoundedToolResult> = receipts
.iter()
.map(|receipt| {
bounded_tool_result(
&receipt.tool,
receipt.ok,
receipt.result.as_deref().unwrap_or_default(),
Some(&receipt.params),
)
})
.collect();
let mut actions: Vec<DesktopActionEvidence> = Vec::new();
let mut omitted = 0usize;
for (index, receipt) in receipts.iter().enumerate() {
if !is_desktop_action_tool(&receipt.tool) {
continue;
}
if actions.len() == CHAT_DESKTOP_ACTIONS {
omitted += 1;
continue;
}
let result = &projected[index];
let identifier = preferred_identifier(&result.evidence);
let verified = receipt.ok
&& (!is_desktop_mutation(&receipt.tool, mutating)
|| identifier.as_deref().is_some_and(|identifier| {
receipts[index + 1..]
.iter()
.zip(&projected[index + 1..])
.any(|(later, later_result)| {
later.ok
&& !is_desktop_mutation(&later.tool, mutating)
&& evidence_has_identifier(&later_result.evidence, identifier)
})
}));
actions.push(DesktopActionEvidence {
action: bound_string(&receipt.tool, CHAT_EVIDENCE_STRING_BYTES),
target: action_target(&receipt.params),
identifier,
verified,
evidence: result.evidence.clone(),
});
}
(actions, omitted)
}
fn is_desktop_action_tool(tool: &str) -> bool {
tool.starts_with("calendar_")
|| tool.starts_with("mail_")
|| tool.starts_with("messages_")
|| tool.starts_with("browse_")
|| tool.starts_with("browser_")
|| tool.starts_with("automation_")
|| matches!(tool, "http_request" | "web_search" | "m365_task")
}
fn is_desktop_mutation(tool: &str, mutating: &std::collections::HashSet<String>) -> bool {
mutating.contains(tool) || is_desktop_mutation_by_name(tool)
}
fn is_desktop_mutation_by_name(tool: &str) -> bool {
tool.contains("create")
|| tool.contains("update")
|| tool.contains("delete")
|| tool.contains("send")
|| matches!(
tool,
"browse_click"
| "browse_type"
| "browse_keypress"
| "browse_paste"
| "browse_navigate"
| "browse_scroll"
| "mail_draft"
| "browser_await_signin"
| "browser_record_start"
| "browser_record_stop"
| "automation_run_applescript"
| "automation_run_powershell"
| "automation_shortcuts_run"
| "m365_task"
)
}
fn action_target(params: &Value) -> Option<String> {
[
"title",
"url",
"to",
"recipient",
"event_id",
"message_id",
"query",
"task",
"path",
]
.into_iter()
.find_map(|key| params.get(key).and_then(Value::as_str))
.map(car_feedback_core::redact::redact_text)
.map(|value| bound_string(&value, CHAT_EVIDENCE_STRING_BYTES))
}
fn preferred_identifier(evidence: &Value) -> Option<String> {
let object = evidence.as_object()?;
["event_id", "message_id", "id", "final_url", "requested_url"]
.into_iter()
.find_map(|key| object.get(key).and_then(Value::as_str))
.map(str::to_string)
.or_else(|| {
object
.get("event_ids")
.and_then(Value::as_array)
.and_then(|ids| ids.first())
.and_then(Value::as_str)
.map(str::to_string)
})
.or_else(|| {
object
.get("message_ids")
.and_then(Value::as_array)
.and_then(|ids| ids.first())
.and_then(Value::as_str)
.map(str::to_string)
})
}
fn evidence_has_identifier(evidence: &Value, identifier: &str) -> bool {
let Some(object) = evidence.as_object() else {
return false;
};
["event_id", "message_id", "id", "final_url", "requested_url"]
.into_iter()
.any(|key| object.get(key).and_then(Value::as_str) == Some(identifier))
|| ["event_ids", "message_ids", "result_ids"]
.into_iter()
.any(|key| {
object
.get(key)
.and_then(Value::as_array)
.is_some_and(|values| {
values
.iter()
.any(|value| value.as_str() == Some(identifier))
})
})
}
fn extract_tool_evidence(parsed: Option<&Value>, params: Option<&Value>) -> Value {
let mut evidence = serde_json::Map::new();
if let Some(requested) = params
.and_then(|value| value.get("url"))
.and_then(Value::as_str)
{
evidence.insert(
"requested_url".into(),
Value::String(bound_string(
&car_feedback_core::redact::redact_url(requested),
CHAT_EVIDENCE_STRING_BYTES,
)),
);
}
let Some(object) = parsed.and_then(Value::as_object) else {
return Value::Object(evidence);
};
for key in [
"requested_url",
"final_url",
"url",
"status",
"title",
"event_id",
"message_id",
"id",
"count",
"total",
] {
if let Some(value) = object.get(key).and_then(bounded_scalar) {
evidence.insert(key.into(), value);
}
}
let redirected = evidence
.get("requested_url")
.and_then(Value::as_str)
.zip(evidence.get("final_url").and_then(Value::as_str))
.map(|(requested, final_url)| requested != final_url);
if let Some(redirected) = redirected {
evidence.insert("redirected".into(), Value::Bool(redirected));
}
if let Some(event) = object.get("event").and_then(Value::as_object) {
copy_nested_evidence(event, "id", "event_id", &mut evidence);
copy_nested_evidence(event, "title", "title", &mut evidence);
copy_nested_evidence(event, "url", "url", &mut evidence);
}
for (array_key, count_key, ids_key, id_field) in [
("events", "event_count", "event_ids", "id"),
("messages", "message_count", "message_ids", "message_id"),
("results", "result_count", "result_ids", "id"),
] {
let Some(items) = object.get(array_key).and_then(Value::as_array) else {
continue;
};
evidence.insert(count_key.into(), serde_json::json!(items.len()));
let ids: Vec<Value> = items
.iter()
.take(CHAT_EVIDENCE_ITEMS)
.filter_map(|item| {
item.get(id_field)
.or_else(|| item.get("id"))
.and_then(Value::as_str)
})
.map(|id| Value::String(bound_string(id, CHAT_EVIDENCE_STRING_BYTES)))
.collect();
if !ids.is_empty() {
evidence.insert(ids_key.into(), Value::Array(ids));
}
}
Value::Object(evidence)
}
fn copy_nested_evidence(
source: &serde_json::Map<String, Value>,
source_key: &str,
destination_key: &str,
destination: &mut serde_json::Map<String, Value>,
) {
if destination.contains_key(destination_key) {
return;
}
if let Some(value) = source.get(source_key).and_then(bounded_scalar) {
destination.insert(destination_key.into(), value);
}
}
fn bounded_scalar(value: &Value) -> Option<Value> {
match value {
Value::String(value) => Some(Value::String(bound_string(
value,
CHAT_EVIDENCE_STRING_BYTES,
))),
Value::Number(_) | Value::Bool(_) | Value::Null => Some(value.clone()),
Value::Array(_) | Value::Object(_) => None,
}
}
fn bound_string(value: &str, cap: usize) -> String {
if value.len() <= cap {
return value.to_string();
}
let mut end = cap;
while !value.is_char_boundary(end) {
end -= 1;
}
format!("{}…[truncated]…", &value[..end])
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AssistantCheckpoint {
pub id: String,
pub session_id: String,
pub revision: u64,
pub repository_root: PathBuf,
pub messages: Vec<Message>,
#[serde(default)]
pub goal: Option<Value>,
#[serde(default)]
pub compaction: Option<Value>,
#[serde(default)]
pub completion: CompletionMatrix,
}
pub fn completion_matrix_from_messages(messages: &[Message]) -> CompletionMatrix {
let mut calls: std::collections::HashMap<String, (String, Value)> =
std::collections::HashMap::new();
let mut matrix = CompletionMatrix::default();
for message in messages {
match message {
Message::Assistant { tool_calls, .. } => {
for call in tool_calls {
if let Some(id) = &call.id {
calls.insert(
id.clone(),
(
call.name.clone(),
serde_json::to_value(&call.arguments).unwrap_or(Value::Null),
),
);
}
}
}
Message::ToolResult {
tool_use_id,
content,
..
} => {
let Some((tool, params)) = calls.get(tool_use_id) else {
continue;
};
let parsed = serde_json::from_str::<Value>(content).ok();
let failed = content.starts_with("[FAILED]")
|| content.starts_with("[REJECTED]")
|| parsed
.as_ref()
.and_then(|value| value.get("error"))
.is_some();
let shell_ok = tool != "shell"
|| parsed
.as_ref()
.and_then(|value| value.get("exit_code"))
.and_then(Value::as_i64)
== Some(0)
|| parsed
.as_ref()
.and_then(|value| value.get("ok"))
.and_then(Value::as_bool)
== Some(true);
if failed || !shell_ok {
continue;
}
let command = params
.get("command")
.and_then(Value::as_str)
.unwrap_or_default()
.to_ascii_lowercase();
let command_tokens: Vec<&str> = command
.split_whitespace()
.map(|token| {
token.trim_matches(|ch: char| {
!ch.is_ascii_alphanumeric() && ch != '-' && ch != '/' && ch != '.'
})
})
.filter(|token| !token.is_empty())
.collect();
let evidence = format!("{tool} receipt {tool_use_id}: {content}");
if tool.starts_with("browser_") {
matrix.production_browser_proof = Some(evidence.clone());
}
if command.contains("git push") {
matrix.remote_main = Some(evidence.clone());
}
let is_ci = command_tokens
.windows(2)
.any(|pair| matches!(pair, ["az", "pipelines"] | ["gh", "run"]))
|| command_tokens.contains(&"pipeline");
if is_ci {
matrix.ci_cd = Some(evidence.clone());
}
let is_deployment = command_tokens
.iter()
.any(|token| matches!(*token, "deploy" | "deployment"))
|| command.contains("/deploy.")
|| command.contains("/deploy/");
if is_deployment {
matrix.deployment = Some(evidence.clone());
}
if command.contains("health") || command.contains("ready") {
matrix.health = Some(evidence.clone());
}
if command.contains("test")
|| command.contains("cargo check")
|| command.contains("dotnet build")
|| command.contains("dotnet run")
|| command.contains("node --test")
|| command.contains("npm test")
|| command.contains("pnpm test")
|| command.contains("yarn test")
{
matrix.local_verification = Some(evidence);
}
}
_ => {}
}
}
matrix
}
#[async_trait::async_trait]
pub trait AssistantDurability: Send + Sync {
async fn load_checkpoint(
&self,
session_id: &str,
) -> Result<Option<AssistantCheckpoint>, String>;
async fn checkpoint(
&self,
session_id: &str,
messages: &[Message],
reason: &str,
goal: Option<Value>,
) -> Result<(), String>;
async fn load_action(&self, action_id: &str) -> Result<Option<SupervisedActionRecord>, String>;
async fn record_action(&self, record: &SupervisedActionRecord) -> Result<(), String>;
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::fs;
fn fixture_repo() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
fs::create_dir(dir.path().join(".git")).unwrap();
dir
}
fn scope(root: &Path) -> ActionScope {
ActionScope {
tool: "shell".into(),
parameters: json!({"command": "git push origin HEAD:main"}),
repository_root: root.to_path_buf(),
target: "origin/main".into(),
environment: "disposable".into(),
credential_capabilities: vec![CredentialCapability("git:origin".into())],
}
}
#[test]
fn explicit_scope_rejects_missing_root_and_home() {
assert!(RepositoryScope::explicit(None).is_err());
assert!(RepositoryScope::explicit(Some(Path::new("/"))).is_err());
if let Some(home) = home_dir() {
assert!(RepositoryScope::explicit(Some(&home)).is_err());
}
}
#[cfg(unix)]
#[test]
fn scope_rejects_symlink_escape_for_reads_and_writes() {
use std::os::unix::fs::symlink;
let repo = fixture_repo();
let outside = tempfile::tempdir().unwrap();
fs::write(outside.path().join("secret"), "nope").unwrap();
symlink(outside.path(), repo.path().join("escape")).unwrap();
let scope = RepositoryScope::explicit(Some(repo.path())).unwrap();
assert!(scope.existing_path(Path::new("escape/secret")).is_err());
assert!(scope.write_path(Path::new("escape/new")).is_err());
}
#[test]
fn grant_is_exact_and_parameter_bound() {
let repo = fixture_repo();
let mut action = SupervisedActionRecord::propose("s", "c", scope(repo.path()));
let grant = ActionGrant {
action_id: action.id.clone(),
scope: action.scope.clone(),
approved: true,
};
assert!(grant.authorizes(&action));
action.scope.target = "other/main".into();
assert!(!grant.authorizes(&action));
}
#[test]
fn dispatched_resume_is_indeterminate_not_replayable() {
let repo = fixture_repo();
let mut action = SupervisedActionRecord::propose("s", "c", scope(repo.path()));
action.transition(ActionState::Approved, None).unwrap();
action.transition(ActionState::Dispatched, None).unwrap();
assert_eq!(
action.resume_directive(),
ResumeDirective::MarkIndeterminate
);
action
.transition(
ActionState::Indeterminate,
Some(json!({"reason": "process restart"})),
)
.unwrap();
assert_eq!(action.resume_directive(), ResumeDirective::DoNotDispatch);
assert!(action.transition(ActionState::Completed, None).is_err());
}
fn no_defs() -> std::collections::HashSet<String> {
std::collections::HashSet::new()
}
fn receipt(
tool: &str,
ok: bool,
params: Value,
result: Value,
) -> super::super::agent_loop::AssistantToolReceipt {
super::super::agent_loop::AssistantToolReceipt {
tool: tool.into(),
call_id: Some(format!("call-{tool}")),
sequence: Some(1),
ok,
params,
result: Some(result.to_string()),
via: None,
}
}
#[test]
fn bounded_tool_results_are_redacted_and_keep_closed_world_evidence() {
let secret = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJK";
let content = json!({
"requested_url": format!("https://example.test/start?token={secret}"),
"final_url": format!("https://example.test/final?token={secret}"),
"status": 200,
"title": "Done",
"body": format!(
"{secret} {}",
"safe words ".repeat(CHAT_TOOL_RESULT_EXCERPT_BYTES)
),
})
.to_string();
let projected = bounded_tool_result("http_request", true, &content, None);
assert!(projected.excerpt.contains("[REDACTED]"));
assert!(!projected.excerpt.contains(secret));
assert!(projected.excerpt.contains("…[truncated]…"));
assert_eq!(projected.evidence["status"], 200);
assert_eq!(projected.evidence["title"], "Done");
assert_eq!(
projected.evidence["final_url"],
"https://example.test/final?token=[REDACTED]"
);
assert_eq!(projected.evidence["redirected"], true);
assert!(projected.evidence.get("body").is_none());
}
#[test]
fn large_http_receipt_keeps_evidence_and_scrubs_userinfo_and_auth_headers() {
let content = json!({
"requested_url": "https://user:pass@example.test/start",
"final_url": "https://user:pass@example.test/final",
"status": 200,
"request_headers": {
"Authorization": "short-secret",
"Content-Type": "application/json"
},
"body": "x".repeat(64 * 1024),
})
.to_string();
let projected = bounded_tool_result("http_request", true, &content, None);
assert_eq!(projected.evidence["status"], 200);
assert_eq!(
projected.evidence["requested_url"],
"https://example.test/start"
);
assert_eq!(
projected.evidence["final_url"],
"https://example.test/final"
);
assert_eq!(projected.evidence["redirected"], true);
assert!(!projected.excerpt.contains("Authorization"));
assert!(!projected.excerpt.contains("short-secret"));
assert!(projected.excerpt.contains("Content-Type"));
}
#[test]
fn per_tool_receipts_are_bounded_and_report_omissions() {
let receipts: Vec<_> = (0..105)
.map(|index| {
receipt(
"calendar_events",
true,
json!({"start": index}),
json!({"events": [{"id": format!("event-{index}")}]}),
)
})
.collect();
let (wire, omitted) = tool_receipts_for_wire(&receipts);
assert_eq!(wire.len(), CHAT_TOOL_RECEIPTS);
assert_eq!(omitted, 5);
assert_eq!(wire[0]["evidence"]["event_count"], 1);
assert_eq!(wire[0]["evidence"]["event_ids"][0], "event-0");
assert_eq!(wire[0]["sequence"], 1);
}
#[test]
fn complete_receipt_report_has_a_hard_serialized_size_ceiling() {
let rows: Vec<Value> = (0..100)
.map(|index| json!({"tool": "http_request", "excerpt": "x".repeat(2048), "index": index}))
.collect();
let report = bound_receipt_report(json!({
"kind": "receipt_report",
"session_id": "s1",
"completion": completion_matrix_for_wire(&CompletionMatrix::default()),
"desktop_actions": [],
"tool_receipts": rows,
"tool_receipts_omitted": 0,
"ungrounded_claims": [],
}));
assert!(serde_json::to_vec(&report).unwrap().len() <= CHAT_RECEIPT_REPORT_BYTES);
assert!(report["tool_receipts_omitted"].as_u64().unwrap() > 0);
}
#[test]
fn desktop_actions_record_calendar_counts_and_verify_reads() {
let receipts = [receipt(
"calendar_events",
true,
json!({"start": "2026-09-17T00:00:00Z", "end": "2026-09-18T00:00:00Z"}),
json!({"events": [{"id": "event-7", "title": "Review"}]}),
)];
let (actions, _) = desktop_actions_from_tool_receipts(&receipts, &no_defs());
assert_eq!(actions.len(), 1);
assert_eq!(actions[0].action, "calendar_events");
assert_eq!(actions[0].identifier.as_deref(), Some("event-7"));
assert_eq!(actions[0].evidence["event_count"], 1);
assert!(actions[0].verified);
}
#[test]
fn desktop_writes_are_attempted_until_a_later_read_confirms_the_identifier() {
let mut receipts = vec![receipt(
"calendar_create_event",
true,
json!({"title": "Review"}),
json!({"ok": true, "event": {"id": "event-7", "title": "Review"}}),
)];
assert!(!desktop_actions_from_tool_receipts(&receipts, &no_defs()).0[0].verified);
receipts.push(receipt(
"calendar_events",
true,
json!({}),
json!({"events": [{"id": "event-70", "title": "Wrong event"}]}),
));
assert!(
!desktop_actions_from_tool_receipts(&receipts, &no_defs()).0[0].verified,
"substring matches are not verification"
);
receipts.push(receipt(
"calendar_events",
true,
json!({}),
json!({"events": [{"id": "event-7", "title": "Review"}]}),
));
let (actions, _) = desktop_actions_from_tool_receipts(&receipts, &no_defs());
assert!(actions[0].verified);
assert!(actions[1].verified);
}
#[test]
fn live_mutating_tools_are_not_verified_without_a_confirming_read() {
let defs = vec![
json!({"name": "mail_draft", "mutating": true}),
json!({"name": "browser_record_start", "mutating": true}),
json!({"name": "browse_click", "mutating": true}),
json!({"name": "mail_inbox"}),
];
let mutating = super::super::agent_loop::mutating_tool_names(&defs);
for tool in ["mail_draft", "browser_record_start", "browse_click"] {
let receipts = [receipt(
tool,
true,
json!({"to": "someone@example.test"}),
json!({"ok": true, "id": "msg-1"}),
)];
let (actions, _) = desktop_actions_from_tool_receipts(&receipts, &mutating);
assert_eq!(actions.len(), 1, "{tool} is a desktop action");
assert!(
!actions[0].verified,
"{tool} mutates; `ok` alone is not verification"
);
}
let receipts = [
receipt(
"mail_draft",
true,
json!({"to": "someone@example.test"}),
json!({"ok": true, "id": "msg-1"}),
),
receipt(
"mail_inbox",
true,
json!({}),
json!({"messages": [{"id": "msg-1", "subject": "Hi"}]}),
),
];
assert!(desktop_actions_from_tool_receipts(&receipts, &mutating).0[0].verified);
let reads = [receipt(
"mail_inbox",
true,
json!({}),
json!({"messages": [{"id": "msg-9"}]}),
)];
assert!(desktop_actions_from_tool_receipts(&reads, &mutating).0[0].verified);
}
#[test]
fn desktop_actions_are_capped_at_build_time_and_report_omissions() {
let receipts: Vec<_> = (0..CHAT_DESKTOP_ACTIONS + 7)
.map(|index| {
receipt(
"calendar_events",
true,
json!({"start": index}),
json!({"events": [{"id": format!("event-{index}")}]}),
)
})
.collect();
let (actions, omitted) = desktop_actions_from_tool_receipts(&receipts, &no_defs());
assert_eq!(actions.len(), CHAT_DESKTOP_ACTIONS);
assert_eq!(omitted, 7);
}
#[test]
fn telemetry_query_is_not_mislabeled_as_deployment_or_local_verification() {
let call_id = "telemetry-1";
let messages = vec![
Message::Assistant {
content: String::new(),
tool_calls: vec![serde_json::from_value(json!({
"id": call_id,
"name": "shell",
"arguments": {
"command": "az monitor app-insights query --app ai-fms --analytics-query \"traces | project customDimensions_DeploymentId\""
}
}))
.unwrap()],
thinking: vec![],
model_id: None,
local_last_resort: false,
},
Message::ToolResult {
tool_use_id: call_id.into(),
content: json!({"exit_code": 0, "output": "{\"tables\":[]}"}).to_string(),
provenance: Default::default(),
},
];
let matrix = completion_matrix_from_messages(&messages);
assert!(matrix.deployment.is_none());
assert!(matrix.ci_cd.is_none());
assert!(matrix.local_verification.is_none());
}
}