Skip to main content

smol_workflow_engine/
environment.rs

1//! Execution environment abstraction for provider filesystem and process IO.
2//!
3//! This module contains the Rust-facing environment capability documented in
4//! `docs/harness-capabilities/environment.md`. Local in-process execution lives
5//! in [`local`].
6
7mod local;
8mod sandbox;
9
10use anyhow::anyhow;
11use std::collections::BTreeMap;
12use std::path::Path;
13
14pub use local::LocalExecutionEnvironment;
15pub use sandbox::SandboxExecutionEnvironment;
16
17/// UTF-8 path in the selected execution environment.
18#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19#[serde(transparent)]
20pub struct EnvironmentPath(pub String);
21
22impl EnvironmentPath {
23    pub fn as_str(&self) -> &str {
24        &self.0
25    }
26}
27
28impl From<String> for EnvironmentPath {
29    fn from(value: String) -> Self {
30        Self(value)
31    }
32}
33
34impl From<&str> for EnvironmentPath {
35    fn from(value: &str) -> Self {
36        Self(value.to_string())
37    }
38}
39
40/// Request to run a foreground/background command inside an environment.
41#[derive(Debug, Clone, Default)]
42pub struct ExecRequest {
43    /// Executable and arguments. `argv[0]` is the executable.
44    pub argv: Vec<String>,
45    /// Optional environment-local working directory override.
46    pub cwd: Option<EnvironmentPath>,
47    /// Per-call process environment overrides.
48    pub env: BTreeMap<String, String>,
49    /// Optional stdin bytes.
50    pub stdin: Option<Vec<u8>>,
51}
52
53/// Foreground command result.
54#[derive(Debug, Clone, Default, PartialEq, Eq)]
55pub struct ExecOutput {
56    pub exit_code: i32,
57    pub stdout: Vec<u8>,
58    pub stderr: Vec<u8>,
59}
60
61/// Generic process event emitted by an execution environment.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum ExecEvent {
64    Started { process_id: Option<String> },
65    Stdout { chunk: Vec<u8> },
66    Stderr { chunk: Vec<u8> },
67    Exited { exit_code: i32 },
68}
69
70#[async_trait::async_trait]
71pub trait ExecEventSink: Send {
72    async fn event(&mut self, event: ExecEvent) -> anyhow::Result<()>;
73}
74
75/// Background process start result.
76#[derive(Debug, Clone, Default, PartialEq, Eq)]
77pub struct SpawnOutput {
78    pub process_id: Option<String>,
79}
80
81#[async_trait::async_trait]
82pub trait AgentExecutionEnvironment: Send + Sync {
83    fn cwd(&self) -> Option<&EnvironmentPath>;
84
85    async fn create_dir_all(&self, path: &EnvironmentPath) -> anyhow::Result<()>;
86
87    async fn write_file(&self, path: &EnvironmentPath, content: &[u8]) -> anyhow::Result<()>;
88
89    async fn read_file(&self, path: &EnvironmentPath) -> anyhow::Result<Vec<u8>>;
90
91    async fn remove(&self, path: &EnvironmentPath) -> anyhow::Result<()>;
92
93    async fn create_temp_dir(&self, prefix: &str) -> anyhow::Result<EnvironmentPath>;
94
95    async fn exec(
96        &self,
97        request: ExecRequest,
98        sink: &mut dyn ExecEventSink,
99    ) -> anyhow::Result<ExecOutput>;
100
101    async fn spawn(
102        &self,
103        request: ExecRequest,
104        sink: Option<Box<dyn ExecEventSink>>,
105    ) -> anyhow::Result<SpawnOutput>;
106}
107
108/// Event sink that ignores all events.
109#[derive(Debug, Default)]
110pub struct NullExecEventSink;
111
112#[async_trait::async_trait]
113impl ExecEventSink for NullExecEventSink {
114    async fn event(&mut self, _event: ExecEvent) -> anyhow::Result<()> {
115        Ok(())
116    }
117}
118
119pub(crate) fn path_to_environment_path(path: impl AsRef<Path>) -> anyhow::Result<EnvironmentPath> {
120    let path = path.as_ref();
121    let value = path
122        .to_str()
123        .ok_or_else(|| anyhow!("environment paths must be valid UTF-8: {path:?}"))?;
124    Ok(EnvironmentPath(value.to_string()))
125}