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