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