1use std::sync::{Arc, Mutex};
8
9use crate::{
10 domain::{AgentError, ContentRef},
11 package_isolation::{
12 IsolatedProcessRef, IsolationAdapterSessionRef, IsolationRuntimeRef, IsolationSessionRef,
13 MountRef, NetworkNamespaceRef, PreparedEnvironmentRef, ProcessIoStreamRef,
14 ProcessStatsSnapshotRef, RootfsRef, SecretMountRef,
15 },
16 ports_isolation::{
17 CleanupRequest, CleanupResult, CleanupStatus, DetachTransferRequest, DetachTransferResult,
18 EnvironmentPrepareRequest, ImageResolution, ImageResolveRequest, IsolationCapabilityReport,
19 IsolationRuntime, MountPlan, MountResolveRequest, NetworkPrepareRequest, ProcessIoFrame,
20 ProcessIoRequest, ProcessIoStream, ProcessSignalRequest, ProcessSignalResult,
21 ProcessStartRequest, ProcessStartResult, ProcessStatsRequest, ProcessStatsSnapshot,
22 ReclaimRequest, ReclaimResult, RootfsPrepareRequest, SecretMaterializationPlan,
23 SecretPrepareRequest, SessionPrepareRequest,
24 },
25};
26
27#[derive(Clone, Debug)]
28pub struct FakeIsolationRuntime {
31 report: IsolationCapabilityReport,
32 calls: Arc<Mutex<Vec<String>>>,
33 cleanup_status: CleanupStatus,
34}
35
36impl FakeIsolationRuntime {
37 pub fn with_report(report: IsolationCapabilityReport) -> Self {
41 Self {
42 report,
43 calls: Arc::new(Mutex::new(Vec::new())),
44 cleanup_status: CleanupStatus::Completed,
45 }
46 }
47
48 pub fn unsupported_host(
52 adapter_ref: impl Into<IsolationRuntimeRef>,
53 missing: impl IntoIterator<Item = impl Into<String>>,
54 ) -> Self {
55 Self::with_report(IsolationCapabilityReport::unsupported(adapter_ref, missing))
56 }
57
58 pub fn host_process_only(adapter_ref: impl Into<IsolationRuntimeRef>) -> Self {
62 Self::with_report(IsolationCapabilityReport::host_process(adapter_ref))
63 }
64
65 pub fn with_cleanup_status(mut self, cleanup_status: CleanupStatus) -> Self {
69 self.cleanup_status = cleanup_status;
70 self
71 }
72
73 pub fn calls(&self) -> Vec<String> {
77 self.calls.lock().expect("fake isolation calls").clone()
78 }
79
80 pub fn call_count(&self) -> usize {
83 self.calls().len()
84 }
85
86 pub fn start_process_call_count(&self) -> usize {
89 self.calls()
90 .into_iter()
91 .filter(|call| call == "start_process")
92 .count()
93 }
94
95 fn push_call(&self, call: impl Into<String>) {
96 self.calls
97 .lock()
98 .expect("fake isolation calls")
99 .push(call.into());
100 }
101}
102
103impl IsolationRuntime for FakeIsolationRuntime {
104 fn runtime_ref(&self) -> &IsolationRuntimeRef {
105 &self.report.adapter_ref
106 }
107
108 fn capability_report(&self) -> Result<IsolationCapabilityReport, AgentError> {
109 self.push_call("capability_report");
110 Ok(self.report.clone())
111 }
112
113 fn prepare_session(
114 &self,
115 _request: SessionPrepareRequest,
116 ) -> Result<IsolationSessionRef, AgentError> {
117 self.push_call("prepare_session");
118 Ok(IsolationSessionRef::new("session.fake.isolation"))
119 }
120
121 fn resolve_image(&self, request: ImageResolveRequest) -> Result<ImageResolution, AgentError> {
122 self.push_call("resolve_image");
123 Ok(ImageResolution {
124 image_ref: request.image.image_ref,
125 digest: "sha256:fake.image".to_string(),
126 redacted_credential_alias: Some("credential.alias.redacted".to_string()),
127 })
128 }
129
130 fn prepare_rootfs(&self, _request: RootfsPrepareRequest) -> Result<RootfsRef, AgentError> {
131 self.push_call("prepare_rootfs");
132 Ok(RootfsRef::new("rootfs.fake.isolation"))
133 }
134
135 fn resolve_mounts(&self, _request: MountResolveRequest) -> Result<MountPlan, AgentError> {
136 self.push_call("resolve_mounts");
137 Ok(MountPlan {
138 mounts: vec![MountRef::new("mount.workspace.primary")],
139 expanded_exposure_audits: vec!["workspace snapshot mounted by alias".to_string()],
140 })
141 }
142
143 fn configure_network(
144 &self,
145 _request: NetworkPrepareRequest,
146 ) -> Result<NetworkNamespaceRef, AgentError> {
147 self.push_call("configure_network");
148 Ok(NetworkNamespaceRef::new("network.fake.isolation"))
149 }
150
151 fn prepare_secrets(
152 &self,
153 request: SecretPrepareRequest,
154 ) -> Result<SecretMaterializationPlan, AgentError> {
155 self.push_call("prepare_secrets");
156 Ok(SecretMaterializationPlan {
157 secret_mount_refs: request
158 .environment
159 .spec
160 .secrets
161 .secret_mounts
162 .iter()
163 .map(|secret| secret.mount_ref.clone())
164 .collect::<Vec<SecretMountRef>>(),
165 teardown_required: true,
166 })
167 }
168
169 fn prepare_environment(
170 &self,
171 _request: EnvironmentPrepareRequest,
172 ) -> Result<PreparedEnvironmentRef, AgentError> {
173 self.push_call("prepare_environment");
174 Ok(PreparedEnvironmentRef::new("prepared.fake.isolation"))
175 }
176
177 fn start_process(
178 &self,
179 request: ProcessStartRequest,
180 ) -> Result<ProcessStartResult, AgentError> {
181 self.push_call("start_process");
182 let process_ref = IsolatedProcessRef::new(format!(
183 "process.ref.{}",
184 request.process.process_id.as_str()
185 ));
186 Ok(ProcessStartResult {
187 process_ref,
188 adapter_session_ref: Some(IsolationAdapterSessionRef::new(
189 "adapter.session.fake.isolation",
190 )),
191 terminal_status: crate::EffectTerminalStatus::Completed,
192 external_operation_id: Some("external.fake.process.start".to_string()),
193 io_frames: vec![
194 ProcessIoFrame {
195 stream_ref: ProcessIoStreamRef::new("stream.stdout.fake"),
196 stream: ProcessIoStream::Stdout,
197 cursor: 1,
198 byte_count: 23,
199 content_hash: "sha256:fake.stdout".to_string(),
200 content_refs: vec![ContentRef::new("content.isolation.stdout")],
201 raw_content_present: false,
202 truncated: false,
203 redacted_summary: "stdout captured as content ref".to_string(),
204 },
205 ProcessIoFrame {
206 stream_ref: ProcessIoStreamRef::new("stream.stderr.fake"),
207 stream: ProcessIoStream::Stderr,
208 cursor: 1,
209 byte_count: 0,
210 content_hash: "sha256:fake.stderr.empty".to_string(),
211 content_refs: Vec::new(),
212 raw_content_present: false,
213 truncated: false,
214 redacted_summary: "stderr empty".to_string(),
215 },
216 ],
217 redacted_summary: "isolated process started".to_string(),
218 })
219 }
220
221 fn stream_io(&self, _request: ProcessIoRequest) -> Result<ProcessIoFrame, AgentError> {
222 self.push_call("stream_io");
223 Ok(ProcessIoFrame {
224 stream_ref: ProcessIoStreamRef::new("stream.stdout.fake"),
225 stream: ProcessIoStream::Stdout,
226 cursor: 1,
227 byte_count: 23,
228 content_hash: "sha256:fake.stdout".to_string(),
229 content_refs: vec![ContentRef::new("content.isolation.stdout")],
230 raw_content_present: false,
231 truncated: false,
232 redacted_summary: "stdout captured as content ref".to_string(),
233 })
234 }
235
236 fn signal_process(
237 &self,
238 request: ProcessSignalRequest,
239 ) -> Result<ProcessSignalResult, AgentError> {
240 self.push_call("signal_process");
241 Ok(ProcessSignalResult {
242 process_ref: request.process_ref,
243 signal: request.signal,
244 delivered: true,
245 redacted_summary: "signal delivered to isolated process".to_string(),
246 })
247 }
248
249 fn collect_stats(
250 &self,
251 request: ProcessStatsRequest,
252 ) -> Result<ProcessStatsSnapshot, AgentError> {
253 self.push_call("collect_stats");
254 Ok(ProcessStatsSnapshot {
255 snapshot_ref: ProcessStatsSnapshotRef::new("stats.fake.isolation"),
256 process_ref: request.process_ref,
257 cpu_millis: Some(10),
258 memory_bytes: Some(1024),
259 process_count: Some(1),
260 filesystem_bytes: Some(2048),
261 network_bytes: Some(0),
262 exit_code: Some(0),
263 redacted_summary: "fake process stats counters".to_string(),
264 })
265 }
266
267 fn cleanup(&self, request: CleanupRequest) -> Result<CleanupResult, AgentError> {
268 self.push_call("cleanup");
269 let redacted_summary = match self.cleanup_status {
270 CleanupStatus::Completed => "isolation cleanup completed",
271 CleanupStatus::RepairNeeded => "isolation cleanup requires host repair",
272 };
273 Ok(CleanupResult {
274 cleanup_plan_ref: request.cleanup_plan_ref,
275 status: self.cleanup_status,
276 external_operation_id: Some("external.fake.cleanup".to_string()),
277 redacted_summary: redacted_summary.to_string(),
278 })
279 }
280
281 fn detach(&self, _request: DetachTransferRequest) -> Result<DetachTransferResult, AgentError> {
282 self.push_call("detach");
283 Ok(DetachTransferResult {
284 host_ack_ref: "host.ack.fake.detach".to_string(),
285 redacted_summary: "detach acknowledged by fake host".to_string(),
286 })
287 }
288
289 fn reclaim(&self, request: ReclaimRequest) -> Result<ReclaimResult, AgentError> {
290 self.push_call("reclaim");
291 Ok(ReclaimResult {
292 ticket_ref: request.ticket_ref,
293 status: CleanupStatus::Completed,
294 redacted_summary: "fake reclaim completed".to_string(),
295 })
296 }
297}