Skip to main content

kcode_k1_ktool_docs/
lib.rs

1#![forbid(unsafe_code)]
2
3use serde::{Deserialize, Serialize};
4
5const INVALID_ARGUMENTS: &str = "invalid KtoolDocs arguments";
6const UNKNOWN_KTOOL: &str = "unknown Ktool";
7
8struct Entry {
9    name: &'static str,
10    version: &'static str,
11    docs: &'static str,
12    replacement: Option<&'static str>,
13}
14
15impl Entry {
16    const fn new(
17        name: &'static str,
18        version: &'static str,
19        docs: &'static str,
20        replacement: Option<&'static str>,
21    ) -> Self {
22        Self {
23            name,
24            version,
25            docs,
26            replacement,
27        }
28    }
29}
30
31static CATALOG: [Entry; 8] = [
32    Entry::new(
33        "KtoolDocs",
34        "1.0.0",
35        r#"Invoke as KtoolDocs {"name":"<exact Ktool name>"}. Arguments must be exactly one nonempty string field, name; matching is exact and case-sensitive. Success returns a compact JSON object with exactly name, latest_version, docs, deprecated, and replacement. Malformed arguments return invalid KtoolDocs arguments; an unrecognized exact name returns unknown Ktool. The operation is stateless metadata lookup only and does not list, search, suggest, authorize, register, or prove live availability."#,
36        None,
37    ),
38    Entry::new(
39        "CurrentTime",
40        "1.0.0",
41        r#"Invoke as CurrentTime {}. Returns current system UTC in RFC3339 with exactly three fractional digits and Z. Any nonempty or nonobject arguments return invalid CurrentTime arguments. It is stateless and needs no Kmap authorization."#,
42        None,
43    ),
44    Entry::new(
45        "KmapCreateNode",
46        "1.0.0",
47        r#"Invoke as KmapCreateNode {"title":string,"navigation_hint":string,"narrative":string,"connections":[string,...]}. Each connection must be an exact 24-character lowercase hexadecimal Kmap node ID. Using the active Kmap authorization, it creates one node under the bound profile and policy, adds the supplied Navigation connections, marks the new node loaded, and returns Node successfully created with id <id>. Malformed arguments or IDs return invalid KmapCreateNode arguments; missing authorization and Kmap dependency failures remain errors."#,
48        None,
49    ),
50    Entry::new(
51        "KmapOpenNode",
52        "1.0.0",
53        r#"Invoke as KmapOpenNode {"node_id":string,"budget":number}. node_id must be an exact 24-character lowercase hexadecimal Kmap node ID and budget must be finite and nonnegative. It opens in Full mode at temperature 1, records loaded nodes and first-pull provenance, and returns only components not already returned during the session. If no new components remain, it returns No new Kmap node components. Malformed arguments return invalid KmapOpenNode arguments; missing authorization and Kmap dependency failures remain errors."#,
54        None,
55    ),
56    Entry::new(
57        "KmapUpdateNode",
58        "1.0.0",
59        r#"Invoke as KmapUpdateNode {"node_id":string,"title":string,"navigation_hint":string,"narrative":string,"connections":[string,...]}. All IDs must be exact 24-character lowercase hexadecimal Kmap node IDs. It replaces the node text fields, additively upserts the supplied Navigation connections, marks the updated node loaded, and returns success. Naming another node does not itself load that node. Malformed arguments return invalid KmapUpdateNode arguments; missing authorization and Kmap dependency failures remain errors."#,
60        None,
61    ),
62    Entry::new(
63        "KmapPenalizeNodes",
64        "1.0.0",
65        r#"Invoke as KmapPenalizeNodes {"node_ids":[string,...]}. Every ID must be an exact 24-character lowercase hexadecimal Kmap node ID. It deduplicates the input and applies one noncritical negative measurement to each newly penalized eligible loaded node with first-pull provenance. Penalized nodes are excluded from the session’s later KmapConnectNodes operation. Unknown, unloaded, or already penalized valid IDs have no additional effect. Success returns success. Malformed arguments return invalid KmapPenalizeNodes arguments; missing authorization and Kmap dependency failures remain errors."#,
66        None,
67    ),
68    Entry::new(
69        "KmapConnectNodes",
70        "1.0.0",
71        r#"Invoke as KmapConnectNodes {}. Using the active Kmap authorization, it adds every missing directed Automated connection among loaded, unpenalized session nodes. Existing connections remain unchanged and self-connections are not added. Success returns success. Nonempty or nonobject arguments return invalid KmapConnectNodes arguments; missing authorization and Kmap dependency failures remain errors."#,
72        None,
73    ),
74    Entry::new(
75        "SendMessage",
76        "1.0.0",
77        r#"Invoke as SendMessage {"message":string}. Arguments must contain exactly one nonempty message string. It durably appends one visible Agent Message to the authenticated current conversation between the Tool Call and Tool Result, returns success, and leaves the provider turn open for continued generation. It has no recipient argument, network effect, or cross-conversation capability. Malformed arguments return invalid SendMessage arguments without appending a message; persistence failures remain errors."#,
78        None,
79    ),
80];
81
82#[derive(Deserialize)]
83#[serde(deny_unknown_fields)]
84struct Arguments {
85    name: String,
86}
87
88#[derive(Serialize)]
89struct Response<'a> {
90    name: &'a str,
91    latest_version: &'static str,
92    docs: &'a str,
93    deprecated: bool,
94    replacement: Option<&'a str>,
95}
96
97fn find_entry<'a>(catalog: &'a [Entry], name: &str) -> Option<&'a Entry> {
98    catalog.iter().find(|entry| entry.name == name)
99}
100
101fn render(entry: &Entry) -> Result<String, String> {
102    let response = Response {
103        name: entry.name,
104        latest_version: entry.version,
105        docs: entry.docs,
106        deprecated: entry.replacement.is_some(),
107        replacement: entry.replacement,
108    };
109
110    serde_json::to_string(&response)
111        .map_err(|error| format!("KtoolDocs response serialization failed: {error}"))
112}
113
114/// Looks up the exact Ktool name supplied in a strict JSON argument object.
115pub fn ktool_docs(arguments: &str) -> Result<String, String> {
116    let arguments: Arguments =
117        serde_json::from_str(arguments).map_err(|_| INVALID_ARGUMENTS.to_owned())?;
118    if arguments.name.is_empty() {
119        return Err(INVALID_ARGUMENTS.to_owned());
120    }
121
122    let entry = find_entry(&CATALOG, &arguments.name).ok_or_else(|| UNKNOWN_KTOOL.to_owned())?;
123    render(entry)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use serde_json::Value;
130
131    const EXPECTED: [(&str, &str); 8] = [
132        (
133            "KtoolDocs",
134            r#"Invoke as KtoolDocs {"name":"<exact Ktool name>"}. Arguments must be exactly one nonempty string field, name; matching is exact and case-sensitive. Success returns a compact JSON object with exactly name, latest_version, docs, deprecated, and replacement. Malformed arguments return invalid KtoolDocs arguments; an unrecognized exact name returns unknown Ktool. The operation is stateless metadata lookup only and does not list, search, suggest, authorize, register, or prove live availability."#,
135        ),
136        (
137            "CurrentTime",
138            r#"Invoke as CurrentTime {}. Returns current system UTC in RFC3339 with exactly three fractional digits and Z. Any nonempty or nonobject arguments return invalid CurrentTime arguments. It is stateless and needs no Kmap authorization."#,
139        ),
140        (
141            "KmapCreateNode",
142            r#"Invoke as KmapCreateNode {"title":string,"navigation_hint":string,"narrative":string,"connections":[string,...]}. Each connection must be an exact 24-character lowercase hexadecimal Kmap node ID. Using the active Kmap authorization, it creates one node under the bound profile and policy, adds the supplied Navigation connections, marks the new node loaded, and returns Node successfully created with id <id>. Malformed arguments or IDs return invalid KmapCreateNode arguments; missing authorization and Kmap dependency failures remain errors."#,
143        ),
144        (
145            "KmapOpenNode",
146            r#"Invoke as KmapOpenNode {"node_id":string,"budget":number}. node_id must be an exact 24-character lowercase hexadecimal Kmap node ID and budget must be finite and nonnegative. It opens in Full mode at temperature 1, records loaded nodes and first-pull provenance, and returns only components not already returned during the session. If no new components remain, it returns No new Kmap node components. Malformed arguments return invalid KmapOpenNode arguments; missing authorization and Kmap dependency failures remain errors."#,
147        ),
148        (
149            "KmapUpdateNode",
150            r#"Invoke as KmapUpdateNode {"node_id":string,"title":string,"navigation_hint":string,"narrative":string,"connections":[string,...]}. All IDs must be exact 24-character lowercase hexadecimal Kmap node IDs. It replaces the node text fields, additively upserts the supplied Navigation connections, marks the updated node loaded, and returns success. Naming another node does not itself load that node. Malformed arguments return invalid KmapUpdateNode arguments; missing authorization and Kmap dependency failures remain errors."#,
151        ),
152        (
153            "KmapPenalizeNodes",
154            r#"Invoke as KmapPenalizeNodes {"node_ids":[string,...]}. Every ID must be an exact 24-character lowercase hexadecimal Kmap node ID. It deduplicates the input and applies one noncritical negative measurement to each newly penalized eligible loaded node with first-pull provenance. Penalized nodes are excluded from the session’s later KmapConnectNodes operation. Unknown, unloaded, or already penalized valid IDs have no additional effect. Success returns success. Malformed arguments return invalid KmapPenalizeNodes arguments; missing authorization and Kmap dependency failures remain errors."#,
155        ),
156        (
157            "KmapConnectNodes",
158            r#"Invoke as KmapConnectNodes {}. Using the active Kmap authorization, it adds every missing directed Automated connection among loaded, unpenalized session nodes. Existing connections remain unchanged and self-connections are not added. Success returns success. Nonempty or nonobject arguments return invalid KmapConnectNodes arguments; missing authorization and Kmap dependency failures remain errors."#,
159        ),
160        (
161            "SendMessage",
162            r#"Invoke as SendMessage {"message":string}. Arguments must contain exactly one nonempty message string. It durably appends one visible Agent Message to the authenticated current conversation between the Tool Call and Tool Result, returns success, and leaves the provider turn open for continued generation. It has no recipient argument, network effect, or cross-conversation capability. Malformed arguments return invalid SendMessage arguments without appending a message; persistence failures remain errors."#,
163        ),
164    ];
165
166    fn arguments_for(name: &str) -> String {
167        format!(
168            "{{\"name\":{}}}",
169            serde_json::to_string(name).expect("a string is JSON-serializable")
170        )
171    }
172
173    fn expected_active_json(name: &str, docs: &str) -> String {
174        format!(
175            "{{\"name\":{},\"latest_version\":\"1.0.0\",\"docs\":{},\"deprecated\":false,\"replacement\":null}}",
176            serde_json::to_string(name).expect("a string is JSON-serializable"),
177            serde_json::to_string(docs).expect("a string is JSON-serializable")
178        )
179    }
180
181    fn replacement_invariants_hold(catalog: &[Entry]) -> bool {
182        catalog.iter().all(|entry| {
183            let Some(replacement) = entry.replacement else {
184                return true;
185            };
186            if replacement.is_empty() || replacement == entry.name {
187                return false;
188            }
189            matches!(
190                find_entry(catalog, replacement),
191                Some(target) if target.replacement.is_none()
192            )
193        })
194    }
195
196    #[test]
197    fn all_exact_names_have_exact_ordered_five_field_outputs() {
198        for (name, docs) in EXPECTED {
199            let output = ktool_docs(&arguments_for(name)).expect("known name must succeed");
200            assert_eq!(output, expected_active_json(name, docs));
201
202            let value: Value = serde_json::from_str(&output).expect("response must be JSON");
203            let object = value.as_object().expect("response must be an object");
204            assert_eq!(object.len(), 5);
205            assert_eq!(object.get("name").and_then(Value::as_str), Some(name));
206            assert_eq!(
207                object.get("latest_version").and_then(Value::as_str),
208                Some("1.0.0")
209            );
210            assert_eq!(object.get("docs").and_then(Value::as_str), Some(docs));
211            assert_eq!(
212                object.get("deprecated").and_then(Value::as_bool),
213                Some(false)
214            );
215            assert!(object.get("replacement").is_some_and(Value::is_null));
216        }
217    }
218
219    #[test]
220    fn self_lookup_documents_ktool_docs() {
221        let output = ktool_docs(r#"{"name":"KtoolDocs"}"#).expect("self lookup must succeed");
222        assert_eq!(output, expected_active_json(EXPECTED[0].0, EXPECTED[0].1));
223    }
224
225    #[test]
226    fn malformed_or_nonconforming_arguments_are_strictly_rejected() {
227        let invalid = [
228            "",
229            " ",
230            "{",
231            "null",
232            "[]",
233            r#""KtoolDocs""#,
234            "0",
235            "true",
236            "{}",
237            r#"{"other":"KtoolDocs"}"#,
238            r#"{"Name":"KtoolDocs"}"#,
239            r#"{"name":null}"#,
240            r#"{"name":true}"#,
241            r#"{"name":1}"#,
242            r#"{"name":[]}"#,
243            r#"{"name":{}}"#,
244            r#"{"name":""}"#,
245            r#"{"name":"KtoolDocs","extra":false}"#,
246            r#"{"extra":false,"name":"KtoolDocs"}"#,
247            r#"{"name":"KtoolDocs","name":"CurrentTime"}"#,
248            r#"{"name":"KtoolDocs"} trailing"#,
249        ];
250
251        for input in invalid {
252            assert_eq!(
253                ktool_docs(input),
254                Err(INVALID_ARGUMENTS.to_owned()),
255                "{input:?}"
256            );
257        }
258    }
259
260    #[test]
261    fn exact_match_variants_return_only_unknown_ktool() {
262        let variants = [
263            "ktoolDocs",
264            "KTOOLDOCS",
265            " KtoolDocs",
266            "KtoolDocs ",
267            "KtoolDoc",
268            "KtoolDocs\n",
269            "Currenttime",
270            "KmapOpenNode/",
271            "SendMessage\0",
272        ];
273
274        for variant in variants {
275            assert_eq!(
276                ktool_docs(&arguments_for(variant)),
277                Err(UNKNOWN_KTOOL.to_owned()),
278                "{variant:?}"
279            );
280        }
281    }
282
283    #[test]
284    fn repeated_lookups_are_deterministic() {
285        for (name, _) in EXPECTED {
286            let arguments = arguments_for(name);
287            let first = ktool_docs(&arguments).expect("known name must succeed");
288            for _ in 0..10 {
289                assert_eq!(ktool_docs(&arguments), Ok(first.clone()));
290            }
291        }
292    }
293
294    #[test]
295    fn rendering_escapes_all_json_string_fields() {
296        let entry = Entry::new(
297            "quote\" slash\\ newline\n",
298            "7.4.2",
299            "tab\t backspace\u{0008} quote\" slash\\",
300            None,
301        );
302        let output = render(&entry).expect("supported response must serialize");
303
304        assert!(!output.contains('\n'));
305        assert!(!output.contains('\t'));
306        assert!(!output.contains('\u{0008}'));
307        assert!(output.contains("\\n"));
308        assert!(output.contains("\\t"));
309        assert!(output.contains("\\b"));
310        assert!(output.contains("\\\""));
311        assert!(output.contains("\\\\"));
312
313        let value: Value = serde_json::from_str(&output).expect("escaped response must parse");
314        assert_eq!(value.get("name").and_then(Value::as_str), Some(entry.name));
315        assert_eq!(value.get("docs").and_then(Value::as_str), Some(entry.docs));
316    }
317
318    #[test]
319    fn private_deprecated_entry_renders_replacement_and_derived_flag() {
320        let entry = Entry::new("RetiredTool", "2.1.3", "Retired docs.", Some("CurrentTime"));
321        let output = render(&entry).expect("deprecated response must serialize");
322        assert_eq!(
323            output,
324            r#"{"name":"RetiredTool","latest_version":"2.1.3","docs":"Retired docs.","deprecated":true,"replacement":"CurrentTime"}"#
325        );
326
327        let value: Value = serde_json::from_str(&output).expect("response must parse");
328        assert_eq!(
329            value.get("deprecated").and_then(Value::as_bool),
330            Some(entry.replacement.is_some())
331        );
332    }
333
334    #[test]
335    fn replacement_invariants_require_nonempty_nonself_known_active_target() {
336        let valid = [
337            Entry::new("NewTool", "3.0.0", "new", None),
338            Entry::new("OldTool", "2.0.0", "old", Some("NewTool")),
339        ];
340        assert!(replacement_invariants_hold(&CATALOG));
341        assert!(replacement_invariants_hold(&valid));
342
343        let empty = [Entry::new("OldTool", "1.0.0", "old", Some(""))];
344        let self_replacement = [Entry::new("OldTool", "1.0.0", "old", Some("OldTool"))];
345        let unknown = [Entry::new("OldTool", "1.0.0", "old", Some("MissingTool"))];
346        let deprecated_target = [
347            Entry::new("NewTool", "3.0.0", "new", None),
348            Entry::new("MiddleTool", "2.0.0", "middle", Some("NewTool")),
349            Entry::new("OldTool", "1.0.0", "old", Some("MiddleTool")),
350        ];
351
352        assert!(!replacement_invariants_hold(&empty));
353        assert!(!replacement_invariants_hold(&self_replacement));
354        assert!(!replacement_invariants_hold(&unknown));
355        assert!(!replacement_invariants_hold(&deprecated_target));
356    }
357}