agent-workspace-contract 0.3.0

Transport-neutral contracts for Agent Infra workspace APIs
Documentation
use super::*;
use crate::CommandSpec;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub enum EnvironmentIdempotencyState {
    Pending,
    Completed,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct EnvironmentIdempotencyRecord {
    pub id: String,
    pub scope: ResourceScope,
    pub action: String,
    /// SHA-256 of the caller-provided key; the key itself is never persisted.
    pub key_hash: String,
    pub request_hash: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resource_id: Option<String>,
    pub state: EnvironmentIdempotencyState,
    /// Runtime instance currently allowed to enter Provider I/O.
    pub claim_owner: String,
    /// A dead instance can be taken over only after this lease expires.
    pub claim_expires_at_ms: u64,
    pub attempt: u32,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub result_json: Option<String>,
    pub started_at_ms: u64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub completed_at_ms: Option<u64>,
}

/// Durable metadata snapshot for one tenant/project scope.
///
/// Provider bytes and credentials are deliberately absent. Repositories may
/// store each collection in a dedicated table while exposing one CAS unit to
/// the application service so fencing and revision changes commit together.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct EnvironmentMetadataSnapshot {
    pub version: u64,
    #[serde(default)]
    pub computers: Vec<ComputerRecord>,
    #[serde(default)]
    pub spaces: Vec<SpaceRecord>,
    #[serde(default)]
    pub sandboxes: Vec<SandboxRecord>,
    #[serde(default)]
    pub snapshots: Vec<SnapshotRecord>,
    #[serde(default)]
    pub templates: Vec<TemplateVersionRecord>,
    #[serde(default)]
    pub grants: Vec<SpaceGrant>,
    #[serde(default)]
    pub sessions: Vec<SpaceCollaborationSession>,
    #[serde(default)]
    pub tickets: Vec<SpaceAccessTicketRecord>,
    #[serde(default)]
    pub leases: Vec<SpaceWriteLease>,
    #[serde(default)]
    pub fencing_tokens: BTreeMap<String, u64>,
    #[serde(default)]
    pub delegations: Vec<DelegationRecord>,
    #[serde(default)]
    pub change_sets: Vec<SpaceChangeSetRecord>,
    #[serde(default)]
    pub idempotency: Vec<EnvironmentIdempotencyRecord>,
}

#[async_trait]
pub trait EnvironmentMetadataRepository: Send + Sync + Debug {
    async fn load(&self, scope: &ResourceScope) -> Result<EnvironmentMetadataSnapshot>;

    /// Stable tenant/project pagination for background lifecycle reconciliation.
    async fn list_scopes(
        &self,
        after: Option<&ResourceScope>,
        limit: usize,
    ) -> Result<Vec<ResourceScope>>;

    /// Atomically replace metadata when the stored scope version equals
    /// `expected_version`. Returns false on a concurrent writer conflict.
    async fn compare_and_swap(
        &self,
        scope: &ResourceScope,
        expected_version: u64,
        snapshot: &EnvironmentMetadataSnapshot,
    ) -> Result<bool>;
}

#[async_trait]
pub trait ComputerProvider: Send + Sync + Debug {
    fn kind(&self) -> &str;
    fn capabilities(&self) -> ProviderCapabilities;
    fn validate_create(&self, _request: &ProviderComputerRequest) -> Result<()> {
        Ok(())
    }
    async fn create_computer(
        &self,
        scope: &ResourceScope,
        request: &ProviderComputerRequest,
        idempotency_key: &str,
    ) -> Result<ProviderResourceRef>;
    async fn delete_computer(&self, external_id: &str, idempotency_key: &str) -> Result<()>;
    async fn create_space_directory(
        &self,
        computer_external_id: &str,
        space_directory: &str,
        request: &ProviderSpaceRequest,
        idempotency_key: &str,
    ) -> Result<()> {
        let _ = (
            computer_external_id,
            space_directory,
            request,
            idempotency_key,
        );
        Err(EnvironmentDomainError::new(
            EnvironmentErrorCode::CapabilityUnsupported,
            "computer does not support Space directories",
        )
        .into())
    }
    async fn delete_space_directory(
        &self,
        computer_external_id: &str,
        space_directory: &str,
        idempotency_key: &str,
    ) -> Result<()> {
        let _ = (computer_external_id, space_directory, idempotency_key);
        Err(EnvironmentDomainError::new(
            EnvironmentErrorCode::CapabilityUnsupported,
            "computer does not support Space directories",
        )
        .into())
    }
    async fn read_file(
        &self,
        computer_external_id: &str,
        space_directory: &str,
        path: &str,
    ) -> Result<ProviderFileContent> {
        let _ = (computer_external_id, space_directory, path);
        Err(EnvironmentDomainError::new(
            EnvironmentErrorCode::CapabilityUnsupported,
            "computer does not support file reads",
        )
        .into())
    }
    async fn write_file(
        &self,
        computer_external_id: &str,
        space_directory: &str,
        path: &str,
        content: &[u8],
        expected_provider_revision: Option<&str>,
        idempotency_key: &str,
    ) -> Result<ProviderWriteResult> {
        let _ = (
            computer_external_id,
            space_directory,
            path,
            content,
            expected_provider_revision,
            idempotency_key,
        );
        Err(EnvironmentDomainError::new(
            EnvironmentErrorCode::CapabilityUnsupported,
            "computer does not support file writes",
        )
        .into())
    }
    async fn create_change_set(
        &self,
        computer_external_id: &str,
        source_space_directory: &str,
        target_space_directory: &str,
        idempotency_key: &str,
    ) -> Result<String> {
        let _ = (
            computer_external_id,
            source_space_directory,
            target_space_directory,
            idempotency_key,
        );
        Err(EnvironmentDomainError::new(
            EnvironmentErrorCode::CapabilityUnsupported,
            "computer does not support change sets",
        )
        .into())
    }
    async fn apply_change_set(
        &self,
        computer_external_id: &str,
        source_space_directory: &str,
        target_space_directory: &str,
        provider_change_ref: &str,
        idempotency_key: &str,
    ) -> Result<ProviderWriteResult> {
        let _ = (
            computer_external_id,
            source_space_directory,
            target_space_directory,
            provider_change_ref,
            idempotency_key,
        );
        Err(EnvironmentDomainError::new(
            EnvironmentErrorCode::CapabilityUnsupported,
            "computer does not support change sets",
        )
        .into())
    }
    async fn exec_command(
        &self,
        computer_external_id: &str,
        space_directory: &str,
        command: &CommandSpec,
    ) -> Result<ProviderCommandOutput> {
        let _ = (computer_external_id, space_directory, command);
        Err(EnvironmentDomainError::new(
            EnvironmentErrorCode::CapabilityUnsupported,
            "computer does not support managed commands",
        )
        .into())
    }
}

#[async_trait]
pub trait SandboxProvider: Send + Sync + Debug {
    fn kind(&self) -> &str;
    fn capabilities(&self) -> ProviderCapabilities;
    fn validate_create(&self, _request: &ProviderSandboxRequest) -> Result<()> {
        Ok(())
    }
    async fn create_sandbox(
        &self,
        scope: &ResourceScope,
        request: &ProviderSandboxRequest,
        idempotency_key: &str,
    ) -> Result<ProviderResourceRef>;
    async fn stop_sandbox(&self, external_id: &str, idempotency_key: &str) -> Result<()>;
}

#[async_trait]
pub trait SpaceProvider: Send + Sync + Debug {
    fn kind(&self) -> &str;
    fn capabilities(&self) -> ProviderCapabilities;
    async fn create_space(
        &self,
        scope: &ResourceScope,
        request: &ProviderSpaceRequest,
        idempotency_key: &str,
    ) -> Result<ProviderResourceRef>;
    async fn read_file(&self, external_id: &str, path: &str) -> Result<ProviderFileContent>;
    async fn write_file(
        &self,
        external_id: &str,
        path: &str,
        content: &[u8],
        expected_provider_revision: Option<&str>,
        idempotency_key: &str,
    ) -> Result<ProviderWriteResult>;
    async fn create_change_set(
        &self,
        source_external_id: &str,
        target_external_id: &str,
        idempotency_key: &str,
    ) -> Result<String>;
    async fn apply_change_set(
        &self,
        source_external_id: &str,
        target_external_id: &str,
        provider_change_ref: &str,
        idempotency_key: &str,
    ) -> Result<ProviderWriteResult>;
    async fn delete_space(&self, external_id: &str, idempotency_key: &str) -> Result<()>;
}

#[async_trait]
pub trait SnapshotProviderPort: Send + Sync + Debug {
    fn kind(&self) -> &str;
    async fn capture_snapshot(
        &self,
        source: &ProviderSnapshotSource,
        idempotency_key: &str,
    ) -> Result<ProviderResourceRef>;
    async fn delete_snapshot(
        &self,
        snapshot_external_id: &str,
        idempotency_key: &str,
    ) -> Result<()>;
}

#[async_trait]
pub trait TemplateProviderPort: Send + Sync + Debug {
    fn kind(&self) -> &str;
    async fn inspect_template(&self, external_id: &str) -> Result<ProviderCapabilities>;
}

pub trait EnvironmentProviderRegistry: Send + Sync + Debug {
    fn computer(&self, provider: &str) -> Result<Arc<dyn ComputerProvider>>;
    fn sandbox(&self, provider: &str) -> Result<Arc<dyn SandboxProvider>>;
    fn space(&self, provider: &str) -> Result<Arc<dyn SpaceProvider>>;
    fn snapshot(&self, provider: &str) -> Result<Arc<dyn SnapshotProviderPort>>;
    fn template(&self, provider: &str) -> Result<Arc<dyn TemplateProviderPort>>;
}