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`, `run_test`, `run_build_command`,
6//! `inspect_test_results`, `manage_packages`, `cancel_handle`).
7//!
8//! Implementation status:
9//!
10//! | Method                  | Status                          |
11//! |-------------------------|---------------------------------|
12//! | `search`                | implemented                     |
13//! | `read_file`             | implemented                     |
14//! | `write_file`            | implemented                     |
15//! | `delete_file`           | implemented                     |
16//! | `list_directory`        | implemented                     |
17//! | `get_file_outline`      | implemented (regex extractor)   |
18//! | `git`                   | implemented (system git CLI)    |
19//! | `run_command`           | implemented                     |
20//! | `run_test`              | implemented                     |
21//! | `run_build_command`     | implemented                     |
22//! | `inspect_test_results`  | implemented                     |
23//! | `manage_packages`       | implemented                     |
24//! | `cancel_handle`         | implemented                     |
25//!
26//! ### Per-session opt-in
27//!
28//! All deterministic tools are gated by a per-thread feature flag.
29//! Pipelines must call `hostlib_enable("tools:deterministic")` (registered
30//! by [`ToolsCapability::register_builtins`]) before any of the tool
31//! methods will execute. Until then, calls return
32//! [`HostlibError::Backend`] with an explanatory message. The per-session
33//! opt-in model keeps the deterministic-tool surface sandbox-friendly.
34
35use std::collections::BTreeMap;
36use std::rc::Rc;
37use std::sync::Arc;
38
39use harn_vm::VmValue;
40
41use crate::error::HostlibError;
42use crate::registry::{BuiltinRegistry, HostlibCapability, RegisteredBuiltin, SyncHandler};
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;
61mod run_test;
62mod search;
63mod test_parsers;
64
65pub use permissions::FEATURE_TOOLS_DETERMINISTIC;
66
67/// Tools capability handle.
68#[derive(Default)]
69pub struct ToolsCapability;
70
71impl HostlibCapability for ToolsCapability {
72    fn module_name(&self) -> &'static str {
73        "tools"
74    }
75
76    fn register_builtins(&self, registry: &mut BuiltinRegistry) {
77        // Register the session-cleanup hook once per process so long-running
78        // tool handles are killed when the agent-loop session ends.
79        long_running::register_cleanup_hook();
80
81        register_gated(registry, "hostlib_tools_search", "search", search::run);
82        register_gated(
83            registry,
84            "hostlib_tools_read_file",
85            "read_file",
86            file_io::read_file,
87        );
88        register_gated(
89            registry,
90            "hostlib_tools_write_file",
91            "write_file",
92            file_io::write_file,
93        );
94        register_gated(
95            registry,
96            "hostlib_tools_delete_file",
97            "delete_file",
98            file_io::delete_file,
99        );
100        register_gated(
101            registry,
102            "hostlib_tools_list_directory",
103            "list_directory",
104            file_io::list_directory,
105        );
106        register_gated(
107            registry,
108            "hostlib_tools_get_file_outline",
109            "get_file_outline",
110            outline::run,
111        );
112        register_gated(registry, "hostlib_tools_git", "git", git::run);
113
114        register_gated(
115            registry,
116            "hostlib_tools_run_command",
117            "run_command",
118            run_command::handle,
119        );
120        register_gated(
121            registry,
122            read_command_output::NAME,
123            "read_command_output",
124            read_command_output::handle,
125        );
126        register_gated(
127            registry,
128            "hostlib_tools_run_test",
129            "run_test",
130            run_test::handle,
131        );
132        register_gated(
133            registry,
134            "hostlib_tools_run_build_command",
135            "run_build_command",
136            run_build_command::handle,
137        );
138        register_gated(
139            registry,
140            "hostlib_tools_inspect_test_results",
141            "inspect_test_results",
142            inspect_test_results::handle,
143        );
144        register_gated(
145            registry,
146            "hostlib_tools_manage_packages",
147            "manage_packages",
148            manage_packages::handle,
149        );
150        register_gated(
151            registry,
152            cancel_handle::NAME,
153            "cancel_handle",
154            cancel_handle::handle,
155        );
156
157        // The opt-in builtin lives in the `tools` module so embedders that
158        // don't compose `ToolsCapability` don't accidentally expose it.
159        let handler: SyncHandler = Arc::new(handle_enable);
160        registry.register(RegisteredBuiltin {
161            name: "hostlib_enable",
162            module: "tools",
163            method: "enable",
164            handler,
165        });
166    }
167}
168
169/// Register a builtin whose handler runs only when the deterministic-tools
170/// feature has been enabled on the current thread.
171fn register_gated(
172    registry: &mut BuiltinRegistry,
173    name: &'static str,
174    method: &'static str,
175    runner: fn(&[VmValue]) -> Result<VmValue, HostlibError>,
176) {
177    let handler: SyncHandler = Arc::new(move |args: &[VmValue]| {
178        if !permissions::is_enabled(permissions::FEATURE_TOOLS_DETERMINISTIC) {
179            return Err(HostlibError::Backend {
180                builtin: name,
181                message: format!(
182                    "feature `{}` is not enabled in this session — call \
183                     `hostlib_enable(\"{}\")` before invoking deterministic tools",
184                    permissions::FEATURE_TOOLS_DETERMINISTIC,
185                    permissions::FEATURE_TOOLS_DETERMINISTIC
186                ),
187            });
188        }
189        runner(args)
190    });
191    registry.register(RegisteredBuiltin {
192        name,
193        module: "tools",
194        method,
195        handler,
196    });
197}
198
199/// Implementation of the `hostlib_enable` builtin. Accepts either a bare
200/// string (`hostlib_enable("tools:deterministic")`) or a dict carrying a
201/// `feature` key (`hostlib_enable({feature: "..."})`) so callers can
202/// supply structured payloads in the future without breaking back-compat.
203fn handle_enable(args: &[VmValue]) -> Result<VmValue, HostlibError> {
204    let feature = match args.first() {
205        Some(VmValue::String(s)) => s.to_string(),
206        Some(VmValue::Dict(dict)) => match dict.get("feature") {
207            Some(VmValue::String(s)) => s.to_string(),
208            _ => {
209                return Err(HostlibError::MissingParameter {
210                    builtin: "hostlib_enable",
211                    param: "feature",
212                });
213            }
214        },
215        _ => {
216            return Err(HostlibError::MissingParameter {
217                builtin: "hostlib_enable",
218                param: "feature",
219            });
220        }
221    };
222
223    match feature.as_str() {
224        permissions::FEATURE_TOOLS_DETERMINISTIC => {
225            let newly_enabled = permissions::enable(&feature);
226            let mut map: BTreeMap<String, VmValue> = BTreeMap::new();
227            map.insert("feature".to_string(), VmValue::String(Rc::from(feature)));
228            map.insert("enabled".to_string(), VmValue::Bool(true));
229            map.insert("newly_enabled".to_string(), VmValue::Bool(newly_enabled));
230            Ok(VmValue::Dict(Rc::new(map)))
231        }
232        other => Err(HostlibError::InvalidParameter {
233            builtin: "hostlib_enable",
234            param: "feature",
235            message: format!("unknown feature `{other}`; supported: [`tools:deterministic`]"),
236        }),
237    }
238}