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