372 lines
13 KiB
Rust
372 lines
13 KiB
Rust
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<String> for RepositorySelector {
|
|
fn from(value: String) -> Self {
|
|
Self(value)
|
|
}
|
|
}
|
|
|
|
impl AsRef<str> 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<RepositorySelector>,
|
|
}
|
|
|
|
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<String>) -> 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<RepositorySshCredentialCandidate>,
|
|
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<RepositorySshMaterializationAccess>,
|
|
}
|
|
|
|
#[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<String>,
|
|
/// Backend-authored, operation-scoped repository access and cache identity.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub materialization: Option<RepositoryMaterializationContext>,
|
|
}
|
|
|
|
/// 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<RepositoryMaterializationContext>,
|
|
}
|
|
|
|
/// 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<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
pub profile_source: ProfileSourceArchiveSource,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub config_bundle: Option<ConfigBundleRef>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub initial_input: Option<WorkerInput>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub working_directory_request: Option<WorkingDirectoryRequest>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub working_directory: Option<WorkingDirectoryClaim>,
|
|
/// 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<RuntimeWorkerRef>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub workspace_api: Option<WorkspaceApiRef>,
|
|
/// Backend-authored immutable Workspace Memory settings snapshot.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub memory_settings: Option<manifest::WorkspaceMemorySettingsSnapshot>,
|
|
}
|
|
|
|
/// 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<protocol::WorkerStateSnapshot>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub workspace_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub working_directory: Option<WorkingDirectoryStatus>,
|
|
pub profile: ProfileSelector,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub display_name: Option<String>,
|
|
pub profile_source: ProfileSourceArchiveRef,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub config_bundle: Option<ConfigBundleRef>,
|
|
}
|
|
|
|
/// 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<protocol::WorkerStateSnapshot>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub workspace_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub working_directory: Option<WorkingDirectoryStatus>,
|
|
pub profile: ProfileSelector,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub display_name: Option<String>,
|
|
pub profile_source: ProfileSourceArchiveRef,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub config_bundle: Option<ConfigBundleRef>,
|
|
}
|
|
|
|
/// 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<protocol::WorkerStateSnapshot>,
|
|
}
|
|
|
|
#[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}");
|
|
}
|
|
}
|
|
}
|