Skip to main content

agent_sdk_toolkit/environment/
profile.rs

1use std::collections::BTreeSet;
2
3use agent_sdk_core::{
4    AgentError, DestinationKind, DestinationRef, ExecutionEnvironment, ExecutionEnvironmentBuilder,
5    ExecutionEnvironmentKind, FilesystemIsolationPolicy, IsolationCapability, IsolationClass,
6    IsolationRequirement, IsolationRequirementSnapshot, IsolationRuntimeRef,
7    NetworkIsolationPolicy, PolicyKind, PolicyRef, RuntimePackageSidecarId, SecretExposurePolicy,
8    SourceKind, SourceRef, WorkspaceMountMode,
9};
10
11use crate::environment::{EgressAllowlist, EnvironmentRuntime};
12
13#[derive(Clone, Debug)]
14/// Builder for common isolated agent workspace environments.
15///
16/// The builder is data-only. It lowers into core isolation DTOs and does not
17/// install runtimes, start containers, open sockets, resolve DNS, or configure
18/// host firewall state.
19pub struct AgentWorkspaceEnvironmentProfile {
20    environment_id: String,
21    isolation_class: IsolationClass,
22    preferred_runtimes: Vec<IsolationRuntimeRef>,
23    workspace_ref: Option<String>,
24    workspace_mode: WorkspaceMountMode,
25    network: NetworkIsolationPolicy,
26    egress_allowlist: Option<EgressAllowlist>,
27    required_capabilities: BTreeSet<IsolationCapability>,
28    source: SourceRef,
29    destination: DestinationRef,
30}
31
32impl AgentWorkspaceEnvironmentProfile {
33    /// Creates a profile with safe defaults: no network, no ambient secrets,
34    /// snapshot workspace mounts when a workspace is supplied, and cleanup-required lifecycle.
35    pub fn new(environment_id: impl Into<String>) -> Self {
36        let mut required_capabilities = BTreeSet::new();
37        required_capabilities.insert(IsolationCapability::Cleanup);
38        required_capabilities.insert(IsolationCapability::ReadOnlyRoot);
39        required_capabilities.insert(IsolationCapability::ProcessTimeout);
40        required_capabilities.insert(IsolationCapability::IoRedaction);
41        required_capabilities.insert(IsolationCapability::NoNetworkGuarantee);
42        Self {
43            environment_id: environment_id.into(),
44            isolation_class: IsolationClass::Container,
45            preferred_runtimes: Vec::new(),
46            workspace_ref: None,
47            workspace_mode: WorkspaceMountMode::Snapshot,
48            network: NetworkIsolationPolicy::Disabled,
49            egress_allowlist: None,
50            required_capabilities,
51            source: SourceRef::with_kind(SourceKind::Sdk, "source.sdk.toolkit.environment"),
52            destination: DestinationRef::with_kind(
53                DestinationKind::ExternalRuntime,
54                "destination.toolkit.environment",
55            ),
56        }
57    }
58
59    /// Sets the minimum isolation class required for this environment.
60    pub fn isolation_class(mut self, isolation_class: IsolationClass) -> Self {
61        self.isolation_class = isolation_class;
62        self
63    }
64
65    /// Adds a preferred runtime adapter ref. Selection still requires a
66    /// registered adapter capability report that satisfies the package policy.
67    pub fn prefer_runtime(mut self, runtime_ref: impl Into<IsolationRuntimeRef>) -> Self {
68        self.preferred_runtimes.push(runtime_ref.into());
69        self
70    }
71
72    /// Sets the minimum isolation class and preferred adapter from a known toolkit runtime.
73    ///
74    /// This is a data-only shortcut for calling [`Self::isolation_class`] with
75    /// the runtime's matching class and [`Self::prefer_runtime`] with its stable
76    /// core [`IsolationRuntimeRef`]. It does not register adapters, start
77    /// containers, open sockets, or relax host policy; runtime selection still
78    /// requires a host-provided adapter capability report during execution.
79    pub fn runtime(mut self, runtime: EnvironmentRuntime) -> Self {
80        self.isolation_class = runtime.isolation_class();
81        self.preferred_runtimes.push(runtime.into());
82        self
83    }
84
85    /// Mounts a workspace by alias using the default snapshot mode.
86    pub fn workspace(mut self, workspace_ref: impl Into<String>) -> Self {
87        self.workspace_ref = Some(workspace_ref.into());
88        self.workspace_mode = WorkspaceMountMode::Snapshot;
89        self
90    }
91
92    /// Mounts a workspace by alias with an explicit core workspace mode.
93    pub fn workspace_with_mode(
94        mut self,
95        workspace_ref: impl Into<String>,
96        mode: WorkspaceMountMode,
97    ) -> Self {
98        self.workspace_ref = Some(workspace_ref.into());
99        self.workspace_mode = mode;
100        self
101    }
102
103    /// Keeps the environment network disabled and requires a no-network guarantee.
104    pub fn no_network(mut self) -> Self {
105        self.network = NetworkIsolationPolicy::Disabled;
106        self.egress_allowlist = None;
107        self.required_capabilities
108            .remove(&IsolationCapability::EgressAllowlist);
109        self.required_capabilities
110            .insert(IsolationCapability::NoNetworkGuarantee);
111        self
112    }
113
114    /// Applies a deterministic egress allowlist and requires adapter support for it.
115    pub fn egress_allowlist(mut self, allowlist: EgressAllowlist) -> Self {
116        if allowlist.is_empty() {
117            return self.no_network();
118        }
119        self.egress_allowlist = Some(allowlist);
120        self.required_capabilities
121            .remove(&IsolationCapability::NoNetworkGuarantee);
122        self.required_capabilities
123            .insert(IsolationCapability::EgressAllowlist);
124        self
125    }
126
127    /// Adds a required isolation capability to the lowered requirement.
128    pub fn require_capability(mut self, capability: IsolationCapability) -> Self {
129        self.required_capabilities.insert(capability);
130        self
131    }
132
133    /// Sets the source ref used for lineage.
134    pub fn source(mut self, source: SourceRef) -> Self {
135        self.source = source;
136        self
137    }
138
139    /// Sets the destination ref used for lineage.
140    pub fn destination(mut self, destination: DestinationRef) -> Self {
141        self.destination = destination;
142        self
143    }
144
145    /// Builds the canonical core environment plus toolkit metadata.
146    pub fn build(self) -> Result<AgentWorkspaceEnvironment, AgentError> {
147        let environment_id = self.environment_id;
148        let network = if let Some(allowlist) = &self.egress_allowlist {
149            allowlist.network_policy()?
150        } else {
151            self.network
152        };
153        let mut requirement = IsolationRequirement::at_least(self.isolation_class);
154        for runtime_ref in self.preferred_runtimes {
155            requirement = requirement.prefer(runtime_ref);
156        }
157        requirement = requirement.require_capabilities(self.required_capabilities);
158
159        let mut builder = ExecutionEnvironment::require(requirement)
160            .environment_id(environment_id.as_str())
161            .network(network)
162            .secrets(SecretExposurePolicy::no_ambient())
163            .ephemeral()
164            .source(self.source)
165            .destination(self.destination);
166
167        builder = apply_workspace(builder, self.workspace_ref, self.workspace_mode);
168
169        let mut environment = builder.build()?;
170        environment.spec.kind = environment_kind_for_class(self.isolation_class);
171
172        Ok(AgentWorkspaceEnvironment {
173            environment,
174            egress_allowlist: self.egress_allowlist,
175        })
176    }
177}
178
179#[derive(Clone, Debug)]
180/// Lowered toolkit environment profile.
181pub struct AgentWorkspaceEnvironment {
182    /// Canonical core execution environment.
183    pub environment: ExecutionEnvironment,
184    /// Optional toolkit egress allowlist that produced the network policy.
185    pub egress_allowlist: Option<EgressAllowlist>,
186}
187
188impl AgentWorkspaceEnvironment {
189    /// Returns the canonical core execution environment.
190    pub fn environment(&self) -> &ExecutionEnvironment {
191        &self.environment
192    }
193
194    /// Consumes this profile into the canonical core execution environment.
195    pub fn into_environment(self) -> ExecutionEnvironment {
196        self.environment
197    }
198
199    /// Returns the optional egress allowlist that produced the network policy.
200    pub fn egress_allowlist(&self) -> Option<&EgressAllowlist> {
201        self.egress_allowlist.as_ref()
202    }
203
204    /// Builds an isolation requirement snapshot for attachment to a `RuntimePackage`.
205    pub fn isolation_snapshot(
206        &self,
207        sidecar_id: RuntimePackageSidecarId,
208        redaction_policy_ref: PolicyRef,
209        cleanup_policy_ref: PolicyRef,
210        child_lifecycle_policy_ref: PolicyRef,
211    ) -> IsolationRequirementSnapshot {
212        IsolationRequirementSnapshot::from_environment(
213            sidecar_id,
214            &self.environment,
215            redaction_policy_ref,
216            cleanup_policy_ref,
217            child_lifecycle_policy_ref,
218        )
219    }
220
221    /// Builds an isolation snapshot using conservative toolkit policy refs.
222    pub fn default_isolation_snapshot(&self) -> IsolationRequirementSnapshot {
223        self.isolation_snapshot(
224            RuntimePackageSidecarId::new("sidecar.toolkit.environment.isolation"),
225            PolicyRef::with_kind(
226                PolicyKind::RuntimePackage,
227                "policy.toolkit.environment.redaction",
228            ),
229            PolicyRef::with_kind(
230                PolicyKind::RuntimePackage,
231                "policy.toolkit.environment.cleanup",
232            ),
233            PolicyRef::with_kind(
234                PolicyKind::RuntimePackage,
235                "policy.toolkit.environment.child_lifecycle",
236            ),
237        )
238    }
239}
240
241fn apply_workspace(
242    builder: ExecutionEnvironmentBuilder,
243    workspace_ref: Option<String>,
244    mode: WorkspaceMountMode,
245) -> ExecutionEnvironmentBuilder {
246    if let Some(workspace_ref) = workspace_ref {
247        builder.workspace(workspace_ref, mode)
248    } else {
249        builder.filesystem(FilesystemIsolationPolicy::no_workspace())
250    }
251}
252
253fn environment_kind_for_class(isolation_class: IsolationClass) -> ExecutionEnvironmentKind {
254    match isolation_class {
255        IsolationClass::HostProcess => ExecutionEnvironmentKind::HostProcess,
256        IsolationClass::Sandbox => ExecutionEnvironmentKind::Sandbox,
257        IsolationClass::Container => ExecutionEnvironmentKind::Container,
258        IsolationClass::LightweightVm => ExecutionEnvironmentKind::LightweightVm,
259        IsolationClass::RemoteSandbox => ExecutionEnvironmentKind::RemoteSandbox,
260    }
261}