Skip to main content

agent_workspace_contract/
environment.rs

1//! Workspace environment domain contracts, strongly typed resources, and provider ports.
2
3use anyhow::Result;
4use async_trait::async_trait;
5use bytes::Bytes;
6use futures_util::Stream;
7use serde::{Deserialize, Serialize};
8use std::collections::{BTreeMap, BTreeSet};
9use std::fmt::Debug;
10use std::pin::Pin;
11use std::sync::Arc;
12
13mod base64_bytes {
14    use base64::{Engine as _, engine::general_purpose::STANDARD};
15    use serde::{Deserialize, Deserializer, Serializer};
16
17    pub fn serialize<S>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error>
18    where
19        S: Serializer,
20    {
21        serializer.serialize_str(&STANDARD.encode(value))
22    }
23
24    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
25    where
26        D: Deserializer<'de>,
27    {
28        let value = String::deserialize(deserializer)?;
29        STANDARD.decode(value).map_err(serde::de::Error::custom)
30    }
31}
32
33mod catalog;
34pub use catalog::*;
35
36mod ports;
37pub use ports::*;
38
39mod owners;
40pub use owners::*;
41
42mod command_context;
43pub use command_context::*;
44
45mod application_execution;
46pub use application_execution::*;
47
48mod commands;
49pub use commands::*;
50
51mod resources;
52pub use resources::*;
53
54mod execution_operation;
55pub use execution_operation::*;
56
57mod skills;
58pub use skills::*;
59
60mod managed_workloads;
61pub use managed_workloads::*;
62
63pub const COMPUTERS_PATH: &str = "/internal/v1/workspace/computers";
64/// Privileged inventory and terminal routes used only by the Infra Admin console.
65/// They are intentionally separate from the owner-scoped public Environment API.
66pub const ADMIN_COMPUTERS_PATH: &str = "/internal/v1/workspace/admin/computers";
67pub const COMPUTER_PATH: &str = "/internal/v1/workspace/computers/{computerId}";
68pub const SPACES_PATH: &str = "/internal/v1/workspace/spaces";
69pub const ADMIN_SPACES_PATH: &str = "/internal/v1/workspace/admin/spaces";
70pub const SPACE_PATH: &str = "/internal/v1/workspace/spaces/{spaceId}";
71pub const SPACE_GRANTS_PATH: &str = "/internal/v1/workspace/spaces/{spaceId}/grants";
72pub const SPACE_GRANT_REVOKE_PATH: &str =
73    "/internal/v1/workspace/spaces/{spaceId}/grants/{grantId}/revoke";
74pub const SPACE_SESSIONS_PATH: &str = "/internal/v1/workspace/spaces/{spaceId}/sessions";
75pub const SPACE_SESSION_HEARTBEAT_PATH: &str =
76    "/internal/v1/workspace/space-sessions/{sessionId}/heartbeat";
77pub const SPACE_SESSION_CLOSE_PATH: &str =
78    "/internal/v1/workspace/space-sessions/{sessionId}/close";
79pub const SPACE_TICKETS_PATH: &str = "/internal/v1/workspace/space-access-tickets";
80pub const SPACE_LEASES_PATH: &str = "/internal/v1/workspace/spaces/{spaceId}/leases";
81pub const SPACE_LEASE_PATH: &str = "/internal/v1/workspace/spaces/{spaceId}/leases/{leaseId}";
82pub const SPACE_FILES_PATH: &str = "/internal/v1/workspace/spaces/{spaceId}/files/{*filePath}";
83pub const SPACE_FILES_READ_PATH: &str = "/internal/v1/workspace/spaces/{spaceId}/files/read";
84pub const SPACE_FILES_DOWNLOAD_PATH: &str =
85    "/internal/v1/workspace/spaces/{spaceId}/files/download";
86pub const SPACE_FILES_LIST_PATH: &str = "/internal/v1/workspace/spaces/{spaceId}/files/list";
87pub const SPACE_SOURCE_ARCHIVE_PATH: &str =
88    "/internal/v1/workspace/spaces/{spaceId}/source-archive";
89pub const SPACE_CHANGE_SETS_PATH: &str = "/internal/v1/workspace/spaces/{spaceId}/change-sets";
90pub const SPACE_CHANGE_SET_APPLY_PATH: &str =
91    "/internal/v1/workspace/spaces/{spaceId}/change-sets/{changeSetId}/apply";
92pub const SANDBOXES_PATH: &str = "/internal/v1/workspace/sandboxes";
93pub const SANDBOX_PATH: &str = "/internal/v1/workspace/sandboxes/{sandboxId}";
94pub const SANDBOX_STOP_PATH: &str = "/internal/v1/workspace/sandboxes/{sandboxId}/stop";
95pub const COMPUTER_SNAPSHOTS_PATH: &str = "/internal/v1/workspace/computers/{computerId}/snapshots";
96pub const SNAPSHOT_ARTIFACT_PATH: &str = "/internal/v1/workspace/snapshots/{snapshotId}/artifact";
97pub const SNAPSHOTS_PATH: &str = "/internal/v1/workspace/snapshots";
98pub const SNAPSHOT_PATH: &str = "/internal/v1/workspace/snapshots/{snapshotId}";
99pub const BUILD_BUILTIN_SNAPSHOTS_PATH: &str = "/internal/v1/workspace/snapshots:build-builtin";
100pub const CATALOG_SNAPSHOTS_PATH: &str = "/internal/v1/workspace/snapshots:catalog";
101pub const TEMPLATES_PATH: &str = "/internal/v1/workspace/templates";
102pub const TEMPLATE_VERSIONS_PATH: &str = "/internal/v1/workspace/templates/{templateId}/versions";
103pub const TEMPLATE_VERSION_PATH: &str =
104    "/internal/v1/workspace/templates/{templateId}/versions/{version}";
105pub const TEMPLATE_VERSION_PUBLISH_PATH: &str =
106    "/internal/v1/workspace/templates/{templateId}/versions/{version}/publish";
107pub const TEMPLATE_VERSION_DEPRECATE_PATH: &str =
108    "/internal/v1/workspace/templates/{templateId}/versions/{version}/deprecate";
109pub const DELEGATIONS_PATH: &str = "/internal/v1/workspace/delegations";
110pub const DELEGATION_REVOKE_PATH: &str = "/internal/v1/workspace/delegations/{delegationId}/revoke";
111pub const ENVIRONMENT_MAX_FILE_BYTES: usize = 8 * 1024 * 1024;
112pub const ENVIRONMENT_MAX_PATH_BYTES: usize = 4096;
113
114/// System resources assigned to one provider Computer/Sandbox. Units match
115/// the provider control planes and keep invalid budgets out of Runtime.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
117#[serde(rename_all = "camelCase")]
118pub struct SandboxResources {
119    cpu_count: u32,
120    memory_mb: u32,
121    disk_mb: u32,
122}
123
124impl SandboxResources {
125    pub fn new(cpu_count: u32, memory_mb: u32, disk_mb: u32) -> std::result::Result<Self, String> {
126        if !(1..=64).contains(&cpu_count) {
127            return Err("cpuCount must be between 1 and 64".into());
128        }
129        if !(128..=131_072).contains(&memory_mb) {
130            return Err("memoryMb must be between 128 and 131072".into());
131        }
132        if !(512..=1_048_576).contains(&disk_mb) {
133            return Err("diskMb must be between 512 and 1048576".into());
134        }
135        Ok(Self {
136            cpu_count,
137            memory_mb,
138            disk_mb,
139        })
140    }
141
142    pub const fn cpu_count(self) -> u32 {
143        self.cpu_count
144    }
145
146    pub const fn memory_mb(self) -> u32 {
147        self.memory_mb
148    }
149
150    pub const fn disk_mb(self) -> u32 {
151        self.disk_mb
152    }
153}
154
155impl Default for SandboxResources {
156    fn default() -> Self {
157        Self::new(1, 1_024, 2_048).expect("system resource defaults are valid")
158    }
159}
160
161impl<'de> Deserialize<'de> for SandboxResources {
162    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
163    where
164        D: serde::Deserializer<'de>,
165    {
166        #[derive(Deserialize)]
167        #[serde(rename_all = "camelCase", deny_unknown_fields)]
168        struct WireResources {
169            cpu_count: u32,
170            memory_mb: u32,
171            disk_mb: u32,
172        }
173
174        let value = WireResources::deserialize(deserializer)?;
175        Self::new(value.cpu_count, value.memory_mb, value.disk_mb).map_err(serde::de::Error::custom)
176    }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(deny_unknown_fields, rename_all = "camelCase")]
181pub struct ExportSpaceSourceArchiveQuery {
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub expected_revision: Option<u64>,
184}
185
186/// Logical directory for a Space on its parent Computer filesystem.
187pub fn space_directory_path(space_id: &str) -> String {
188    format!("spaces/{space_id}")
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
193pub enum EnvironmentErrorCode {
194    NotFound,
195    Forbidden,
196    Conflict,
197    InvalidState,
198    ArtifactUnavailable,
199    CapabilityUnsupported,
200    TicketExpired,
201    LeaseRequired,
202    StaleFencingToken,
203    RevisionConflict,
204}
205
206#[derive(Debug, Clone)]
207pub struct EnvironmentDomainError {
208    pub code: EnvironmentErrorCode,
209    pub message: String,
210}
211
212impl EnvironmentDomainError {
213    pub fn new(code: EnvironmentErrorCode, message: impl Into<String>) -> Self {
214        Self {
215            code,
216            message: message.into(),
217        }
218    }
219}
220
221impl std::fmt::Display for EnvironmentDomainError {
222    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        formatter.write_str(&self.message)
224    }
225}
226
227impl std::error::Error for EnvironmentDomainError {}
228
229#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
230#[serde(rename_all = "snake_case")]
231pub enum PrincipalKind {
232    User,
233    Agent,
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
237#[serde(deny_unknown_fields, rename_all = "camelCase")]
238pub struct PrincipalRef {
239    pub kind: PrincipalKind,
240    pub id: String,
241}
242
243impl PrincipalRef {
244    pub fn user(id: impl Into<String>) -> Self {
245        Self {
246            kind: PrincipalKind::User,
247            id: id.into(),
248        }
249    }
250
251    pub fn agent(id: impl Into<String>) -> Self {
252        Self {
253            kind: PrincipalKind::Agent,
254            id: id.into(),
255        }
256    }
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
260#[serde(deny_unknown_fields, rename_all = "camelCase")]
261pub struct ResourceScope {
262    pub tenant_id: String,
263    pub project_id: String,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
267#[serde(deny_unknown_fields, rename_all = "camelCase")]
268pub struct ProviderResourceRef {
269    pub provider: String,
270    pub external_id: String,
271}
272
273#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
274#[serde(deny_unknown_fields, rename_all = "camelCase")]
275pub struct ProviderCapabilities {
276    #[serde(default)]
277    pub capabilities: BTreeSet<String>,
278}
279
280impl ProviderCapabilities {
281    pub fn supports(&self, capability: &str) -> bool {
282        self.capabilities.contains(capability)
283    }
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
287#[serde(rename_all = "snake_case")]
288pub enum ComputerLifecycle {
289    Ready,
290    Deleted,
291}
292
293#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
294#[serde(deny_unknown_fields, rename_all = "camelCase")]
295pub struct ComputerRecord {
296    pub id: String,
297    #[serde(default, skip_serializing_if = "Option::is_none")]
298    pub alias: Option<String>,
299    pub scope: ResourceScope,
300    pub provider_ref: ProviderResourceRef,
301    /// Effective system resources after resolving the Admin default and any
302    /// developer override. Old persisted records inherit the compatible default.
303    #[serde(default)]
304    pub resources: SandboxResources,
305    pub lifecycle: ComputerLifecycle,
306    pub capabilities: ProviderCapabilities,
307    pub owners: Vec<OwnerRef>,
308    pub version: u64,
309    pub created_at_ms: u64,
310    pub updated_at_ms: u64,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
314#[serde(deny_unknown_fields, rename_all = "camelCase")]
315pub struct ComputerBinding {
316    pub computer_id: String,
317    pub principal: PrincipalRef,
318    pub role: String,
319    pub active: bool,
320    pub bound_at_ms: u64,
321    #[serde(default, skip_serializing_if = "Option::is_none")]
322    pub released_at_ms: Option<u64>,
323    pub version: u64,
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
327#[serde(rename_all = "snake_case")]
328pub enum SpacePersistence {
329    Persistent,
330    CopyOnWrite,
331}
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
334#[serde(rename_all = "snake_case")]
335pub enum SpaceLifecycle {
336    Ready,
337    Deleted,
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
341#[serde(deny_unknown_fields, rename_all = "camelCase")]
342pub struct SpaceRecord {
343    pub id: String,
344    #[serde(default, skip_serializing_if = "Option::is_none")]
345    pub alias: Option<String>,
346    pub computer_id: String,
347    pub scope: ResourceScope,
348    pub persistence: SpacePersistence,
349    pub lifecycle: SpaceLifecycle,
350    pub capabilities: ProviderCapabilities,
351    pub owners: Vec<OwnerRef>,
352    #[serde(default, skip_serializing_if = "Option::is_none")]
353    pub source_snapshot_id: Option<String>,
354    pub revision: u64,
355    pub version: u64,
356    /// Durable ownership journal for Skill assets and their CLI dependencies.
357    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
358    pub installed_skills: BTreeMap<String, SpaceSkillInstallation>,
359    /// Merged environment from Space skill installs. Applied to later execs.
360    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
361    pub skill_env: BTreeMap<String, String>,
362    pub created_at_ms: u64,
363    pub updated_at_ms: u64,
364}
365
366#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
367#[serde(rename_all = "snake_case")]
368pub enum SandboxPlacement {
369    Computer(String),
370    ProviderPool(String),
371}
372
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
374#[serde(rename_all = "snake_case")]
375pub enum SandboxLifecycle {
376    Ready,
377    Stopped,
378    Expired,
379}
380
381#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
382#[serde(deny_unknown_fields, rename_all = "camelCase")]
383pub struct SandboxMount {
384    pub alias: String,
385    pub space_id: String,
386    pub mode: SpaceAccessMode,
387    #[serde(default, skip_serializing_if = "Option::is_none")]
388    pub path_prefix: Option<String>,
389    pub access_ticket_token: String,
390}
391
392/// Persisted mount metadata. The bearer token from [`SandboxMount`] is
393/// deliberately replaced by its non-secret ticket id before persistence.
394#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
395#[serde(deny_unknown_fields, rename_all = "camelCase")]
396pub struct SandboxMountRecord {
397    pub alias: String,
398    pub space_id: String,
399    pub mode: SpaceAccessMode,
400    #[serde(default, skip_serializing_if = "Option::is_none")]
401    pub path_prefix: Option<String>,
402    pub access_ticket_id: String,
403}
404
405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
406#[serde(deny_unknown_fields, rename_all = "camelCase")]
407pub struct SandboxRecord {
408    pub id: String,
409    #[serde(default, skip_serializing_if = "Option::is_none")]
410    pub alias: Option<String>,
411    pub scope: ResourceScope,
412    pub provider_ref: ProviderResourceRef,
413    /// Effective system resources after resolving the Admin default and any
414    /// developer override. Old persisted records inherit the compatible default.
415    #[serde(default)]
416    pub resources: SandboxResources,
417    pub placement: SandboxPlacement,
418    pub lifecycle: SandboxLifecycle,
419    pub owners: Vec<OwnerRef>,
420    #[serde(default, skip_serializing_if = "Option::is_none")]
421    pub access_delegation_id: Option<String>,
422    pub capabilities: ProviderCapabilities,
423    #[serde(default)]
424    pub mounts: Vec<SandboxMountRecord>,
425    pub expires_at_ms: u64,
426    pub version: u64,
427    /// Merged environment from Sandbox skill installs. Applied to later execs.
428    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
429    pub skill_env: BTreeMap<String, String>,
430    pub created_at_ms: u64,
431    pub updated_at_ms: u64,
432}
433
434#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
435#[serde(rename_all = "snake_case")]
436pub enum SnapshotLifecycle {
437    Building,
438    Ready,
439    Failed,
440    Deleted,
441}
442
443#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
444#[serde(tag = "kind", rename_all = "snake_case")]
445pub enum SnapshotSourceRecord {
446    Build { spec: SnapshotBuildSpec },
447    Computer { computer_id: String },
448}
449
450#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
451#[serde(deny_unknown_fields, rename_all = "camelCase")]
452pub struct SnapshotRecord {
453    pub id: String,
454    #[serde(default, skip_serializing_if = "Option::is_none")]
455    pub alias: Option<String>,
456    pub scope: ResourceScope,
457    pub provider: String,
458    pub provider_ref: ProviderResourceRef,
459    pub source: SnapshotSourceRecord,
460    pub visibility: SnapshotVisibility,
461    pub owners: Vec<OwnerRef>,
462    #[serde(default)]
463    pub builtin: bool,
464    pub created_by: PrincipalRef,
465    pub lifecycle: SnapshotLifecycle,
466    #[serde(default, skip_serializing_if = "Option::is_none")]
467    pub digest: Option<String>,
468    #[serde(default, skip_serializing_if = "Option::is_none")]
469    pub size_bytes: Option<u64>,
470    #[serde(default, skip_serializing_if = "Option::is_none")]
471    pub error: Option<String>,
472    #[serde(default, skip_serializing_if = "Option::is_none")]
473    pub provider_build_id: Option<String>,
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub build_stage: Option<String>,
476    #[serde(default, skip_serializing_if = "Vec::is_empty")]
477    pub build_logs: Vec<String>,
478    #[serde(default, skip_serializing_if = "String::is_empty")]
479    pub build_key: String,
480    pub created_at_ms: u64,
481    pub updated_at_ms: u64,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
485#[serde(rename_all = "snake_case")]
486pub enum TemplateTarget {
487    Computer,
488    Sandbox,
489}
490
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
492#[serde(rename_all = "snake_case")]
493pub enum TemplateVersionState {
494    Draft,
495    Published,
496    Deprecated,
497}
498
499#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
500#[serde(deny_unknown_fields, rename_all = "camelCase")]
501pub struct TemplateVersionRecord {
502    pub template_id: String,
503    pub version: String,
504    pub scope: ResourceScope,
505    pub provider: String,
506    pub created_by: PrincipalRef,
507    #[serde(default, skip_serializing_if = "Option::is_none")]
508    pub provider_template_id: Option<String>,
509    pub target: TemplateTarget,
510    #[serde(default, skip_serializing_if = "Option::is_none")]
511    pub base_snapshot_id: Option<String>,
512    #[serde(default)]
513    pub resource_profile: BTreeMap<String, String>,
514    #[serde(default)]
515    pub parameter_schema: BTreeMap<String, String>,
516    pub state: TemplateVersionState,
517    pub created_at_ms: u64,
518}
519
520#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
521#[serde(rename_all = "snake_case")]
522pub enum SpaceCapability {
523    Read,
524    Execute,
525    Write,
526    Share,
527    Manage,
528    Mount,
529    Snapshot,
530    Restore,
531    Delete,
532}
533
534#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
535#[serde(deny_unknown_fields, rename_all = "camelCase")]
536pub struct SpaceGrant {
537    pub id: String,
538    pub scope: ResourceScope,
539    pub space_id: String,
540    pub principal: PrincipalRef,
541    pub capabilities: BTreeSet<SpaceCapability>,
542    #[serde(default)]
543    pub path_prefixes: Vec<String>,
544    #[serde(default, skip_serializing_if = "Option::is_none")]
545    pub expires_at_ms: Option<u64>,
546    pub active: bool,
547    pub version: u64,
548}
549
550#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
551#[serde(rename_all = "snake_case")]
552pub enum SpaceAccessMode {
553    ReadOnly,
554    DirectWrite,
555    CopyOnWrite,
556}
557
558#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
559#[serde(rename_all = "snake_case")]
560pub enum SpaceSessionLifecycle {
561    Opening,
562    Active,
563    Closing,
564    Closed,
565    Expired,
566    Revoked,
567}
568
569#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
570#[serde(deny_unknown_fields, rename_all = "camelCase")]
571pub struct SpaceCollaborationSession {
572    pub id: String,
573    pub scope: ResourceScope,
574    pub space_id: String,
575    pub principal: PrincipalRef,
576    #[serde(default, skip_serializing_if = "Option::is_none")]
577    pub delegation_id: Option<String>,
578    pub requested_mode: SpaceAccessMode,
579    pub effective_mode: SpaceAccessMode,
580    pub lifecycle: SpaceSessionLifecycle,
581    pub last_heartbeat_at_ms: u64,
582    pub expires_at_ms: u64,
583    pub version: u64,
584}
585
586#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
587#[serde(deny_unknown_fields, rename_all = "camelCase")]
588pub struct SpaceAccessTicketRecord {
589    pub id: String,
590    pub token_hash: String,
591    pub scope: ResourceScope,
592    pub space_id: String,
593    pub session_id: String,
594    pub mode: SpaceAccessMode,
595    /// Capability-specific path rules captured at issue time. Runtime
596    /// intersects these with current Grant/Delegation authority on every use.
597    #[serde(default)]
598    pub capability_paths: BTreeMap<SpaceCapability, Vec<String>>,
599    #[serde(default, skip_serializing_if = "Option::is_none")]
600    pub fencing_token: Option<u64>,
601    pub expires_at_ms: u64,
602    pub revoked: bool,
603}
604
605#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
606#[serde(deny_unknown_fields, rename_all = "camelCase")]
607pub struct IssuedSpaceAccessTicket {
608    pub ticket: SpaceAccessTicketRecord,
609    /// Returned exactly once. Repositories and logs must never persist this
610    /// bearer value.
611    pub token: String,
612}
613
614#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
615#[serde(deny_unknown_fields, rename_all = "camelCase")]
616pub struct SpaceWriteLease {
617    pub id: String,
618    pub scope: ResourceScope,
619    pub space_id: String,
620    pub session_id: String,
621    pub fencing_token: u64,
622    pub version: u64,
623    pub acquired_at_ms: u64,
624    pub expires_at_ms: u64,
625    pub active: bool,
626}
627
628#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
629#[serde(deny_unknown_fields, rename_all = "camelCase")]
630pub struct DelegationRecord {
631    pub id: String,
632    pub scope: ResourceScope,
633    pub grantor_user_id: String,
634    pub delegate_agent_id: String,
635    #[serde(default)]
636    pub computer_ids: BTreeSet<String>,
637    #[serde(default)]
638    pub space_ids: BTreeSet<String>,
639    pub capabilities: BTreeSet<SpaceCapability>,
640    #[serde(default)]
641    pub path_prefixes: Vec<String>,
642    pub purpose: String,
643    pub expires_at_ms: u64,
644    #[serde(default, skip_serializing_if = "Option::is_none")]
645    pub revoked_at_ms: Option<u64>,
646}
647
648#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
649#[serde(rename_all = "snake_case")]
650pub enum ChangeSetLifecycle {
651    Ready,
652    Applied,
653}
654
655#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
656#[serde(deny_unknown_fields, rename_all = "camelCase")]
657pub struct SpaceChangeSetRecord {
658    pub id: String,
659    pub scope: ResourceScope,
660    pub source_space_id: String,
661    pub target_space_id: String,
662    pub base_revision: u64,
663    pub created_by_session_id: String,
664    #[serde(default, skip_serializing_if = "Option::is_none")]
665    pub provider_change_ref: Option<String>,
666    pub lifecycle: ChangeSetLifecycle,
667    pub created_at_ms: u64,
668}
669
670/// Default runtime provider for newly created remote Workspace resources.
671pub fn default_workspace_provider() -> String {
672    "e2b".into()
673}
674
675#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
676#[serde(deny_unknown_fields, rename_all = "camelCase")]
677pub struct CreateComputerRequest {
678    #[serde(default, skip_serializing_if = "Option::is_none")]
679    pub alias: Option<String>,
680    #[serde(default = "default_workspace_provider")]
681    pub provider: String,
682    #[serde(default, skip_serializing_if = "Option::is_none")]
683    pub snapshot_id: Option<String>,
684    #[serde(default, skip_serializing_if = "Option::is_none")]
685    pub template_id: Option<String>,
686    #[serde(default, skip_serializing_if = "Option::is_none")]
687    pub template_version: Option<String>,
688    #[serde(default)]
689    pub profile: BTreeMap<String, String>,
690    /// Optional developer override. Missing inherits the Admin default.
691    #[serde(default, skip_serializing_if = "Option::is_none")]
692    pub resources: Option<SandboxResources>,
693    /// Optional per-computer override. Missing inherits the Moulin provider
694    /// default; an explicit empty list requests unrestricted egress.
695    #[serde(default, skip_serializing_if = "Option::is_none")]
696    pub egress_rules: Option<Vec<crate::MoulinEgressRule>>,
697    /// Additional owners beyond the authenticated creator.
698    #[serde(default)]
699    pub owners: Vec<OwnerRef>,
700}
701
702#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
703#[serde(deny_unknown_fields, rename_all = "camelCase")]
704pub struct CreateSpaceRequest {
705    #[serde(default, skip_serializing_if = "Option::is_none")]
706    pub alias: Option<String>,
707    pub computer_id: String,
708    pub persistence: SpacePersistence,
709    #[serde(default, skip_serializing_if = "Option::is_none")]
710    pub source_snapshot_id: Option<String>,
711    /// Must be a subset of the parent Computer owners when non-empty.
712    #[serde(default)]
713    pub owners: Vec<OwnerRef>,
714}
715
716/// Mutable human label for an existing Environment resource; identity remains unchanged.
717#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
718#[serde(deny_unknown_fields, rename_all = "camelCase")]
719pub struct PatchResourceAliasRequest {
720    pub alias: String,
721}
722
723#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
724#[serde(deny_unknown_fields, rename_all = "camelCase")]
725pub struct CreateSandboxRequest {
726    #[serde(default, skip_serializing_if = "Option::is_none")]
727    pub alias: Option<String>,
728    #[serde(default = "default_workspace_provider")]
729    pub provider: String,
730    pub placement: SandboxPlacement,
731    #[serde(default, skip_serializing_if = "Option::is_none")]
732    pub delegation_id: Option<String>,
733    pub ttl_ms: u64,
734    #[serde(default)]
735    pub mounts: Vec<SandboxMount>,
736    #[serde(default, skip_serializing_if = "Option::is_none")]
737    pub snapshot_id: Option<String>,
738    #[serde(default, skip_serializing_if = "Option::is_none")]
739    pub template_id: Option<String>,
740    #[serde(default, skip_serializing_if = "Option::is_none")]
741    pub template_version: Option<String>,
742    #[serde(default)]
743    pub profile: BTreeMap<String, String>,
744    /// Optional developer override. Missing inherits the Admin default.
745    #[serde(default, skip_serializing_if = "Option::is_none")]
746    pub resources: Option<SandboxResources>,
747    /// Optional per-sandbox override. Missing inherits the Moulin provider
748    /// default; an explicit empty list requests unrestricted egress.
749    #[serde(default, skip_serializing_if = "Option::is_none")]
750    pub egress_rules: Option<Vec<crate::MoulinEgressRule>>,
751}
752
753/// Provider-facing request after product identifiers and access tickets have
754/// been resolved. These DTOs cannot be supplied by HTTP callers.
755#[derive(Debug, Clone, PartialEq, Eq)]
756pub struct ProviderComputerRequest {
757    pub template_external_id: Option<String>,
758    pub profile: BTreeMap<String, String>,
759    pub resources: SandboxResources,
760    pub egress_rules: Option<Vec<crate::MoulinEgressRule>>,
761}
762
763#[derive(Debug, Clone, PartialEq, Eq)]
764pub struct ProviderSpaceRequest {
765    pub persistence: SpacePersistence,
766    pub source_snapshot_external_id: Option<String>,
767}
768
769#[derive(Debug, Clone, PartialEq, Eq)]
770pub struct ProviderSandboxMount {
771    pub alias: String,
772    pub space_directory: String,
773    pub mode: SpaceAccessMode,
774    pub path_prefix: Option<String>,
775}
776
777#[derive(Debug, Clone, PartialEq, Eq)]
778pub enum ProviderSandboxPlacement {
779    Computer(String),
780    ProviderPool(String),
781}
782
783#[derive(Debug, Clone, PartialEq, Eq)]
784pub struct ProviderSandboxRequest {
785    pub placement: ProviderSandboxPlacement,
786    pub ttl_ms: u64,
787    pub mounts: Vec<ProviderSandboxMount>,
788    pub template_external_id: Option<String>,
789    pub source_snapshot_external_id: Option<String>,
790    pub profile: BTreeMap<String, String>,
791    pub resources: SandboxResources,
792    pub egress_rules: Option<Vec<crate::MoulinEgressRule>>,
793}
794
795#[derive(Debug, Clone, PartialEq, Eq)]
796pub enum ProviderSnapshotSource {
797    Computer(String),
798    Sandbox(String),
799    Space(String),
800}
801
802#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
803#[serde(deny_unknown_fields, rename_all = "camelCase")]
804pub struct ListEnvironmentResourcesQuery {
805    #[serde(default, skip_serializing_if = "Option::is_none")]
806    pub owner_kind: Option<OwnerKind>,
807    #[serde(default, skip_serializing_if = "Option::is_none")]
808    pub owner_id: Option<String>,
809    /// When set, resource owners must contain every listed owner (JSON array).
810    #[serde(default, skip_serializing_if = "Vec::is_empty")]
811    pub contains_all: Vec<OwnerRef>,
812}
813
814#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
815#[serde(deny_unknown_fields, rename_all = "camelCase")]
816pub struct RevokeSpaceGrantRequest {
817    pub expected_version: u64,
818}
819
820#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
821#[serde(deny_unknown_fields, rename_all = "camelCase")]
822pub struct CreateSpaceGrantRequest {
823    pub principal: PrincipalRef,
824    pub capabilities: BTreeSet<SpaceCapability>,
825    #[serde(default)]
826    pub path_prefixes: Vec<String>,
827    #[serde(default, skip_serializing_if = "Option::is_none")]
828    pub expires_at_ms: Option<u64>,
829}
830
831#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
832#[serde(deny_unknown_fields, rename_all = "camelCase")]
833pub struct OpenSpaceSessionRequest {
834    pub mode: SpaceAccessMode,
835    #[serde(default, skip_serializing_if = "Option::is_none")]
836    pub delegation_id: Option<String>,
837    pub ttl_ms: u64,
838}
839
840#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
841#[serde(deny_unknown_fields, rename_all = "camelCase")]
842pub struct IssueSpaceAccessTicketRequest {
843    pub session_id: String,
844    pub ttl_ms: u64,
845}
846
847#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
848#[serde(deny_unknown_fields, rename_all = "camelCase")]
849pub struct AcquireSpaceWriteLeaseRequest {
850    pub session_id: String,
851    pub ttl_ms: u64,
852}
853
854#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
855#[serde(deny_unknown_fields, rename_all = "camelCase")]
856pub struct ReleaseSpaceWriteLeaseRequest {
857    pub session_id: String,
858    pub fencing_token: u64,
859}
860
861#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
862#[serde(deny_unknown_fields, rename_all = "camelCase")]
863pub struct HeartbeatSpaceSessionRequest {
864    pub ttl_ms: u64,
865}
866
867#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
868#[serde(deny_unknown_fields, rename_all = "camelCase")]
869pub struct CreateDelegationRequest {
870    pub delegate_agent_id: String,
871    #[serde(default)]
872    pub computer_ids: BTreeSet<String>,
873    #[serde(default)]
874    pub space_ids: BTreeSet<String>,
875    pub capabilities: BTreeSet<SpaceCapability>,
876    #[serde(default)]
877    pub path_prefixes: Vec<String>,
878    pub purpose: String,
879    pub expires_at_ms: u64,
880}
881
882#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
883#[serde(deny_unknown_fields, rename_all = "camelCase")]
884pub struct CreateTemplateVersionRequest {
885    pub template_id: String,
886    pub version: String,
887    pub provider: String,
888    #[serde(default, skip_serializing_if = "Option::is_none")]
889    pub provider_template_id: Option<String>,
890    pub target: TemplateTarget,
891    #[serde(default, skip_serializing_if = "Option::is_none")]
892    pub base_snapshot_id: Option<String>,
893    #[serde(default)]
894    pub resource_profile: BTreeMap<String, String>,
895    #[serde(default)]
896    pub parameter_schema: BTreeMap<String, String>,
897}
898
899#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
900#[serde(deny_unknown_fields, rename_all = "camelCase")]
901pub struct CreateSpaceChangeSetRequest {
902    pub source_space_id: String,
903    pub target_space_id: String,
904    pub created_by_session_id: String,
905}
906
907#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
908#[serde(deny_unknown_fields, rename_all = "camelCase")]
909pub struct ApplySpaceChangeSetRequest {
910    pub access_ticket_token: String,
911    pub fencing_token: u64,
912    pub expected_revision: u64,
913}
914
915#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
916#[serde(deny_unknown_fields, rename_all = "camelCase")]
917pub struct WriteSpaceFileRequest {
918    pub access_ticket_token: String,
919    pub fencing_token: u64,
920    pub expected_revision: u64,
921    #[serde(with = "base64_bytes")]
922    pub content: Vec<u8>,
923}
924
925#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
926#[serde(deny_unknown_fields, rename_all = "camelCase")]
927pub struct ReadSpaceFileRequest {
928    pub access_ticket_token: String,
929    pub path: String,
930}
931
932/// Directory archive format; regular files always retain their original bytes.
933#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
934pub enum WorkspaceArchiveFormat {
935    #[default]
936    #[serde(rename = "zip")]
937    Zip,
938    #[serde(rename = "tar.gz")]
939    TarGz,
940    #[serde(rename = "tar")]
941    Tar,
942}
943
944#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
945#[serde(deny_unknown_fields, rename_all = "camelCase")]
946pub struct DownloadSpaceFileRequest {
947    pub access_ticket_token: String,
948    pub path: String,
949    #[serde(default)]
950    pub archive_format: WorkspaceArchiveFormat,
951}
952
953#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
954#[serde(deny_unknown_fields, rename_all = "camelCase")]
955pub struct ListSpaceFilesRequest {
956    pub access_ticket_token: String,
957    #[serde(default)]
958    pub path: String,
959}
960
961#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
962#[serde(deny_unknown_fields, rename_all = "camelCase")]
963pub struct DeleteSpaceFileRequest {
964    pub access_ticket_token: String,
965    pub fencing_token: u64,
966    pub expected_revision: u64,
967}
968
969#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
970#[serde(deny_unknown_fields, rename_all = "camelCase")]
971pub struct ProviderFileContent {
972    pub bytes: Vec<u8>,
973    #[serde(default, skip_serializing_if = "Option::is_none")]
974    pub provider_revision: Option<String>,
975}
976
977pub type ProviderByteStream = Pin<Box<dyn Stream<Item = Result<Bytes>> + Send + 'static>>;
978
979pub struct ProviderSourceArchive {
980    pub stream: ProviderByteStream,
981    pub digest: String,
982    pub size_bytes: u64,
983}
984
985impl Debug for ProviderSourceArchive {
986    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
987        formatter
988            .debug_struct("ProviderSourceArchive")
989            .field("digest", &self.digest)
990            .field("size_bytes", &self.size_bytes)
991            .finish_non_exhaustive()
992    }
993}
994
995#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
996#[serde(deny_unknown_fields, rename_all = "camelCase")]
997pub struct ProviderFileEntry {
998    pub path: String,
999    pub is_dir: bool,
1000    #[serde(default, skip_serializing_if = "Option::is_none")]
1001    pub size_bytes: Option<u64>,
1002}
1003
1004#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1005#[serde(deny_unknown_fields, rename_all = "camelCase")]
1006pub struct ProviderWriteResult {
1007    #[serde(default, skip_serializing_if = "Option::is_none")]
1008    pub provider_revision: Option<String>,
1009}