Skip to main content

harness_bash/
types.rs

1use harness_core::{PermissionPolicy, ToolError};
2use serde::{Deserialize, Serialize};
3use std::sync::{Arc, Mutex};
4
5use crate::executor::BashExecutor;
6
7/// Permission policy with the autonomous-mode escape hatch for tests.
8#[derive(Clone)]
9pub struct BashPermissionPolicy {
10    pub inner: PermissionPolicy,
11    pub unsafe_allow_bash_without_hook: bool,
12}
13
14impl BashPermissionPolicy {
15    pub fn new(inner: PermissionPolicy) -> Self {
16        Self {
17            inner,
18            unsafe_allow_bash_without_hook: false,
19        }
20    }
21
22    pub fn with_unsafe_bypass(mut self, bypass: bool) -> Self {
23        self.unsafe_allow_bash_without_hook = bypass;
24        self
25    }
26}
27
28impl std::fmt::Debug for BashPermissionPolicy {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        f.debug_struct("BashPermissionPolicy")
31            .field("unsafe_allow_bash_without_hook", &self.unsafe_allow_bash_without_hook)
32            .field("inner", &self.inner)
33            .finish()
34    }
35}
36
37/// Runtime-tracked mutable cwd for the session. Shared across calls via
38/// `Arc<Mutex<_>>`. Mirrors the TS `session.logicalCwd.value`.
39#[derive(Debug, Clone)]
40pub struct LogicalCwd {
41    pub inner: Arc<Mutex<String>>,
42}
43
44impl LogicalCwd {
45    pub fn new(initial: impl Into<String>) -> Self {
46        Self {
47            inner: Arc::new(Mutex::new(initial.into())),
48        }
49    }
50
51    pub fn get(&self) -> String {
52        self.inner.lock().unwrap().clone()
53    }
54
55    pub fn set(&self, v: impl Into<String>) {
56        *self.inner.lock().unwrap() = v.into();
57    }
58}
59
60#[derive(Clone)]
61pub struct BashSessionConfig {
62    pub cwd: String,
63    pub permissions: BashPermissionPolicy,
64    pub env: Option<std::collections::HashMap<String, String>>,
65    pub executor: Arc<dyn BashExecutor>,
66    pub default_inactivity_timeout_ms: Option<u64>,
67    pub wallclock_backstop_ms: Option<u64>,
68    pub max_command_length: Option<usize>,
69    pub max_output_bytes_inline: Option<usize>,
70    pub max_output_bytes_file: Option<usize>,
71    pub max_background_jobs: Option<usize>,
72    pub logical_cwd: Option<LogicalCwd>,
73}
74
75impl BashSessionConfig {
76    pub fn new(
77        cwd: impl Into<String>,
78        permissions: BashPermissionPolicy,
79        executor: Arc<dyn BashExecutor>,
80    ) -> Self {
81        let cwd = cwd.into();
82        Self {
83            cwd: cwd.clone(),
84            permissions,
85            env: None,
86            executor,
87            default_inactivity_timeout_ms: None,
88            wallclock_backstop_ms: None,
89            max_command_length: None,
90            max_output_bytes_inline: None,
91            max_output_bytes_file: None,
92            max_background_jobs: None,
93            logical_cwd: None,
94        }
95    }
96
97    pub fn with_logical_cwd_carry(mut self) -> Self {
98        self.logical_cwd = Some(LogicalCwd::new(self.cwd.clone()));
99        self
100    }
101}
102
103impl std::fmt::Debug for BashSessionConfig {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        f.debug_struct("BashSessionConfig")
106            .field("cwd", &self.cwd)
107            .field("permissions", &self.permissions)
108            .field("has_env", &self.env.is_some())
109            .finish()
110    }
111}
112
113// ---- Result union ----
114
115#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
116#[serde(rename_all = "snake_case")]
117pub enum TimeoutReason {
118    InactivityTimeout,
119    WallClockBackstop,
120}
121
122impl TimeoutReason {
123    pub fn as_str(&self) -> &'static str {
124        match self {
125            Self::InactivityTimeout => "inactivity timeout",
126            Self::WallClockBackstop => "wall-clock backstop",
127        }
128    }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct BashOk {
133    pub output: String,
134    pub exit_code: i32,
135    pub stdout: String,
136    pub stderr: String,
137    pub duration_ms: u64,
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub log_path: Option<String>,
140    pub byte_cap: bool,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct BashNonzeroExit {
145    pub output: String,
146    pub exit_code: i32,
147    pub stdout: String,
148    pub stderr: String,
149    pub duration_ms: u64,
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub log_path: Option<String>,
152    pub byte_cap: bool,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct BashTimeout {
157    pub output: String,
158    pub stdout: String,
159    pub stderr: String,
160    pub reason: TimeoutReason,
161    pub duration_ms: u64,
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub log_path: Option<String>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct BashBackgroundStarted {
168    pub output: String,
169    pub job_id: String,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct BashError {
174    pub error: ToolError,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178#[serde(tag = "kind", rename_all = "snake_case")]
179pub enum BashResult {
180    #[serde(rename = "ok")]
181    Ok(BashOk),
182    #[serde(rename = "nonzero_exit")]
183    NonzeroExit(BashNonzeroExit),
184    #[serde(rename = "timeout")]
185    Timeout(BashTimeout),
186    #[serde(rename = "background_started")]
187    BackgroundStarted(BashBackgroundStarted),
188    #[serde(rename = "error")]
189    Error(BashError),
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
193#[serde(tag = "kind", rename_all = "snake_case")]
194pub enum BashOutputResult {
195    #[serde(rename = "output")]
196    Output {
197        output: String,
198        running: bool,
199        exit_code: Option<i32>,
200        stdout: String,
201        stderr: String,
202        total_bytes_stdout: u64,
203        total_bytes_stderr: u64,
204        next_since_byte: u64,
205    },
206    #[serde(rename = "error")]
207    Error(BashError),
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
211#[serde(tag = "kind", rename_all = "snake_case")]
212pub enum BashKillResult {
213    #[serde(rename = "killed")]
214    Killed {
215        output: String,
216        job_id: String,
217        signal: String,
218    },
219    #[serde(rename = "error")]
220    Error(BashError),
221}