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(name: &'static str, docs: &'static str) -> Self {
17 Self {
18 name,
19 version: "1.0.0",
20 docs,
21 replacement: None,
22 }
23 }
24}
25
26static CATALOG: [Entry; 19] = [
27 Entry::new(
28 "KtoolDocs",
29 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 compact JSON 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."#,
30 ),
31 Entry::new(
32 "CurrentTime",
33 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."#,
34 ),
35 Entry::new(
36 "KmapCreateNode",
37 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."#,
38 ),
39 Entry::new(
40 "KmapOpenNode",
41 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."#,
42 ),
43 Entry::new(
44 "KmapUpdateNode",
45 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."#,
46 ),
47 Entry::new(
48 "KmapPenalizeNodes",
49 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."#,
50 ),
51 Entry::new(
52 "KmapConnectNodes",
53 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."#,
54 ),
55 Entry::new(
56 "SendMessage",
57 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."#,
58 ),
59 Entry::new(
60 "WebSearch",
61 r#"Invoke as WebSearch {"query":string,"model":"codex/<model>","reasoning_effort":string?}. Arguments are strict: query and model are required strings, model must have the exact case-sensitive codex/ prefix, and reasoning_effort is optional, non-null, and defaults to medium. It starts one isolated asynchronous search with an absolute 60-minute deadline and emits no progress. Malformed arguments return invalid WebSearch arguments. This metadata does not authorize a provider call or prove runtime availability."#,
62 ),
63 Entry::new(
64 "SetLaunchNode",
65 r#"Invoke as SetLaunchNode {"target":string,"node_id":string}. Arguments are strict: target is validated as an exact target name and node_id must be a visible AccessId of exactly 24 lowercase hexadecimal characters. With current-user authority, it creates or updates that target’s launch-node reference. It makes no runtime-selection claim."#,
66 ),
67 Entry::new(
68 "ListContacts",
69 r#"Invoke as ListContacts {}. Arguments are strict and must be an empty object. It returns the complete caller-owned exact Known Contacts union. Every row has exactly user_id, username, person_id, full_name, and nullable kmap_launch_node. Authorization and dependency failures remain errors."#,
70 ),
71 Entry::new(
72 "ListGroups",
73 r#"Invoke as ListGroups {}. Arguments are strict and must be an empty object. It returns complete visible ordinary human memberships, excluding Known Contacts and filtered groups. Every row has exactly group_id, name, revision, caller_role, and kmap_launch_node. It returns no counts. Authorization and dependency failures remain errors."#,
74 ),
75 Entry::new(
76 "GetGroup",
77 r#"Invoke as GetGroup {"group_id":string}. Arguments are strict and require group_id. Mere user-or-active-model visibility is sufficient. It returns exactly top-level fields group_id, name, revision, kmap_launch_node, users, and models; each user row has user_id, username, person_id, full_name, role, and kmap_launch_node; each model row has model_id and nullable name. It returns no messages, history, narrative, or counts. Authorization and dependency failures remain errors."#,
78 ),
79 Entry::new(
80 "RustCodeCreate",
81 r#"Invoke as RustCodeCreate with exact string arguments authority, library, and version. It starts an unpublished three-file library and emits editable Documentation.md, Cargo.toml, and Rust source boxes. Edit those boxes with RustCodeOverwrite, then check or publish the library."#,
82 ),
83 Entry::new(
84 "RustCodeDocs",
85 r#"Invoke as RustCodeDocs with exact string arguments authority, library, and version. It returns only the exact Documentation.md contents of the published supported three-file library and does not open editable source."#,
86 ),
87 Entry::new(
88 "RustCodeOpen",
89 r#"Invoke as RustCodeOpen with exact string arguments authority, library, and version. It opens a published supported three-file library and emits editable Documentation.md, Cargo.toml, and ordered Rust source boxes."#,
90 ),
91 Entry::new(
92 "RustCodeOverwrite",
93 r#"Invoke as RustCodeOverwrite with box_id from the current RustCodeCreate or RustCodeOpen output and contents containing the complete replacement for that source box. It changes only that segment; later edits keep using the original box ID."#,
94 ),
95 Entry::new(
96 "RustCodeCheck",
97 r#"Invoke as RustCodeCheck with box_id from any box in the current Rust library. It always runs a fresh complete check and returns only the plain success or failure text."#,
98 ),
99 Entry::new(
100 "RustCodePublish",
101 r#"Invoke as RustCodePublish with box_id from any box in the current Rust library. It may be called without RustCodeCheck; it checks automatically when needed, aborts on failure, and immutably publishes only when authorized."#,
102 ),
103];
104
105#[derive(Deserialize)]
106#[serde(deny_unknown_fields)]
107struct Arguments {
108 name: String,
109}
110
111#[derive(Serialize)]
112struct Response<'a> {
113 name: &'a str,
114 latest_version: &'static str,
115 docs: &'a str,
116 deprecated: bool,
117 replacement: Option<&'a str>,
118}
119
120fn find_entry(name: &str) -> Option<&'static Entry> {
121 CATALOG.iter().find(|entry| entry.name == name)
122}
123
124fn render(entry: &Entry) -> Result<String, String> {
125 let response = Response {
126 name: entry.name,
127 latest_version: entry.version,
128 docs: entry.docs,
129 deprecated: entry.replacement.is_some(),
130 replacement: entry.replacement,
131 };
132 serde_json::to_string(&response)
133 .map_err(|error| format!("KtoolDocs response serialization failed: {error}"))
134}
135
136pub fn is_known_ktool(name: &str) -> bool {
138 find_entry(name).is_some()
139}
140
141pub fn ktool_docs(arguments: &str) -> Result<String, String> {
143 let arguments: Arguments =
144 serde_json::from_str(arguments).map_err(|_| INVALID_ARGUMENTS.to_owned())?;
145 if arguments.name.is_empty() {
146 return Err(INVALID_ARGUMENTS.to_owned());
147 }
148 let entry = find_entry(&arguments.name).ok_or_else(|| UNKNOWN_KTOOL.to_owned())?;
149 render(entry)
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use serde_json::Value;
156
157 fn arguments_for(name: &str) -> String {
158 format!("{{\"name\":{}}}", serde_json::to_string(name).unwrap())
159 }
160
161 #[test]
162 fn every_catalog_entry_has_an_exact_five_field_active_response() {
163 assert_eq!(CATALOG.len(), 19);
164 for entry in &CATALOG {
165 let output = ktool_docs(&arguments_for(entry.name)).unwrap();
166 let value: Value = serde_json::from_str(&output).unwrap();
167 let object = value.as_object().unwrap();
168 assert_eq!(object.len(), 5);
169 assert_eq!(object.get("name").and_then(Value::as_str), Some(entry.name));
170 assert_eq!(
171 object.get("latest_version").and_then(Value::as_str),
172 Some("1.0.0")
173 );
174 assert_eq!(object.get("docs").and_then(Value::as_str), Some(entry.docs));
175 assert_eq!(
176 object.get("deprecated").and_then(Value::as_bool),
177 Some(false)
178 );
179 assert!(object.get("replacement").is_some_and(Value::is_null));
180 }
181 }
182
183 #[test]
184 fn known_lookup_uses_the_same_exact_catalog() {
185 for entry in &CATALOG {
186 assert!(is_known_ktool(entry.name));
187 }
188 for name in ["websearch", " WebSearch", "GetGroup ", "Unknown"] {
189 assert!(!is_known_ktool(name));
190 assert_eq!(
191 ktool_docs(&arguments_for(name)),
192 Err(UNKNOWN_KTOOL.to_owned())
193 );
194 }
195 assert!(!is_known_ktool(""));
196 assert_eq!(
197 ktool_docs(&arguments_for("")),
198 Err(INVALID_ARGUMENTS.to_owned())
199 );
200 }
201
202 #[test]
203 fn new_contracts_state_the_approved_boundaries() {
204 let docs = |name| find_entry(name).unwrap().docs;
205 assert!(docs("WebSearch").contains("exact case-sensitive codex/ prefix"));
206 assert!(docs("WebSearch").contains("defaults to medium"));
207 assert!(docs("WebSearch").contains("one isolated asynchronous search"));
208 assert!(docs("WebSearch").contains("absolute 60-minute deadline"));
209 assert!(docs("SetLaunchNode").contains("visible AccessId"));
210 assert!(docs("SetLaunchNode").contains("current-user authority"));
211 assert!(docs("SetLaunchNode").contains("creates or updates"));
212 assert!(docs("ListContacts").contains("complete caller-owned exact Known Contacts union"));
213 assert!(docs("ListGroups").contains("excluding Known Contacts and filtered groups"));
214 assert!(docs("ListGroups").contains("no counts"));
215 let get_group = docs("GetGroup");
216 assert!(get_group.contains("Mere user-or-active-model visibility"));
217 assert!(get_group.contains(
218 "top-level fields group_id, name, revision, kmap_launch_node, users, and models"
219 ));
220 assert!(
221 get_group
222 .contains("user_id, username, person_id, full_name, role, and kmap_launch_node")
223 );
224 assert!(get_group.contains("model_id and nullable name"));
225 assert!(get_group.contains("no messages, history, narrative, or counts"));
226 assert!(!get_group.contains("caller_role"));
227
228 assert!(docs("RustCodeCreate").contains("authority, library, and version"));
229 assert!(docs("RustCodeCreate").contains("emits editable"));
230 assert!(docs("RustCodeDocs").contains("returns only the exact Documentation.md"));
231 assert!(docs("RustCodeOpen").contains("ordered Rust source boxes"));
232 assert!(docs("RustCodeOverwrite").contains("complete replacement"));
233 assert!(docs("RustCodeOverwrite").contains("original box ID"));
234 assert!(docs("RustCodeCheck").contains("always runs a fresh complete check"));
235 assert!(docs("RustCodePublish").contains("without RustCodeCheck"));
236 assert!(docs("RustCodePublish").contains("checks automatically"));
237 assert!(docs("RustCodePublish").contains("aborts on failure"));
238 }
239
240 #[test]
241 fn malformed_arguments_are_strictly_rejected() {
242 for input in [
243 "",
244 "null",
245 "[]",
246 "{}",
247 r#"{"name":""}"#,
248 r#"{"name":1}"#,
249 r#"{"name":"KtoolDocs","extra":false}"#,
250 r#"{"name":"KtoolDocs","name":"CurrentTime"}"#,
251 ] {
252 assert_eq!(
253 ktool_docs(input),
254 Err(INVALID_ARGUMENTS.to_owned()),
255 "{input:?}"
256 );
257 }
258 }
259
260 #[test]
261 fn response_serialization_escapes_strings() {
262 let entry = Entry {
263 name: "quote\"",
264 version: "1.0.0",
265 docs: "line\n",
266 replacement: None,
267 };
268 let output = render(&entry).unwrap();
269 assert!(output.contains("\\\""));
270 assert!(output.contains("\\n"));
271 assert_eq!(
272 serde_json::from_str::<Value>(&output).unwrap()["docs"],
273 "line\n"
274 );
275 }
276}