use std::collections::BTreeMap;
use std::path::PathBuf;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use serde_json::Value;
pub const FLEET_PROTOCOL_VERSION: &str = "0.1.0";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct FleetRunId(pub String);
impl From<String> for FleetRunId {
fn from(value: String) -> Self {
Self(value)
}
}
impl From<&str> for FleetRunId {
fn from(value: &str) -> Self {
Self(value.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FleetRun {
pub id: FleetRunId,
pub name: String,
pub status: FleetRunStatus,
#[serde(default)]
pub task_specs: Vec<FleetTaskSpec>,
#[serde(default)]
pub worker_specs: Vec<FleetWorkerSpec>,
#[serde(default)]
pub labels: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub security_policy: Option<FleetSecurityPolicy>,
pub created_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FleetRunStatus {
Pending,
Queued,
Running,
Paused,
Completed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FleetTaskSpec {
pub id: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub objective: Option<String>,
pub instructions: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub worker: Option<FleetTaskWorkerProfile>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace: Option<FleetWorkspaceRequirements>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub input_files: Vec<PathBuf>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub context: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub budget: Option<FleetTaskBudget>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default)]
pub expected_artifacts: Vec<FleetArtifactKind>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scorer: Option<FleetScorerSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry_policy: Option<FleetRetryPolicy>,
#[serde(skip_serializing_if = "Option::is_none")]
pub alert_policy: Option<FleetAlertPolicy>,
#[serde(default)]
pub timeout_seconds: Option<u64>,
#[serde(default)]
pub metadata: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct FleetTaskWorkerProfile {
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_profile: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct FleetWorkspaceRequirements {
#[serde(skip_serializing_if = "Option::is_none")]
pub root: Option<PathBuf>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub required_files: Vec<PathBuf>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub writable_paths: Vec<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub environment: Option<FleetEnvironmentRequirements>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct FleetEnvironmentRequirements {
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub required: Vec<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub allowlist: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct FleetTaskBudget {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tool_calls: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_seconds: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FleetArtifactRef {
pub kind: FleetArtifactKind,
pub path: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(default)]
pub size_bytes: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FleetArtifactKind {
Log,
Patch,
TestResult,
Report,
Checkpoint,
Receipt,
Other(String),
}
impl FleetArtifactKind {
fn as_wire_str(&self) -> &str {
match self {
Self::Log => "log",
Self::Patch => "patch",
Self::TestResult => "test_result",
Self::Report => "report",
Self::Checkpoint => "checkpoint",
Self::Receipt => "receipt",
Self::Other(kind) => kind.as_str(),
}
}
fn from_wire_str(value: &str) -> Self {
match value {
"log" => Self::Log,
"patch" => Self::Patch,
"test_result" => Self::TestResult,
"report" => Self::Report,
"checkpoint" => Self::Checkpoint,
"receipt" => Self::Receipt,
other => Self::Other(other.to_string()),
}
}
}
impl Serialize for FleetArtifactKind {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_wire_str())
}
}
impl<'de> Deserialize<'de> for FleetArtifactKind {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Ok(Self::from_wire_str(&value))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum FleetScorerSpec {
ExitCode,
FileExists {
path: PathBuf,
},
RegexMatch {
path: PathBuf,
pattern: String,
},
JsonPath {
path: PathBuf,
expression: String,
},
Command {
command: String,
#[serde(default)]
args: Vec<String>,
},
CodeWhaleVerifierPrompt {
prompt: String,
},
Manual,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FleetWorkerSpec {
pub id: String,
pub name: String,
pub host: FleetHostSpec,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub trust_level: Option<FleetTrustLevel>,
#[serde(default)]
pub labels: BTreeMap<String, String>,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_concurrent_tasks: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum FleetHostSpec {
Local,
Ssh {
host: String,
#[serde(skip_serializing_if = "Option::is_none")]
port: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
identity: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
known_hosts: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
host_key_fingerprint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
working_directory: Option<PathBuf>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
env_allowlist: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
codewhale_binary: Option<String>,
},
#[serde(alias = "container")]
#[serde(alias = "Container")]
Docker {
image: String,
#[serde(default)]
args: Vec<String>,
},
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default)]
#[serde(rename_all = "snake_case")]
pub enum FleetTrustLevel {
#[default]
Sandbox = 0,
Local = 1,
#[serde(alias = "remote-verified", alias = "remoteVerified")]
RemoteVerified = 2,
Operator = 3,
}
impl FleetTrustLevel {
#[must_use]
pub fn may_access_secrets(&self) -> bool {
matches!(self, Self::Operator | Self::RemoteVerified | Self::Local)
}
#[must_use]
pub fn may_write_workspace(&self) -> bool {
matches!(self, Self::Operator | Self::Local)
}
#[must_use]
pub fn may_access_network(&self) -> bool {
matches!(self, Self::Operator | Self::RemoteVerified | Self::Local)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FleetSecurityPolicy {
#[serde(default)]
pub default_trust_level: FleetTrustLevel,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub allowed_secrets: Vec<FleetSecretRef>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub capability_grants: Vec<FleetCapabilityGrant>,
#[serde(default = "default_max_trust_level")]
pub max_trust_level: FleetTrustLevel,
#[serde(default)]
pub require_identity_verification: bool,
#[serde(default)]
pub allow_parallel_reads: bool,
}
fn default_max_trust_level() -> FleetTrustLevel {
FleetTrustLevel::Operator
}
impl Default for FleetSecurityPolicy {
fn default() -> Self {
Self {
default_trust_level: FleetTrustLevel::Sandbox,
allowed_secrets: Vec::new(),
capability_grants: Vec::new(),
max_trust_level: FleetTrustLevel::Operator,
require_identity_verification: false,
allow_parallel_reads: false,
}
}
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FleetSecretRef {
pub key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
}
impl FleetSecretRef {
#[must_use]
pub fn new(key: impl Into<String>) -> Self {
Self {
key: key.into(),
source: None,
}
}
#[must_use]
pub fn with_source(key: impl Into<String>, source: impl Into<String>) -> Self {
Self {
key: key.into(),
source: Some(source.into()),
}
}
#[must_use]
pub fn redacted(&self) -> String {
match &self.source {
Some(src) => format!("<secret:{}.{}>", src, self.key),
None => format!("<secret:{}>", self.key),
}
}
}
impl std::fmt::Display for FleetSecretRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.redacted())
}
}
impl From<&str> for FleetSecretRef {
fn from(key: &str) -> Self {
Self::new(key)
}
}
impl From<String> for FleetSecretRef {
fn from(key: String) -> Self {
Self::new(key)
}
}
impl<'de> Deserialize<'de> for FleetSecretRef {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum SecretRefWire {
Key(String),
Structured {
key: String,
#[serde(default)]
source: Option<String>,
},
}
match SecretRefWire::deserialize(deserializer)? {
SecretRefWire::Key(key) if !key.trim().is_empty() => Ok(FleetSecretRef::new(key)),
SecretRefWire::Key(_) => Err(de::Error::custom("secret ref key cannot be empty")),
SecretRefWire::Structured { key, source } if !key.trim().is_empty() => {
Ok(FleetSecretRef { key, source })
}
SecretRefWire::Structured { .. } => {
Err(de::Error::custom("secret ref key cannot be empty"))
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "method", rename_all = "snake_case")]
pub enum FleetWorkerAuth {
None,
SshKey {
identity: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
known_hosts: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
host_key_fingerprint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
user: Option<String>,
},
Token {
token_ref: FleetSecretRef,
},
Mtls {
cert_path: PathBuf,
key_ref: FleetSecretRef,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FleetCapabilityGrant {
pub capability: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FleetWorkerStatus {
Unknown,
Online,
Busy,
Offline,
Unhealthy,
Draining,
Retired,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FleetInboxEntry {
pub run_id: FleetRunId,
pub task_id: String,
pub priority: i32,
pub enqueued_at: String,
#[serde(default)]
pub lease_deadline: Option<String>,
#[serde(default)]
pub attempts: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FleetWorkerEvent {
pub seq: u64,
pub run_id: FleetRunId,
pub worker_id: String,
pub task_id: String,
pub timestamp: String,
#[serde(flatten)]
pub payload: FleetWorkerEventPayload,
#[serde(default)]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum FleetWorkerEventPayload {
Queued,
Leased {
#[serde(skip_serializing_if = "Option::is_none")]
lease_expires_at: Option<String>,
},
Starting,
Running,
ModelWait {
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<String>,
},
RunningTool {
tool: String,
#[serde(skip_serializing_if = "Option::is_none")]
call_id: Option<String>,
},
Heartbeat {
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
cpu_percent: Option<f32>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
memory_mb: Option<u64>,
},
Artifact(FleetArtifactRef),
Completed {
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
exit_code: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
summary: Option<String>,
},
Failed {
reason: String,
#[serde(default)]
recoverable: bool,
},
Cancelled {
#[serde(skip_serializing_if = "Option::is_none")]
cancelled_by: Option<String>,
},
Interrupted {
#[serde(skip_serializing_if = "Option::is_none")]
signal: Option<String>,
},
Stale {
#[serde(skip_serializing_if = "Option::is_none")]
last_heartbeat_at: Option<String>,
},
Restarted {
#[serde(default)]
restart_count: u32,
},
Escalated {
channel: String,
#[serde(skip_serializing_if = "Option::is_none")]
alert_id: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FleetRetryPolicy {
#[serde(default = "default_retry_max_attempts")]
pub max_attempts: u32,
#[serde(default = "default_retry_initial_backoff_seconds")]
pub initial_backoff_seconds: u64,
#[serde(default = "default_retry_max_backoff_seconds")]
pub max_backoff_seconds: u64,
#[serde(default = "default_retry_backoff_multiplier")]
pub backoff_multiplier: u32,
}
impl Default for FleetRetryPolicy {
fn default() -> Self {
Self {
max_attempts: 3,
initial_backoff_seconds: 5,
max_backoff_seconds: 300,
backoff_multiplier: 2,
}
}
}
fn default_retry_max_attempts() -> u32 {
FleetRetryPolicy::default().max_attempts
}
fn default_retry_initial_backoff_seconds() -> u64 {
FleetRetryPolicy::default().initial_backoff_seconds
}
fn default_retry_max_backoff_seconds() -> u64 {
FleetRetryPolicy::default().max_backoff_seconds
}
fn default_retry_backoff_multiplier() -> u32 {
FleetRetryPolicy::default().backoff_multiplier
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FleetAlertPolicy {
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub events: Vec<FleetAlertEventClass>,
#[serde(default)]
pub channels: Vec<FleetAlertChannel>,
#[serde(default)]
pub after_attempts: Option<u32>,
#[serde(default)]
pub after_minutes_stale: Option<u64>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "snake_case")]
pub enum FleetAlertEventClass {
Stale,
RestartExhausted,
NeedsHuman,
BudgetExceeded,
VerifierFailed,
RunCompleted,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum FleetAlertChannel {
Slack {
#[serde(flatten)]
webhook: FleetAlertEndpoint,
},
Webhook {
#[serde(flatten)]
endpoint: FleetAlertEndpoint,
},
#[serde(alias = "pager_duty")]
#[serde(alias = "pagerduty")]
PagerDuty {
routing_key: String,
severity: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FleetAlertEndpoint {
#[serde(
alias = "webhook_url",
alias = "endpoint_url",
skip_serializing_if = "Option::is_none"
)]
pub url: Option<String>,
#[serde(
alias = "webhook_url_ref",
alias = "webhook_ref",
alias = "url_secret_ref",
skip_serializing_if = "Option::is_none"
)]
pub url_ref: Option<FleetSecretRef>,
#[serde(
alias = "secret",
alias = "webhook_secret",
alias = "signing_secret",
skip_serializing_if = "Option::is_none"
)]
pub secret_ref: Option<FleetSecretRef>,
}
impl FleetAlertEndpoint {
#[must_use]
pub fn inline(url: impl Into<String>) -> Self {
Self {
url: Some(url.into()),
url_ref: None,
secret_ref: None,
}
}
#[must_use]
pub fn from_secret(url_ref: FleetSecretRef) -> Self {
Self {
url: None,
url_ref: Some(url_ref),
secret_ref: None,
}
}
#[must_use]
pub fn redacted(&self) -> String {
self.url_ref
.as_ref()
.map_or_else(|| "<inline-url>".to_string(), |r| r.redacted())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FleetReceipt {
pub run_id: FleetRunId,
pub task_id: String,
pub worker_id: String,
pub completed_at: String,
pub result: FleetTaskResult,
#[serde(skip_serializing_if = "Option::is_none")]
pub failure_kind: Option<FleetTaskFailureKind>,
#[serde(default)]
pub artifacts: Vec<FleetArtifactRef>,
#[serde(default)]
pub score: Option<FleetScore>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FleetTaskResult {
Pass,
Partial,
Fail,
Skip,
Timeout,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FleetTaskFailureKind {
Transport,
Task,
Verifier,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FleetScore {
pub value: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fleet_run_round_trip() {
let run = FleetRun {
id: FleetRunId::from("run-001"),
name: "dogfood smoke".to_string(),
status: FleetRunStatus::Running,
task_specs: vec![FleetTaskSpec {
id: "task-1".to_string(),
name: "lint".to_string(),
description: None,
objective: Some("Keep the workspace lint-clean".to_string()),
instructions: "run cargo clippy".to_string(),
worker: Some(FleetTaskWorkerProfile {
role: Some("release-checker".to_string()),
tool_profile: Some("read-only".to_string()),
tools: vec!["cargo".to_string()],
capabilities: vec!["rust".to_string()],
}),
workspace: Some(FleetWorkspaceRequirements {
root: Some(PathBuf::from(".")),
required_files: vec![PathBuf::from("Cargo.toml")],
writable_paths: vec![],
environment: Some(FleetEnvironmentRequirements {
required: vec!["PATH".to_string()],
allowlist: vec!["RUST_LOG".to_string()],
}),
}),
input_files: vec![PathBuf::from("crates/tui/src/main.rs")],
context: vec!["release gate".to_string()],
budget: Some(FleetTaskBudget {
max_tokens: Some(8000),
max_tool_calls: Some(20),
max_seconds: Some(300),
}),
tags: vec!["release".to_string()],
expected_artifacts: vec![FleetArtifactKind::Log],
scorer: Some(FleetScorerSpec::ExitCode),
retry_policy: Some(FleetRetryPolicy::default()),
alert_policy: None,
timeout_seconds: Some(300),
metadata: BTreeMap::new(),
}],
worker_specs: vec![],
labels: BTreeMap::new(),
security_policy: None,
created_at: "2026-06-12T17:00:00Z".to_string(),
updated_at: None,
completed_at: None,
};
let json = serde_json::to_string(&run).unwrap();
let back: FleetRun = serde_json::from_str(&json).unwrap();
assert_eq!(back.id, run.id);
assert_eq!(back.status, FleetRunStatus::Running);
assert_eq!(back.task_specs.len(), 1);
assert_eq!(
back.task_specs[0].worker.as_ref().unwrap().role.as_deref(),
Some("release-checker")
);
assert_eq!(
back.task_specs[0]
.workspace
.as_ref()
.unwrap()
.required_files,
vec![PathBuf::from("Cargo.toml")]
);
}
#[test]
fn worker_event_lifecycle_round_trip() {
let events = vec![
FleetWorkerEvent {
seq: 1,
run_id: FleetRunId::from("run-002"),
worker_id: "worker-a".to_string(),
task_id: "task-1".to_string(),
timestamp: "2026-06-12T17:01:00Z".to_string(),
payload: FleetWorkerEventPayload::Queued,
extra: BTreeMap::new(),
},
FleetWorkerEvent {
seq: 2,
run_id: FleetRunId::from("run-002"),
worker_id: "worker-a".to_string(),
task_id: "task-1".to_string(),
timestamp: "2026-06-12T17:01:05Z".to_string(),
payload: FleetWorkerEventPayload::RunningTool {
tool: "bash".to_string(),
call_id: Some("call-1".to_string()),
},
extra: BTreeMap::new(),
},
FleetWorkerEvent {
seq: 3,
run_id: FleetRunId::from("run-002"),
worker_id: "worker-a".to_string(),
task_id: "task-1".to_string(),
timestamp: "2026-06-12T17:02:00Z".to_string(),
payload: FleetWorkerEventPayload::Completed {
exit_code: Some(0),
summary: Some("ok".to_string()),
},
extra: BTreeMap::new(),
},
];
let json = serde_json::to_string(&events).unwrap();
let back: Vec<FleetWorkerEvent> = serde_json::from_str(&json).unwrap();
assert_eq!(back.len(), 3);
assert!(matches!(back[0].payload, FleetWorkerEventPayload::Queued));
assert!(matches!(
back[2].payload,
FleetWorkerEventPayload::Completed { .. }
));
}
#[test]
fn alert_policy_round_trip() {
let policy = FleetAlertPolicy {
events: vec![FleetAlertEventClass::Stale],
channels: vec![FleetAlertChannel::Slack {
webhook: FleetAlertEndpoint::inline("https://hooks.slack.com/test"),
}],
after_attempts: Some(2),
after_minutes_stale: Some(10),
};
let json = serde_json::to_string(&policy).unwrap();
assert!(json.contains("\"events\":[\"stale\"]"));
assert!(json.contains("\"kind\":\"slack\""));
let back: FleetAlertPolicy = serde_json::from_str(&json).unwrap();
assert_eq!(back.events, vec![FleetAlertEventClass::Stale]);
assert_eq!(back.after_attempts, Some(2));
}
#[test]
fn artifact_other_kind_round_trip() {
let artifact = FleetArtifactRef {
kind: FleetArtifactKind::Other("coverage.xml".to_string()),
path: PathBuf::from("/tmp/coverage.xml"),
checksum: Some("sha256:abc".to_string()),
mime_type: Some("application/xml".to_string()),
size_bytes: Some(1024),
};
let json = serde_json::to_string(&artifact).unwrap();
let back: FleetArtifactRef = serde_json::from_str(&json).unwrap();
assert_eq!(back.kind, artifact.kind);
assert_eq!(back.size_bytes, Some(1024));
}
#[test]
fn ssh_host_spec_accepts_minimal_legacy_json() {
let json = r#"{"kind":"ssh","host":"builder.example.test"}"#;
let host: FleetHostSpec = serde_json::from_str(json).unwrap();
match host {
FleetHostSpec::Ssh {
host,
port,
user,
identity,
known_hosts,
host_key_fingerprint,
working_directory,
env_allowlist,
codewhale_binary,
} => {
assert_eq!(host, "builder.example.test");
assert_eq!(port, None);
assert_eq!(user, None);
assert_eq!(identity, None);
assert_eq!(known_hosts, None);
assert_eq!(host_key_fingerprint, None);
assert_eq!(working_directory, None);
assert!(env_allowlist.is_empty());
assert_eq!(codewhale_binary, None);
}
other => panic!("expected ssh host spec, got {other:?}"),
}
}
#[test]
fn artifact_kind_uses_flat_string_json() {
let known = serde_json::to_string(&FleetArtifactKind::TestResult).unwrap();
assert_eq!(known, "\"test_result\"");
let custom =
serde_json::to_string(&FleetArtifactKind::Other("coverage.xml".to_string())).unwrap();
assert_eq!(custom, "\"coverage.xml\"");
let parsed: FleetArtifactKind = serde_json::from_str("\"coverage.xml\"").unwrap();
assert_eq!(parsed, FleetArtifactKind::Other("coverage.xml".to_string()));
}
#[test]
fn retry_policy_missing_fields_use_nonzero_defaults() {
let policy: FleetRetryPolicy = serde_json::from_value(serde_json::json!({})).unwrap();
assert_eq!(policy, FleetRetryPolicy::default());
let policy: FleetRetryPolicy =
serde_json::from_value(serde_json::json!({"max_attempts": 5})).unwrap();
assert_eq!(policy.max_attempts, 5);
assert_eq!(
policy.initial_backoff_seconds,
FleetRetryPolicy::default().initial_backoff_seconds
);
assert_eq!(
policy.max_backoff_seconds,
FleetRetryPolicy::default().max_backoff_seconds
);
assert_eq!(
policy.backoff_multiplier,
FleetRetryPolicy::default().backoff_multiplier
);
}
#[test]
fn sparse_worker_events_omit_absent_optional_fields() {
let heartbeat = FleetWorkerEventPayload::Heartbeat {
cpu_percent: None,
memory_mb: None,
};
let heartbeat_json = serde_json::to_value(&heartbeat).unwrap();
assert_eq!(heartbeat_json, serde_json::json!({"state": "heartbeat"}));
let completed = FleetWorkerEventPayload::Completed {
exit_code: None,
summary: None,
};
let completed_json = serde_json::to_value(&completed).unwrap();
assert_eq!(completed_json, serde_json::json!({"state": "completed"}));
}
#[test]
fn receipt_round_trip() {
let receipt = FleetReceipt {
run_id: FleetRunId::from("run-003"),
task_id: "task-1".to_string(),
worker_id: "worker-b".to_string(),
completed_at: "2026-06-12T17:03:00Z".to_string(),
result: FleetTaskResult::Pass,
failure_kind: None,
artifacts: vec![],
score: Some(FleetScore {
value: 0.95,
max: Some(1.0),
notes: None,
}),
};
let json = serde_json::to_string(&receipt).unwrap();
let back: FleetReceipt = serde_json::from_str(&json).unwrap();
assert_eq!(back.result, FleetTaskResult::Pass);
assert_eq!(back.score.as_ref().unwrap().value, 0.95);
}
#[test]
fn partial_receipt_records_failure_source_when_needed() {
let receipt = FleetReceipt {
run_id: FleetRunId::from("run-004"),
task_id: "task-2".to_string(),
worker_id: "worker-c".to_string(),
completed_at: "2026-06-12T17:04:00Z".to_string(),
result: FleetTaskResult::Partial,
failure_kind: Some(FleetTaskFailureKind::Verifier),
artifacts: vec![],
score: Some(FleetScore {
value: 0.5,
max: Some(1.0),
notes: Some("manual verification required".to_string()),
}),
};
let json = serde_json::to_string(&receipt).unwrap();
assert!(json.contains("\"result\":\"partial\""));
assert!(json.contains("\"failure_kind\":\"verifier\""));
let back: FleetReceipt = serde_json::from_str(&json).unwrap();
assert_eq!(back.result, FleetTaskResult::Partial);
assert_eq!(back.failure_kind, Some(FleetTaskFailureKind::Verifier));
}
#[test]
fn ssh_host_spec_with_key_pinning_round_trip() {
let spec = FleetHostSpec::Ssh {
host: "builder.trusted.example.com".to_string(),
port: Some(22),
user: Some("codewhale".to_string()),
identity: Some(PathBuf::from("~/.ssh/codewhale_fleet")),
known_hosts: Some(PathBuf::from("~/.ssh/known_hosts")),
host_key_fingerprint: Some("SHA256:aLGqZo1M6c...".to_string()),
working_directory: Some(PathBuf::from("/srv/codewhale/work")),
env_allowlist: vec!["CODEWHALE_PROFILE".to_string()],
codewhale_binary: Some("/usr/local/bin/codewhale".to_string()),
};
let json = serde_json::to_string_pretty(&spec).unwrap();
assert!(json.contains("\"known_hosts\""));
assert!(json.contains("\"host_key_fingerprint\""));
assert!(json.contains("SHA256:aLGqZo1M6c..."));
let back: FleetHostSpec = serde_json::from_str(&json).unwrap();
match back {
FleetHostSpec::Ssh {
host,
known_hosts,
host_key_fingerprint,
..
} => {
assert_eq!(host, "builder.trusted.example.com");
assert_eq!(known_hosts, Some(PathBuf::from("~/.ssh/known_hosts")));
assert_eq!(
host_key_fingerprint,
Some("SHA256:aLGqZo1M6c...".to_string())
);
}
other => panic!("expected ssh host spec, got {other:?}"),
}
}
#[test]
fn secret_ref_redacted_never_exposes_value() {
let ref_ = FleetSecretRef::new("DEEPSEEK_API_KEY");
let redacted = ref_.redacted();
assert!(redacted.contains("DEEPSEEK_API_KEY"));
assert!(!redacted.contains("sk-"));
assert!(redacted.contains("<secret:"));
let ref_ = FleetSecretRef::with_source("GH_TOKEN", "env");
let redacted = ref_.redacted();
assert!(redacted.contains("env.GH_TOKEN"));
assert!(!redacted.contains("ghp_"));
}
#[test]
fn alert_endpoint_from_secret_round_trip() {
let endpoint = FleetAlertEndpoint::from_secret(FleetSecretRef::new("SLACK_WEBHOOK"));
let json = serde_json::to_string(&endpoint).unwrap();
assert!(json.contains("SLACK_WEBHOOK"));
assert!(!json.contains("hooks.slack.com"));
let back: FleetAlertEndpoint = serde_json::from_str(&json).unwrap();
assert_eq!(back.url_ref.as_ref().unwrap().key, "SLACK_WEBHOOK");
assert_eq!(back.url, None);
}
#[test]
fn secret_ref_accepts_legacy_string_wire_shape() {
let ref_: FleetSecretRef = serde_json::from_str(r#""CODEWHALE_FLEET_TOKEN""#).unwrap();
assert_eq!(ref_, FleetSecretRef::new("CODEWHALE_FLEET_TOKEN"));
let ref_: FleetSecretRef =
serde_json::from_str(r#"{"key":"GH_TOKEN","source":"env"}"#).unwrap();
assert_eq!(ref_, FleetSecretRef::with_source("GH_TOKEN", "env"));
}
#[test]
fn trust_level_accepts_hyphenated_remote_verified() {
let trust: FleetTrustLevel = serde_json::from_str(r#""remote-verified""#).unwrap();
assert_eq!(trust, FleetTrustLevel::RemoteVerified);
let canonical = serde_json::to_string(&trust).unwrap();
assert_eq!(canonical, r#""remote_verified""#);
}
#[test]
fn alert_channel_accepts_legacy_webhook_fields() {
let channel: FleetAlertChannel = serde_json::from_str(
r#"{
"kind": "slack",
"webhook_url": "https://hooks.slack.com/test",
"secret": "SLACK_SIGNING_SECRET"
}"#,
)
.unwrap();
match channel {
FleetAlertChannel::Slack { webhook } => {
assert_eq!(webhook.url.as_deref(), Some("https://hooks.slack.com/test"));
assert_eq!(
webhook.secret_ref,
Some(FleetSecretRef::new("SLACK_SIGNING_SECRET"))
);
}
other => panic!("expected slack channel, got {other:?}"),
}
}
#[test]
fn security_policy_defaults_are_conservative() {
let policy = FleetSecurityPolicy::default();
assert_eq!(policy.default_trust_level, FleetTrustLevel::Sandbox);
assert!(policy.allowed_secrets.is_empty());
assert!(policy.capability_grants.is_empty());
assert_eq!(policy.max_trust_level, FleetTrustLevel::Operator);
assert!(!policy.require_identity_verification);
}
#[test]
fn trust_level_ordinal_reflects_privilege() {
assert!(FleetTrustLevel::Operator > FleetTrustLevel::RemoteVerified);
assert!(FleetTrustLevel::RemoteVerified > FleetTrustLevel::Local);
assert!(FleetTrustLevel::Local > FleetTrustLevel::Sandbox);
assert!(FleetTrustLevel::Operator.may_access_secrets());
assert!(!FleetTrustLevel::Sandbox.may_access_secrets());
assert!(!FleetTrustLevel::Sandbox.may_write_workspace());
assert!(FleetTrustLevel::Operator.may_write_workspace());
}
}