Skip to main content

lean_ctx_server/server/
dynamic_tools.rs

1// SPDX-License-Identifier: Apache-2.0
2//! The name logic lives in [`lean_ctx::core::dynamic_tools`]; this module keeps the
3//! helpers that resolve a call through `ctx_call`/`ctx` to its inner tool.
4
5pub use lean_ctx::core::dynamic_tools::*;
6
7/// Whether a call — direct, or through `ctx_call`/`ctx` — targets an internal
8/// host hook. A host replaces a native tool's output with the hook's success
9/// body, so every gate refusal on such a call must read as an error: then the
10/// host keeps the original output instead of a block message (#2089).
11#[must_use]
12pub fn targets_internal_host_tool(
13    name: &str,
14    args: Option<&serde_json::Map<String, serde_json::Value>>,
15) -> bool {
16    crate::tools::registered::ctx_call::resolve(name, args)
17        .is_ok_and(|(inner, _)| INTERNAL_HOST_TOOLS.contains(&inner.as_str()))
18}
19
20/// The fixed answer for a call — direct, or through `ctx_call`/`ctx` — to a
21/// withheld Hive tool, else `None`. Stable text (#498).
22#[must_use]
23pub fn withheld_hive_tool_refusal(
24    name: &str,
25    args: Option<&serde_json::Map<String, serde_json::Value>>,
26) -> Option<&'static str> {
27    crate::tools::registered::ctx_call::resolve(name, args)
28        .is_ok_and(|(inner, _)| is_withheld_hive_tool(&inner))
29        .then_some(lean_ctx_hive::TOOL_NOT_AVAILABLE)
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35
36    #[test]
37    fn removed_tools_point_at_a_live_public_tool() {
38        assert_eq!(
39            removed_tool("ctx_multi_read").unwrap().replacement,
40            Some("ctx_read")
41        );
42        assert!(removed_tool("ctx_read").is_none());
43        // No removed name may send agents to a tool that is gone or hidden.
44        let registry = crate::server::registry::build_registry();
45        let live = registry.names();
46        for name in [
47            "ctx_smart_read",
48            "ctx_multi_read",
49            "ctx_semantic_search",
50            "ctx_symbol",
51            "ctx_overview",
52            "ctx_summary",
53            "ctx_explore",
54            "ctx_plan",
55            "ctx_compile",
56            "ctx_fill",
57            "ctx_impact",
58            "ctx_architecture",
59            "ctx_repomap",
60            "ctx_routes",
61            "ctx_retrieve",
62            "ctx_cache",
63        ] {
64            let replacement = removed_tool(name).and_then(|r| r.replacement).unwrap();
65            assert!(!live.contains(&name), "{name} is still registered");
66            assert!(
67                live.contains(&replacement) && is_publicly_advertised_tool(replacement),
68                "{name} points at {replacement}, which is not a live public tool"
69            );
70        }
71    }
72
73    /// #2089: hosts reach ctx_shape through the listed ctx_call, so a call is a
74    /// host-hook call in every spelling — and only then.
75    #[test]
76    fn host_hook_calls_are_recognised_in_every_spelling() {
77        let args = |v: serde_json::Value| v.as_object().cloned();
78        let shape = args(serde_json::json!({"tool": "Bash", "output": "x"}));
79        assert!(targets_internal_host_tool("ctx_shape", shape.as_ref()));
80        let via_call =
81            args(serde_json::json!({"name": "ctx_shape", "arguments": {"tool": "Bash"}}));
82        assert!(targets_internal_host_tool("ctx_call", via_call.as_ref()));
83        let via_ctx = args(serde_json::json!({"tool": "shape", "output": "x"}));
84        assert!(targets_internal_host_tool("ctx", via_ctx.as_ref()));
85
86        let read = args(serde_json::json!({"name": "ctx_read", "arguments": {"path": "a"}}));
87        assert!(!targets_internal_host_tool("ctx_call", read.as_ref()));
88        assert!(!targets_internal_host_tool("ctx_read", None));
89        assert!(!targets_internal_host_tool("ctx_call", None));
90    }
91}