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::sync::Arc;
37
38use harn_vm::VmValue;
39
40use crate::error::HostlibError;
41use crate::registry::{BuiltinRegistry, HostlibCapability, RegisteredBuiltin, SyncHandler};
42
43pub(crate) mod args;
44mod cancel_handle;
45mod diagnostics;
46mod file_io;
47mod git;
48mod inspect_test_results;
49mod lang;
50pub mod long_running;
51mod manage_packages;
52mod outline;
53mod payload;
54pub mod permissions;
55mod proc;
56mod read_command_output;
57mod response;
58mod run_build_command;
59mod run_command;
60mod run_test;
61mod search;
62mod test_parsers;
63
64pub use permissions::FEATURE_TOOLS_DETERMINISTIC;
65
66/// Tools capability handle.
67#[derive(Default)]
68pub struct ToolsCapability;
69
70impl HostlibCapability for ToolsCapability {
71    fn module_name(&self) -> &'static str {
72        "tools"
73    }
74
75    fn register_builtins(&self, registry: &mut BuiltinRegistry) {
76        // Register the session-cleanup hook once per process so long-running
77        // tool handles are killed when the agent-loop session ends.
78        long_running::register_cleanup_hook();
79
80        register_gated(registry, "hostlib_tools_search", "search", search::run);
81        register_gated(
82            registry,
83            "hostlib_tools_read_file",
84            "read_file",
85            file_io::read_file,
86        );
87        register_gated(
88            registry,
89            "hostlib_tools_write_file",
90            "write_file",
91            file_io::write_file,
92        );
93        register_gated(
94            registry,
95            "hostlib_tools_delete_file",
96            "delete_file",
97            file_io::delete_file,
98        );
99        register_gated(
100            registry,
101            "hostlib_tools_list_directory",
102            "list_directory",
103            file_io::list_directory,
104        );
105        register_gated(
106            registry,
107            "hostlib_tools_get_file_outline",
108            "get_file_outline",
109            outline::run,
110        );
111        register_gated(registry, "hostlib_tools_git", "git", git::run);
112
113        register_gated(
114            registry,
115            "hostlib_tools_run_command",
116            "run_command",
117            run_command::handle,
118        );
119        register_gated(
120            registry,
121            read_command_output::NAME,
122            "read_command_output",
123            read_command_output::handle,
124        );
125        register_gated(
126            registry,
127            "hostlib_tools_run_test",
128            "run_test",
129            run_test::handle,
130        );
131        register_gated(
132            registry,
133            "hostlib_tools_run_build_command",
134            "run_build_command",
135            run_build_command::handle,
136        );
137        register_gated(
138            registry,
139            "hostlib_tools_inspect_test_results",
140            "inspect_test_results",
141            inspect_test_results::handle,
142        );
143        register_gated(
144            registry,
145            "hostlib_tools_manage_packages",
146            "manage_packages",
147            manage_packages::handle,
148        );
149        register_gated(
150            registry,
151            cancel_handle::NAME,
152            "cancel_handle",
153            cancel_handle::handle,
154        );
155
156        // The opt-in builtin lives in the `tools` module so embedders that
157        // don't compose `ToolsCapability` don't accidentally expose it.
158        let handler: SyncHandler = Arc::new(handle_enable);
159        registry.register(RegisteredBuiltin {
160            name: "hostlib_enable",
161            module: "tools",
162            method: "enable",
163            handler,
164        });
165    }
166}
167
168/// Register a builtin whose handler runs only when the deterministic-tools
169/// feature has been enabled on the current thread.
170fn register_gated(
171    registry: &mut BuiltinRegistry,
172    name: &'static str,
173    method: &'static str,
174    runner: fn(&[VmValue]) -> Result<VmValue, HostlibError>,
175) {
176    registry.register(RegisteredBuiltin {
177        name,
178        module: "tools",
179        method,
180        handler: permissions::gated_handler(name, runner),
181    });
182}
183
184/// Implementation of the `hostlib_enable` builtin. Accepts either a bare
185/// string (`hostlib_enable("tools:deterministic")`) or a dict carrying a
186/// `feature` key (`hostlib_enable({feature: "..."})`) so callers can
187/// supply structured payloads in the future without breaking back-compat.
188fn handle_enable(args: &[VmValue]) -> Result<VmValue, HostlibError> {
189    let feature = match args.first() {
190        Some(VmValue::String(s)) => s.to_string(),
191        Some(VmValue::Dict(dict)) => match dict.get("feature") {
192            Some(VmValue::String(s)) => s.to_string(),
193            _ => {
194                return Err(HostlibError::MissingParameter {
195                    builtin: "hostlib_enable",
196                    param: "feature",
197                });
198            }
199        },
200        _ => {
201            return Err(HostlibError::MissingParameter {
202                builtin: "hostlib_enable",
203                param: "feature",
204            });
205        }
206    };
207
208    match feature.as_str() {
209        permissions::FEATURE_TOOLS_DETERMINISTIC => {
210            let newly_enabled = permissions::enable(&feature);
211            let mut map: BTreeMap<String, VmValue> = BTreeMap::new();
212            map.insert("feature".to_string(), VmValue::String(Arc::from(feature)));
213            map.insert("enabled".to_string(), VmValue::Bool(true));
214            map.insert("newly_enabled".to_string(), VmValue::Bool(newly_enabled));
215            Ok(VmValue::Dict(Arc::new(map)))
216        }
217        other => Err(HostlibError::InvalidParameter {
218            builtin: "hostlib_enable",
219            param: "feature",
220            message: format!("unknown feature `{other}`; supported: [`tools:deterministic`]"),
221        }),
222    }
223}