1use std::sync::Arc;
6
7use serde::{Deserialize, Serialize};
8
9use crate::{
10 domain::{
11 AgentError, AgentErrorKind, AgentId, PrivacyClass, RetryClassification, RunId, SourceRef,
12 },
13 effect::{EffectResult, EffectTerminalStatus},
14 journal::{
15 JournalRecord, JournalRecordBase, JournalRecordKind, JournalRecordPayload,
16 PendingSideEffect, RecoveryMarker,
17 },
18 journal_ports::RunJournal,
19 package::{RuntimePackage, RuntimePackageFingerprint},
20 package_isolation::{
21 CleanupPlanRef, ExecutionEnvironment, IsolatedProcessSpec, IsolationCapability,
22 IsolationClass, IsolationFallback, IsolationRuntimeRef, IsolationTrustField,
23 PolicyDecisionRef,
24 },
25 ports_isolation::{
26 CleanupRequest, CleanupResult, CleanupStatus, IsolationCapabilityReport, IsolationRuntime,
27 IsolationRuntimeHealth, IsolationRuntimeRegistry, ProcessStartRequest, ProcessStartResult,
28 },
29 records_isolation::{
30 IsolationCapabilityMatchRecord, IsolationCleanupIntentRecord, IsolationCleanupResultRecord,
31 IsolationDowngradeDecisionRecord, IsolationProcessStartIntentRecord,
32 IsolationProcessStartResultRecord, IsolationRecord,
33 },
34};
35
36#[derive(Clone)]
37pub struct IsolationLifecycleCoordinator {
40 journal: Arc<dyn RunJournal>,
41 registry: IsolationRuntimeRegistry,
42}
43
44impl IsolationLifecycleCoordinator {
45 pub fn new(journal: Arc<dyn RunJournal>, registry: IsolationRuntimeRegistry) -> Self {
49 Self { journal, registry }
50 }
51
52 pub fn select_environment(
56 &self,
57 package: &RuntimePackage,
58 environment: &ExecutionEnvironment,
59 context: IsolationLifecycleContext,
60 approval: Option<IsolationDowngradeApproval>,
61 ) -> Result<IsolationSelectionOutcome, AgentError> {
62 self.validate_package_sidecar(package, environment)?;
63 let (runtime, report) = self.selected_runtime(environment)?;
64 Ok(evaluate_report(
65 environment,
66 &context,
67 runtime.runtime_ref(),
68 report,
69 approval,
70 ))
71 }
72
73 pub fn start_process(
77 &self,
78 package: &RuntimePackage,
79 environment: ExecutionEnvironment,
80 process: IsolatedProcessSpec,
81 context: IsolationLifecycleContext,
82 approval: Option<IsolationDowngradeApproval>,
83 ) -> Result<IsolationProcessOutcome, AgentError> {
84 process.validate()?;
85 let selection =
86 self.select_environment(package, &environment, context.clone(), approval)?;
87 if !selection.status.allows_adapter_call() {
88 let status = selection.status;
89 self.append_selection_record(&selection, &environment, &context, 1)?;
90 return Ok(IsolationProcessOutcome {
91 selection,
92 status,
93 intent_record: None,
94 result_record: None,
95 io_frames: Vec::new(),
96 recovery_required: false,
97 terminal_error: None,
98 });
99 }
100 let runtime_ref = selection
101 .selected_runtime_ref
102 .clone()
103 .ok_or_else(|| missing_runtime_error("isolation selection has no runtime ref"))?;
104 let runtime = self
105 .registry
106 .get(&runtime_ref)
107 .ok_or_else(|| missing_runtime_error("selected isolation runtime disappeared"))?;
108
109 let effect_intent = process.effect_intent(&environment);
110 let intent_record =
111 IsolationRecord::ProcessStartIntent(IsolationProcessStartIntentRecord {
112 environment_id: environment.environment_id.clone(),
113 process_ref: crate::IsolatedProcessRef::new(format!(
114 "process.ref.{}",
115 process.process_id.as_str()
116 )),
117 effect_intent: effect_intent.clone(),
118 redacted_summary: "start isolated process with redacted argv and environment"
119 .to_string(),
120 });
121 let intent_journal = JournalRecord::effect_intent(
122 context.record_base(1, "isolation.process.intent", &environment),
123 effect_intent.clone(),
124 );
125 self.journal
126 .append(intent_journal)
127 .map_err(journal_failure)?;
128 self.append_isolation_record(
129 &context,
130 2,
131 "isolation.process.intent",
132 &environment,
133 intent_record.clone(),
134 )?;
135 self.append_selection_record(&selection, &environment, &context, 3)?;
136
137 let start_result = runtime.start_process(ProcessStartRequest {
138 environment: environment.clone(),
139 process,
140 effect_intent: effect_intent.clone(),
141 })?;
142 let effect_result = process_start_effect_result(&effect_intent, &start_result);
143 let result_record =
144 IsolationRecord::ProcessStartResult(IsolationProcessStartResultRecord {
145 environment_id: environment.environment_id.clone(),
146 process_ref: start_result.process_ref.clone(),
147 effect_result: effect_result.clone(),
148 content_refs: effect_result.content_refs.clone(),
149 redacted_summary: start_result.redacted_summary.clone(),
150 });
151 let result_journal = JournalRecord::effect_result(
152 context.record_base(4, "isolation.process.result", &environment),
153 effect_result.clone(),
154 );
155
156 match self.journal.append(result_journal) {
157 Ok(_) => {
158 self.append_isolation_record(
159 &context,
160 5,
161 "isolation.process.result.record",
162 &environment,
163 result_record.clone(),
164 )?;
165 Ok(IsolationProcessOutcome {
166 status: selection.status,
167 selection,
168 intent_record: Some(intent_record),
169 result_record: Some(result_record),
170 io_frames: start_result.io_frames,
171 recovery_required: false,
172 terminal_error: None,
173 })
174 }
175 Err(result_error) => {
176 let recovery = RecoveryMarker {
177 unsafe_pending: vec![PendingSideEffect {
178 effect_id: effect_intent.effect_id.clone(),
179 intent_record_id: "journal.isolation.process.intent".to_string(),
180 idempotency_key: effect_intent.idempotency_key.clone(),
181 dedupe_key: effect_intent.dedupe_key.clone(),
182 unsafe_pending_reason:
183 "isolated process may have started before terminal result append"
184 .to_string(),
185 }],
186 recovery_reason: format!(
187 "isolated process terminal result append failed: {}",
188 result_error.context().message
189 ),
190 policy_refs: effect_intent.policy_refs.clone(),
191 };
192 let recovery_record = JournalRecord::recovery(
193 context.record_base(5, "isolation.process.recovery", &environment),
194 recovery,
195 );
196 self.journal
197 .append(recovery_record)
198 .map_err(|recovery_error| {
199 AgentError::new(
200 AgentErrorKind::RecoveryRepairNeeded,
201 RetryClassification::RepairNeeded,
202 format!(
203 "process result append failed and recovery append failed: {}",
204 recovery_error.context().message
205 ),
206 )
207 })?;
208 Ok(IsolationProcessOutcome {
209 status: selection.status,
210 selection,
211 intent_record: Some(intent_record),
212 result_record: Some(result_record),
213 io_frames: start_result.io_frames,
214 recovery_required: true,
215 terminal_error: Some(AgentError::new(
216 AgentErrorKind::RecoveryRepairNeeded,
217 RetryClassification::RepairNeeded,
218 "isolated process terminal result append failed; recovery required",
219 )),
220 })
221 }
222 }
223 }
224
225 pub fn cleanup_environment(
229 &self,
230 environment: ExecutionEnvironment,
231 context: IsolationLifecycleContext,
232 ) -> Result<IsolationCleanupOutcome, AgentError> {
233 let runtime = self
234 .registry
235 .first()
236 .ok_or_else(|| missing_runtime_error("no isolation runtime is registered"))?;
237 let cleanup_plan_ref =
238 CleanupPlanRef::new(format!("cleanup.{}", environment.environment_id.as_str()));
239 let mut intent = crate::EffectIntent::new(
240 crate::EffectId::new(format!("effect.{}", cleanup_plan_ref.as_str())),
241 crate::EffectKind::ChildArtifactShutdown,
242 environment.subject_ref(),
243 environment.source.clone(),
244 "cleanup isolated environment and child artifacts",
245 );
246 intent.destination = Some(environment.destination.clone());
247
248 let intent_record = JournalRecord::effect_intent(
249 context.record_base(1, "isolation.cleanup.intent", &environment),
250 intent.clone(),
251 );
252 self.journal
253 .append(intent_record)
254 .map_err(journal_failure)?;
255 let isolation_intent = IsolationRecord::CleanupIntent(IsolationCleanupIntentRecord {
256 environment_id: environment.environment_id.clone(),
257 cleanup_plan_ref: cleanup_plan_ref.clone(),
258 effect_intent: intent.clone(),
259 redacted_summary: "cleanup isolated environment and child artifacts".to_string(),
260 });
261 self.append_isolation_record(
262 &context,
263 2,
264 "isolation.cleanup.intent.record",
265 &environment,
266 isolation_intent,
267 )?;
268
269 let cleanup = runtime.cleanup(CleanupRequest {
270 environment: environment.clone(),
271 cleanup_plan_ref: cleanup_plan_ref.clone(),
272 })?;
273 let terminal_status = match cleanup.status {
274 CleanupStatus::Completed => EffectTerminalStatus::Completed,
275 CleanupStatus::RepairNeeded => EffectTerminalStatus::Failed,
276 };
277 let result = EffectResult {
278 effect_id: intent.effect_id.clone(),
279 terminal_status,
280 external_operation_id: cleanup.external_operation_id.clone(),
281 reconciliation_ref: None,
282 error_ref: (cleanup.status == CleanupStatus::RepairNeeded)
283 .then(|| "cleanup.repair_needed".to_string()),
284 content_refs: Vec::new(),
285 redacted_summary: cleanup.redacted_summary.clone(),
286 };
287 let result_record = JournalRecord::effect_result(
288 context.record_base(3, "isolation.cleanup.result", &environment),
289 result.clone(),
290 );
291 self.journal
292 .append(result_record)
293 .map_err(journal_failure)?;
294 let isolation_result = IsolationRecord::CleanupResult(IsolationCleanupResultRecord {
295 environment_id: environment.environment_id.clone(),
296 cleanup_plan_ref,
297 status: cleanup.status,
298 effect_result: result,
299 redacted_summary: cleanup.redacted_summary.clone(),
300 });
301 self.append_isolation_record(
302 &context,
303 4,
304 "isolation.cleanup.result.record",
305 &environment,
306 isolation_result,
307 )?;
308
309 Ok(IsolationCleanupOutcome {
310 status: cleanup.status,
311 cleanup_result: cleanup,
312 })
313 }
314
315 fn validate_package_sidecar(
316 &self,
317 package: &RuntimePackage,
318 environment: &ExecutionEnvironment,
319 ) -> Result<(), AgentError> {
320 let snapshot = package
321 .isolation_requirements
322 .iter()
323 .find(|snapshot| snapshot.requirement_ref == environment.requirement_ref)
324 .ok_or_else(|| {
325 AgentError::new(
326 AgentErrorKind::IsolationFailure,
327 RetryClassification::HostConfigurationNeeded,
328 "runtime package is missing the requested isolation sidecar",
329 )
330 })?;
331 snapshot.validate()
332 }
333
334 fn selected_runtime(
335 &self,
336 environment: &ExecutionEnvironment,
337 ) -> Result<(Arc<dyn IsolationRuntime>, IsolationCapabilityReport), AgentError> {
338 let runtime = environment
339 .spec
340 .requirement
341 .preferred_adapters
342 .iter()
343 .find_map(|runtime_ref| self.registry.get(runtime_ref))
344 .or_else(|| self.registry.first())
345 .ok_or_else(|| missing_runtime_error("no isolation runtime is registered"))?;
346 let report = runtime.capability_report()?;
347 Ok((runtime, report))
348 }
349
350 fn append_selection_record(
351 &self,
352 selection: &IsolationSelectionOutcome,
353 environment: &ExecutionEnvironment,
354 context: &IsolationLifecycleContext,
355 journal_seq: u64,
356 ) -> Result<(), AgentError> {
357 let record = selection
358 .match_record
359 .clone()
360 .map(IsolationRecord::CapabilityMatch)
361 .or_else(|| {
362 selection
363 .downgrade_record
364 .clone()
365 .map(IsolationRecord::DowngradeDecision)
366 });
367 if let Some(record) = record {
368 self.append_isolation_record(
369 context,
370 journal_seq,
371 "isolation.selection",
372 environment,
373 record,
374 )?;
375 }
376 Ok(())
377 }
378
379 fn append_isolation_record(
380 &self,
381 context: &IsolationLifecycleContext,
382 journal_seq: u64,
383 record_id: &str,
384 environment: &ExecutionEnvironment,
385 record: IsolationRecord,
386 ) -> Result<(), AgentError> {
387 let base = context.record_base(journal_seq, record_id, environment);
388 self.journal
389 .append(JournalRecord::feature_record(
390 base,
391 JournalRecordKind::Isolation,
392 "isolation",
393 isolation_event_kind(&record),
394 environment.subject_ref(),
395 Vec::new(),
396 isolation_content_refs(&record),
397 JournalRecordPayload::Isolation(record),
398 ))
399 .map(|_| ())
400 .map_err(journal_failure)
401 }
402}
403
404#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
405pub struct IsolationLifecycleContext {
408 pub run_id: RunId,
410 pub agent_id: AgentId,
412 pub source: SourceRef,
415 pub runtime_package_fingerprint: RuntimePackageFingerprint,
419 pub privacy: PrivacyClass,
422 pub redaction_policy_id: String,
424 pub readiness_profile: IsolationReadinessProfile,
428}
429
430impl IsolationLifecycleContext {
431 pub fn test(runtime_package_fingerprint: RuntimePackageFingerprint) -> Self {
435 Self {
436 run_id: RunId::new("run.isolation.contract"),
437 agent_id: AgentId::new("agent.isolation.contract"),
438 source: SourceRef::with_kind(crate::SourceKind::Sdk, "source.sdk.isolation"),
439 runtime_package_fingerprint,
440 privacy: PrivacyClass::ContentRefsOnly,
441 redaction_policy_id: "policy.redaction.isolation".to_string(),
442 readiness_profile: IsolationReadinessProfile::TestOnly,
443 }
444 }
445
446 fn record_base(
447 &self,
448 journal_seq: u64,
449 record_id: &str,
450 environment: &ExecutionEnvironment,
451 ) -> JournalRecordBase {
452 let mut base = JournalRecordBase::new(
453 journal_seq,
454 format!("journal.{record_id}"),
455 self.run_id.clone(),
456 self.agent_id.clone(),
457 self.source.clone(),
458 );
459 base.destination = Some(environment.destination.clone());
460 base.runtime_package_fingerprint = self.runtime_package_fingerprint.as_str().to_string();
461 base.privacy = self.privacy;
462 base.redaction_policy_id = self.redaction_policy_id.clone();
463 base.tags = vec!["isolation".to_string()];
464 base
465 }
466}
467
468#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
469#[serde(rename_all = "snake_case")]
470pub enum IsolationReadinessProfile {
473 Production,
475 TestOnly,
477}
478
479#[derive(Clone, Debug)]
480pub struct IsolationSelectionOutcome {
483 pub status: IsolationMatchStatus,
485 pub selected_runtime_ref: Option<IsolationRuntimeRef>,
488 pub capability_report: Option<IsolationCapabilityReport>,
491 pub match_record: Option<IsolationCapabilityMatchRecord>,
494 pub downgrade_record: Option<IsolationDowngradeDecisionRecord>,
497 pub terminal_error: Option<AgentError>,
500}
501
502#[derive(Clone, Debug)]
503pub struct IsolationProcessOutcome {
506 pub selection: IsolationSelectionOutcome,
508 pub status: IsolationMatchStatus,
510 pub intent_record: Option<IsolationRecord>,
513 pub result_record: Option<IsolationRecord>,
516 pub io_frames: Vec<crate::ProcessIoFrame>,
520 pub recovery_required: bool,
523 pub terminal_error: Option<AgentError>,
526}
527
528#[derive(Clone, Debug)]
529pub struct IsolationCleanupOutcome {
532 pub status: CleanupStatus,
534 pub cleanup_result: CleanupResult,
536}
537
538#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
539pub enum IsolationMatchStatus {
542 Matched,
544 DowngradeApproved,
546 DowngradeDenied,
548 UnsupportedHost,
550}
551
552impl IsolationMatchStatus {
553 fn allows_adapter_call(self) -> bool {
554 matches!(self, Self::Matched | Self::DowngradeApproved)
555 }
556}
557
558#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
559pub struct IsolationDowngradeApproval {
562 pub decision_ref: PolicyDecisionRef,
565 pub scope: PolicyDecisionScope,
567 #[serde(default, skip_serializing_if = "Vec::is_empty")]
568 pub approved_classes: Vec<IsolationClass>,
571 #[serde(default, skip_serializing_if = "Vec::is_empty")]
572 pub approved_capability_downgrades: Vec<IsolationCapability>,
576 #[serde(default, skip_serializing_if = "Vec::is_empty")]
577 pub approved_trust_downgrades: Vec<IsolationTrustField>,
581}
582
583impl IsolationDowngradeApproval {
584 pub fn approved_for_isolation(decision_ref: impl Into<PolicyDecisionRef>) -> Self {
588 Self {
589 decision_ref: decision_ref.into(),
590 scope: PolicyDecisionScope::IsolationDowngrade,
591 approved_classes: Vec::new(),
592 approved_capability_downgrades: Vec::new(),
593 approved_trust_downgrades: Vec::new(),
594 }
595 }
596
597 pub fn approved_for_tool(decision_ref: impl Into<PolicyDecisionRef>) -> Self {
601 Self {
602 decision_ref: decision_ref.into(),
603 scope: PolicyDecisionScope::ToolApproval,
604 approved_classes: Vec::new(),
605 approved_capability_downgrades: Vec::new(),
606 approved_trust_downgrades: Vec::new(),
607 }
608 }
609
610 pub fn approve_capability(mut self, capability: IsolationCapability) -> Self {
614 self.approved_capability_downgrades.push(capability);
615 self
616 }
617
618 pub fn approve_trust(mut self, field: IsolationTrustField) -> Self {
622 self.approved_trust_downgrades.push(field);
623 self
624 }
625}
626
627#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
628pub enum PolicyDecisionScope {
631 IsolationDowngrade,
633 ToolApproval,
635}
636
637fn evaluate_report(
638 environment: &ExecutionEnvironment,
639 context: &IsolationLifecycleContext,
640 selected_runtime_ref: &IsolationRuntimeRef,
641 report: IsolationCapabilityReport,
642 approval: Option<IsolationDowngradeApproval>,
643) -> IsolationSelectionOutcome {
644 let selected_class = report
645 .supported_classes
646 .first()
647 .copied()
648 .unwrap_or(IsolationClass::HostProcess);
649 let requested_class = environment.spec.requirement.minimum_class;
650 let capability_gaps = environment
651 .spec
652 .requirement
653 .required_capabilities
654 .missing_from(&report.capabilities);
655 let trust_gaps = environment
656 .spec
657 .requirement
658 .trust
659 .gaps_against(&report.trust);
660 let class_gap = !class_satisfies(requested_class, selected_class);
661
662 if matches!(
663 report.health,
664 IsolationRuntimeHealth::UnsupportedHost { .. }
665 ) {
666 return denied_outcome(
667 IsolationMatchStatus::UnsupportedHost,
668 environment,
669 selected_runtime_ref,
670 requested_class,
671 selected_class,
672 capability_gaps,
673 trust_gaps,
674 approval,
675 Some(report),
676 "isolation runtime reports unsupported host",
677 );
678 }
679
680 if !class_gap && capability_gaps.is_empty() && trust_gaps.is_empty() {
681 let match_record = IsolationCapabilityMatchRecord {
682 environment_id: environment.environment_id.clone(),
683 adapter_ref: selected_runtime_ref.clone(),
684 requested_class,
685 selected_class,
686 missing_capabilities: Vec::new(),
687 trust_gaps: Vec::new(),
688 redacted_summary: "isolation capability report satisfies requirement".to_string(),
689 };
690 return IsolationSelectionOutcome {
691 status: IsolationMatchStatus::Matched,
692 selected_runtime_ref: Some(selected_runtime_ref.clone()),
693 capability_report: Some(report),
694 match_record: Some(match_record),
695 downgrade_record: None,
696 terminal_error: None,
697 };
698 }
699
700 if fallback_allows(
701 &environment.spec.requirement.fallback,
702 context,
703 selected_class,
704 class_gap,
705 &capability_gaps,
706 &trust_gaps,
707 approval.as_ref(),
708 ) {
709 return IsolationSelectionOutcome {
710 status: IsolationMatchStatus::DowngradeApproved,
711 selected_runtime_ref: Some(selected_runtime_ref.clone()),
712 capability_report: Some(report),
713 match_record: None,
714 downgrade_record: Some(downgrade_record(
715 environment,
716 selected_runtime_ref,
717 requested_class,
718 selected_class,
719 capability_gaps,
720 trust_gaps,
721 true,
722 approval,
723 "isolation downgrade approved by package and isolation policy decision",
724 )),
725 terminal_error: None,
726 };
727 }
728
729 denied_outcome(
730 IsolationMatchStatus::DowngradeDenied,
731 environment,
732 selected_runtime_ref,
733 requested_class,
734 selected_class,
735 capability_gaps,
736 trust_gaps,
737 approval,
738 Some(report),
739 "isolation downgrade denied before adapter execution",
740 )
741}
742
743fn class_satisfies(requested: IsolationClass, selected: IsolationClass) -> bool {
744 if requested == selected || requested == IsolationClass::HostProcess {
745 return true;
746 }
747 matches!(
748 (requested, selected),
749 (
750 IsolationClass::Sandbox,
751 IsolationClass::Container
752 | IsolationClass::LightweightVm
753 | IsolationClass::RemoteSandbox
754 )
755 )
756}
757
758fn fallback_allows(
759 fallback: &IsolationFallback,
760 context: &IsolationLifecycleContext,
761 selected_class: IsolationClass,
762 class_gap: bool,
763 capability_gaps: &[IsolationCapability],
764 trust_gaps: &[IsolationTrustField],
765 approval: Option<&IsolationDowngradeApproval>,
766) -> bool {
767 match fallback {
768 IsolationFallback::Deny => false,
769 IsolationFallback::TestOnlyHostProcess => {
770 selected_class == IsolationClass::HostProcess
771 && context.readiness_profile == IsolationReadinessProfile::TestOnly
772 }
773 IsolationFallback::AllowIfPackageAndPolicyApprove {
774 accepted_classes,
775 accepted_capability_downgrades,
776 accepted_trust_downgrades,
777 ..
778 } => {
779 let Some(approval) = approval else {
780 return false;
781 };
782 if approval.scope != PolicyDecisionScope::IsolationDowngrade {
783 return false;
784 }
785 if class_gap && !accepted_classes.contains(&selected_class) {
786 return false;
787 }
788 if class_gap
789 && !approval.approved_classes.is_empty()
790 && !approval.approved_classes.contains(&selected_class)
791 {
792 return false;
793 }
794 capability_gaps.iter().all(|gap| {
795 accepted_capability_downgrades.contains(gap)
796 && approval.approved_capability_downgrades.contains(gap)
797 }) && trust_gaps.iter().all(|gap| {
798 accepted_trust_downgrades.contains(gap)
799 && approval.approved_trust_downgrades.contains(gap)
800 })
801 }
802 }
803}
804
805#[expect(
806 clippy::too_many_arguments,
807 reason = "private isolation helper keeps downgrade evidence explicit; grouping belongs with a broader selection-outcome builder"
808)]
809fn denied_outcome(
810 status: IsolationMatchStatus,
811 environment: &ExecutionEnvironment,
812 selected_runtime_ref: &IsolationRuntimeRef,
813 requested_class: IsolationClass,
814 selected_class: IsolationClass,
815 capability_gaps: Vec<IsolationCapability>,
816 trust_gaps: Vec<IsolationTrustField>,
817 approval: Option<IsolationDowngradeApproval>,
818 report: Option<IsolationCapabilityReport>,
819 summary: &str,
820) -> IsolationSelectionOutcome {
821 IsolationSelectionOutcome {
822 status,
823 selected_runtime_ref: Some(selected_runtime_ref.clone()),
824 capability_report: report,
825 match_record: None,
826 downgrade_record: Some(downgrade_record(
827 environment,
828 selected_runtime_ref,
829 requested_class,
830 selected_class,
831 capability_gaps,
832 trust_gaps,
833 false,
834 approval,
835 summary,
836 )),
837 terminal_error: None,
838 }
839}
840
841#[expect(
842 clippy::too_many_arguments,
843 reason = "private isolation helper mirrors the durable downgrade decision record fields for auditability"
844)]
845fn downgrade_record(
846 environment: &ExecutionEnvironment,
847 selected_runtime_ref: &IsolationRuntimeRef,
848 requested_class: IsolationClass,
849 selected_class: IsolationClass,
850 capability_gaps: Vec<IsolationCapability>,
851 trust_gaps: Vec<IsolationTrustField>,
852 approved: bool,
853 approval: Option<IsolationDowngradeApproval>,
854 summary: &str,
855) -> IsolationDowngradeDecisionRecord {
856 IsolationDowngradeDecisionRecord {
857 environment_id: environment.environment_id.clone(),
858 adapter_ref: selected_runtime_ref.clone(),
859 requested_class,
860 selected_class,
861 capability_gaps,
862 trust_gaps,
863 approved,
864 policy_decision_scope: approval.as_ref().map(|approval| approval.scope),
865 policy_decision_refs: approval
866 .map(|approval| vec![approval.decision_ref])
867 .unwrap_or_default(),
868 redacted_summary: summary.to_string(),
869 }
870}
871
872fn process_start_effect_result(
873 intent: &crate::EffectIntent,
874 result: &ProcessStartResult,
875) -> EffectResult {
876 EffectResult {
877 effect_id: intent.effect_id.clone(),
878 terminal_status: result.terminal_status.clone(),
879 external_operation_id: result.external_operation_id.clone(),
880 reconciliation_ref: None,
881 error_ref: None,
882 content_refs: result
883 .io_frames
884 .iter()
885 .flat_map(|frame| frame.content_refs.clone())
886 .collect(),
887 redacted_summary: result.redacted_summary.clone(),
888 }
889}
890
891fn isolation_event_kind(record: &IsolationRecord) -> &'static str {
892 match record {
893 IsolationRecord::Requested(_) => "isolation_requested",
894 IsolationRecord::AdapterCapabilityReported(_) => "isolation_adapter_health_checked",
895 IsolationRecord::CapabilityMatch(_) => "isolation_capability_matched",
896 IsolationRecord::DowngradeDecision(record) if record.approved => {
897 "isolation_downgrade_approved"
898 }
899 IsolationRecord::DowngradeDecision(_) => "isolation_downgrade_denied",
900 IsolationRecord::EnvironmentPrepareIntent(_)
901 | IsolationRecord::EnvironmentPrepareResult(_) => "isolation_environment_prepared",
902 IsolationRecord::ProcessStartIntent(_) | IsolationRecord::ProcessStartResult(_) => {
903 "isolation_process_started"
904 }
905 IsolationRecord::ProcessIoFrame(_) => "isolation_process_io_captured",
906 IsolationRecord::ProcessStatsSnapshot(_) => "isolation_process_stats_recorded",
907 IsolationRecord::CleanupIntent(_) => "isolation_cleanup_started",
908 IsolationRecord::CleanupResult(record) if record.status == CleanupStatus::Completed => {
909 "isolation_cleanup_completed"
910 }
911 IsolationRecord::CleanupResult(_) => "isolation_cleanup_failed",
912 IsolationRecord::Failed(_) => "isolation_failed",
913 }
914}
915
916fn isolation_content_refs(record: &IsolationRecord) -> Vec<crate::domain::ContentRef> {
917 match record {
918 IsolationRecord::ProcessStartResult(record) => record.content_refs.clone(),
919 IsolationRecord::CleanupResult(record) => record.effect_result.content_refs.clone(),
920 _ => Vec::new(),
921 }
922}
923
924fn missing_runtime_error(message: impl Into<String>) -> AgentError {
925 AgentError::new(
926 AgentErrorKind::IsolationFailure,
927 RetryClassification::HostConfigurationNeeded,
928 message,
929 )
930}
931
932fn journal_failure(error: AgentError) -> AgentError {
933 AgentError::new(
934 AgentErrorKind::JournalFailure,
935 RetryClassification::RepairNeeded,
936 error.context().message,
937 )
938}