agent-deploy-contract 0.3.1

Transport-neutral deploy contracts for Agent Infra
Documentation
//! Snapshot/sandbox management and sandbox ↔ Runtime Identity bindings.
//!
//! Product traffic stays on Gateway `/v1/moulin`. Deploy serves the same
//! `/internal/v1/moulin` paths. Workspace Moulin remains a separate
//! Environment provider connection.

use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

pub const SNAPSHOTS_PATH: &str = "/internal/v1/moulin/snapshots";
pub const SANDBOXES_PATH: &str = "/internal/v1/moulin/sandboxes";
pub const SANDBOX_PATH: &str = "/internal/v1/moulin/sandboxes/{sandbox}";
pub const BINDINGS_PATH: &str = "/internal/v1/moulin/runtime-bindings";
pub const BINDING_PATH: &str = "/internal/v1/moulin/runtime-bindings/{bindingId}";
pub const BINDING_BY_RUNTIME_PATH: &str = "/internal/v1/moulin/runtime-bindings:by-runtime";

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SnapshotView {
    pub id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub created_at: Option<String>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SnapshotPage {
    pub items: Vec<SnapshotView>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CreateSandboxRequest {
    pub snapshot: String,
    #[serde(default)]
    pub env: Option<BTreeMap<String, String>>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SandboxView {
    pub sandbox: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub snapshot: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub state: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub runtime_identity_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub binding_id: Option<String>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SandboxPage {
    pub items: Vec<SandboxView>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct BindRuntimeRequest {
    pub runtime_identity_id: String,
    pub sandbox: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeSandboxBinding {
    pub id: String,
    pub tenant_id: String,
    pub project_id: String,
    pub runtime_identity_id: String,
    pub sandbox: String,
    pub created_at_ms: u64,
    pub updated_at_ms: u64,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BindingPage {
    pub items: Vec<RuntimeSandboxBinding>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TenantProjectScope {
    pub tenant_id: String,
    pub project_id: String,
}

impl TenantProjectScope {
    pub fn new(
        tenant_id: impl Into<String>,
        project_id: impl Into<String>,
    ) -> Result<Self, String> {
        let tenant_id = tenant_id.into();
        let project_id = project_id.into();
        if tenant_id.trim().is_empty() || tenant_id.len() > 128 {
            return Err("tenant_id must contain 1..=128 bytes".into());
        }
        if project_id.trim().is_empty() || project_id.len() > 128 {
            return Err("project_id must contain 1..=128 bytes".into());
        }
        Ok(Self {
            tenant_id,
            project_id,
        })
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LookupBindingQuery {
    pub runtime_identity_id: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn create_sandbox_requires_snapshot() {
        let request: CreateSandboxRequest =
            serde_json::from_str(r#"{"snapshot":"python"}"#).unwrap();
        assert_eq!(request.snapshot, "python");
        assert!(request.env.is_none());
    }
}