use std::path::PathBuf;
use serde::{Deserialize, Serialize};
pub const CONTROL_SOCKET_EXTENSION: &str = "control.sock";
pub const CONTROL_PROTOCOL_VERSION: u16 = 1;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum ControlRequest {
DiskCheckpointCreate {
checkpoint_id: String,
},
BranchCreate {
#[serde(default)]
record_integrity: bool,
branch_id: String,
child_name: String,
memory_cache_dir: PathBuf,
},
BranchCreateMemfd {
#[serde(default)]
record_integrity: bool,
branch_id: String,
child_name: String,
memory_cache_dir: PathBuf,
#[serde(skip)]
backing: Option<std::sync::Arc<std::fs::File>>,
},
Pause,
Resume,
PauseState,
RootDiskGrow {
size_bytes: u64,
},
DiskCompact {
#[serde(default)]
target: microsandbox_types::DiskCompactionTarget,
layers: Option<usize>,
#[serde(default)]
dry_run: bool,
},
Capabilities,
MemoryTarget {
total_mib: u64,
},
MemoryState,
CpuTarget {
online: u32,
},
CpuState,
SecretsUpdate {
changes: Vec<SecretLiveChange>,
},
CheckpointCreate {
#[serde(default)]
record_integrity: bool,
checkpoint_id: String,
intent: CheckpointCaptureIntent,
},
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CheckpointCaptureIntent {
FullSnapshot,
Park,
TransparentTransfer,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "change", rename_all = "snake_case")]
pub enum SecretLiveChange {
Rotate {
name: String,
value: SecretValue,
},
Remove {
name: String,
},
SetAllowedHosts {
name: String,
hosts: Vec<String>,
},
}
#[derive(Clone, Serialize, Deserialize, zeroize::ZeroizeOnDrop)]
#[serde(transparent)]
pub struct SecretValue(pub String);
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ControlResponse {
#[serde(skip)]
pub secret_result: Option<microsandbox_protocol::control::SecretsResult>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub control_protocols: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub branch: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pause: Option<PauseControlState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root_disk: Option<RootDiskGrowthResult>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compaction: Option<crate::checkpoint::DiskCompactionResult>,
pub ok: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_code: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory: Option<MemoryControlState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu: Option<CpuControlState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capabilities: Option<ControlCapabilities>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checkpoint: Option<CheckpointControlState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disk_checkpoint: Option<DiskCheckpointControlState>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CheckpointControlState {
pub checkpoint_id: String,
pub checkpoint_root: String,
pub path: PathBuf,
pub memory_mode: String,
pub memory_logical_bytes: u64,
pub memory_emitted_bytes: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DiskCheckpointControlState {
pub checkpoint_id: String,
pub path: PathBuf,
pub disk: microsandbox_image::checkpoint::DiskGenerationManifest,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub owned_volumes: Vec<microsandbox_image::snapshot::OwnedVolumeCapture>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RootDiskGrowthResult {
pub filesystem_bytes: u64,
pub device_bytes: u64,
pub total_us: u64,
pub pause_us: u64,
pub guest_us: u64,
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
pub struct ControlCapabilities {
#[serde(default)]
pub optional_disk_integrity: bool,
#[serde(default)]
pub branch_create: bool,
#[serde(default)]
pub branch_memfd: bool,
#[serde(default)]
pub pause_resume: bool,
#[serde(default)]
pub root_disk_grow: bool,
#[serde(default)]
pub disk_compact: bool,
#[serde(default)]
pub disk_compact_owned: bool,
pub cpu_resize: bool,
pub memory_resize: bool,
pub secrets_update: bool,
#[serde(default)]
pub checkpoint_create: bool,
#[serde(default)]
pub disk_checkpoint_create: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PauseControlState {
pub paused: bool,
pub recovery_required: bool,
pub capture_unavailable: Option<String>,
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
pub struct MemoryControlState {
pub boot_mib: u64,
pub target_mib: u64,
pub current_mib: u64,
pub max_mib: u64,
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
pub struct CpuControlState {
pub possible: u32,
pub requested_online: u32,
pub actual_online: u32,
pub enforced: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ControlEnvelope {
pub protocol_version: u16,
pub request_id: String,
pub runtime_boot_id: String,
pub expected_revision: Option<u64>,
pub operation_id: Option<String>,
pub command: ControlRequest,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeLifecycle {
Running,
Quiescing,
Quiesced,
Retiring,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RuntimeControlState {
pub runtime_boot_id: String,
pub revision: u64,
pub lifecycle: RuntimeLifecycle,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ControlEnvelopeResponse {
pub request_id: String,
pub runtime: RuntimeControlState,
pub response: ControlResponse,
}
impl std::fmt::Debug for SecretValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("[redacted]")
}
}
pub fn control_socket_path_for(agent_sock: &std::path::Path) -> PathBuf {
crate::ipc::control_socket_path_for(agent_sock)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn secret_value_debug_is_redacted() {
let request = ControlRequest::SecretsUpdate {
changes: vec![SecretLiveChange::Rotate {
name: "API_KEY".into(),
value: SecretValue("sentinel-secret-value".into()),
}],
};
let debug = format!("{request:?}");
assert!(!debug.contains("sentinel-secret-value"));
assert!(debug.contains("[redacted]"));
assert!(debug.contains("API_KEY"));
}
#[test]
fn secrets_update_round_trips_through_json() {
let request = ControlRequest::SecretsUpdate {
changes: vec![
SecretLiveChange::Rotate {
name: "API_KEY".into(),
value: SecretValue("new-material".into()),
},
SecretLiveChange::Remove {
name: "OLD_KEY".into(),
},
SecretLiveChange::SetAllowedHosts {
name: "API_KEY".into(),
hosts: vec!["api.example.com".into(), "*".into()],
},
],
};
let json = serde_json::to_string(&request).unwrap();
let parsed: ControlRequest = serde_json::from_str(&json).unwrap();
let ControlRequest::SecretsUpdate { changes } = parsed else {
panic!("expected secrets_update");
};
assert_eq!(changes.len(), 3);
let SecretLiveChange::Rotate { name, value } = &changes[0] else {
panic!("expected rotate");
};
assert_eq!(name, "API_KEY");
assert_eq!(value.0, "new-material");
}
#[test]
fn capabilities_response_serializes_flags() {
let response = ControlResponse {
ok: true,
capabilities: Some(ControlCapabilities {
optional_disk_integrity: true,
root_disk_grow: true,
cpu_resize: true,
memory_resize: false,
secrets_update: true,
checkpoint_create: true,
disk_checkpoint_create: true,
branch_create: true,
branch_memfd: false,
pause_resume: true,
disk_compact: true,
disk_compact_owned: true,
}),
..Default::default()
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"secrets_update\":true"));
assert!(json.contains("\"memory_resize\":false"));
assert!(json.contains("\"disk_compact_owned\":true"));
let parsed: ControlResponse = serde_json::from_str(&json).unwrap();
assert!(parsed.capabilities.unwrap().secrets_update);
}
#[test]
fn checkpoint_disk_integrity_defaults_off_and_round_trips_opt_in() {
for enabled in [false, true] {
let mut request = serde_json::json!({
"op": "checkpoint_create", "checkpoint_id": "fixture", "intent": "full_snapshot"
});
if enabled {
request["record_integrity"] = serde_json::json!(true);
}
let parsed: ControlRequest = serde_json::from_value(request).unwrap();
let ControlRequest::CheckpointCreate {
record_integrity, ..
} = parsed
else {
panic!("expected full checkpoint request");
};
assert_eq!(record_integrity, enabled);
assert_eq!(
serde_json::to_value(parsed).unwrap()["record_integrity"],
enabled
);
}
}
#[test]
fn legacy_responses_without_capabilities_still_parse() {
let parsed: ControlResponse = serde_json::from_str(r#"{"ok":true}"#).unwrap();
assert!(parsed.ok);
assert!(parsed.capabilities.is_none());
}
#[test]
fn compaction_target_defaults_to_all_and_roundtrips_explicit_selection() {
use microsandbox_types::DiskCompactionTarget;
let omitted: ControlRequest =
serde_json::from_str(r#"{"op":"disk_compact","layers":3}"#).unwrap();
assert!(matches!(
omitted,
ControlRequest::DiskCompact {
target: DiskCompactionTarget::All,
layers: Some(3),
dry_run: false,
}
));
for target in [
DiskCompactionTarget::All,
DiskCompactionTarget::Root,
DiskCompactionTarget::Disk {
guest_path: "/data".into(),
},
] {
let encoded = serde_json::to_string(&ControlRequest::DiskCompact {
target: target.clone(),
layers: Some(999),
dry_run: true,
})
.unwrap();
let decoded: ControlRequest = serde_json::from_str(&encoded).unwrap();
assert!(matches!(decoded, ControlRequest::DiskCompact {
target: actual, layers: Some(999), dry_run: true,
} if actual == target));
}
assert!(
serde_json::from_str::<ControlRequest>(
r#"{"op":"disk_compact","target":{"kind":"disk"}}"#
)
.is_err()
);
}
#[test]
fn root_only_runtime_does_not_advertise_owned_compaction() {
let old: ControlCapabilities = serde_json::from_str(
r#"{"cpu_resize":false,"memory_resize":false,"secrets_update":false,"disk_compact":true}"#
).unwrap();
assert!(old.disk_compact);
assert!(!old.disk_compact_owned);
}
}
#[cfg(feature = "runner")]
pub use crate::runner::control::{ControlContext, spawn_control_listener};