pub mod daemon;
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,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolExecuteResponse {
pub action_id: String,
#[serde(default)]
pub output: Option<Value>,
#[serde(default)]
pub error: Option<String>,
}
#[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>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolveHostApprovalRequest {
pub approval_id: String,
pub resolution: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostEvent {
pub id: String,
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)]
pub struct HostSnapshot {
pub subscribed: bool,
#[serde(default)]
pub agents: Vec<HostAgent>,
#[serde(default)]
pub approvals: Vec<HostApprovalRequest>,
#[serde(default)]
pub events: Vec<HostEvent>,
}
#[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>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifyIssueProto {
pub action_id: String,
pub severity: String,
pub message: 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,
};
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));
}
#[test]
fn tool_execute_response_success() {
let resp = ToolExecuteResponse {
action_id: "a1".to_string(),
output: Some(Value::from("results")),
error: None,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("results"));
}
#[test]
fn tool_execute_response_error() {
let resp = ToolExecuteResponse {
action_id: "a1".to_string(),
output: None,
error: Some("timeout".to_string()),
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("timeout"));
}
#[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"));
}
}