use std::collections::HashMap;
use std::path::PathBuf;
use a3s_box_core::config::ResourceLimits;
use a3s_box_core::log::LogConfig;
use a3s_box_core::{
CreateExecutionRequest, ExecutionGeneration, ExecutionIsolation, ExecutionSnapshotId,
NetworkMode, OperationId, ResolvedExecutionPlan,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub use a3s_box_core::ExecutionHealthCheck as HealthCheck;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BoxRecord {
pub id: String,
pub short_id: String,
pub name: String,
pub image: String,
#[serde(default)]
pub isolation: ExecutionIsolation,
#[serde(default)]
pub managed_execution: Option<ManagedExecutionMetadata>,
pub status: String,
pub pid: Option<u32>,
#[serde(default)]
pub pid_start_time: Option<u64>,
pub cpus: u32,
pub memory_mb: u32,
pub volumes: Vec<String>,
#[serde(default)]
pub virtiofs_cache: Option<String>,
pub env: HashMap<String, String>,
pub cmd: Vec<String>,
#[serde(default)]
pub entrypoint: Option<Vec<String>>,
pub box_dir: PathBuf,
#[serde(default)]
pub exec_socket_path: PathBuf,
pub console_log: PathBuf,
pub created_at: DateTime<Utc>,
pub started_at: Option<DateTime<Utc>>,
pub auto_remove: bool,
#[serde(default)]
pub hostname: Option<String>,
#[serde(default)]
pub user: Option<String>,
#[serde(default)]
pub workdir: Option<String>,
#[serde(default = "default_restart_policy")]
pub restart_policy: String,
#[serde(default)]
pub port_map: Vec<String>,
#[serde(default)]
pub labels: HashMap<String, String>,
#[serde(default)]
pub stopped_by_user: bool,
#[serde(default)]
pub restart_count: u32,
#[serde(default)]
pub max_restart_count: u32,
#[serde(default)]
pub exit_code: Option<i32>,
#[serde(default)]
pub health_check: Option<HealthCheck>,
#[serde(default)]
pub healthcheck_disabled: bool,
#[serde(default = "default_health_status")]
pub health_status: String,
#[serde(default)]
pub health_retries: u32,
#[serde(default)]
pub health_last_check: Option<DateTime<Utc>>,
#[serde(default)]
pub network_mode: NetworkMode,
#[serde(default)]
pub network_name: Option<String>,
#[serde(default)]
pub volume_names: Vec<String>,
#[serde(default)]
pub tmpfs: Vec<String>,
#[serde(default)]
pub anonymous_volumes: Vec<String>,
#[serde(default)]
pub resource_limits: ResourceLimits,
#[serde(default)]
pub log_config: LogConfig,
#[serde(default)]
pub add_host: Vec<String>,
#[serde(default)]
pub platform: Option<String>,
#[serde(default)]
pub init: bool,
#[serde(default)]
pub read_only: bool,
#[serde(default)]
pub cap_add: Vec<String>,
#[serde(default)]
pub cap_drop: Vec<String>,
#[serde(default)]
pub security_opt: Vec<String>,
#[serde(default)]
pub privileged: bool,
#[serde(default)]
pub devices: Vec<String>,
#[serde(default)]
pub gpus: Option<String>,
#[serde(default)]
pub shm_size: Option<u64>,
#[serde(default)]
pub stop_signal: Option<String>,
#[serde(default)]
pub stop_timeout: Option<u64>,
#[serde(default)]
pub oom_kill_disable: bool,
#[serde(default)]
pub oom_score_adj: Option<i32>,
}
impl BoxRecord {
pub fn make_short_id(id: &str) -> String {
id.replace('-', "").chars().take(12).collect()
}
pub fn is_active(&self) -> bool {
if self.managed_execution.is_some() {
return self
.managed_state()
.is_ok_and(|state| state.is_some_and(ManagedExecutionState::keeps_resources));
}
matches!(self.status.as_str(), "running" | "paused")
}
pub fn managed_state(&self) -> a3s_box_core::Result<Option<ManagedExecutionState>> {
let Some(metadata) = self.managed_execution.as_ref() else {
return Ok(None);
};
let state = ManagedExecutionState::from_status(&self.status)?;
validate_pending_operation(state, metadata)?;
Ok(Some(state))
}
pub fn status_summary(&self) -> String {
let mut annotations = Vec::new();
if self.is_active() && self.health_check.is_some() && self.health_status != "none" {
annotations.push(self.health_status.clone());
}
if matches!(self.status.as_str(), "stopped" | "dead") {
if let Some(exit_code) = self.exit_code {
annotations.push(format!("Exit {exit_code}"));
}
}
if self.restart_count > 0 {
annotations.push(format!("Restarts: {}", self.restart_count));
}
if annotations.is_empty() {
self.status.clone()
} else {
format!("{} ({})", self.status, annotations.join(", "))
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ManagedExecutionState {
Creating,
Created,
Starting,
Running,
Pausing,
Paused,
Resuming,
Snapshotting,
Killing,
RestartStopping,
RestartStarting,
Removing,
Stopped,
Failed,
}
impl ManagedExecutionState {
pub const fn as_status(self) -> &'static str {
match self {
Self::Creating => "creating",
Self::Created => "created",
Self::Starting => "starting",
Self::Running => "running",
Self::Pausing => "pausing",
Self::Paused => "paused",
Self::Resuming => "resuming",
Self::Snapshotting => "snapshotting",
Self::Killing => "killing",
Self::RestartStopping => "restart_stopping",
Self::RestartStarting => "restart_starting",
Self::Removing => "removing",
Self::Stopped => "stopped",
Self::Failed => "failed",
}
}
pub fn from_status(status: &str) -> a3s_box_core::Result<Self> {
match status {
"creating" => Ok(Self::Creating),
"created" => Ok(Self::Created),
"starting" => Ok(Self::Starting),
"running" => Ok(Self::Running),
"pausing" => Ok(Self::Pausing),
"paused" => Ok(Self::Paused),
"resuming" => Ok(Self::Resuming),
"snapshotting" => Ok(Self::Snapshotting),
"killing" => Ok(Self::Killing),
"restart_stopping" => Ok(Self::RestartStopping),
"restart_starting" => Ok(Self::RestartStarting),
"removing" => Ok(Self::Removing),
"stopped" => Ok(Self::Stopped),
"dead" | "failed" => Ok(Self::Failed),
other => Err(a3s_box_core::BoxError::StateError(format!(
"unknown managed execution state: {other}"
))),
}
}
pub const fn keeps_resources(self) -> bool {
!matches!(
self,
Self::Creating | Self::Created | Self::Stopped | Self::Failed
)
}
pub const fn is_terminal(self) -> bool {
matches!(self, Self::Stopped | Self::Failed)
}
}
impl std::fmt::Display for ManagedExecutionState {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_status())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManagedExecutionMetadata {
pub operation_id: OperationId,
pub generation: ExecutionGeneration,
pub request: CreateExecutionRequest,
pub plan: ResolvedExecutionPlan,
#[serde(default)]
pub pending_operation: Option<ManagedExecutionOperation>,
#[serde(default)]
pub last_restart: Option<ManagedRestartCompletion>,
#[serde(default)]
pub finished_at: Option<DateTime<Utc>>,
#[serde(default = "default_paused_with_memory")]
pub paused_with_memory: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ManagedExecutionOperation {
Start,
Pause {
keep_memory: bool,
},
Resume,
Snapshot {
snapshot_id: ExecutionSnapshotId,
source_state: ManagedExecutionState,
},
Kill {
#[serde(default)]
signal: Option<i32>,
#[serde(default)]
timeout_secs: Option<u64>,
},
Remove,
Restart {
operation_id: OperationId,
source_generation: ExecutionGeneration,
source_state: ManagedExecutionState,
#[serde(default)]
stop_timeout_secs: Option<u64>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ManagedRestartOutcome {
Running,
Failed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ManagedRestartCompletion {
pub operation_id: OperationId,
pub source_generation: ExecutionGeneration,
pub target_generation: ExecutionGeneration,
pub outcome: ManagedRestartOutcome,
#[serde(default)]
pub stop_timeout_secs: Option<u64>,
}
impl ManagedExecutionMetadata {
pub fn new(
operation_id: OperationId,
generation: ExecutionGeneration,
request: CreateExecutionRequest,
) -> a3s_box_core::Result<Self> {
if request.external_sandbox_id.trim().is_empty() {
return Err(a3s_box_core::BoxError::ConfigError(
"external sandbox ID cannot be empty".to_string(),
));
}
let plan = a3s_box_core::resolve_execution(&request.config)?;
Ok(Self {
operation_id,
generation,
request,
plan,
pending_operation: None,
last_restart: None,
finished_at: None,
paused_with_memory: true,
})
}
pub fn validate(&self) -> a3s_box_core::Result<()> {
if self.request.external_sandbox_id.trim().is_empty() {
return Err(a3s_box_core::BoxError::StateError(
"managed execution has an empty external sandbox ID".to_string(),
));
}
let resolved = a3s_box_core::resolve_execution(&self.request.config)?;
if resolved != self.plan {
return Err(a3s_box_core::BoxError::StateError(
"managed execution plan does not match its persisted creation request".to_string(),
));
}
if let Some(completed) = &self.last_restart {
let expected_target = next_generation(completed.source_generation)?;
if completed.target_generation != expected_target {
return Err(a3s_box_core::BoxError::StateError(format!(
"completed restart {} has inconsistent generations",
completed.operation_id
)));
}
validate_stop_timeout(completed.stop_timeout_secs)?;
}
Ok(())
}
}
fn validate_pending_operation(
state: ManagedExecutionState,
metadata: &ManagedExecutionMetadata,
) -> a3s_box_core::Result<()> {
let operation = metadata.pending_operation.as_ref();
let consistent = matches!(
(state, operation),
(
ManagedExecutionState::Starting,
Some(ManagedExecutionOperation::Start)
) | (
ManagedExecutionState::Pausing,
Some(ManagedExecutionOperation::Pause { .. })
) | (
ManagedExecutionState::Resuming,
Some(ManagedExecutionOperation::Resume)
) | (
ManagedExecutionState::Snapshotting,
Some(ManagedExecutionOperation::Snapshot { .. })
) | (
ManagedExecutionState::Killing,
Some(ManagedExecutionOperation::Kill { .. })
) | (
ManagedExecutionState::Removing,
Some(ManagedExecutionOperation::Remove)
) | (
ManagedExecutionState::RestartStopping | ManagedExecutionState::RestartStarting,
Some(ManagedExecutionOperation::Restart { .. })
) | (
ManagedExecutionState::Creating
| ManagedExecutionState::Created
| ManagedExecutionState::Running
| ManagedExecutionState::Paused
| ManagedExecutionState::Stopped
| ManagedExecutionState::Failed,
None
)
);
if !consistent {
return Err(a3s_box_core::BoxError::StateError(format!(
"managed execution state {state} has inconsistent pending operation"
)));
}
if let Some(ManagedExecutionOperation::Restart {
source_generation,
source_state,
stop_timeout_secs,
..
}) = operation
{
if !matches!(
source_state,
ManagedExecutionState::Created
| ManagedExecutionState::Running
| ManagedExecutionState::Paused
| ManagedExecutionState::Stopped
| ManagedExecutionState::Failed
) {
return Err(a3s_box_core::BoxError::StateError(
"restart source state is not stable".to_string(),
));
}
let expected = match state {
ManagedExecutionState::RestartStopping => *source_generation,
ManagedExecutionState::RestartStarting => next_generation(*source_generation)?,
_ => {
return Err(a3s_box_core::BoxError::StateError(
"restart operation is attached to a non-restart state".to_string(),
))
}
};
if metadata.generation != expected {
return Err(a3s_box_core::BoxError::StateError(format!(
"restart state {state} has generation {}, expected {}",
metadata.generation.get(),
expected.get()
)));
}
validate_stop_timeout(*stop_timeout_secs)?;
}
if let Some(ManagedExecutionOperation::Snapshot { source_state, .. }) = operation {
if state != ManagedExecutionState::Snapshotting
|| !matches!(
source_state,
ManagedExecutionState::Running | ManagedExecutionState::Paused
)
{
return Err(a3s_box_core::BoxError::StateError(
"snapshot operation has an invalid source state".to_string(),
));
}
}
if let Some(ManagedExecutionOperation::Kill {
signal,
timeout_secs,
}) = operation
{
if signal.is_some_and(|signal| signal <= 0) {
return Err(a3s_box_core::BoxError::StateError(
"kill signal must be positive".to_string(),
));
}
validate_stop_timeout(*timeout_secs)?;
}
if !metadata.paused_with_memory {
let valid_cold_pause_state = match (state, operation) {
(
ManagedExecutionState::Pausing,
Some(ManagedExecutionOperation::Pause { keep_memory }),
) => !keep_memory,
(ManagedExecutionState::Paused | ManagedExecutionState::Resuming, _) => true,
(
ManagedExecutionState::Snapshotting,
Some(ManagedExecutionOperation::Snapshot { source_state, .. }),
) => *source_state == ManagedExecutionState::Paused,
(ManagedExecutionState::Killing | ManagedExecutionState::Removing, _) => true,
(
ManagedExecutionState::RestartStopping,
Some(ManagedExecutionOperation::Restart { source_state, .. }),
) => *source_state == ManagedExecutionState::Paused,
_ => false,
};
if !valid_cold_pause_state {
return Err(a3s_box_core::BoxError::StateError(format!(
"managed execution state {state} cannot retain a filesystem-only pause"
)));
}
}
Ok(())
}
fn validate_stop_timeout(timeout_secs: Option<u64>) -> a3s_box_core::Result<()> {
if timeout_secs.is_some_and(|timeout| timeout.checked_mul(1_000).is_none()) {
Err(a3s_box_core::BoxError::StateError(
"managed stop timeout is too large".to_string(),
))
} else {
Ok(())
}
}
fn next_generation(generation: ExecutionGeneration) -> a3s_box_core::Result<ExecutionGeneration> {
let value = generation.get().checked_add(1).ok_or_else(|| {
a3s_box_core::BoxError::StateError("execution generation is exhausted".to_string())
})?;
ExecutionGeneration::new(value).map_err(|error| {
a3s_box_core::BoxError::StateError(format!("invalid execution generation: {error}"))
})
}
fn default_restart_policy() -> String {
"no".to_string()
}
fn default_health_status() -> String {
"none".to_string()
}
const fn default_paused_with_memory() -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
fn minimal_record() -> serde_json::Value {
serde_json::json!({
"id": "11111111-1111-4111-8111-111111111111",
"short_id": "111111111111",
"name": "fixture",
"image": "alpine:latest",
"status": "created",
"pid": null,
"cpus": 1,
"memory_mb": 128,
"volumes": [],
"env": {},
"cmd": ["sh"],
"box_dir": "/tmp/fixture",
"console_log": "/tmp/fixture/console.log",
"created_at": "2026-07-14T12:00:00Z",
"started_at": null,
"auto_remove": false
})
}
#[test]
fn legacy_records_default_without_losing_runtime_fields() {
let mut value = minimal_record();
value["virtiofs_cache"] = serde_json::json!("always");
let record: BoxRecord = serde_json::from_value(value).unwrap();
assert_eq!(record.isolation, ExecutionIsolation::Microvm);
assert!(record.managed_execution.is_none());
assert_eq!(record.virtiofs_cache.as_deref(), Some("always"));
assert_eq!(record.restart_policy, "no");
assert_eq!(record.health_status, "none");
assert_eq!(
serde_json::to_value(record).unwrap()["virtiofs_cache"],
"always"
);
}
#[test]
fn managed_execution_metadata_round_trips_recovery_intent() {
let mut config = a3s_box_core::BoxConfig {
image: "alpine:latest".to_string(),
isolation: ExecutionIsolation::Sandbox,
..Default::default()
};
config.resources.vcpus = 1;
config.resources.memory_mb = 128;
let metadata = ManagedExecutionMetadata::new(
OperationId::new("create-op-1").unwrap(),
ExecutionGeneration::INITIAL,
CreateExecutionRequest {
external_sandbox_id: "sandbox-1".to_string(),
config,
labels: Default::default(),
policy: Default::default(),
rootfs_snapshot_id: None,
},
)
.unwrap();
let mut value = minimal_record();
value["managed_execution"] = serde_json::to_value(metadata).unwrap();
value["managed_execution"]
.as_object_mut()
.unwrap()
.remove("paused_with_memory");
let record: BoxRecord = serde_json::from_value(value).unwrap();
let encoded = serde_json::to_value(&record).unwrap();
assert_eq!(
record.managed_state().unwrap(),
Some(ManagedExecutionState::Created)
);
assert!(!record.is_active());
let managed = record.managed_execution.unwrap();
assert_eq!(managed.operation_id.as_str(), "create-op-1");
assert_eq!(managed.generation, ExecutionGeneration::INITIAL);
assert_eq!(managed.request.external_sandbox_id, "sandbox-1");
assert!(managed.paused_with_memory);
assert_eq!(
managed.request.config.isolation,
ExecutionIsolation::Sandbox
);
assert_eq!(encoded["managed_execution"]["generation"], 1);
assert_eq!(encoded["managed_execution"]["paused_with_memory"], true);
}
#[test]
fn legacy_kill_operation_defaults_new_termination_options() {
let operation: ManagedExecutionOperation =
serde_json::from_value(serde_json::json!({ "kind": "kill" })).unwrap();
assert_eq!(
operation,
ManagedExecutionOperation::Kill {
signal: None,
timeout_secs: None,
}
);
}
#[test]
fn managed_execution_rejects_a_cold_pause_marker_in_running_state() {
let config = a3s_box_core::BoxConfig {
image: "alpine:latest".to_string(),
isolation: ExecutionIsolation::Sandbox,
..Default::default()
};
let mut metadata = ManagedExecutionMetadata::new(
OperationId::new("create-op-cold-invalid").unwrap(),
ExecutionGeneration::INITIAL,
CreateExecutionRequest {
external_sandbox_id: "sandbox-cold-invalid".to_string(),
config,
labels: Default::default(),
policy: Default::default(),
rootfs_snapshot_id: None,
},
)
.unwrap();
metadata.paused_with_memory = false;
let mut value = minimal_record();
value["status"] = serde_json::json!("running");
value["managed_execution"] = serde_json::to_value(metadata).unwrap();
let record: BoxRecord = serde_json::from_value(value).unwrap();
assert!(record.managed_state().is_err());
}
#[test]
fn managed_execution_validation_rejects_plan_drift() {
let config = a3s_box_core::BoxConfig {
image: "alpine:latest".to_string(),
isolation: ExecutionIsolation::Sandbox,
..Default::default()
};
let mut metadata = ManagedExecutionMetadata::new(
OperationId::new("create-op-1").unwrap(),
ExecutionGeneration::INITIAL,
CreateExecutionRequest {
external_sandbox_id: "sandbox-1".to_string(),
config,
labels: Default::default(),
policy: Default::default(),
rootfs_snapshot_id: None,
},
)
.unwrap();
metadata.plan =
a3s_box_core::resolve_execution(&a3s_box_core::BoxConfig::default()).unwrap();
assert!(metadata.validate().is_err());
}
}