Skip to main content

harn_hostlib/tools/
mod.rs

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