use crate::identity::{RuntimeWorkerRef, WorkerId, WorkerRef}; use crate::interaction::WorkerInput; use crate::profile_archive::{ProfileSourceArchive, ProfileSourceArchiveRef}; use serde::{Deserialize, Serialize}; fn is_false(value: &bool) -> bool { !*value } /// Profile selector boundary. This is a selector, not a resolved runtime config. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", content = "value", rename_all = "snake_case")] pub enum ProfileSelector { Builtin(String), Named(String), } /// Profile source material available to a Runtime during Worker creation. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum ProfileSourceArchiveSource { /// Backend-internal embedded runtimes may receive already-built archive bytes. Embedded { archive: ProfileSourceArchive }, /// Standalone runtimes resolve this immutable archive from the latest /// Workspace Config bundle before creating the Worker. WorkspaceConfig { archive: ProfileSourceArchiveRef }, } impl ProfileSourceArchiveSource { pub fn reference(&self) -> ProfileSourceArchiveRef { match self { Self::Embedded { archive } => archive.reference.clone(), Self::WorkspaceConfig { archive } => archive.clone(), } } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ConfigBundleRef { pub id: String, pub digest: String, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RepositorySelector(pub String); impl From<&str> for RepositorySelector { fn from(value: &str) -> Self { Self(value.to_string()) } } impl From for RepositorySelector { fn from(value: String) -> Self { Self(value) } } impl AsRef for RepositorySelector { fn as_ref(&self) -> &str { &self.0 } } impl std::ops::Deref for RepositorySelector { type Target = str; fn deref(&self) -> &Self::Target { &self.0 } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct WorkingDirectoryRepository { pub id: String, pub provider: String, pub source: workspace_api::RepositorySource, pub source_revision: u64, pub source_fingerprint: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub selector: Option, } pub use workdir::workspace::{ MaterializerKind, RuntimeWorkingDirectoryCleanupTarget as WorkingDirectoryCleanupTarget, RuntimeWorkingDirectorySummary as WorkingDirectorySummary, WorkingDirectoryCurrentObservation, WorkingDirectoryOccupancy, WorkingDirectoryProvenance, WorkingDirectoryStatusKind, }; #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SensitiveString(String); impl SensitiveString { pub fn new(value: impl Into) -> Self { Self(value.into()) } pub fn expose(&self) -> &str { &self.0 } } impl Drop for SensitiveString { fn drop(&mut self) { zeroize::Zeroize::zeroize(&mut self.0); } } impl Default for SensitiveString { fn default() -> Self { Self(String::new()) } } impl std::fmt::Debug for SensitiveString { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str("[REDACTED]") } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RepositorySshCredentialCandidate { pub credential_id: String, pub credential_revision: u64, #[serde(skip, default)] pub private_key: SensitiveString, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RepositorySshMaterializationAccess { pub credential_candidates: Vec, pub host_trust_id: String, pub host_trust_revision: u64, pub access: workspace_api::RepositoryAccessMode, pub expires_at_epoch_seconds: u64, pub repository_id: String, pub repository_source_fingerprint: String, pub repository_uri: String, pub secret_resource: crate::resource::BackendResourceHandle, #[serde(skip, default)] pub known_hosts_entry: SensitiveString, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RepositoryMaterializationContext { pub workspace_id: String, pub runtime_id: String, pub operation_id: String, pub config_revision: u64, pub config_projection_digest: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub ssh: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct WorkingDirectoryRepositoryAccessRequest { pub working_directory_id: String, pub materialization: RepositoryMaterializationContext, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct WorkingDirectoryRequest { pub repository: WorkingDirectoryRepository, #[serde(default)] pub materializer: MaterializerKind, /// Backend-assigned stable Workdir id. Runtimes use this when present so the /// Backend can create canonical registry rows before materialization. #[serde(default, skip_serializing_if = "Option::is_none")] pub backend_workdir_id: Option, /// Backend-authored, operation-scoped repository access and cache identity. #[serde(default, skip_serializing_if = "Option::is_none")] pub materialization: Option, } /// Backend-authorized request to freshly resolve one Repository provider ref. /// /// Runtime executes this against the registered source itself rather than a Workdir /// or Runtime cache. Secret material is fetched through `materialization` and never /// appears in the result. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RepositoryRefObservationRequest { pub repository: WorkingDirectoryRepository, pub selector: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub materialization: Option, } /// Provider-neutral proof of one freshly observed Repository ref. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RepositoryRefObservation { pub repository_id: String, pub source_revision: u64, pub source_fingerprint: String, pub selector: String, pub revision_ref: String, pub observed_at_epoch_seconds: u64, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct WorkingDirectoryClaim { pub working_directory_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub relative_cwd: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct WorkingDirectoryStatus { pub summary: WorkingDirectorySummary, } #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct WorkspaceApiRef { pub workspace_id: String, pub base_url: String, } impl std::fmt::Debug for WorkspaceApiRef { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter .debug_struct("WorkspaceApiRef") .field("workspace_id", &self.workspace_id) .field("base_url", &self.base_url) .finish() } } /// Canonical Runtime Worker creation request. /// /// Browser/product launch semantics are resolved by a backend before this /// request is built. The request contains only durable Runtime identity inputs: /// a backend-decided profile selector, the Decodal profile source archive source /// used to resolve that selector, optional initial user input committed as a /// protocol observation event, and an optional Runtime-owned working directory /// binding. Browser-facing status for materialized working directories is /// summarized without exposing raw host paths. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CreateWorkerRequest { /// Workspace-owned stable identity reserved before this request reaches a Runtime. pub worker_id: WorkerId, /// Canonical create-intent fingerprint bound to `worker_id` for retry recovery. pub create_fingerprint: String, pub profile: ProfileSelector, #[serde(default, skip_serializing_if = "Option::is_none")] pub display_name: Option, pub profile_source: ProfileSourceArchiveSource, #[serde(default, skip_serializing_if = "Option::is_none")] pub config_bundle: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub initial_input: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory_request: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, /// Backend-only feature enablement. Grants still define local Runtime peers; /// the Workspace provider reauthorizes its dynamic set per operation. #[serde(default, skip_serializing_if = "is_false")] pub worker_observation_enabled: bool, /// Backend-authored, bounded peer session grants. Runtime revalidates each /// requested capture against this exact canonical `(runtime_id, worker_id)` set. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub worker_observation_grants: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub workspace_api: Option, /// Backend-authored immutable Workspace Memory settings snapshot. #[serde(default, skip_serializing_if = "Option::is_none")] pub memory_settings: Option, } /// Worker lifecycle status for the in-memory embedded runtime. /// /// Run termination details are carried separately by the Worker protocol. In /// particular, cancellation returns a Worker to `Idle`; it is not a lifecycle /// state of its own. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum WorkerStatus { Idle, Running, Paused, Stopped, } impl WorkerStatus { pub fn is_active(self) -> bool { matches!(self, Self::Idle | Self::Running | Self::Paused) } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub(crate) enum WorkerRestoreIntent { Automatic, Explicit, } /// Lightweight catalog row. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct WorkerSummary { pub worker_ref: WorkerRef, pub worker_id: WorkerId, pub status: WorkerStatus, #[serde(default, skip_serializing_if = "Option::is_none")] pub worker_state: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub workspace_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, pub profile: ProfileSelector, #[serde(default, skip_serializing_if = "Option::is_none")] pub display_name: Option, pub profile_source: ProfileSourceArchiveRef, #[serde(default, skip_serializing_if = "Option::is_none")] pub config_bundle: Option, } /// Full Worker catalog/lifecycle detail. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct WorkerDetail { pub worker_ref: WorkerRef, pub worker_id: WorkerId, pub status: WorkerStatus, #[serde(default, skip_serializing_if = "Option::is_none")] pub worker_state: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub workspace_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, pub profile: ProfileSelector, #[serde(default, skip_serializing_if = "Option::is_none")] pub display_name: Option, pub profile_source: ProfileSourceArchiveRef, #[serde(default, skip_serializing_if = "Option::is_none")] pub config_bundle: Option, } /// Acknowledgement returned by stop/cancel lifecycle operations. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct WorkerLifecycleAck { pub worker_ref: WorkerRef, pub status: WorkerStatus, #[serde(default, skip_serializing_if = "Option::is_none")] pub worker_state: Option, } #[cfg(test)] mod tests { use super::WorkspaceApiRef; #[test] fn workspace_api_ref_public_schema_contains_no_source_credentials_or_claim_choices() { let value = serde_json::to_value(WorkspaceApiRef { workspace_id: "workspace-a".to_string(), base_url: "https://server.invalid".to_string(), }) .unwrap(); let object = value.as_object().unwrap(); assert_eq!(object.len(), 2); assert!(object.contains_key("workspace_id")); assert!(object.contains_key("base_url")); for forbidden in [ "runtime_id", "worker_id", "permission", "private_key", "bearer_token", "signing_handle", ] { assert!(!object.contains_key(forbidden), "unexpected {forbidden}"); } } }