Skip to main content

agent_deploy_contract/
moulin.rs

1//! Snapshot/sandbox management and sandbox ↔ Runtime Identity bindings.
2//!
3//! Product traffic stays on Gateway `/v1/moulin`. Deploy serves the same
4//! `/internal/v1/moulin` paths. Workspace Moulin remains a separate
5//! Environment provider connection.
6
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10pub const SNAPSHOTS_PATH: &str = "/internal/v1/moulin/snapshots";
11pub const SANDBOXES_PATH: &str = "/internal/v1/moulin/sandboxes";
12pub const SANDBOX_PATH: &str = "/internal/v1/moulin/sandboxes/{sandbox}";
13pub const BINDINGS_PATH: &str = "/internal/v1/moulin/runtime-bindings";
14pub const BINDING_PATH: &str = "/internal/v1/moulin/runtime-bindings/{bindingId}";
15pub const BINDING_BY_RUNTIME_PATH: &str = "/internal/v1/moulin/runtime-bindings:by-runtime";
16
17#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct SnapshotView {
20    pub id: String,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub name: Option<String>,
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub created_at: Option<String>,
25}
26
27#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "camelCase")]
29pub struct SnapshotPage {
30    pub items: Vec<SnapshotView>,
31}
32
33#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase", deny_unknown_fields)]
35pub struct CreateSandboxRequest {
36    pub snapshot: String,
37    #[serde(default)]
38    pub env: Option<BTreeMap<String, String>>,
39}
40
41#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct SandboxView {
44    pub sandbox: String,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub snapshot: Option<String>,
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub state: Option<String>,
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub runtime_identity_id: Option<String>,
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub binding_id: Option<String>,
53}
54
55#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub struct SandboxPage {
58    pub items: Vec<SandboxView>,
59}
60
61#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase", deny_unknown_fields)]
63pub struct BindRuntimeRequest {
64    pub runtime_identity_id: String,
65    pub sandbox: String,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct RuntimeSandboxBinding {
71    pub id: String,
72    pub tenant_id: String,
73    pub project_id: String,
74    pub runtime_identity_id: String,
75    pub sandbox: String,
76    pub created_at_ms: u64,
77    pub updated_at_ms: u64,
78}
79
80#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct BindingPage {
83    pub items: Vec<RuntimeSandboxBinding>,
84}
85
86#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "camelCase")]
88pub struct TenantProjectScope {
89    pub tenant_id: String,
90    pub project_id: String,
91}
92
93impl TenantProjectScope {
94    pub fn new(
95        tenant_id: impl Into<String>,
96        project_id: impl Into<String>,
97    ) -> Result<Self, String> {
98        let tenant_id = tenant_id.into();
99        let project_id = project_id.into();
100        if tenant_id.trim().is_empty() || tenant_id.len() > 128 {
101            return Err("tenant_id must contain 1..=128 bytes".into());
102        }
103        if project_id.trim().is_empty() || project_id.len() > 128 {
104            return Err("project_id must contain 1..=128 bytes".into());
105        }
106        Ok(Self {
107            tenant_id,
108            project_id,
109        })
110    }
111}
112
113#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct LookupBindingQuery {
116    pub runtime_identity_id: String,
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn create_sandbox_requires_snapshot() {
125        let request: CreateSandboxRequest =
126            serde_json::from_str(r#"{"snapshot":"python"}"#).unwrap();
127        assert_eq!(request.snapshot, "python");
128        assert!(request.env.is_none());
129    }
130}