Skip to main content

agent_workspace_contract/environment/
ports.rs

1use super::*;
2use crate::CommandSpec;
3
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(deny_unknown_fields, rename_all = "camelCase")]
6pub enum EnvironmentIdempotencyState {
7    Pending,
8    Completed,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(deny_unknown_fields, rename_all = "camelCase")]
13pub struct EnvironmentIdempotencyRecord {
14    pub id: String,
15    pub scope: ResourceScope,
16    pub action: String,
17    /// SHA-256 of the caller-provided key; the key itself is never persisted.
18    pub key_hash: String,
19    pub request_hash: String,
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub resource_id: Option<String>,
22    pub state: EnvironmentIdempotencyState,
23    /// Runtime instance currently allowed to enter Provider I/O.
24    pub claim_owner: String,
25    /// A dead instance can be taken over only after this lease expires.
26    pub claim_expires_at_ms: u64,
27    pub attempt: u32,
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub result_json: Option<String>,
30    pub started_at_ms: u64,
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub completed_at_ms: Option<u64>,
33}
34
35/// Durable metadata snapshot for one tenant/project scope.
36///
37/// Provider bytes and credentials are deliberately absent. Repositories may
38/// store each collection in a dedicated table while exposing one CAS unit to
39/// the application service so fencing and revision changes commit together.
40#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(deny_unknown_fields, rename_all = "camelCase")]
42pub struct EnvironmentMetadataSnapshot {
43    pub version: u64,
44    #[serde(default)]
45    pub computers: Vec<ComputerRecord>,
46    #[serde(default)]
47    pub spaces: Vec<SpaceRecord>,
48    #[serde(default)]
49    pub sandboxes: Vec<SandboxRecord>,
50    #[serde(default)]
51    pub snapshots: Vec<SnapshotRecord>,
52    #[serde(default)]
53    pub templates: Vec<TemplateVersionRecord>,
54    #[serde(default)]
55    pub grants: Vec<SpaceGrant>,
56    #[serde(default)]
57    pub sessions: Vec<SpaceCollaborationSession>,
58    #[serde(default)]
59    pub tickets: Vec<SpaceAccessTicketRecord>,
60    #[serde(default)]
61    pub leases: Vec<SpaceWriteLease>,
62    #[serde(default)]
63    pub fencing_tokens: BTreeMap<String, u64>,
64    #[serde(default)]
65    pub delegations: Vec<DelegationRecord>,
66    #[serde(default)]
67    pub change_sets: Vec<SpaceChangeSetRecord>,
68    #[serde(default)]
69    pub idempotency: Vec<EnvironmentIdempotencyRecord>,
70}
71
72#[async_trait]
73pub trait EnvironmentMetadataRepository: Send + Sync + Debug {
74    async fn load(&self, scope: &ResourceScope) -> Result<EnvironmentMetadataSnapshot>;
75
76    /// Stable tenant/project pagination for background lifecycle reconciliation.
77    async fn list_scopes(
78        &self,
79        after: Option<&ResourceScope>,
80        limit: usize,
81    ) -> Result<Vec<ResourceScope>>;
82
83    /// Atomically replace metadata when the stored scope version equals
84    /// `expected_version`. Returns false on a concurrent writer conflict.
85    async fn compare_and_swap(
86        &self,
87        scope: &ResourceScope,
88        expected_version: u64,
89        snapshot: &EnvironmentMetadataSnapshot,
90    ) -> Result<bool>;
91}
92
93#[async_trait]
94pub trait ComputerProvider: Send + Sync + Debug {
95    fn kind(&self) -> &str;
96    fn capabilities(&self) -> ProviderCapabilities;
97    fn validate_create(&self, _request: &ProviderComputerRequest) -> Result<()> {
98        Ok(())
99    }
100    async fn create_computer(
101        &self,
102        scope: &ResourceScope,
103        request: &ProviderComputerRequest,
104        idempotency_key: &str,
105    ) -> Result<ProviderResourceRef>;
106    async fn delete_computer(&self, external_id: &str, idempotency_key: &str) -> Result<()>;
107    async fn create_space_directory(
108        &self,
109        computer_external_id: &str,
110        space_directory: &str,
111        request: &ProviderSpaceRequest,
112        idempotency_key: &str,
113    ) -> Result<()> {
114        let _ = (
115            computer_external_id,
116            space_directory,
117            request,
118            idempotency_key,
119        );
120        Err(EnvironmentDomainError::new(
121            EnvironmentErrorCode::CapabilityUnsupported,
122            "computer does not support Space directories",
123        )
124        .into())
125    }
126    async fn delete_space_directory(
127        &self,
128        computer_external_id: &str,
129        space_directory: &str,
130        idempotency_key: &str,
131    ) -> Result<()> {
132        let _ = (computer_external_id, space_directory, idempotency_key);
133        Err(EnvironmentDomainError::new(
134            EnvironmentErrorCode::CapabilityUnsupported,
135            "computer does not support Space directories",
136        )
137        .into())
138    }
139    async fn read_file(
140        &self,
141        computer_external_id: &str,
142        space_directory: &str,
143        path: &str,
144    ) -> Result<ProviderFileContent> {
145        let _ = (computer_external_id, space_directory, path);
146        Err(EnvironmentDomainError::new(
147            EnvironmentErrorCode::CapabilityUnsupported,
148            "computer does not support file reads",
149        )
150        .into())
151    }
152    async fn write_file(
153        &self,
154        computer_external_id: &str,
155        space_directory: &str,
156        path: &str,
157        content: &[u8],
158        expected_provider_revision: Option<&str>,
159        idempotency_key: &str,
160    ) -> Result<ProviderWriteResult> {
161        let _ = (
162            computer_external_id,
163            space_directory,
164            path,
165            content,
166            expected_provider_revision,
167            idempotency_key,
168        );
169        Err(EnvironmentDomainError::new(
170            EnvironmentErrorCode::CapabilityUnsupported,
171            "computer does not support file writes",
172        )
173        .into())
174    }
175    async fn create_change_set(
176        &self,
177        computer_external_id: &str,
178        source_space_directory: &str,
179        target_space_directory: &str,
180        idempotency_key: &str,
181    ) -> Result<String> {
182        let _ = (
183            computer_external_id,
184            source_space_directory,
185            target_space_directory,
186            idempotency_key,
187        );
188        Err(EnvironmentDomainError::new(
189            EnvironmentErrorCode::CapabilityUnsupported,
190            "computer does not support change sets",
191        )
192        .into())
193    }
194    async fn apply_change_set(
195        &self,
196        computer_external_id: &str,
197        source_space_directory: &str,
198        target_space_directory: &str,
199        provider_change_ref: &str,
200        idempotency_key: &str,
201    ) -> Result<ProviderWriteResult> {
202        let _ = (
203            computer_external_id,
204            source_space_directory,
205            target_space_directory,
206            provider_change_ref,
207            idempotency_key,
208        );
209        Err(EnvironmentDomainError::new(
210            EnvironmentErrorCode::CapabilityUnsupported,
211            "computer does not support change sets",
212        )
213        .into())
214    }
215    async fn exec_command(
216        &self,
217        computer_external_id: &str,
218        space_directory: &str,
219        command: &CommandSpec,
220    ) -> Result<ProviderCommandOutput> {
221        let _ = (computer_external_id, space_directory, command);
222        Err(EnvironmentDomainError::new(
223            EnvironmentErrorCode::CapabilityUnsupported,
224            "computer does not support managed commands",
225        )
226        .into())
227    }
228}
229
230#[async_trait]
231pub trait SandboxProvider: Send + Sync + Debug {
232    fn kind(&self) -> &str;
233    fn capabilities(&self) -> ProviderCapabilities;
234    fn validate_create(&self, _request: &ProviderSandboxRequest) -> Result<()> {
235        Ok(())
236    }
237    async fn create_sandbox(
238        &self,
239        scope: &ResourceScope,
240        request: &ProviderSandboxRequest,
241        idempotency_key: &str,
242    ) -> Result<ProviderResourceRef>;
243    async fn stop_sandbox(&self, external_id: &str, idempotency_key: &str) -> Result<()>;
244    async fn write_file(
245        &self,
246        external_id: &str,
247        path: &str,
248        content: &[u8],
249        expected_provider_revision: Option<&str>,
250        idempotency_key: &str,
251    ) -> Result<ProviderWriteResult> {
252        let _ = (
253            external_id,
254            path,
255            content,
256            expected_provider_revision,
257            idempotency_key,
258        );
259        Err(EnvironmentDomainError::new(
260            EnvironmentErrorCode::CapabilityUnsupported,
261            "sandbox does not support file writes",
262        )
263        .into())
264    }
265    async fn exec_command(
266        &self,
267        external_id: &str,
268        command: &CommandSpec,
269    ) -> Result<ProviderCommandOutput> {
270        let _ = (external_id, command);
271        Err(EnvironmentDomainError::new(
272            EnvironmentErrorCode::CapabilityUnsupported,
273            "sandbox does not support managed commands",
274        )
275        .into())
276    }
277}
278
279#[async_trait]
280pub trait SpaceProvider: Send + Sync + Debug {
281    fn kind(&self) -> &str;
282    fn capabilities(&self) -> ProviderCapabilities;
283    async fn create_space(
284        &self,
285        scope: &ResourceScope,
286        request: &ProviderSpaceRequest,
287        idempotency_key: &str,
288    ) -> Result<ProviderResourceRef>;
289    async fn read_file(&self, external_id: &str, path: &str) -> Result<ProviderFileContent>;
290    async fn write_file(
291        &self,
292        external_id: &str,
293        path: &str,
294        content: &[u8],
295        expected_provider_revision: Option<&str>,
296        idempotency_key: &str,
297    ) -> Result<ProviderWriteResult>;
298    async fn create_change_set(
299        &self,
300        source_external_id: &str,
301        target_external_id: &str,
302        idempotency_key: &str,
303    ) -> Result<String>;
304    async fn apply_change_set(
305        &self,
306        source_external_id: &str,
307        target_external_id: &str,
308        provider_change_ref: &str,
309        idempotency_key: &str,
310    ) -> Result<ProviderWriteResult>;
311    async fn delete_space(&self, external_id: &str, idempotency_key: &str) -> Result<()>;
312}
313
314#[async_trait]
315pub trait SnapshotProviderPort: Send + Sync + Debug {
316    fn kind(&self) -> &str;
317    async fn capture_snapshot(
318        &self,
319        source: &ProviderSnapshotSource,
320        idempotency_key: &str,
321    ) -> Result<ProviderResourceRef>;
322    async fn delete_snapshot(
323        &self,
324        snapshot_external_id: &str,
325        idempotency_key: &str,
326    ) -> Result<()>;
327}
328
329#[async_trait]
330pub trait TemplateProviderPort: Send + Sync + Debug {
331    fn kind(&self) -> &str;
332    async fn inspect_template(&self, external_id: &str) -> Result<ProviderCapabilities>;
333}
334
335pub trait EnvironmentProviderRegistry: Send + Sync + Debug {
336    fn computer(&self, provider: &str) -> Result<Arc<dyn ComputerProvider>>;
337    fn sandbox(&self, provider: &str) -> Result<Arc<dyn SandboxProvider>>;
338    fn space(&self, provider: &str) -> Result<Arc<dyn SpaceProvider>>;
339    fn snapshot(&self, provider: &str) -> Result<Arc<dyn SnapshotProviderPort>>;
340    fn template(&self, provider: &str) -> Result<Arc<dyn TemplateProviderPort>>;
341}