Skip to main content

leviath_tools/
platform.rs

1//! Platform capabilities and process-group management.
2
3use super::*;
4
5/// Put `cmd` in its own process group, so the whole command tree can be
6/// signalled as one. `leviath_sys::configure_detached` does this for a
7/// `std::process::Command`; the tool lane uses tokio's, which has its own
8/// (Unix-only) setter. A no-op where the platform has no process groups -
9/// there, killing the direct child is all that exists.
10pub fn own_process_group(cmd: &mut Command) {
11    #[cfg(unix)]
12    cmd.process_group(0);
13    #[cfg(not(unix))]
14    let _ = cmd;
15}
16
17/// Start `cmd` without a console window, so an agent's shell calls do not flash
18/// consoles across the desktop on Windows (issue #228).
19///
20/// The tokio twin of [`leviath_sys::hide_console_window`], which takes a
21/// `std::process::Command`; `as_std_mut` reaches the one tokio wraps, so the
22/// flag has a single implementation rather than a second copy of the `#[cfg]`.
23/// A no-op everywhere but Windows. Only for a child whose stdio is piped, which
24/// is every caller here.
25pub fn hide_console_window(cmd: &mut Command) {
26    leviath_sys::hide_console_window(cmd.as_std_mut());
27}
28
29/// SIGKILLs a shell's whole process group when dropped.
30///
31/// `kill_on_drop` reaps the shell itself; this reaps what the shell started.
32/// Held for the duration of one shell tool call, so it fires on every exit path -
33/// normal completion (where the group is already gone and the signal is a
34/// harmless no-op), timeout, and the future being dropped because the agent was
35/// cancelled.
36pub struct ProcessGroupReaper(pub u32);
37
38impl Drop for ProcessGroupReaper {
39    fn drop(&mut self) {
40        // The group is usually already gone; failing to signal it is expected.
41        let _ = leviath_sys::kill_process_group(self.0);
42    }
43}
44
45/// A platform feature a built-in tool depends on.
46///
47/// Each built-in declares the capabilities it requires (see
48/// [`tool_required_capabilities`]); the current platform declares what it
49/// provides (see [`PlatformCapabilities`]). A tool whose requirements aren't
50/// met by the platform simply doesn't register - it's dropped from
51/// advertisement, name-recognition, and dispatch. This lets platform-specific
52/// tools (like `shell`, which spawns OS processes) coexist without per-tool
53/// `#[cfg]` gates or one-off boolean flags.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub enum ToolCapability {
56    /// Can launch child processes (e.g. the `shell` tool, stdio MCP servers).
57    ProcessSpawn,
58    /// Can read and write files.
59    FileSystem,
60    /// Can make outbound network/HTTP requests.
61    Network,
62}
63
64/// The set of [`ToolCapability`]s the current platform provides.
65///
66/// Detection is compile-time: [`current`](Self::current) resolves to the
67/// capability set for the build target. Desktop targets provide everything; a
68/// future mobile or wasm host would provide a reduced set (no
69/// [`ProcessSpawn`](ToolCapability::ProcessSpawn)). This mirrors the
70/// `leviath-sys` crate's approach of centralizing platform `#[cfg]` branching
71/// in one place rather than scattering it across call sites.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct PlatformCapabilities {
74    capabilities: HashSet<ToolCapability>,
75}
76
77impl PlatformCapabilities {
78    /// Capabilities for a desktop host: process spawning, filesystem, network.
79    pub fn desktop() -> Self {
80        Self {
81            capabilities: HashSet::from([
82                ToolCapability::ProcessSpawn,
83                ToolCapability::FileSystem,
84                ToolCapability::Network,
85            ]),
86        }
87    }
88
89    /// Capabilities for a mobile host: filesystem and network, but no process
90    /// spawning (so the `shell` tool doesn't register).
91    pub fn mobile() -> Self {
92        Self {
93            capabilities: HashSet::from([ToolCapability::FileSystem, ToolCapability::Network]),
94        }
95    }
96
97    /// The capability set for the platform this binary was built for.
98    ///
99    /// Only desktop targets are built today, so this resolves to
100    /// [`desktop`](Self::desktop). A future mobile/wasm target adds a `cfg` arm
101    /// here returning [`mobile`](Self::mobile) - every built-in then auto-filters
102    /// against it with no other change.
103    pub fn current() -> Self {
104        Self::desktop()
105    }
106
107    /// Build a capability set from an explicit collection (tests, future hosts).
108    pub fn from_capabilities(caps: impl IntoIterator<Item = ToolCapability>) -> Self {
109        Self {
110            capabilities: caps.into_iter().collect(),
111        }
112    }
113
114    /// Whether the platform provides `cap`.
115    pub fn supports(&self, cap: ToolCapability) -> bool {
116        self.capabilities.contains(&cap)
117    }
118
119    /// Whether the platform provides *every* capability in `required`. An empty
120    /// requirement slice is always satisfied.
121    pub fn satisfies(&self, required: &[ToolCapability]) -> bool {
122        required.iter().all(|c| self.supports(*c))
123    }
124}
125
126impl Default for PlatformCapabilities {
127    fn default() -> Self {
128        Self::current()
129    }
130}
131
132/// The capabilities a built-in tool requires to function.
133///
134/// Keyed by *canonical* tool name (resolve aliases via [`canonical_tool_name`]
135/// first). An empty slice means the tool is platform-agnostic and always
136/// available - this covers the runtime-handled tools (`present_for_review`,
137/// `ask_user_*`, `edit_document`, `context_*`) which touch neither the OS
138/// process table nor the filesystem directly.
139pub fn tool_required_capabilities(canonical_name: &str) -> &'static [ToolCapability] {
140    match canonical_name {
141        "shell" => &[ToolCapability::ProcessSpawn],
142        "read_file" | "read_files" | "write_file" | "edit_file" | "list_dir" => {
143            &[ToolCapability::FileSystem]
144        }
145        _ => &[],
146    }
147}