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`, `wait_command_output`, `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//! | `wait_command_output`   | implemented                     |
23//! | `run_test`              | implemented                     |
24//! | `run_build_command`     | implemented                     |
25//! | `inspect_test_results`  | implemented                     |
26//! | `manage_packages`       | implemented                     |
27//! | `cancel_handle`         | implemented                     |
28//!
29//! ### Per-session opt-in
30//!
31//! All deterministic tools are gated by a per-thread feature flag.
32//! Pipelines must call `hostlib_enable("tools:deterministic")` (registered
33//! by [`ToolsCapability::register_builtins`]) before any of the tool
34//! methods will execute. Until then, calls return
35//! [`HostlibError::Backend`] with an explanatory message. The per-session
36//! opt-in model keeps the deterministic-tool surface sandbox-friendly.
37
38use harn_vm::VmDictExt;
39
40use harn_vm::VmValue;
41
42use crate::error::HostlibError;
43use crate::registry::{BuiltinRegistry, HostlibCapability};
44
45pub(crate) mod args;
46mod cancel_handle;
47mod diagnostics;
48mod file_io;
49mod git;
50pub(crate) mod inspect_test_results;
51mod lang;
52mod list_handles;
53pub mod long_running;
54mod manage_packages;
55mod outline;
56pub(crate) mod payload;
57pub mod permissions;
58mod proc;
59mod read_command_output;
60mod response;
61mod run_build_command;
62mod run_command;
63pub(crate) use run_command::policy_blocked_response as policy_blocked_run_command_response;
64pub(crate) use run_command::request_is_background as run_command_request_is_background;
65pub(crate) mod run_test;
66mod search;
67mod test_parsers;
68mod toolchain_facts;
69mod wait_command;
70mod wait_command_output;
71
72pub use permissions::{FEATURE_TERMINAL_SESSION, FEATURE_TOOLS_DETERMINISTIC};
73
74/// Tools capability handle.
75#[derive(Default)]
76pub struct ToolsCapability;
77
78impl HostlibCapability for ToolsCapability {
79    fn module_name(&self) -> &'static str {
80        "tools"
81    }
82
83    fn register_builtins(&self, registry: &mut BuiltinRegistry) {
84        // Register the session-cleanup hook once per process so long-running
85        // tool handles are killed when the agent-loop session ends.
86        long_running::register_cleanup_hook();
87
88        registry.register_gated_fn("tools", "hostlib_tools_search", "search", search::run);
89        registry.register_gated_fn(
90            "tools",
91            "hostlib_tools_read_file",
92            "read_file",
93            file_io::read_file,
94        );
95        registry.register_gated_fn(
96            "tools",
97            "hostlib_tools_write_file",
98            "write_file",
99            file_io::write_file,
100        );
101        registry.register_gated_fn(
102            "tools",
103            "hostlib_tools_delete_file",
104            "delete_file",
105            file_io::delete_file,
106        );
107        registry.register_gated_fn(
108            "tools",
109            "hostlib_tools_list_directory",
110            "list_directory",
111            file_io::list_directory,
112        );
113        registry.register_gated_fn(
114            "tools",
115            "hostlib_tools_get_file_outline",
116            "get_file_outline",
117            outline::run,
118        );
119        registry.register_gated_fn("tools", "hostlib_tools_git", "git", git::run);
120
121        registry.register_gated_command_fn(
122            "tools",
123            "hostlib_tools_run_command",
124            "run_command",
125            run_command::handle,
126        );
127        registry.register_gated_fn(
128            "tools",
129            read_command_output::NAME,
130            "read_command_output",
131            read_command_output::handle,
132        );
133        registry.register_gated_fn(
134            "tools",
135            wait_command::NAME,
136            "wait_command",
137            wait_command::handle,
138        );
139        registry.register_gated_async_fn(
140            "tools",
141            wait_command_output::NAME,
142            "wait_command_output",
143            wait_command_output::handle,
144        );
145        registry.register_gated_fn(
146            "tools",
147            "hostlib_tools_run_test",
148            "run_test",
149            run_test::handle,
150        );
151        registry.register_gated_fn(
152            "tools",
153            "hostlib_tools_run_build_command",
154            "run_build_command",
155            run_build_command::handle,
156        );
157        registry.register_gated_fn(
158            "tools",
159            "hostlib_tools_inspect_test_results",
160            "inspect_test_results",
161            inspect_test_results::handle,
162        );
163        registry.register_gated_fn(
164            "tools",
165            "hostlib_tools_manage_packages",
166            "manage_packages",
167            manage_packages::handle,
168        );
169        registry.register_gated_fn(
170            "tools",
171            cancel_handle::NAME,
172            "cancel_handle",
173            cancel_handle::handle,
174        );
175        registry.register_gated_fn(
176            "tools",
177            list_handles::NAME,
178            "list_handles",
179            list_handles::handle,
180        );
181        registry.register_gated_fn(
182            "tools",
183            toolchain_facts::NAME,
184            "toolchain_facts",
185            toolchain_facts::handle,
186        );
187
188        // The opt-in builtin lives in the `tools` module so embedders that
189        // don't compose `ToolsCapability` don't accidentally expose it.
190        registry.register_fn("tools", "hostlib_enable", "enable", handle_enable);
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    let supported = feature == permissions::FEATURE_TOOLS_DETERMINISTIC
219        || cfg!(feature = "terminal-session") && feature == permissions::FEATURE_TERMINAL_SESSION;
220    if !supported {
221        return Err(HostlibError::InvalidParameter {
222            builtin: "hostlib_enable",
223            param: "feature",
224            message: format!(
225                "unknown feature `{feature}`; supported: [`tools:deterministic`{}]",
226                if cfg!(feature = "terminal-session") {
227                    ", `terminal:session`"
228                } else {
229                    ""
230                }
231            ),
232        });
233    }
234
235    let newly_enabled = permissions::enable(&feature);
236    let mut map: harn_vm::value::DictMap = harn_vm::value::DictMap::new();
237    map.put_str("feature", feature);
238    map.insert(harn_vm::value::intern_key("enabled"), VmValue::Bool(true));
239    map.insert(
240        harn_vm::value::intern_key("newly_enabled"),
241        VmValue::Bool(newly_enabled),
242    );
243    Ok(VmValue::dict(map))
244}