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/// Command metadata and output are stored independently from the Environment
94/// aggregate snapshot. This keeps output appends O(chunks) instead of copying
95/// every resource in a tenant/project scope.
96#[async_trait]
97pub trait EnvironmentCommandRepository: Send + Sync + Debug {
98    async fn store_computer_command(
99        &self,
100        request: StoreComputerCommandRequest,
101    ) -> Result<StoreComputerCommandOutcome>;
102
103    async fn get_computer_command(
104        &self,
105        scope: &ResourceScope,
106        computer_id: &str,
107        command_id: &str,
108    ) -> Result<Option<ComputerCommandResource>>;
109
110    async fn transition_computer_command(
111        &self,
112        request: ComputerCommandTransition,
113    ) -> Result<ComputerCommandResource>;
114
115    async fn complete_computer_command(
116        &self,
117        request: CompleteComputerCommandRequest,
118    ) -> Result<ComputerCommandResource>;
119
120    async fn computer_command_output(
121        &self,
122        scope: &ResourceScope,
123        computer_id: &str,
124        command_id: &str,
125        after: u64,
126        limit: usize,
127    ) -> Result<ComputerCommandOutputPage>;
128}
129
130/// Production Environment persistence owns both aggregate metadata and the
131/// independently indexed command log.
132pub trait EnvironmentRepository:
133    EnvironmentMetadataRepository + EnvironmentCommandRepository
134{
135}
136impl<T> EnvironmentRepository for T where
137    T: EnvironmentMetadataRepository + EnvironmentCommandRepository
138{
139}
140
141#[async_trait]
142pub trait ComputerProvider: Send + Sync + Debug {
143    fn kind(&self) -> &str;
144    fn capabilities(&self) -> ProviderCapabilities;
145    fn validate_create(&self, _request: &ProviderComputerRequest) -> Result<()> {
146        Ok(())
147    }
148    async fn create_computer(
149        &self,
150        scope: &ResourceScope,
151        request: &ProviderComputerRequest,
152        idempotency_key: &str,
153    ) -> Result<ProviderResourceRef>;
154    async fn delete_computer(&self, external_id: &str, idempotency_key: &str) -> Result<()>;
155    async fn create_space_directory(
156        &self,
157        computer_external_id: &str,
158        space_directory: &str,
159        request: &ProviderSpaceRequest,
160        idempotency_key: &str,
161    ) -> Result<()> {
162        let _ = (
163            computer_external_id,
164            space_directory,
165            request,
166            idempotency_key,
167        );
168        Err(EnvironmentDomainError::new(
169            EnvironmentErrorCode::CapabilityUnsupported,
170            "computer does not support Space directories",
171        )
172        .into())
173    }
174    async fn delete_space_directory(
175        &self,
176        computer_external_id: &str,
177        space_directory: &str,
178        idempotency_key: &str,
179    ) -> Result<()> {
180        let _ = (computer_external_id, space_directory, idempotency_key);
181        Err(EnvironmentDomainError::new(
182            EnvironmentErrorCode::CapabilityUnsupported,
183            "computer does not support Space directories",
184        )
185        .into())
186    }
187    async fn read_file(
188        &self,
189        computer_external_id: &str,
190        space_directory: &str,
191        path: &str,
192    ) -> Result<ProviderFileContent> {
193        let _ = (computer_external_id, space_directory, path);
194        Err(EnvironmentDomainError::new(
195            EnvironmentErrorCode::CapabilityUnsupported,
196            "computer does not support file reads",
197        )
198        .into())
199    }
200    async fn write_file(
201        &self,
202        computer_external_id: &str,
203        space_directory: &str,
204        path: &str,
205        content: &[u8],
206        expected_provider_revision: Option<&str>,
207        idempotency_key: &str,
208    ) -> Result<ProviderWriteResult> {
209        let _ = (
210            computer_external_id,
211            space_directory,
212            path,
213            content,
214            expected_provider_revision,
215            idempotency_key,
216        );
217        Err(EnvironmentDomainError::new(
218            EnvironmentErrorCode::CapabilityUnsupported,
219            "computer does not support file writes",
220        )
221        .into())
222    }
223    async fn delete_file(
224        &self,
225        computer_external_id: &str,
226        space_directory: &str,
227        path: &str,
228        idempotency_key: &str,
229    ) -> Result<()> {
230        let _ = (computer_external_id, space_directory, path, idempotency_key);
231        Err(EnvironmentDomainError::new(
232            EnvironmentErrorCode::CapabilityUnsupported,
233            "computer does not support file deletes",
234        )
235        .into())
236    }
237    async fn list_files(
238        &self,
239        computer_external_id: &str,
240        space_directory: &str,
241        path: &str,
242    ) -> Result<Vec<ProviderFileEntry>> {
243        let _ = (computer_external_id, space_directory, path);
244        Err(EnvironmentDomainError::new(
245            EnvironmentErrorCode::CapabilityUnsupported,
246            "computer does not support directory listing",
247        )
248        .into())
249    }
250    async fn create_change_set(
251        &self,
252        computer_external_id: &str,
253        source_space_directory: &str,
254        target_space_directory: &str,
255        idempotency_key: &str,
256    ) -> Result<String> {
257        let _ = (
258            computer_external_id,
259            source_space_directory,
260            target_space_directory,
261            idempotency_key,
262        );
263        Err(EnvironmentDomainError::new(
264            EnvironmentErrorCode::CapabilityUnsupported,
265            "computer does not support change sets",
266        )
267        .into())
268    }
269    async fn apply_change_set(
270        &self,
271        computer_external_id: &str,
272        source_space_directory: &str,
273        target_space_directory: &str,
274        provider_change_ref: &str,
275        idempotency_key: &str,
276    ) -> Result<ProviderWriteResult> {
277        let _ = (
278            computer_external_id,
279            source_space_directory,
280            target_space_directory,
281            provider_change_ref,
282            idempotency_key,
283        );
284        Err(EnvironmentDomainError::new(
285            EnvironmentErrorCode::CapabilityUnsupported,
286            "computer does not support change sets",
287        )
288        .into())
289    }
290    async fn exec_command(
291        &self,
292        computer_external_id: &str,
293        space_directory: &str,
294        command: &CommandSpec,
295    ) -> Result<ProviderCommandOutput> {
296        let _ = (computer_external_id, space_directory, command);
297        Err(EnvironmentDomainError::new(
298            EnvironmentErrorCode::CapabilityUnsupported,
299            "computer does not support managed commands",
300        )
301        .into())
302    }
303    async fn exec_managed_command(
304        &self,
305        computer_external_id: &str,
306        space_directory: &str,
307        command_id: &str,
308        command: &CommandSpec,
309    ) -> Result<ProviderCommandOutput> {
310        let _ = command_id;
311        self.exec_command(computer_external_id, space_directory, command)
312            .await
313    }
314    async fn resume_managed_command(
315        &self,
316        computer_external_id: &str,
317        space_directory: &str,
318        command_id: &str,
319        command: &CommandSpec,
320    ) -> Result<Option<ProviderCommandOutput>> {
321        let _ = (computer_external_id, space_directory, command_id, command);
322        Ok(None)
323    }
324    async fn retry_managed_command(
325        &self,
326        computer_external_id: &str,
327        space_directory: &str,
328        command_id: &str,
329        command: &CommandSpec,
330    ) -> Result<Option<ProviderCommandOutput>> {
331        self.resume_managed_command(computer_external_id, space_directory, command_id, command)
332            .await
333    }
334    async fn cancel_command(
335        &self,
336        computer_external_id: &str,
337        space_directory: &str,
338        command_id: &str,
339    ) -> Result<()> {
340        let _ = (computer_external_id, space_directory, command_id);
341        Ok(())
342    }
343}
344
345#[async_trait]
346pub trait SandboxProvider: Send + Sync + Debug {
347    fn kind(&self) -> &str;
348    fn capabilities(&self) -> ProviderCapabilities;
349    fn validate_create(&self, _request: &ProviderSandboxRequest) -> Result<()> {
350        Ok(())
351    }
352    async fn create_sandbox(
353        &self,
354        scope: &ResourceScope,
355        request: &ProviderSandboxRequest,
356        idempotency_key: &str,
357    ) -> Result<ProviderResourceRef>;
358    async fn stop_sandbox(&self, external_id: &str, idempotency_key: &str) -> Result<()>;
359    async fn write_file(
360        &self,
361        external_id: &str,
362        path: &str,
363        content: &[u8],
364        expected_provider_revision: Option<&str>,
365        idempotency_key: &str,
366    ) -> Result<ProviderWriteResult> {
367        let _ = (
368            external_id,
369            path,
370            content,
371            expected_provider_revision,
372            idempotency_key,
373        );
374        Err(EnvironmentDomainError::new(
375            EnvironmentErrorCode::CapabilityUnsupported,
376            "sandbox does not support file writes",
377        )
378        .into())
379    }
380    async fn exec_command(
381        &self,
382        external_id: &str,
383        command: &CommandSpec,
384    ) -> Result<ProviderCommandOutput> {
385        let _ = (external_id, command);
386        Err(EnvironmentDomainError::new(
387            EnvironmentErrorCode::CapabilityUnsupported,
388            "sandbox does not support managed commands",
389        )
390        .into())
391    }
392}
393
394#[async_trait]
395pub trait SpaceProvider: Send + Sync + Debug {
396    fn kind(&self) -> &str;
397    fn capabilities(&self) -> ProviderCapabilities;
398    async fn create_space(
399        &self,
400        scope: &ResourceScope,
401        request: &ProviderSpaceRequest,
402        idempotency_key: &str,
403    ) -> Result<ProviderResourceRef>;
404    async fn read_file(&self, external_id: &str, path: &str) -> Result<ProviderFileContent>;
405    async fn write_file(
406        &self,
407        external_id: &str,
408        path: &str,
409        content: &[u8],
410        expected_provider_revision: Option<&str>,
411        idempotency_key: &str,
412    ) -> Result<ProviderWriteResult>;
413    async fn create_change_set(
414        &self,
415        source_external_id: &str,
416        target_external_id: &str,
417        idempotency_key: &str,
418    ) -> Result<String>;
419    async fn apply_change_set(
420        &self,
421        source_external_id: &str,
422        target_external_id: &str,
423        provider_change_ref: &str,
424        idempotency_key: &str,
425    ) -> Result<ProviderWriteResult>;
426    async fn delete_space(&self, external_id: &str, idempotency_key: &str) -> Result<()>;
427}
428
429#[async_trait]
430pub trait SnapshotProviderPort: Send + Sync + Debug {
431    fn kind(&self) -> &str;
432    async fn capture_snapshot(
433        &self,
434        source: &ProviderSnapshotSource,
435        idempotency_key: &str,
436    ) -> Result<ProviderResourceRef>;
437    async fn delete_snapshot(
438        &self,
439        snapshot_external_id: &str,
440        idempotency_key: &str,
441    ) -> Result<()>;
442}
443
444#[async_trait]
445pub trait TemplateProviderPort: Send + Sync + Debug {
446    fn kind(&self) -> &str;
447    async fn inspect_template(&self, external_id: &str) -> Result<ProviderCapabilities>;
448}
449
450pub trait EnvironmentProviderRegistry: Send + Sync + Debug {
451    fn computer(&self, provider: &str) -> Result<Arc<dyn ComputerProvider>>;
452    fn sandbox(&self, provider: &str) -> Result<Arc<dyn SandboxProvider>>;
453    fn space(&self, provider: &str) -> Result<Arc<dyn SpaceProvider>>;
454    fn snapshot(&self, provider: &str) -> Result<Arc<dyn SnapshotProviderPort>>;
455    fn template(&self, provider: &str) -> Result<Arc<dyn TemplateProviderPort>>;
456}