harn_cli/commands/run/
sandbox.rs1use std::path::{Path, PathBuf};
2
3#[derive(Clone, Debug, PartialEq, Eq)]
4pub struct RunSandboxOptions {
5 pub enabled: bool,
7 pub workspace_root: Option<PathBuf>,
11 pub write_roots: Vec<PathBuf>,
15 pub read_only_roots: Vec<PathBuf>,
18 pub allow_process_network: bool,
21}
22
23impl Default for RunSandboxOptions {
24 fn default() -> Self {
25 Self {
26 enabled: true,
27 workspace_root: None,
28 write_roots: Vec::new(),
29 read_only_roots: Vec::new(),
30 allow_process_network: false,
31 }
32 }
33}
34
35impl RunSandboxOptions {
36 pub fn sandboxed(allow_process_network: bool) -> Self {
38 Self::default().with_process_network(allow_process_network)
39 }
40
41 pub fn with_process_network(mut self, enabled: bool) -> Self {
44 self.allow_process_network = enabled;
45 self
46 }
47
48 pub fn disabled() -> Self {
50 Self {
51 enabled: false,
52 workspace_root: None,
53 write_roots: Vec::new(),
54 read_only_roots: Vec::new(),
55 allow_process_network: false,
56 }
57 }
58
59 pub fn with_workspace_root(mut self, workspace_root: impl Into<PathBuf>) -> Self {
61 self.workspace_root = Some(workspace_root.into());
62 self
63 }
64
65 pub fn with_write_roots<I>(mut self, write_roots: I) -> Self
67 where
68 I: IntoIterator<Item = PathBuf>,
69 {
70 self.write_roots = write_roots.into_iter().collect();
71 self
72 }
73
74 pub fn with_read_only_roots<I>(mut self, read_only_roots: I) -> Self
76 where
77 I: IntoIterator<Item = PathBuf>,
78 {
79 self.read_only_roots = read_only_roots.into_iter().collect();
80 self
81 }
82}
83
84struct ExecutionPolicyGuard;
85
86impl Drop for ExecutionPolicyGuard {
87 fn drop(&mut self) {
88 harn_vm::orchestration::pop_execution_policy();
89 }
90}
91
92pub(super) struct RunSandboxScope {
93 _execution_policy: Option<ExecutionPolicyGuard>,
94 _egress_policy: Option<harn_vm::egress::ExplicitEgressPolicyGuard>,
95 _ssrf_guard: Option<harn_vm::egress::SsrfGuardScope>,
96}
97
98impl RunSandboxScope {
99 fn disabled() -> Self {
100 Self {
101 _execution_policy: None,
102 _egress_policy: None,
103 _ssrf_guard: None,
104 }
105 }
106}
107
108pub(super) fn install_run_sandbox_scope(
109 options: &RunSandboxOptions,
110 workspace_root: &Path,
111 stderr: &mut String,
112) -> RunSandboxScope {
113 if !options.enabled {
114 stderr.push_str(
115 "warning: harn run --no-sandbox disables filesystem, process, and egress sandbox defaults\n",
116 );
117 return RunSandboxScope::disabled();
118 }
119
120 let execution_policy = if harn_vm::orchestration::current_execution_policy().is_none() {
121 harn_vm::orchestration::push_execution_policy(default_run_capability_policy(
122 workspace_root,
123 &options.write_roots,
124 &options.read_only_roots,
125 options.allow_process_network,
126 ));
127 Some(ExecutionPolicyGuard)
128 } else {
129 None
130 };
131 let egress_policy = Some(harn_vm::egress::require_explicit_egress_policy_for_host());
132 let ssrf_guard = Some(harn_vm::egress::require_ssrf_guard_for_host());
136
137 RunSandboxScope {
138 _execution_policy: execution_policy,
139 _egress_policy: egress_policy,
140 _ssrf_guard: ssrf_guard,
141 }
142}
143
144pub(super) fn default_run_capability_policy(
145 workspace_root: &Path,
146 write_roots: &[PathBuf],
147 read_only_roots: &[PathBuf],
148 allow_process_network: bool,
149) -> harn_vm::orchestration::CapabilityPolicy {
150 let mut workspace_roots = Vec::with_capacity(1 + write_roots.len());
151 workspace_roots.push(
152 normalize_run_workspace_root(workspace_root)
153 .display()
154 .to_string(),
155 );
156 workspace_roots.extend(
157 write_roots
158 .iter()
159 .map(|path| normalize_run_workspace_root(path.as_path()))
160 .map(|path| path.display().to_string()),
161 );
162
163 harn_vm::orchestration::CapabilityPolicy {
164 workspace_roots,
165 read_only_roots: read_only_roots
166 .iter()
167 .map(|path| normalize_run_workspace_root(path.as_path()))
168 .map(|path| path.display().to_string())
169 .collect(),
170 side_effect_level: Some(
171 if allow_process_network {
172 harn_vm::tool_annotations::SideEffectLevel::Network
173 } else {
174 harn_vm::tool_annotations::SideEffectLevel::ProcessExec
175 }
176 .as_str()
177 .to_string(),
178 ),
179 sandbox_profile: harn_vm::orchestration::SandboxProfile::Worktree,
180 ..harn_vm::orchestration::CapabilityPolicy::default()
181 }
182}
183
184fn normalize_run_workspace_root(path: &Path) -> PathBuf {
185 if path.is_absolute() {
186 return path.to_path_buf();
187 }
188 std::env::current_dir()
189 .map(|cwd| cwd.join(path))
190 .unwrap_or_else(|_| path.to_path_buf())
191}
192
193pub(super) fn default_run_workspace_root(
194 project_root: Option<&Path>,
195 source_parent: &Path,
196) -> PathBuf {
197 project_root
198 .map(Path::to_path_buf)
199 .or_else(|| std::env::current_dir().ok())
200 .unwrap_or_else(|| source_parent.to_path_buf())
201}
202
203pub(super) fn run_sandbox_attestation(sandbox: &RunSandboxOptions) -> serde_json::Value {
204 let active_policy = harn_vm::orchestration::current_execution_policy();
205 let active = active_policy.is_some();
206 let workspace_roots = active_policy
207 .as_ref()
208 .map(|policy| policy.workspace_roots.clone())
209 .unwrap_or_default();
210 let read_only_roots = active_policy
211 .as_ref()
212 .map(|policy| policy.read_only_roots.clone())
213 .unwrap_or_default();
214 let profile = active_policy
215 .as_ref()
216 .map(|policy| policy.sandbox_profile.as_str())
217 .unwrap_or("unrestricted");
218 let side_effect_level = active_policy
219 .as_ref()
220 .and_then(|policy| policy.side_effect_level.as_deref())
221 .unwrap_or(harn_vm::tool_annotations::SideEffectLevel::MAX.as_str());
222 let process_network_enabled =
223 harn_vm::tool_annotations::SideEffectLevel::rank_str(side_effect_level)
224 >= harn_vm::tool_annotations::SideEffectLevel::Network.rank();
225 let egress = if sandbox.enabled {
226 "explicit_policy_required"
227 } else if active {
228 "host_policy"
229 } else {
230 "unrestricted"
231 };
232 let write_roots = sandbox
233 .write_roots
234 .iter()
235 .map(|path| normalize_run_workspace_root(path).display().to_string())
236 .collect::<Vec<_>>();
237
238 serde_json::json!({
239 "run_default_enabled": sandbox.enabled,
240 "active": active,
241 "workspace_roots": workspace_roots,
242 "write_roots": write_roots,
243 "read_only_roots": read_only_roots,
244 "profile": profile,
245 "process_network_requested": sandbox.allow_process_network,
246 "process_network_enabled": process_network_enabled,
247 "side_effect_level": side_effect_level,
248 "egress": egress,
249 })
250}