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 pub argv: Vec<String>,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub cwd: Option<String>,
188 #[serde(default)]
189 pub env: BTreeMap<String, SecretRef>,
190 #[serde(default)]
191 pub shell: bool,
192 pub timeout_ms: u64,
193 pub memory_bytes: u64,
194 pub cpu_millis: u64,
195 pub max_processes: u32,
196 #[serde(default)]
199 pub network: CommandNetworkPolicy,
200 #[serde(default = "default_command_disk_bytes")]
202 pub disk_bytes: u64,
203 pub max_output_bytes: usize,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
207#[serde(rename_all = "snake_case")]
208pub enum CommandNetworkPolicy {
209 #[default]
210 Deny,
211 Allow,
212}
213
214fn default_command_disk_bytes() -> u64 {
215 1024 * 1024 * 1024
216}
217
218#[async_trait]
219pub trait WorkspaceControlRepository: Send + Sync + Debug {
220 async fn put_workspace(&self, workspace: &WorkspaceRecord) -> Result<()>;
221 async fn get_workspace(&self, tenant_id: &str, id: &str) -> Result<Option<WorkspaceRecord>>;
222 async fn list_workspaces(
223 &self,
224 tenant_id: &str,
225 project_id: &str,
226 after: Option<&str>,
227 limit: usize,
228 ) -> Result<WorkspacePage>;
229 async fn list_workspaces_global(
230 &self,
231 after: Option<(&str, &str)>,
232 limit: usize,
233 ) -> Result<WorkspacePage>;
234 async fn put_workspace_operation(
235 &self,
236 workspace: &WorkspaceRecord,
237 operation: &WorkspaceOperation,
238 payload_json: &str,
239 idempotency_key: &str,
240 request_digest: &str,
241 ) -> Result<WorkspaceOperation>;
242 async fn compare_and_swap_workspace(
243 &self,
244 tenant_id: &str,
245 id: &str,
246 expected_version: u64,
247 lifecycle: WorkspaceLifecycle,
248 now_ms: u64,
249 ) -> Result<WorkspaceRecord>;
250 async fn claim_workspace_lease(
251 &self,
252 tenant_id: &str,
253 id: &str,
254 owner: &str,
255 now_ms: u64,
256 lease_ms: u64,
257 ) -> Result<WorkspaceLease>;
258 async fn release_workspace_lease(
259 &self,
260 tenant_id: &str,
261 id: &str,
262 owner: &str,
263 fencing_token: u64,
264 now_ms: u64,
265 ) -> Result<()>;
266 async fn fenced_workspace_update(
267 &self,
268 tenant_id: &str,
269 id: &str,
270 owner: &str,
271 fencing_token: u64,
272 lifecycle: WorkspaceLifecycle,
273 now_ms: u64,
274 ) -> Result<WorkspaceRecord>;
275 async fn fenced_replace_workspace(
276 &self,
277 workspace: &WorkspaceRecord,
278 owner: &str,
279 fencing_token: u64,
280 now_ms: u64,
281 ) -> Result<WorkspaceRecord>;
282 async fn put_operation(&self, operation: &WorkspaceOperation) -> Result<()>;
283 async fn put_operation_with_input(
284 &self,
285 operation: &WorkspaceOperation,
286 payload_json: &str,
287 ) -> Result<()>;
288 async fn put_idempotent_operation(
289 &self,
290 operation: &WorkspaceOperation,
291 payload_json: &str,
292 idempotency_key: &str,
293 request_digest: &str,
294 ) -> Result<WorkspaceOperation>;
295 async fn get_operation(&self, operation_id: &str) -> Result<Option<WorkspaceOperation>>;
296 async fn claim_operation(
297 &self,
298 operation_id: &str,
299 owner: &str,
300 now_ms: u64,
301 lease_ms: u64,
302 ) -> Result<WorkspaceOperation>;
303 async fn renew_operation_lease(
304 &self,
305 operation_id: &str,
306 owner: &str,
307 fencing_token: u64,
308 now_ms: u64,
309 lease_ms: u64,
310 ) -> Result<WorkspaceOperation>;
311 async fn transition_operation(
312 &self,
313 transition: &OperationTransition,
314 ) -> Result<WorkspaceOperation>;
315 async fn request_operation_cancel(
316 &self,
317 tenant_id: &str,
318 operation_id: &str,
319 expected_version: u64,
320 now_ms: u64,
321 ) -> Result<WorkspaceOperation>;
322 async fn put_operation_input(&self, operation_id: &str, payload_json: &str) -> Result<()>;
323 async fn get_operation_input(&self, operation_id: &str) -> Result<Option<String>>;
324 async fn list_claimable_operations(
325 &self,
326 tenant_id: &str,
327 now_ms: u64,
328 limit: usize,
329 ) -> Result<Vec<WorkspaceOperation>>;
330 async fn list_claimable_operations_global(
331 &self,
332 now_ms: u64,
333 limit: usize,
334 ) -> Result<Vec<WorkspaceOperation>>;
335 async fn put_change_set(&self, value: &StoredChangeSet) -> Result<()>;
336 async fn get_change_set(&self, tenant_id: &str, id: &str) -> Result<Option<StoredChangeSet>>;
337 async fn put_preview(&self, value: &StoredPreview) -> Result<()>;
338 async fn get_preview(&self, tenant_id: &str, id: &str) -> Result<Option<StoredPreview>>;
339 async fn stop_preview(
340 &self,
341 tenant_id: &str,
342 id: &str,
343 expected_version: u64,
344 ) -> Result<StoredPreview>;
345 async fn retain_tenant(&self, tenant_id: &str, now_ms: u64, max_rows: usize) -> Result<usize>;
346 async fn record_worker_heartbeat(&self, worker_id: &str, now_ms: u64) -> Result<()> {
347 let _ = (worker_id, now_ms);
348 Ok(())
349 }
350 async fn check_readiness(
351 &self,
352 worker_id: &str,
353 now_ms: u64,
354 max_worker_staleness_ms: u64,
355 max_operation_backlog: usize,
356 ) -> Result<()> {
357 let _ = (
358 worker_id,
359 now_ms,
360 max_worker_staleness_ms,
361 max_operation_backlog,
362 );
363 Ok(())
364 }
365}
366
367#[async_trait]
368pub trait SecretResolver: Send + Sync + Debug {
369 async fn resolve(&self, tenant_id: &str, secret: &SecretRef) -> Result<Vec<u8>>;
371}
372
373#[async_trait]
374pub trait WorkspaceProviderAdapter: Send + Sync + Debug {
375 async fn provision(
376 &self,
377 tenant_id: &str,
378 workspace_id: &str,
379 config: &WorkspaceConfig,
380 idempotency_key: &str,
381 ) -> Result<ProvisionedWorkspace>;
382 async fn reconnect(&self, durable_backend_id: &str) -> Result<()>;
383 async fn suspend(&self, durable_backend_id: &str, idempotency_key: &str) -> Result<()>;
384 async fn resume(&self, durable_backend_id: &str, idempotency_key: &str) -> Result<()>;
385 async fn inspect(&self, workspace: &WorkspaceRecord) -> Result<WorkspaceResourceUsage>;
386 async fn delete(&self, durable_backend_id: &str, idempotency_key: &str) -> Result<()>;
387 async fn cancel_command(&self, durable_backend_id: &str, command_id: &str) -> Result<()>;
388}
389
390#[async_trait]
391pub trait WorkspaceProviderRegistry: Send + Sync + Debug {
392 async fn provider(&self, backend: &str) -> Result<Arc<dyn WorkspaceProviderAdapter>>;
393}
394
395#[async_trait]
396pub trait WorkspaceCommandExecutor: Send + Sync + Debug {
397 async fn execute(
398 &self,
399 tenant_id: &str,
400 workspace_id: &str,
401 command_id: &str,
402 command: &CommandSpec,
403 ) -> Result<CmdOutput>;
404 async fn cancel(&self, tenant_id: &str, command_id: &str) -> Result<()>;
405}