Skip to main content

leviath_tools/
context.rs

1//! The sandbox context, tool-name aliases, and the shell-executor seam.
2
3use super::*;
4
5/// Context for tool execution - defines the sandbox root.
6pub struct ToolContext {
7    /// Absolute working directory. All file operations are confined here.
8    pub workdir: PathBuf,
9    /// Per-path advisory locks serializing concurrent mutating file operations
10    /// (`write_file`/`edit_file`) on the *same* file. Fan-out sub-agent workers
11    /// share one process and one workdir, so an in-process lock map keyed by
12    /// canonical path is sufficient (no OS `flock` needed) to prevent lost
13    /// updates when two workers touch the same file. Different files never
14    /// contend.
15    file_locks: Arc<Mutex<HashMap<PathBuf, Arc<tokio::sync::Mutex<()>>>>>,
16}
17
18impl ToolContext {
19    /// Create a new context. Attempts to canonicalize the working directory.
20    pub fn new(workdir: PathBuf) -> Self {
21        let workdir = std::fs::canonicalize(&workdir).unwrap_or(workdir);
22        Self {
23            workdir,
24            file_locks: Arc::new(Mutex::new(HashMap::new())),
25        }
26    }
27
28    /// Get (or create) the advisory lock for `path`. The map mutex is held only
29    /// briefly to look up / insert; the returned per-file lock is what callers
30    /// `.await` on across their read-modify-write.
31    pub(crate) fn lock_for(&self, path: &Path) -> Arc<tokio::sync::Mutex<()>> {
32        let mut map = self
33            .file_locks
34            .lock()
35            .unwrap_or_else(PoisonError::into_inner);
36        map.entry(path.to_path_buf())
37            .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
38            .clone()
39    }
40}
41
42/// Alias → canonical built-in tool name.
43///
44/// A blueprint's `available_tools` may name a built-in by any alias listed here;
45/// it resolves to the canonical tool that is advertised to the model and
46/// executed. This is the single source of truth for aliases - [`names`],
47/// [`BuiltinTools::execute`], and the daemon's `available_tools` filtering all go
48/// through [`canonical_tool_name`], so adding a row here is all it takes to add
49/// an alias everywhere. Add rows only for genuine synonyms of an existing tool.
50///
51/// [`names`]: BuiltinTools::names
52pub const TOOL_ALIASES: &[(&str, &str)] = &[
53    // `bash` is the familiar name for the general shell tool.
54    ("bash", "shell"),
55];
56
57/// Resolve `name` through [`TOOL_ALIASES`] to its canonical built-in name.
58///
59/// Returns the input unchanged when it is not an alias - which includes every
60/// canonical built-in and every MCP tool name, so this is safe to apply to any
61/// tool name before matching it against a definition.
62pub fn canonical_tool_name(name: &str) -> &str {
63    for (alias, canonical) in TOOL_ALIASES {
64        if *alias == name {
65            return canonical;
66        }
67    }
68    name
69}
70
71/// Redirects shell command execution off the host into a sandbox.
72///
73/// The default (no executor) runs the command directly on the host - the exact
74/// prior behavior. An implementor (the daemon's `SandboxManager`) returns a
75/// [`tokio::process::Command`] that runs `command` inside a container or Linux
76/// namespace instead. The implementor owns any per-stage sandbox state, so the
77/// same handle is used for the agent's whole life; only shell execution is
78/// affected (file tools stay on the host, over the bind-mounted workdir).
79pub trait ShellExecutor: Send + Sync {
80    /// Build the process that runs `command` via `shell flag` for `workdir`.
81    fn build_command(&self, shell: &str, flag: &str, command: &str, workdir: &Path) -> Command;
82}