Skip to main content

harn_hostlib/tools/
mod.rs

1//! Deterministic tools capability.
2//!
3//! Provides search (ripgrep via `grep-searcher` + `ignore`), file I/O,
4//! listing, file outline, git inspection, and
5//! process lifecycle (`run_command`, `wait_command`, `run_test`,
6//! `run_build_command`, `inspect_test_results`, `manage_packages`,
7//! `cancel_handle`).
8//!
9//! Implementation status:
10//!
11//! | Method                  | Status                          |
12//! |-------------------------|---------------------------------|
13//! | `search`                | implemented                     |
14//! | `read_file`             | implemented                     |
15//! | `write_file`            | implemented                     |
16//! | `delete_file`           | implemented                     |
17//! | `list_directory`        | implemented                     |
18//! | `get_file_outline`      | implemented (regex extractor)   |
19//! | `git`                   | implemented (system git CLI)    |
20//! | `run_command`           | implemented                     |
21//! | `wait_command`          | implemented                     |
22//! | `run_test`              | implemented                     |
23//! | `run_build_command`     | implemented                     |
24//! | `inspect_test_results`  | implemented                     |
25//! | `manage_packages`       | implemented                     |
26//! | `cancel_handle`         | implemented                     |
27//!
28//! ### Per-session opt-in
29//!
30//! All deterministic tools are gated by a per-thread feature flag.
31//! Pipelines must call `hostlib_enable("tools:deterministic")` (registered
32//! by [`ToolsCapability::register_builtins`]) before any of the tool
33//! methods will execute. Until then, calls return
34//! [`HostlibError::Backend`] with an explanatory message. The per-session
35//! opt-in model keeps the deterministic-tool surface sandbox-friendly.
36
37use harn_vm::VmDictExt;
38
39use harn_vm::VmValue;
40
41use crate::error::HostlibError;
42use crate::registry::{BuiltinRegistry, HostlibCapability};
43
44pub(crate) mod args;
45mod cancel_handle;
46mod diagnostics;
47mod file_io;
48mod git;
49mod inspect_test_results;
50mod lang;
51pub mod long_running;
52mod manage_packages;
53mod outline;
54mod payload;
55pub mod permissions;
56mod proc;
57mod read_command_output;
58mod response;
59mod run_build_command;
60mod run_command;
61pub(crate) use run_command::policy_blocked_response as policy_blocked_run_command_response;
62pub(crate) use run_command::request_is_background as run_command_request_is_background;
63mod run_test;
64mod search;
65mod test_parsers;
66mod toolchain_facts;
67mod wait_command;
68
69pub use permissions::{FEATURE_TERMINAL_SESSION, FEATURE_TOOLS_DETERMINISTIC};
70
71/// Tools capability handle.
72#[derive(Default)]
73pub struct ToolsCapability;
74
75impl HostlibCapability for ToolsCapability {
76    fn module_name(&self) -> &'static str {
77        "tools"
78    }
79
80    fn register_builtins(&self, registry: &mut BuiltinRegistry) {
81        // Register the session-cleanup hook once per process so long-running
82        // tool handles are killed when the agent-loop session ends.
83        long_running::register_cleanup_hook();
84
85        registry.register_gated_fn("tools", "hostlib_tools_search", "search", search::run);
86        registry.register_gated_fn(
87            "tools",
88            "hostlib_tools_read_file",
89            "read_file",
90            file_io::read_file,
91        );
92        registry.register_gated_fn(
93            "tools",
94            "hostlib_tools_write_file",
95            "write_file",
96            file_io::write_file,
97        );
98        registry.register_gated_fn(
99            "tools",
100            "hostlib_tools_delete_file",
101            "delete_file",
102            file_io::delete_file,
103        );
104        registry.register_gated_fn(
105            "tools",
106            "hostlib_tools_list_directory",
107            "list_directory",
108            file_io::list_directory,
109        );
110        registry.register_gated_fn(
111            "tools",
112            "hostlib_tools_get_file_outline",
113            "get_file_outline",
114            outline::run,
115        );
116        registry.register_gated_fn("tools", "hostlib_tools_git", "git", git::run);
117
118        registry.register_gated_command_fn(
119            "tools",
120            "hostlib_tools_run_command",
121            "run_command",
122            run_command::handle,
123        );
124        registry.register_gated_fn(
125            "tools",
126            read_command_output::NAME,
127            "read_command_output",
128            read_command_output::handle,
129        );
130        registry.register_gated_fn(
131            "tools",
132            wait_command::NAME,
133            "wait_command",
134            wait_command::handle,
135        );
136        registry.register_gated_fn(
137            "tools",
138            "hostlib_tools_run_test",
139            "run_test",
140            run_test::handle,
141        );
142        registry.register_gated_fn(
143            "tools",
144            "hostlib_tools_run_build_command",
145            "run_build_command",
146            run_build_command::handle,
147        );
148        registry.register_gated_fn(
149            "tools",
150            "hostlib_tools_inspect_test_results",
151            "inspect_test_results",
152            inspect_test_results::handle,
153        );
154        registry.register_gated_fn(
155            "tools",
156            "hostlib_tools_manage_packages",
157            "manage_packages",
158            manage_packages::handle,
159        );
160        registry.register_gated_fn(
161            "tools",
162            cancel_handle::NAME,
163            "cancel_handle",
164            cancel_handle::handle,
165        );
166        registry.register_gated_fn(
167            "tools",
168            toolchain_facts::NAME,
169            "toolchain_facts",
170            toolchain_facts::handle,
171        );
172
173        // The opt-in builtin lives in the `tools` module so embedders that
174        // don't compose `ToolsCapability` don't accidentally expose it.
175        registry.register_fn("tools", "hostlib_enable", "enable", handle_enable);
176    }
177}
178
179/// Implementation of the `hostlib_enable` builtin. Accepts either a bare
180/// string (`hostlib_enable("tools:deterministic")`) or a dict carrying a
181/// `feature` key (`hostlib_enable({feature: "..."})`) so callers can
182/// supply structured payloads in the future without breaking back-compat.
183fn handle_enable(args: &[VmValue]) -> Result<VmValue, HostlibError> {
184    let feature = match args.first() {
185        Some(VmValue::String(s)) => s.to_string(),
186        Some(VmValue::Dict(dict)) => match dict.get("feature") {
187            Some(VmValue::String(s)) => s.to_string(),
188            _ => {
189                return Err(HostlibError::MissingParameter {
190                    builtin: "hostlib_enable",
191                    param: "feature",
192                });
193            }
194        },
195        _ => {
196            return Err(HostlibError::MissingParameter {
197                builtin: "hostlib_enable",
198                param: "feature",
199            });
200        }
201    };
202
203    let supported = feature == permissions::FEATURE_TOOLS_DETERMINISTIC
204        || cfg!(feature = "terminal-session") && feature == permissions::FEATURE_TERMINAL_SESSION;
205    if !supported {
206        return Err(HostlibError::InvalidParameter {
207            builtin: "hostlib_enable",
208            param: "feature",
209            message: format!(
210                "unknown feature `{feature}`; supported: [`tools:deterministic`{}]",
211                if cfg!(feature = "terminal-session") {
212                    ", `terminal:session`"
213                } else {
214                    ""
215                }
216            ),
217        });
218    }
219
220    let newly_enabled = permissions::enable(&feature);
221    let mut map: harn_vm::value::DictMap = harn_vm::value::DictMap::new();
222    map.put_str("feature", feature);
223    map.insert(harn_vm::value::intern_key("enabled"), VmValue::Bool(true));
224    map.insert(
225        harn_vm::value::intern_key("newly_enabled"),
226        VmValue::Bool(newly_enabled),
227    );
228    Ok(VmValue::dict(map))
229}