use serde::{Deserialize, Serialize}; /// IPC credentials are supplied by the interactive CLI, never a daemon-side /// path lookup. Debug is deliberately redacted because command routing logs /// the request value. #[derive(Clone, Deserialize, Serialize)] pub struct SecretString(pub String); impl std::fmt::Debug for SecretString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("") } } // --------------------------------------------------------------------------- // Client → Daemon // --------------------------------------------------------------------------- #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type", content = "data", rename_all = "snake_case")] pub enum ClientMessage { Hello { supported_versions: Vec, }, Subscribe { log_classes: Vec, metric_interval_ms: Option, }, Request(RequestEnvelope), Ping { seq: u64, }, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct RequestEnvelope { pub request_id: u64, pub protocol_version: u16, pub request: LocalRequest, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type", content = "data", rename_all = "snake_case")] pub enum LocalRequest { GetStatus, ListTasks, ListUsers, CreateUser { username: String, }, AttachUserFromTu { credential: SecretString, }, PurgeUserData { user_id: i64, }, ReleaseUser { user_id: i64, }, CompleteDeleteUser { user_id: i64, credential: Option, }, /// Retained only to return an actionable deprecation error to old IPC /// clients. It must never select lifecycle semantics implicitly. RemoveUser { user_id: i64, }, ReconnectOmikron, RotateIotaIdentity, RequestProcessExit { intent: ExitIntent, }, GetDaemonStatus, #[serde(skip)] RestartDaemon, #[serde(skip)] StopDaemon, GetConfig, SetConfig { key: String, value: String, }, ReloadConfig, GetOmikronStatus, ListComponents, GetUser { user_id: i64, }, ImportUser { username: String, }, GetLogs { limit: usize, }, CheckUpdate, ListCommunities, } #[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum ExitIntent { Stop, Restart, } // --------------------------------------------------------------------------- // Daemon → Client // --------------------------------------------------------------------------- #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type", content = "data", rename_all = "snake_case")] pub enum DaemonMessage { HelloAck(HelloAck), /// Confirms that the server has installed this connection's subscription. Subscribed, LogEntry(LogEntry), StateUpdate(StateSnapshot), MetricSample(MetricSample), Response(ResponseEnvelope), Pong { seq: u64, }, LifecycleEvent(LifecycleEvent), Gap { skipped: u64, }, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct HelloAck { pub protocol_version: u16, pub daemon_version: String, pub instance_id: String, pub startup_phase: StartupPhase, pub capabilities: Vec, #[serde(default)] pub lifecycle: LifecyclePhase, #[serde(default)] pub health: HealthStatus, #[serde(default)] pub deployment_mode: DeploymentMode, #[serde(default)] pub supervisor: SupervisorKind, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ResponseEnvelope { pub request_id: u64, pub result: ResponseResult, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type", content = "data", rename_all = "snake_case")] pub enum ResponseResult { Ok(ResponsePayload), Error(IpcErrorCode), } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type", content = "data", rename_all = "snake_case")] pub enum ResponsePayload { Status(StatusResponse), Tasks(Vec), Users(Vec), UserCreated { user_id: i64, username: String }, /// Retained only for wire compatibility. New lifecycle code never emits it. UserRemoved { user_id: i64 }, UserDataPurged { user_id: i64 }, Acknowledged { message: String }, DaemonStatus(DaemonStatusResponse), Config(ConfigResponse), OmikronStatus(OmikronStatusResponse), Components(Vec), UserDetail(UserDetailResponse), LogEntries(LogEntriesResponse), UpdateStatus(UpdateStatusResponse), Communities(Vec), } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ConfigResponse { pub yaml: String, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct OmikronStatusResponse { pub connected: bool, pub iota_id: Option, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ComponentStatusResponse { pub id: ComponentId, pub status: HealthStatus, pub message: Option, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct UserDetailResponse { pub user_id: i64, pub username: String, pub display_name: Option, pub created_at: i64, pub trusted_apps: Vec, pub state: LocalUserState, pub data_present: bool, pub credential_present: bool, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct LogEntriesResponse { pub entries: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct UpdateStatusResponse { pub available: bool, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct CommunitySummary { pub name: String, pub title: String, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct StatusResponse { pub phase: String, pub tasks: Vec, pub degraded_reason: Option, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct TaskSummary { pub name: String, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct UserSummary { pub user_id: i64, pub username: String, pub state: LocalUserState, pub data_present: bool, pub credential_present: bool, } #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum LocalUserState { Managed, Released, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct DaemonStatusResponse { pub formatted: String, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum IpcErrorCode { InvalidRequest, NotFound, Conflict, StorageFailure, OmikronUnavailable, UnsupportedVersion, NotReady, Disconnected, Timeout, Cancelled, Unauthorized, InternalFailure, } impl std::fmt::Display for IpcErrorCode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { Self::InvalidRequest => "the daemon rejected the request", Self::NotFound => "the requested resource was not found", Self::Conflict => "the request conflicts with current daemon state", Self::StorageFailure => "the daemon could not access local storage", Self::OmikronUnavailable => "Omikron is unavailable", Self::UnsupportedVersion => "the client and daemon protocol versions are incompatible", Self::NotReady => "the daemon is not ready yet", Self::Disconnected => "the daemon connection was lost", Self::Timeout => "the daemon did not respond in time", Self::Cancelled => "the daemon cancelled the request", Self::Unauthorized => "the daemon denied this operation", Self::InternalFailure => "the daemon encountered an internal failure", }) } } #[cfg(test)] mod error_tests { use super::IpcErrorCode; #[test] fn error_codes_have_operator_facing_messages() { assert_eq!( IpcErrorCode::NotReady.to_string(), "the daemon is not ready yet" ); assert!( !IpcErrorCode::InternalFailure .to_string() .contains("InternalFailure") ); } } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type", content = "data", rename_all = "snake_case")] pub enum LifecycleEvent { StateChanged(ConnectionStatus), Shutdown { reason: String }, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ConnectionStatus { Connected, Reconnecting, Degraded, Disconnected, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum StartupPhase { Starting, MigratingStorage, LoadingUsers, StartingServices, Ready, Degraded, Stopping, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum LifecyclePhase { #[default] Starting, Ready, Stopping, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum HealthStatus { #[default] Healthy, Degraded, Failed, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ComponentId { Storage, Ipc, Omikron, Web, Updater, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum DeploymentMode { SessionChild, UiAutoStart, UserService, SystemSocketActivated, SystemAlwaysOn, #[default] External, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum SupervisorKind { #[default] None, IotaUi, Systemd, External, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ComponentHealth { pub status: HealthStatus, pub message: Option, pub changed_at_ms: u128, } impl Default for StartupPhase { fn default() -> Self { Self::Starting } } // --------------------------------------------------------------------------- // Shared types // --------------------------------------------------------------------------- #[derive(Clone, Debug, Deserialize, Serialize)] pub struct LogEntry { pub timestamp_ms: u128, pub sender: String, pub message: String, pub is_error: bool, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct StateSnapshot { pub cpu: Vec<(f64, f64)>, pub ram: Vec<(f64, f64)>, pub ping: Vec<(f64, f64)>, pub net_up: Vec<(f64, f64)>, pub net_down: Vec<(f64, f64)>, pub sys_info: String, #[serde(default)] pub startup_phase: StartupPhase, #[serde(default)] pub degraded_reason: Option, #[serde(default)] pub lifecycle: LifecyclePhase, #[serde(default)] pub startup_step: Option, #[serde(default)] pub overall_health: HealthStatus, #[serde(default)] pub components: std::collections::BTreeMap, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct MetricSample { pub cpu: Option, pub ram: Option, pub ping: Option, pub net_up: Option, pub net_down: Option, }