pub mod approval_summary;
mod canonical;
pub use canonical::{canonical_json, canonical_sha256};
pub const PROTOCOL_VERSION: u32 = 3;
pub const MODELS_CATALOG_IDENTITY_CAPABILITY: &str = "models.catalog-identity.v1";
pub const INFER_MODEL_IDENTITY_CAPABILITY: &str = "infer.model-identity.v1";
pub const INFER_CANCEL_CAPABILITY: &str = "infer.cancel.v1";
pub const INFER_DEADLINE_CAPABILITY: &str = "infer.deadline.v1";
pub const RUNS_PAGINATION_CAPABILITY: &str = "runs.pagination.v1";
pub const RUNS_RESUME_CAPABILITY: &str = "runs.resume.v1";
pub const RUNS_CANCEL_CAPABILITY: &str = "runs.cancel.v1";
pub const TOOLS_CALLBACK_STATE_CAPABILITY: &str = "tools.callback-state.v1";
pub const AGENT_TOOL_OVERRIDES_CAPABILITY: &str = "permissions.agent-tool-overrides.v1";
pub const FEEDBACK_CAPABILITY: &str = "feedback.v1";
pub const SUPPORTED_CAPABILITIES: &[&str] = &[
AGENT_TOOL_OVERRIDES_CAPABILITY,
FEEDBACK_CAPABILITY,
INFER_CANCEL_CAPABILITY,
INFER_DEADLINE_CAPABILITY,
INFER_MODEL_IDENTITY_CAPABILITY,
MODELS_CATALOG_IDENTITY_CAPABILITY,
RUNS_CANCEL_CAPABILITY,
RUNS_PAGINATION_CAPABILITY,
RUNS_RESUME_CAPABILITY,
TOOLS_CALLBACK_STATE_CAPABILITY,
];
pub const REQUIRED_CLIENT_CAPABILITIES: &[&str] = &[
INFER_MODEL_IDENTITY_CAPABILITY,
MODELS_CATALOG_IDENTITY_CAPABILITY,
];
pub const PROTOCOL_HANDSHAKE_REQUIRED_ERROR_CODE: i32 = -32005;
pub const PROTOCOL_VERSION_MISMATCH_ERROR_CODE: i32 = -32006;
pub const PROTOCOL_CAPABILITY_MISMATCH_ERROR_CODE: i32 = -32008;
pub const CATALOG_PRECONDITION_MISMATCH_ERROR_CODE: i32 = -32009;
pub const RUN_OWNERSHIP_CONFLICT_ERROR_CODE: i32 = -32010;
pub const RUN_TRACE_CORRUPTION_ERROR_CODE: i32 = -32011;
pub const PROTOCOL_HANDSHAKE_REQUIRED_MESSAGE_PREFIX: &str = "protocol handshake required:";
pub const PROTOCOL_VERSION_MISMATCH_MESSAGE_PREFIX: &str = "protocol version mismatch:";
pub const PROTOCOL_CAPABILITY_MISMATCH_MESSAGE_PREFIX: &str = "protocol capability mismatch:";
pub const CATALOG_PRECONDITION_MISMATCH_MESSAGE_PREFIX: &str = "catalog precondition mismatch:";
pub const RUN_OWNERSHIP_CONFLICT_MESSAGE_PREFIX: &str = "run ownership conflict:";
pub const RUN_TRACE_CORRUPTION_MESSAGE_PREFIX: &str = "run trace corruption:";
pub fn negotiate_capabilities(
required: &[String],
optional: &[String],
) -> Result<Vec<String>, Vec<String>> {
use std::collections::BTreeSet;
let supported: BTreeSet<&str> = SUPPORTED_CAPABILITIES.iter().copied().collect();
let missing: Vec<String> = required
.iter()
.filter(|capability| !supported.contains(capability.as_str()))
.cloned()
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
if !missing.is_empty() {
return Err(missing);
}
Ok(required
.iter()
.chain(optional)
.filter(|capability| supported.contains(capability.as_str()))
.cloned()
.collect::<BTreeSet<_>>()
.into_iter()
.collect())
}
pub const CONTENT_REFUSED_ERROR_CODE: i32 = -32007;
pub const CONTENT_REFUSED_MESSAGE_PREFIX: &str = "content refused:";
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InferenceControlStatus {
AlreadyTerminal,
CancelledConfirmed,
TerminationUnconfirmed,
DeadlineExceededConfirmed,
DeadlineExceededUnconfirmed,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct InferenceControlResponse {
pub inference_id: String,
pub status: InferenceControlStatus,
}
pub mod daemon;
pub use daemon::{method_accepts_host_authority, HOST_MANAGEMENT_METHODS};
pub fn deterministic_run_id(agent_id: &str, intent: &str, salt: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(agent_id.as_bytes());
hasher.update(b"\x1f");
hasher.update(intent.as_bytes());
hasher.update(b"\x1f");
hasher.update(salt.as_bytes());
let digest = hasher.finalize();
let hex: String = digest.iter().take(16).map(|b| format!("{b:02x}")).collect();
format!("run-{hex}")
}
#[cfg(test)]
mod run_id_tests {
use super::deterministic_run_id;
#[test]
fn same_inputs_same_id() {
let a = deterministic_run_id("agent-1", "summarize inbox", "2026-06-30");
let b = deterministic_run_id("agent-1", "summarize inbox", "2026-06-30");
assert_eq!(a, b, "deterministic: identical inputs → identical id");
assert!(a.starts_with("run-"));
assert_eq!(a.len(), 4 + 32);
}
#[test]
fn distinct_inputs_distinct_ids() {
let base = deterministic_run_id("agent-1", "intent", "s");
assert_ne!(base, deterministic_run_id("agent-2", "intent", "s"));
assert_ne!(base, deterministic_run_id("agent-1", "other", "s"));
assert_ne!(base, deterministic_run_id("agent-1", "intent", "s2"));
}
#[test]
fn no_field_separator_collision() {
assert_ne!(
deterministic_run_id("ab", "c", ""),
deterministic_run_id("a", "bc", "")
);
}
}
#[cfg(test)]
mod capability_negotiation_tests {
use super::*;
#[test]
fn protocol_v3_advertises_catalog_and_inference_identity() {
assert_eq!(PROTOCOL_VERSION, 3);
assert!(SUPPORTED_CAPABILITIES.contains(&MODELS_CATALOG_IDENTITY_CAPABILITY));
assert!(SUPPORTED_CAPABILITIES.contains(&INFER_MODEL_IDENTITY_CAPABILITY));
assert!(SUPPORTED_CAPABILITIES.contains(&INFER_CANCEL_CAPABILITY));
assert!(SUPPORTED_CAPABILITIES.contains(&INFER_DEADLINE_CAPABILITY));
assert!(!REQUIRED_CLIENT_CAPABILITIES.contains(&INFER_CANCEL_CAPABILITY));
assert!(!REQUIRED_CLIENT_CAPABILITIES.contains(&INFER_DEADLINE_CAPABILITY));
}
#[test]
fn callback_state_is_an_optional_v3_capability() {
assert!(SUPPORTED_CAPABILITIES.contains(&TOOLS_CALLBACK_STATE_CAPABILITY));
assert!(!REQUIRED_CLIENT_CAPABILITIES.contains(&TOOLS_CALLBACK_STATE_CAPABILITY));
}
#[test]
fn feedback_is_an_optional_v3_capability() {
assert!(SUPPORTED_CAPABILITIES.contains(&FEEDBACK_CAPABILITY));
assert!(!REQUIRED_CLIENT_CAPABILITIES.contains(&FEEDBACK_CAPABILITY));
}
#[test]
fn exact_agent_tool_overrides_are_an_optional_v3_capability() {
assert!(SUPPORTED_CAPABILITIES.contains(&AGENT_TOOL_OVERRIDES_CAPABILITY));
assert!(!REQUIRED_CLIENT_CAPABILITIES.contains(&AGENT_TOOL_OVERRIDES_CAPABILITY));
}
#[test]
fn run_resume_is_optional_strict_and_carries_no_caller_owner_identity() {
assert!(SUPPORTED_CAPABILITIES.contains(&RUNS_RESUME_CAPABILITY));
assert!(!REQUIRED_CLIENT_CAPABILITIES.contains(&RUNS_RESUME_CAPABILITY));
let request: RunResumeRequest =
serde_json::from_value(serde_json::json!({"run_id":"run-1"})).unwrap();
assert_eq!(request.run_id, "run-1");
assert_eq!(
serde_json::to_value(&request).unwrap(),
serde_json::json!({"run_id":"run-1"})
);
for caller_supplied_credential in [
serde_json::json!({"run_id":"run-1", "agent_id":"caller-controlled"}),
serde_json::json!({"run_id":"run-1", "client_id":"caller-controlled"}),
serde_json::json!({"run_id":"run-1", "idempotency_key":"caller-controlled"}),
serde_json::json!({"run_id":"run-1", "owner_token":"caller-controlled"}),
] {
assert!(
serde_json::from_value::<RunResumeRequest>(caller_supplied_credential).is_err()
);
}
let response = RunResumeResponse {
run_id: "run-1".into(),
agent_id: "agent-1".into(),
client_id: "client-new".into(),
resumed_from_client_id: "client-old".into(),
};
assert_eq!(
serde_json::to_value(response).unwrap(),
serde_json::json!({
"run_id":"run-1",
"agent_id":"agent-1",
"client_id":"client-new",
"resumed_from_client_id":"client-old"
})
);
}
#[test]
fn inference_control_statuses_have_stable_typed_wire_names() {
let cases = [
(InferenceControlStatus::AlreadyTerminal, "already_terminal"),
(
InferenceControlStatus::CancelledConfirmed,
"cancelled_confirmed",
),
(
InferenceControlStatus::TerminationUnconfirmed,
"termination_unconfirmed",
),
(
InferenceControlStatus::DeadlineExceededConfirmed,
"deadline_exceeded_confirmed",
),
(
InferenceControlStatus::DeadlineExceededUnconfirmed,
"deadline_exceeded_unconfirmed",
),
(InferenceControlStatus::Unknown, "unknown"),
];
for (status, expected) in cases {
assert_eq!(serde_json::to_value(status).unwrap(), expected);
}
}
#[test]
fn unknown_mandatory_capability_fails_loud() {
let error = negotiate_capabilities(
&["future.mandatory.v1".to_string()],
&[MODELS_CATALOG_IDENTITY_CAPABILITY.to_string()],
)
.expect_err("an unsupported mandatory capability must reject the handshake");
assert_eq!(error, vec!["future.mandatory.v1"]);
}
#[test]
fn negotiated_capabilities_are_sorted_deduplicated_and_optional_safe() {
let negotiated = negotiate_capabilities(
&[
INFER_MODEL_IDENTITY_CAPABILITY.to_string(),
MODELS_CATALOG_IDENTITY_CAPABILITY.to_string(),
INFER_MODEL_IDENTITY_CAPABILITY.to_string(),
],
&[
"future.optional.v1".to_string(),
MODELS_CATALOG_IDENTITY_CAPABILITY.to_string(),
],
)
.unwrap();
assert_eq!(
negotiated,
vec![
INFER_MODEL_IDENTITY_CAPABILITY.to_string(),
MODELS_CATALOG_IDENTITY_CAPABILITY.to_string(),
]
);
}
#[test]
fn run_pagination_capability_is_negotiable() {
let capability = "runs.pagination.v1".to_string();
assert_eq!(
negotiate_capabilities(std::slice::from_ref(&capability), &[]),
Ok(vec![capability])
);
}
#[test]
fn run_page_requests_require_explicit_cursor_and_limit() {
assert!(serde_json::from_value::<RunListRequest>(serde_json::json!({
"agent_id": "agent-a"
}))
.is_err());
assert!(
serde_json::from_value::<RunGetTraceRequest>(serde_json::json!({
"run_id": "run-a"
}))
.is_err()
);
assert!(
serde_json::from_value::<RunSubscribeRequest>(serde_json::json!({
"run_id": "run-a"
}))
.is_err()
);
}
}
use car_ir::ActionProposal;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDefinition {
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub parameters: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub returns: Option<Value>,
#[serde(default)]
pub idempotent: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_ttl_secs: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rate_limit: Option<ToolRateLimit>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolRateLimit {
pub max_calls: u32,
pub interval_secs: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInitRequest {
pub client_id: String,
#[serde(default)]
pub tools: Vec<ToolDefinition>,
#[serde(default)]
pub policies: Vec<PolicyDefinition>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyDefinition {
pub name: String,
pub rule: String, #[serde(default)]
pub target: String,
#[serde(default)]
pub key: String,
#[serde(default)]
pub value: Value,
#[serde(default)]
pub pattern: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProposalSubmitRequest {
pub proposal: ActionProposal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifyRequest {
pub proposal: ActionProposal,
#[serde(default)]
pub initial_state: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolExecuteRequest {
pub action_id: String,
pub tool: String,
pub parameters: Value,
#[serde(default)]
pub timeout_ms: Option<u64>,
#[serde(default)]
pub attempt: u32,
#[serde(default)]
pub request_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCancelRequest {
pub request_id: String,
#[serde(default)]
pub action_id: String,
#[serde(default)]
pub reason: String,
}
fn is_false(value: &bool) -> bool {
!*value
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolExecuteResponse {
pub action_id: String,
#[serde(default)]
pub output: Option<Value>,
#[serde(default)]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub terminal: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionEvent {
pub kind: String, #[serde(default)]
pub action_id: Option<String>,
#[serde(default)]
pub proposal_id: Option<String>,
#[serde(default)]
pub data: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum HostAgentStatus {
Idle,
Running,
WaitingForApproval,
Paused,
Completed,
Errored,
Stopped,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct HostAgentDisplay {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accent: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostAgent {
pub id: String,
pub name: String,
#[serde(default)]
pub kind: String,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
pub status: HostAgentStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_task: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
#[serde(default)]
pub display: HostAgentDisplay,
pub updated_at: DateTime<Utc>,
#[serde(default)]
pub metadata: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisterHostAgentRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub name: String,
#[serde(default)]
pub kind: String,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
#[serde(default)]
pub display: HostAgentDisplay,
#[serde(default)]
pub metadata: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetHostAgentStatusRequest {
pub agent_id: String,
pub status: HostAgentStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_task: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default)]
pub payload: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum HostApprovalStatus {
Pending,
Resolved,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostApprovalRequest {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
pub action: String,
pub details: Value,
#[serde(default)]
pub options: Vec<String>,
pub status: HostApprovalStatus,
pub created_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolved_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolution: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateHostApprovalRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
pub action: String,
#[serde(default)]
pub details: Value,
#[serde(default)]
pub options: Vec<String>,
#[serde(default)]
pub system_level: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolveHostApprovalRequest {
pub approval_id: String,
pub resolution: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct MessagingConfigView {
#[serde(default = "default_channel_key")]
pub channel: String,
pub enabled: bool,
pub allowlisted_handles: Vec<String>,
pub pairing_active: bool,
}
fn default_channel_key() -> String {
"imessage".to_string()
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MessagingConfigSetRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channel: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allowlisted_handles: Option<Vec<String>>,
#[serde(default)]
pub add_handles: Vec<String>,
#[serde(default)]
pub remove_handles: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bot_token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub app_token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slack_channel: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessagingPairingStartResponse {
pub pairing_code: String,
pub config: MessagingConfigView,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MessagingPairingStatusResponse {
pub pairing_active: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pairing_code: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct MessagingStatusView {
#[serde(default = "default_channel_key")]
pub channel: String,
pub enabled: bool,
pub paired: bool,
pub watcher_running: bool,
pub fda_readable: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_send_at_ms: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_send_ok: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_error: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct MessagingTestSendResponse {
pub ok: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostEvent {
pub id: String,
#[serde(default)]
pub sequence: u64,
pub timestamp: DateTime<Utc>,
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
pub message: String,
#[serde(default)]
pub payload: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct BrowserSignInSnapshot {
pub conversation_id: String,
pub standing_session: bool,
pub message: String,
}
impl BrowserSignInSnapshot {
pub fn new(conversation_id: Option<&str>, message: impl Into<String>) -> Self {
Self {
conversation_id: conversation_id.unwrap_or("").to_string(),
standing_session: conversation_id.is_none(),
message: message.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HostDevice {
pub id: String,
pub name: String,
pub platform: String,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default)]
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
pub updated_at: DateTime<Utc>,
#[serde(default)]
pub metadata: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisterHostDeviceRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub name: String,
pub platform: String,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default)]
pub status: Option<String>,
#[serde(default)]
pub metadata: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateHostDeviceRequest {
pub device_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub platform: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capabilities: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HostManifestRole {
Owner,
Observer,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostIdentity {
pub version: String,
pub pid: u32,
pub manifest_path: Option<String>,
pub manifest_role: HostManifestRole,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parslee: Option<ParsleeIdentity>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParsleeIdentity {
pub account_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_organization: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub organization_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostSnapshot {
pub subscribed: bool,
#[serde(default)]
pub agents: Vec<HostAgent>,
#[serde(default)]
pub devices: Vec<HostDevice>,
#[serde(default)]
pub approvals: Vec<HostApprovalRequest>,
#[serde(default)]
pub events: Vec<HostEvent>,
#[serde(default)]
pub pending_signins: Vec<BrowserSignInSnapshot>,
#[serde(default)]
pub event_sequence: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identity: Option<HostIdentity>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RunTermination {
Outcome {
status: car_ir::OutcomeStatus,
outcome: car_ir::AgentOutcome,
},
Incomplete,
Cancelled {
cancellation: RunCancellationIdentity,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunCancellationIdentity {
pub receipt_version: u32,
pub run_id: String,
pub idempotency_key: String,
pub reason_digest: String,
pub principal: String,
pub action_id: Option<String>,
pub request_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RunCancellationStatus {
CancelledConfirmed,
AlreadyTerminal,
TerminationUnconfirmed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunCancelRequest {
pub run_id: String,
pub idempotency_key: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunCancelResponse {
pub receipt_version: u32,
pub run_id: String,
pub idempotency_key: String,
pub reason_digest: String,
pub principal: String,
pub status: RunCancellationStatus,
pub terminal_digest: Option<String>,
pub action_id: Option<String>,
pub request_id: Option<String>,
pub receipt_digest: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunCancellationRequested {
pub receipt_version: u32,
pub run_id: String,
pub idempotency_key: String,
pub reason_digest: String,
pub principal: String,
pub action_id: Option<String>,
pub request_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunStarted {
pub run_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
pub agent_id: String,
pub intent: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outcome_description: Option<String>,
pub started_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunEnded {
pub run_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
pub agent_id: String,
pub termination: RunTermination,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completion_digest: Option<String>,
pub ended_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CliOutcome {
Exited { code: i64 },
Killed,
Timeout,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VerifierVerdict {
Pass,
Fail,
NotRun,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolicyRejection {
pub rule: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub param: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunTurn {
pub index: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proposal_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action_status: Option<car_ir::ActionStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action_duration_ms: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action_completed_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub depends_on: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state_dependencies: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool: Option<String>,
#[serde(default, skip_serializing_if = "Value::is_null")]
pub parameters: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cli_outcome: Option<CliOutcome>,
pub verifier_verdict: VerifierVerdict,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub policy_rejected: Option<PolicyRejection>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "record", rename_all = "snake_case")]
pub enum RunRecord {
Started(RunStarted),
Ended(RunEnded),
Turn(RunTurn),
CancellationRequested(RunCancellationRequested),
CancellationResult(RunCancelResponse),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunStartRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_name: Option<String>,
pub intent: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outcome_description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub idempotency_key: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunStartResponse {
pub run_id: String,
pub agent_id: String,
pub client_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunResumeRequest {
pub run_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunResumeResponse {
pub run_id: String,
pub agent_id: String,
pub client_id: String,
pub resumed_from_client_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunCompleteRequest {
pub run_id: String,
pub outcome: car_ir::AgentOutcome,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunCompleteResponse {
pub run_id: String,
pub ok: bool,
pub completion_digest: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunRecordTurnsRequest {
pub run_id: String,
pub turns: Vec<RunTurn>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunRecordTurnsResponse {
pub run_id: String,
pub base_index: usize,
pub count: usize,
pub ok: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dropped: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RunLiveStatus {
InProgress,
Completed,
Incomplete,
CancellationPending,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunSubscribeRequest {
pub run_id: String,
pub cursor: usize,
pub limit: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunSubscribeResponse {
pub run_id: String,
pub agent_id: String,
pub turns: Vec<RunRecord>,
pub cursor: usize,
pub limit: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<usize>,
pub live_cursor: usize,
pub subscribed: bool,
pub status: RunLiveStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunUnsubscribeRequest {
pub run_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunUnsubscribeResponse {
pub run_id: String,
pub removed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunTraceEvent {
pub run_id: String,
pub agent_id: String,
pub record: RunRecord,
pub cursor: usize,
pub status: RunLiveStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunListRequest {
pub agent_id: String,
pub cursor: usize,
pub limit: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunGetTraceRequest {
pub run_id: String,
pub cursor: usize,
pub limit: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInitResponse {
pub session_id: String,
pub tools_registered: usize,
pub policies_registered: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifyResponse {
pub valid: bool,
pub issues: Vec<VerifyIssueProto>,
pub simulated_state: HashMap<String, Value>,
#[serde(default)]
pub execution_levels: Vec<Vec<String>>,
#[serde(default)]
pub conflicts: Vec<(String, String, String)>,
#[serde(default)]
pub evidence: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifyIssueProto {
pub action_id: String,
pub severity: String,
pub message: String,
#[serde(default)]
pub tier: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_definition_roundtrip() {
let td = ToolDefinition {
name: "search".to_string(),
description: "Search the web".to_string(),
parameters: serde_json::json!({"type": "object", "properties": {"query": {"type": "string"}}}),
returns: None,
idempotent: false,
cache_ttl_secs: None,
rate_limit: None,
};
let json = serde_json::to_string(&td).unwrap();
let rt: ToolDefinition = serde_json::from_str(&json).unwrap();
assert_eq!(rt.name, "search");
}
#[test]
fn tool_definition_back_compat_pre_v05_clients() {
let legacy = r#"{"name":"read","description":"","parameters":{}}"#;
let td: ToolDefinition = serde_json::from_str(legacy).unwrap();
assert_eq!(td.name, "read");
assert!(td.returns.is_none());
assert!(!td.idempotent);
assert!(td.cache_ttl_secs.is_none());
assert!(td.rate_limit.is_none());
}
#[test]
fn tool_execute_request_roundtrip() {
let req = ToolExecuteRequest {
action_id: "a1".to_string(),
tool: "search".to_string(),
parameters: serde_json::json!({"query": "rust"}),
timeout_ms: Some(5000),
attempt: 1,
request_id: "cb-7".to_string(),
session_id: Some("sess-7".to_string()),
};
let json = serde_json::to_string(&req).unwrap();
let rt: ToolExecuteRequest = serde_json::from_str(&json).unwrap();
assert_eq!(rt.tool, "search");
assert_eq!(rt.timeout_ms, Some(5000));
assert_eq!(rt.request_id, "cb-7");
assert_eq!(rt.session_id.as_deref(), Some("sess-7"));
}
#[test]
fn tool_execute_request_session_id_is_additive_both_ways() {
let legacy =
r#"{"action_id":"a1","tool":"x","parameters":{},"attempt":1,"request_id":"cb-1"}"#;
let rt: ToolExecuteRequest = serde_json::from_str(legacy).unwrap();
assert_eq!(rt.session_id, None);
let sessionless = ToolExecuteRequest {
action_id: "a1".to_string(),
tool: "x".to_string(),
parameters: serde_json::json!({}),
timeout_ms: None,
attempt: 1,
request_id: "cb-1".to_string(),
session_id: None,
};
let json = serde_json::to_string(&sessionless).unwrap();
assert!(
!json.contains("session_id"),
"a sessionless call must omit the key entirely, got: {json}"
);
}
#[test]
fn tool_execute_request_request_id_defaults_for_pre264_hosts() {
let legacy = r#"{"action_id":"a1","tool":"x","parameters":{},"attempt":1}"#;
let rt: ToolExecuteRequest = serde_json::from_str(legacy).unwrap();
assert_eq!(rt.request_id, "");
assert_eq!(rt.tool, "x");
}
#[test]
fn tool_cancel_request_roundtrip() {
let c = ToolCancelRequest {
request_id: "cb-3".to_string(),
action_id: "a2".to_string(),
reason: "tool 'drive_cli' callback timed out (185s)".to_string(),
};
let json = serde_json::to_string(&c).unwrap();
let rt: ToolCancelRequest = serde_json::from_str(&json).unwrap();
assert_eq!(rt.request_id, "cb-3");
assert_eq!(rt.action_id, "a2");
assert!(rt.reason.contains("timed out"));
let minimal: ToolCancelRequest = serde_json::from_str(r#"{"request_id":"cb-9"}"#).unwrap();
assert_eq!(minimal.request_id, "cb-9");
assert_eq!(minimal.action_id, "");
assert_eq!(minimal.reason, "");
}
#[test]
fn tool_execute_response_success() {
let resp = ToolExecuteResponse {
action_id: "a1".to_string(),
output: Some(Value::from("results")),
error: None,
terminal: false,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("results"));
assert!(!json.contains("terminal"));
}
#[test]
fn tool_execute_response_error() {
let resp = ToolExecuteResponse {
action_id: "a1".to_string(),
output: None,
error: Some("timeout".to_string()),
terminal: true,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("timeout"));
assert!(json.contains(r#""terminal":true"#));
let legacy: ToolExecuteResponse =
serde_json::from_str(r#"{"action_id":"a1","error":"legacy callback failure"}"#)
.unwrap();
assert!(!legacy.terminal);
}
#[test]
fn session_init_request() {
let req = SessionInitRequest {
client_id: "client-1".to_string(),
tools: vec![ToolDefinition {
name: "read".to_string(),
description: "Read file".to_string(),
parameters: serde_json::json!({}),
returns: None,
idempotent: false,
cache_ttl_secs: None,
rate_limit: None,
}],
policies: vec![],
};
let json = serde_json::to_string(&req).unwrap();
let rt: SessionInitRequest = serde_json::from_str(&json).unwrap();
assert_eq!(rt.tools.len(), 1);
}
#[test]
fn verify_request() {
let req = VerifyRequest {
proposal: ActionProposal {
id: "p1".to_string(),
source: "test".to_string(),
actions: vec![],
timestamp: chrono::Utc::now(),
context: HashMap::new(),
},
initial_state: [("x".to_string(), Value::from(1))].into(),
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("p1"));
}
#[test]
fn run_start_request_resolves_optional_agent_id() {
let wire = r#"{"intent":"ship the feature","agent_name":"Bulldozer"}"#;
let req: RunStartRequest = serde_json::from_str(wire).unwrap();
assert_eq!(req.intent, "ship the feature");
assert_eq!(req.agent_id, None);
assert_eq!(req.agent_name.as_deref(), Some("Bulldozer"));
assert_eq!(req.outcome_description, None);
}
#[test]
fn run_record_started_ended_roundtrip() {
let started = RunRecord::Started(RunStarted {
run_id: "run-1".to_string(),
client_id: Some("client-1".to_string()),
agent_id: "agent-1".to_string(),
intent: "do the thing".to_string(),
outcome_description: Some("the thing is done".to_string()),
started_at: chrono::Utc::now(),
});
let json = serde_json::to_string(&started).unwrap();
assert!(json.contains("\"record\":\"started\""));
let rt: RunRecord = serde_json::from_str(&json).unwrap();
match rt {
RunRecord::Started(s) => assert_eq!(s.run_id, "run-1"),
other => panic!("expected Started, got {other:?}"),
}
let ended = RunRecord::Ended(RunEnded {
run_id: "run-1".to_string(),
client_id: Some("client-1".to_string()),
agent_id: "agent-1".to_string(),
termination: RunTermination::Outcome {
status: car_ir::OutcomeStatus::Success,
outcome: car_ir::AgentOutcome::success("done"),
},
completion_digest: Some("abc123".to_string()),
ended_at: chrono::Utc::now(),
});
let json = serde_json::to_string(&ended).unwrap();
assert!(json.contains("\"record\":\"ended\""));
assert!(json.contains("\"kind\":\"outcome\""));
let rt: RunRecord = serde_json::from_str(&json).unwrap();
match rt {
RunRecord::Ended(e) => match e.termination {
RunTermination::Outcome { status, .. } => {
assert_eq!(status, car_ir::OutcomeStatus::Success)
}
other => panic!("expected Outcome, got {other:?}"),
},
other => panic!("expected Ended, got {other:?}"),
}
}
#[test]
fn run_termination_incomplete_serializes_distinctly() {
let term = RunTermination::Incomplete;
let json = serde_json::to_string(&term).unwrap();
assert_eq!(json, r#"{"kind":"incomplete"}"#);
}
#[test]
fn historical_run_records_replay_without_client_binding_fields() {
let started: RunRecord = serde_json::from_str(
r#"{"record":"started","run_id":"old","agent_id":"agent","intent":"go","started_at":"2026-01-02T03:04:05Z"}"#,
)
.expect("historical started row");
let ended: RunRecord = serde_json::from_str(
r#"{"record":"ended","run_id":"old","agent_id":"agent","termination":{"kind":"incomplete"},"ended_at":"2026-01-02T03:05:05Z"}"#,
)
.expect("historical ended row");
match started {
RunRecord::Started(row) => assert!(row.client_id.is_none()),
other => panic!("expected started, got {other:?}"),
}
match ended {
RunRecord::Ended(row) => {
assert!(row.client_id.is_none());
assert!(row.completion_digest.is_none());
}
other => panic!("expected ended, got {other:?}"),
}
}
#[test]
fn cli_outcome_tagged_variants_roundtrip() {
let exited = CliOutcome::Exited { code: 0 };
let json = serde_json::to_string(&exited).unwrap();
assert_eq!(json, r#"{"kind":"exited","code":0}"#);
assert_eq!(serde_json::from_str::<CliOutcome>(&json).unwrap(), exited);
assert_eq!(
serde_json::to_string(&CliOutcome::Killed).unwrap(),
r#"{"kind":"killed"}"#
);
assert_eq!(
serde_json::to_string(&CliOutcome::Timeout).unwrap(),
r#"{"kind":"timeout"}"#
);
assert_eq!(
serde_json::from_str::<CliOutcome>(r#"{"kind":"timeout"}"#).unwrap(),
CliOutcome::Timeout
);
}
#[test]
fn verifier_verdict_serializes_snake_case() {
assert_eq!(
serde_json::to_string(&VerifierVerdict::Pass).unwrap(),
r#""pass""#
);
assert_eq!(
serde_json::to_string(&VerifierVerdict::Fail).unwrap(),
r#""fail""#
);
assert_eq!(
serde_json::to_string(&VerifierVerdict::NotRun).unwrap(),
r#""not_run""#
);
assert_eq!(
serde_json::from_str::<VerifierVerdict>(r#""not_run""#).unwrap(),
VerifierVerdict::NotRun
);
}
#[test]
fn policy_rejection_omits_none_param() {
let pr = PolicyRejection {
rule: "policy 'x': denied".to_string(),
param: None,
};
let json = serde_json::to_string(&pr).unwrap();
assert!(
!json.contains("param"),
"None param must be omitted: {json}"
);
let with_param = PolicyRejection {
rule: "policy 'x': param 'prompt' matches 'rm -rf'".to_string(),
param: Some("prompt".to_string()),
};
let json = serde_json::to_string(&with_param).unwrap();
assert!(json.contains("\"param\":\"prompt\""));
assert_eq!(
serde_json::from_str::<PolicyRejection>(&json).unwrap(),
with_param
);
}
#[test]
fn run_record_turn_variant_roundtrip() {
let turn = RunRecord::Turn(RunTurn {
index: 0,
proposal_id: None,
action_id: None,
action_status: None,
action_duration_ms: None,
action_completed_at: None,
depends_on: None,
state_dependencies: None,
prompt: Some("make the test pass".to_string()),
tool: Some("drive_cli".to_string()),
parameters: serde_json::json!({ "cli": "claude", "prompt": "make the test pass" }),
output: Some(serde_json::json!({ "exit_code": 0, "output_tail": "done" })),
cli_outcome: Some(CliOutcome::Exited { code: 0 }),
verifier_verdict: VerifierVerdict::NotRun,
policy_rejected: None,
});
let json = serde_json::to_string(&turn).unwrap();
assert!(
json.contains("\"record\":\"turn\""),
"turn must tag on `record`: {json}"
);
match serde_json::from_str::<RunRecord>(&json).unwrap() {
RunRecord::Turn(t) => {
assert_eq!(t.index, 0);
assert_eq!(t.tool.as_deref(), Some("drive_cli"));
assert_eq!(t.cli_outcome, Some(CliOutcome::Exited { code: 0 }));
assert_eq!(t.verifier_verdict, VerifierVerdict::NotRun);
}
other => panic!("expected Turn, got {other:?}"),
}
}
#[test]
fn run_turn_minimal_omits_optional_fields() {
let turn = RunTurn {
index: 3,
proposal_id: None,
action_id: None,
action_status: None,
action_duration_ms: None,
action_completed_at: None,
depends_on: None,
state_dependencies: None,
prompt: None,
tool: Some("search".to_string()),
parameters: serde_json::json!({ "query": "rust" }),
output: Some(Value::from("results")),
cli_outcome: None,
verifier_verdict: VerifierVerdict::NotRun,
policy_rejected: None,
};
let json = serde_json::to_string(&turn).unwrap();
assert!(!json.contains("prompt"));
assert!(!json.contains("cli_outcome"));
assert!(!json.contains("policy_rejected"));
assert!(json.contains("\"verifier_verdict\":\"not_run\""));
let rt: RunTurn = serde_json::from_str(&json).unwrap();
assert_eq!(rt, turn);
}
#[test]
fn run_live_status_roundtrip() {
assert_eq!(
serde_json::to_string(&RunLiveStatus::InProgress).unwrap(),
"\"in_progress\""
);
assert_eq!(
serde_json::from_str::<RunLiveStatus>("\"completed\"").unwrap(),
RunLiveStatus::Completed
);
assert_eq!(
serde_json::from_str::<RunLiveStatus>("\"incomplete\"").unwrap(),
RunLiveStatus::Incomplete
);
}
#[test]
fn run_trace_event_wraps_record_and_cursor() {
let ev = RunTraceEvent {
run_id: "run-1".to_string(),
agent_id: "agent-a".to_string(),
record: RunRecord::Turn(RunTurn {
index: 4,
proposal_id: None,
action_id: None,
action_status: None,
action_duration_ms: None,
action_completed_at: None,
depends_on: None,
state_dependencies: None,
prompt: Some("fix it".to_string()),
tool: Some("drive_cli".to_string()),
parameters: serde_json::json!({ "prompt": "fix it" }),
output: Some(serde_json::json!({ "exit_code": 0 })),
cli_outcome: Some(CliOutcome::Exited { code: 0 }),
verifier_verdict: VerifierVerdict::NotRun,
policy_rejected: None,
}),
cursor: 5,
status: RunLiveStatus::InProgress,
};
let json = serde_json::to_string(&ev).unwrap();
let back: RunTraceEvent = serde_json::from_str(&json).unwrap();
assert_eq!(back.run_id, "run-1");
assert_eq!(back.cursor, 5);
assert_eq!(back.status, RunLiveStatus::InProgress);
match back.record {
RunRecord::Turn(t) => assert_eq!(t.index, 4),
other => panic!("expected Turn, got {other:?}"),
}
}
#[test]
fn run_subscribe_response_turns_only_snapshot() {
let resp = RunSubscribeResponse {
run_id: "run-1".to_string(),
agent_id: "agent-a".to_string(),
turns: vec![RunRecord::Turn(RunTurn {
index: 0,
proposal_id: None,
action_id: None,
action_status: None,
action_duration_ms: None,
action_completed_at: None,
depends_on: None,
state_dependencies: None,
prompt: None,
tool: Some("drive_cli".to_string()),
parameters: Value::Null,
output: None,
cli_outcome: None,
verifier_verdict: VerifierVerdict::NotRun,
policy_rejected: None,
})],
cursor: 0,
limit: 100,
next_cursor: None,
live_cursor: 1,
subscribed: true,
status: RunLiveStatus::InProgress,
};
let json = serde_json::to_string(&resp).unwrap();
let back: RunSubscribeResponse = serde_json::from_str(&json).unwrap();
assert_eq!(back.cursor, 0);
assert_eq!(back.live_cursor, 1);
assert!(back.subscribed);
assert_eq!(back.turns.len(), 1);
assert!(matches!(back.turns[0], RunRecord::Turn(_)));
}
#[test]
fn run_complete_request_accepts_harness_outcome_shape() {
let req_json = serde_json::json!({
"run_id": "r1",
"outcome": {
"status": "success",
"summary": "Created file",
"evidence": [],
"metrics": {
"turns": 3,
"tool_calls": 3,
"actions_succeeded": 3,
"actions_failed": 0
},
"tools_called": ["drive_cli", "check_outcome", "finish"]
}
});
let req: RunCompleteRequest =
serde_json::from_value(req_json).expect("harness outcome shape must deserialize");
assert_eq!(req.run_id, "r1");
assert_eq!(req.outcome.status, car_ir::OutcomeStatus::Success);
assert_eq!(req.outcome.summary, "Created file");
assert_eq!(req.outcome.metrics.turns, 3);
assert_eq!(req.outcome.metrics.tool_calls, 3);
assert_eq!(req.outcome.metrics.actions_succeeded, 3);
assert_eq!(req.outcome.metrics.actions_failed, 0);
assert_eq!(req.outcome.metrics.duration_ms, 0.0);
assert_eq!(req.outcome.metrics.retries, 0);
}
#[test]
fn run_cancel_contract_is_strict_and_capability_gated() {
let negotiated = negotiate_capabilities(
&[RUNS_CANCEL_CAPABILITY.to_string()],
&[RUNS_PAGINATION_CAPABILITY.to_string()],
)
.unwrap();
assert_eq!(
negotiated,
vec![
RUNS_CANCEL_CAPABILITY.to_string(),
RUNS_PAGINATION_CAPABILITY.to_string()
]
);
let request: RunCancelRequest = serde_json::from_value(serde_json::json!({
"run_id":"run-1","idempotency_key":"cancel-1","reason":"operator stop"
}))
.unwrap();
assert_eq!(request.reason, "operator stop");
assert!(
serde_json::from_value::<RunCancelRequest>(serde_json::json!({
"run_id":"run-1","idempotency_key":"cancel-1","reason":"stop","extra":true
}))
.is_err()
);
}
#[test]
fn cancelled_termination_and_body_free_records_round_trip() {
let identity = RunCancellationIdentity {
receipt_version: 1,
run_id: "run-1".into(),
idempotency_key: "cancel-1".into(),
reason_digest: "a".repeat(64),
principal: "agent:daily-continuity-newsroom".into(),
action_id: Some("editor".into()),
request_id: Some("cb-7".into()),
};
let ended = RunRecord::Ended(RunEnded {
run_id: "run-1".into(),
client_id: Some("client-1".into()),
agent_id: "daily-continuity-newsroom".into(),
termination: RunTermination::Cancelled {
cancellation: identity,
},
completion_digest: Some("b".repeat(64)),
ended_at: Utc::now(),
});
let json = serde_json::to_string(&ended).unwrap();
assert!(json.contains("\"kind\":\"cancelled\""));
assert!(!json.contains("operator stop"));
assert!(matches!(
serde_json::from_str::<RunRecord>(&json).unwrap(),
RunRecord::Ended(RunEnded {
termination: RunTermination::Cancelled { .. },
..
})
));
}
}