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 via
5//! `gix` (currently shelled-out — see [`git`] for the rationale), 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`           | unimplemented (issue C2)        |
21//! | `run_test`              | unimplemented (issue C2)        |
22//! | `run_build_command`     | unimplemented (issue C2)        |
23//! | `inspect_test_results`  | unimplemented (issue C2)        |
24//! | `manage_packages`       | unimplemented (issue C2)        |
25//!
26//! ### Per-session opt-in
27//!
28//! All seven 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. This matches the
33//! "per-session opt-in" model called out in issue #567 and keeps the
34//! deterministic-tool surface sandbox-friendly.
35
36use std::collections::BTreeMap;
37use std::rc::Rc;
38use std::sync::Arc;
39
40use harn_vm::VmValue;
41
42use crate::error::HostlibError;
43use crate::registry::{BuiltinRegistry, HostlibCapability, RegisteredBuiltin, SyncHandler};
44
45mod args;
46mod file_io;
47mod git;
48mod outline;
49pub mod permissions;
50mod search;
51
52pub use permissions::FEATURE_TOOLS_DETERMINISTIC;
53
54/// Tools capability handle.
55#[derive(Default)]
56pub struct ToolsCapability;
57
58impl HostlibCapability for ToolsCapability {
59    fn module_name(&self) -> &'static str {
60        "tools"
61    }
62
63    fn register_builtins(&self, registry: &mut BuiltinRegistry) {
64        register_gated(registry, "hostlib_tools_search", "search", search::run);
65        register_gated(
66            registry,
67            "hostlib_tools_read_file",
68            "read_file",
69            file_io::read_file,
70        );
71        register_gated(
72            registry,
73            "hostlib_tools_write_file",
74            "write_file",
75            file_io::write_file,
76        );
77        register_gated(
78            registry,
79            "hostlib_tools_delete_file",
80            "delete_file",
81            file_io::delete_file,
82        );
83        register_gated(
84            registry,
85            "hostlib_tools_list_directory",
86            "list_directory",
87            file_io::list_directory,
88        );
89        register_gated(
90            registry,
91            "hostlib_tools_get_file_outline",
92            "get_file_outline",
93            outline::run,
94        );
95        register_gated(registry, "hostlib_tools_git", "git", git::run);
96
97        // Process tools land in C2; keep the contract surface visible.
98        registry.register_unimplemented("hostlib_tools_run_command", "tools", "run_command");
99        registry.register_unimplemented("hostlib_tools_run_test", "tools", "run_test");
100        registry.register_unimplemented(
101            "hostlib_tools_run_build_command",
102            "tools",
103            "run_build_command",
104        );
105        registry.register_unimplemented(
106            "hostlib_tools_inspect_test_results",
107            "tools",
108            "inspect_test_results",
109        );
110        registry.register_unimplemented(
111            "hostlib_tools_manage_packages",
112            "tools",
113            "manage_packages",
114        );
115
116        // The opt-in builtin lives in the `tools` module so embedders that
117        // don't compose `ToolsCapability` don't accidentally expose it.
118        let handler: SyncHandler = Arc::new(handle_enable);
119        registry.register(RegisteredBuiltin {
120            name: "hostlib_enable",
121            module: "tools",
122            method: "enable",
123            handler,
124        });
125    }
126}
127
128/// Register a builtin whose handler runs only when the deterministic-tools
129/// feature has been enabled on the current thread.
130fn register_gated(
131    registry: &mut BuiltinRegistry,
132    name: &'static str,
133    method: &'static str,
134    runner: fn(&[VmValue]) -> Result<VmValue, HostlibError>,
135) {
136    let handler: SyncHandler = Arc::new(move |args: &[VmValue]| {
137        if !permissions::is_enabled(permissions::FEATURE_TOOLS_DETERMINISTIC) {
138            return Err(HostlibError::Backend {
139                builtin: name,
140                message: format!(
141                    "feature `{}` is not enabled in this session — call \
142                     `hostlib_enable(\"{}\")` before invoking deterministic tools",
143                    permissions::FEATURE_TOOLS_DETERMINISTIC,
144                    permissions::FEATURE_TOOLS_DETERMINISTIC
145                ),
146            });
147        }
148        runner(args)
149    });
150    registry.register(RegisteredBuiltin {
151        name,
152        module: "tools",
153        method,
154        handler,
155    });
156}
157
158/// Implementation of the `hostlib_enable` builtin. Accepts either a bare
159/// string (`hostlib_enable("tools:deterministic")`) or a dict carrying a
160/// `feature` key (`hostlib_enable({feature: "..."})`) so callers can
161/// supply structured payloads in the future without breaking back-compat.
162fn handle_enable(args: &[VmValue]) -> Result<VmValue, HostlibError> {
163    let feature = match args.first() {
164        Some(VmValue::String(s)) => s.to_string(),
165        Some(VmValue::Dict(dict)) => match dict.get("feature") {
166            Some(VmValue::String(s)) => s.to_string(),
167            _ => {
168                return Err(HostlibError::MissingParameter {
169                    builtin: "hostlib_enable",
170                    param: "feature",
171                });
172            }
173        },
174        _ => {
175            return Err(HostlibError::MissingParameter {
176                builtin: "hostlib_enable",
177                param: "feature",
178            });
179        }
180    };
181
182    match feature.as_str() {
183        permissions::FEATURE_TOOLS_DETERMINISTIC => {
184            let newly_enabled = permissions::enable(&feature);
185            let mut map: BTreeMap<String, VmValue> = BTreeMap::new();
186            map.insert("feature".to_string(), VmValue::String(Rc::from(feature)));
187            map.insert("enabled".to_string(), VmValue::Bool(true));
188            map.insert("newly_enabled".to_string(), VmValue::Bool(newly_enabled));
189            Ok(VmValue::Dict(Rc::new(map)))
190        }
191        other => Err(HostlibError::InvalidParameter {
192            builtin: "hostlib_enable",
193            param: "feature",
194            message: format!("unknown feature `{other}`; supported: [`tools:deterministic`]"),
195        }),
196    }
197}