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