Skip to main content

earl_protocol_bash/
builder.rs

1use anyhow::{Result, bail};
2use serde_json::Value;
3
4use crate::schema::BashOperationTemplate;
5use crate::{PreparedBashScript, ResolvedBashSandbox};
6use earl_core::render::{TemplateRenderer, render_key_value_map};
7
8/// Global sandbox limits passed from the main crate's SandboxConfig.
9#[derive(Debug, Clone, Default)]
10pub struct GlobalBashLimits {
11    pub allow_network: bool,
12    pub max_time_ms: Option<u64>,
13    pub max_output_bytes: Option<u64>,
14    pub max_memory_bytes: Option<u64>,
15    pub max_cpu_time_ms: Option<u64>,
16}
17
18/// Build a complete `PreparedBashScript` from a Bash operation template.
19pub fn build_bash_request(
20    bash_op: &BashOperationTemplate,
21    context: &Value,
22    renderer: &dyn TemplateRenderer,
23    global_limits: &GlobalBashLimits,
24) -> Result<PreparedBashScript> {
25    let script = renderer.render_str(&bash_op.bash.script, context)?;
26    if script.trim().is_empty() {
27        bail!("operation.bash.script rendered empty");
28    }
29
30    let env = render_key_value_map(bash_op.bash.env.as_ref(), context, renderer)?;
31
32    let cwd = bash_op
33        .bash
34        .cwd
35        .as_ref()
36        .map(|value| renderer.render_str(value, context))
37        .transpose()?
38        .filter(|value| !value.trim().is_empty());
39
40    // Extract per-template sandbox config with safe defaults, then apply
41    // global limits (most-restrictive-wins).
42    let template_sandbox = &bash_op.bash.sandbox;
43
44    let network = template_sandbox
45        .as_ref()
46        .and_then(|s| s.network)
47        .unwrap_or(false)
48        && global_limits.allow_network;
49
50    let writable_paths = template_sandbox
51        .as_ref()
52        .and_then(|s| s.writable_paths.clone())
53        .unwrap_or_default();
54
55    let max_time_ms = most_restrictive_option(
56        template_sandbox.as_ref().and_then(|s| s.max_time_ms),
57        global_limits.max_time_ms,
58    );
59
60    let max_output_bytes = most_restrictive_option(
61        template_sandbox.as_ref().and_then(|s| s.max_output_bytes),
62        global_limits.max_output_bytes,
63    )
64    .map(|v| v as usize);
65
66    let max_memory_bytes = most_restrictive_option(
67        template_sandbox.as_ref().and_then(|s| s.max_memory_bytes),
68        global_limits.max_memory_bytes,
69    )
70    .map(|v| v as usize);
71
72    let max_cpu_time_ms = most_restrictive_option(
73        template_sandbox.as_ref().and_then(|s| s.max_cpu_time_ms),
74        global_limits.max_cpu_time_ms,
75    );
76
77    Ok(PreparedBashScript {
78        script,
79        env,
80        cwd,
81        stdin: None,
82        sandbox: ResolvedBashSandbox {
83            network,
84            writable_paths,
85            max_time_ms,
86            max_output_bytes,
87            max_memory_bytes,
88            max_cpu_time_ms,
89        },
90    })
91}
92
93/// Return the smaller of two optional limits (most-restrictive-wins).
94fn most_restrictive_option(a: Option<u64>, b: Option<u64>) -> Option<u64> {
95    match (a, b) {
96        (Some(a), Some(b)) => Some(a.min(b)),
97        (Some(a), None) => Some(a),
98        (None, Some(b)) => Some(b),
99        (None, None) => None,
100    }
101}