1use super::*;
2use std::collections::BTreeMap;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum WorkspaceLifecycle {
7 Provisioning,
8 Ready,
9 Suspended,
10 Deleting,
11 Deleted,
12 Error,
13}
14
15pub const WORKSPACES_PATH: &str = "/internal/v1/workspaces";
16pub const WORKSPACE_PATH: &str = "/internal/v1/workspaces/{workspaceId}";
17pub const WORKSPACE_SUSPEND_PATH: &str = "/internal/v1/workspaces/{workspaceId}/suspend";
18pub const WORKSPACE_RESUME_PATH: &str = "/internal/v1/workspaces/{workspaceId}/resume";
19pub const WORKSPACE_RECONCILE_PATH: &str = "/internal/v1/workspaces/{workspaceId}/reconcile";
20pub const WORKSPACE_USAGE_PATH: &str = "/internal/v1/workspaces/{workspaceId}/usage";
21pub const WORKSPACE_OPERATION_CANCEL_PATH: &str =
22 "/internal/v1/workspace-operations/{operationId}/cancel";
23pub const WORKSPACE_MAX_PAGE_SIZE: usize = 200;
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields, rename_all = "camelCase")]
27pub struct CreateWorkspaceRequest {
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub id: Option<String>,
30 pub project_id: String,
31 pub config: WorkspaceConfig,
32 #[serde(default)]
33 pub labels: BTreeMap<String, String>,
34 #[serde(default)]
35 pub relations: agent_registry_contract::ResourceRelations,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(deny_unknown_fields, rename_all = "camelCase")]
40pub struct WorkspaceActionRequest {
41 pub expected_version: u64,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub reason: Option<String>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct WorkspacePage {
49 pub items: Vec<WorkspaceRecord>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub next_cursor: Option<String>,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct ProvisionedWorkspace {
57 pub durable_backend_id: String,
58 pub root: String,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct WorkspaceLease {
64 pub owner: String,
65 pub deadline_ms: u64,
66 pub fencing_token: u64,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "camelCase")]
71pub struct WorkspaceRecord {
72 pub id: String,
73 pub tenant_id: String,
74 pub project_id: String,
75 pub backend: String,
76 pub durable_backend_id: String,
77 pub root: String,
78 #[serde(default)]
79 pub config: WorkspaceConfig,
80 pub lifecycle: WorkspaceLifecycle,
81 pub version: u64,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub lease: Option<WorkspaceLease>,
84 pub created_at_ms: u64,
85 pub updated_at_ms: u64,
86 #[serde(default)]
87 pub labels: BTreeMap<String, String>,
88 #[serde(default)]
89 pub relations: agent_registry_contract::ResourceRelations,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "snake_case")]
94pub enum OperationState {
95 Queued,
96 Running,
97 Succeeded,
98 Failed,
99 Canceling,
100 Canceled,
101}
102
103impl OperationState {
104 pub fn terminal(self) -> bool {
105 matches!(self, Self::Succeeded | Self::Failed | Self::Canceled)
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct WorkspaceOperation {
112 pub id: String,
113 pub tenant_id: String,
114 pub workspace_id: String,
115 pub kind: String,
116 pub state: OperationState,
117 pub phase: String,
118 pub attempt: u32,
119 pub version: u64,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub lease: Option<WorkspaceLease>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub error_code: Option<String>,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub result_resource_id: Option<String>,
126 pub created_at_ms: u64,
127 pub updated_at_ms: u64,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub next_poll_after_ms: Option<u64>,
130}
131
132impl WorkspaceOperation {
133 pub fn refresh_poll_hint(&mut self) {
134 self.next_poll_after_ms = match self.state {
135 OperationState::Queued => Some(250),
136 OperationState::Running | OperationState::Canceling => Some(500),
137 OperationState::Succeeded | OperationState::Failed | OperationState::Canceled => None,
138 };
139 }
140}
141
142#[derive(Debug, Clone)]
143pub struct OperationTransition {
144 pub operation_id: String,
145 pub owner: String,
146 pub fencing_token: u64,
147 pub expected_version: u64,
148 pub state: OperationState,
149 pub phase: String,
150 pub error_code: Option<String>,
151 pub result_resource_id: Option<String>,
152 pub now_ms: u64,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct StoredChangeSet {
158 pub tenant_id: String,
159 pub workspace_id: String,
160 pub change_set: WorkspaceChangeSet,
161 pub base_version: u64,
162 pub workspace_version: u64,
163 pub expires_at_ms: u64,
164 pub request_digest: String,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct StoredPreview {
170 pub id: String,
171 pub tenant_id: String,
172 pub workspace_id: String,
173 pub provider_ref: String,
174 pub url: String,
175 pub state: String,
176 pub expires_at_ms: u64,
177 pub version: u64,
178 #[serde(default)]
179 pub request_digest: String,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct CommandSpec {
185 #[serde(default, skip_serializing_if = "command_env_inherited")]
187 pub isolated_env: bool,
188 pub argv: Vec<String>,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub cwd: Option<String>,
191 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
196 pub env: BTreeMap<String, String>,
197 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
202 pub env_refs: BTreeMap<String, agent_secret_contract::SecretRef>,
203 #[serde(default)]
204 pub shell: bool,
205 pub timeout_ms: u64,
206 pub memory_bytes: u64,
207 pub cpu_millis: u64,
208 pub max_processes: u32,
209 #[serde(default)]
212 pub network: CommandNetworkPolicy,
213 #[serde(default = "default_command_disk_bytes")]
215 pub disk_bytes: u64,
216 pub max_output_bytes: usize,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
220#[serde(rename_all = "snake_case")]
221pub enum CommandNetworkPolicy {
222 #[default]
223 Deny,
224 Allow,
225}
226
227fn default_command_disk_bytes() -> u64 {
228 1024 * 1024 * 1024
229}
230
231#[async_trait]
232pub trait WorkspaceControlRepository: Send + Sync + Debug {
233 async fn put_workspace(&self, workspace: &WorkspaceRecord) -> Result<()>;
234 async fn get_workspace(&self, tenant_id: &str, id: &str) -> Result<Option<WorkspaceRecord>>;
235 async fn list_workspaces(
236 &self,
237 tenant_id: &str,
238 project_id: &str,
239 after: Option<&str>,
240 limit: usize,
241 ) -> Result<WorkspacePage>;
242 async fn list_workspaces_global(
243 &self,
244 after: Option<(&str, &str)>,
245 limit: usize,
246 ) -> Result<WorkspacePage>;
247 async fn put_workspace_operation(
248 &self,
249 workspace: &WorkspaceRecord,
250 operation: &WorkspaceOperation,
251 payload_json: &str,
252 idempotency_key: &str,
253 request_digest: &str,
254 ) -> Result<WorkspaceOperation>;
255 async fn compare_and_swap_workspace(
256 &self,
257 tenant_id: &str,
258 id: &str,
259 expected_version: u64,
260 lifecycle: WorkspaceLifecycle,
261 now_ms: u64,
262 ) -> Result<WorkspaceRecord>;
263 async fn claim_workspace_lease(
264 &self,
265 tenant_id: &str,
266 id: &str,
267 owner: &str,
268 now_ms: u64,
269 lease_ms: u64,
270 ) -> Result<WorkspaceLease>;
271 async fn release_workspace_lease(
272 &self,
273 tenant_id: &str,
274 id: &str,
275 owner: &str,
276 fencing_token: u64,
277 now_ms: u64,
278 ) -> Result<()>;
279 async fn fenced_workspace_update(
280 &self,
281 tenant_id: &str,
282 id: &str,
283 owner: &str,
284 fencing_token: u64,
285 lifecycle: WorkspaceLifecycle,
286 now_ms: u64,
287 ) -> Result<WorkspaceRecord>;
288 async fn fenced_replace_workspace(
289 &self,
290 workspace: &WorkspaceRecord,
291 owner: &str,
292 fencing_token: u64,
293 now_ms: u64,
294 ) -> Result<WorkspaceRecord>;
295 async fn put_operation(&self, operation: &WorkspaceOperation) -> Result<()>;
296 async fn put_operation_with_input(
297 &self,
298 operation: &WorkspaceOperation,
299 payload_json: &str,
300 ) -> Result<()>;
301 async fn put_idempotent_operation(
302 &self,
303 operation: &WorkspaceOperation,
304 payload_json: &str,
305 idempotency_key: &str,
306 request_digest: &str,
307 ) -> Result<WorkspaceOperation>;
308 async fn get_operation(&self, operation_id: &str) -> Result<Option<WorkspaceOperation>>;
309 async fn claim_operation(
310 &self,
311 operation_id: &str,
312 owner: &str,
313 now_ms: u64,
314 lease_ms: u64,
315 ) -> Result<WorkspaceOperation>;
316 async fn renew_operation_lease(
317 &self,
318 operation_id: &str,
319 owner: &str,
320 fencing_token: u64,
321 now_ms: u64,
322 lease_ms: u64,
323 ) -> Result<WorkspaceOperation>;
324 async fn transition_operation(
325 &self,
326 transition: &OperationTransition,
327 ) -> Result<WorkspaceOperation>;
328 async fn request_operation_cancel(
329 &self,
330 tenant_id: &str,
331 operation_id: &str,
332 expected_version: u64,
333 now_ms: u64,
334 ) -> Result<WorkspaceOperation>;
335 async fn put_operation_input(&self, operation_id: &str, payload_json: &str) -> Result<()>;
336 async fn get_operation_input(&self, operation_id: &str) -> Result<Option<String>>;
337 async fn list_claimable_operations(
338 &self,
339 tenant_id: &str,
340 now_ms: u64,
341 limit: usize,
342 ) -> Result<Vec<WorkspaceOperation>>;
343 async fn list_claimable_operations_global(
344 &self,
345 now_ms: u64,
346 limit: usize,
347 ) -> Result<Vec<WorkspaceOperation>>;
348 async fn put_change_set(&self, value: &StoredChangeSet) -> Result<()>;
349 async fn get_change_set(&self, tenant_id: &str, id: &str) -> Result<Option<StoredChangeSet>>;
350 async fn put_preview(&self, value: &StoredPreview) -> Result<()>;
351 async fn get_preview(&self, tenant_id: &str, id: &str) -> Result<Option<StoredPreview>>;
352 async fn stop_preview(
353 &self,
354 tenant_id: &str,
355 id: &str,
356 expected_version: u64,
357 ) -> Result<StoredPreview>;
358 async fn retain_tenant(&self, tenant_id: &str, now_ms: u64, max_rows: usize) -> Result<usize>;
359 async fn record_worker_heartbeat(&self, worker_id: &str, now_ms: u64) -> Result<()> {
360 let _ = (worker_id, now_ms);
361 Ok(())
362 }
363 async fn check_readiness(
364 &self,
365 worker_id: &str,
366 now_ms: u64,
367 max_worker_staleness_ms: u64,
368 max_operation_backlog: usize,
369 ) -> Result<()> {
370 let _ = (
371 worker_id,
372 now_ms,
373 max_worker_staleness_ms,
374 max_operation_backlog,
375 );
376 Ok(())
377 }
378}
379
380#[async_trait]
381pub trait SecretResolver: Send + Sync + Debug {
382 async fn resolve(&self, tenant_id: &str, secret: &SecretRef) -> Result<Vec<u8>>;
384}
385
386#[async_trait]
387pub trait WorkspaceProviderAdapter: Send + Sync + Debug {
388 async fn provision(
389 &self,
390 tenant_id: &str,
391 workspace_id: &str,
392 config: &WorkspaceConfig,
393 idempotency_key: &str,
394 ) -> Result<ProvisionedWorkspace>;
395 async fn reconnect(&self, durable_backend_id: &str) -> Result<()>;
396 async fn suspend(&self, durable_backend_id: &str, idempotency_key: &str) -> Result<()>;
397 async fn resume(&self, durable_backend_id: &str, idempotency_key: &str) -> Result<()>;
398 async fn inspect(&self, workspace: &WorkspaceRecord) -> Result<WorkspaceResourceUsage>;
399 async fn delete(&self, durable_backend_id: &str, idempotency_key: &str) -> Result<()>;
400 async fn cancel_command(&self, durable_backend_id: &str, command_id: &str) -> Result<()>;
401}
402
403#[async_trait]
404pub trait WorkspaceProviderRegistry: Send + Sync + Debug {
405 async fn provider(&self, backend: &str) -> Result<Arc<dyn WorkspaceProviderAdapter>>;
406}
407
408#[async_trait]
409pub trait WorkspaceCommandExecutor: Send + Sync + Debug {
410 async fn execute(
411 &self,
412 tenant_id: &str,
413 workspace_id: &str,
414 command_id: &str,
415 command: &CommandSpec,
416 ) -> Result<CmdOutput>;
417 async fn cancel(&self, tenant_id: &str, command_id: &str) -> Result<()>;
418}
419
420fn command_env_inherited(value: &bool) -> bool {
421 !*value
422}