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}
15
16/// Build a complete `PreparedBashScript` from a Bash operation template.
17pub fn build_bash_request(
18    bash_op: &BashOperationTemplate,
19    context: &Value,
20    renderer: &dyn TemplateRenderer,
21    global_limits: &GlobalBashLimits,
22) -> Result<PreparedBashScript> {
23    let script = renderer.render_str(&bash_op.bash.script, context)?;
24    if script.trim().is_empty() {
25        bail!("operation.bash.script rendered empty");
26    }
27
28    let env = render_key_value_map(bash_op.bash.env.as_ref(), context, renderer)?;
29
30    let cwd = bash_op
31        .bash
32        .cwd
33        .as_ref()
34        .map(|value| renderer.render_str(value, context))
35        .transpose()?
36        .filter(|value| !value.trim().is_empty());
37
38    // Extract per-template sandbox config with safe defaults, then apply
39    // global limits (most-restrictive-wins).
40    let template_sandbox = &bash_op.bash.sandbox;
41
42    let network = template_sandbox
43        .as_ref()
44        .and_then(|s| s.network)
45        .unwrap_or(false)
46        && global_limits.allow_network;
47
48    let writable_paths = template_sandbox
49        .as_ref()
50        .and_then(|s| s.writable_paths.clone())
51        .unwrap_or_default();
52
53    let max_time_ms = most_restrictive_option(
54        template_sandbox.as_ref().and_then(|s| s.max_time_ms),
55        global_limits.max_time_ms,
56    );
57
58    let max_output_bytes = most_restrictive_option(
59        template_sandbox.as_ref().and_then(|s| s.max_output_bytes),
60        global_limits.max_output_bytes,
61    )
62    .map(|v| v as usize);
63
64    Ok(PreparedBashScript {
65        script,
66        env,
67        cwd,
68        stdin: None,
69        sandbox: ResolvedBashSandbox {
70            network,
71            writable_paths,
72            max_time_ms,
73            max_output_bytes,
74        },
75    })
76}
77
78/// Return the smaller of two optional limits (most-restrictive-wins).
79fn most_restrictive_option(a: Option<u64>, b: Option<u64>) -> Option<u64> {
80    match (a, b) {
81        (Some(a), Some(b)) => Some(a.min(b)),
82        (Some(a), None) => Some(a),
83        (None, Some(b)) => Some(b),
84        (None, None) => None,
85    }
86}