1use std::cell::RefCell;
19use std::collections::{BTreeMap, HashSet};
20use std::sync::{Mutex, OnceLock};
21
22use serde_json::{Value, json};
23
24#[cfg(feature = "http-server")]
25use super::compress::{bare_tool_name, expand_tool_name};
26
27const MAX_TRACKED: usize = 4096;
28const EXPAND_TOOL_NAME: &str = "ctx_expand";
29
30fn active_sessions() -> &'static Mutex<HashSet<u64>> {
31 static SESSIONS: OnceLock<Mutex<HashSet<u64>>> = OnceLock::new();
32 SESSIONS.get_or_init(|| Mutex::new(HashSet::new()))
33}
34
35pub fn mark_ccr_active(conv_id: u64) {
37 if let Ok(mut guard) = active_sessions().lock() {
38 if guard.len() >= MAX_TRACKED
39 && !guard.contains(&conv_id)
40 && let Some(&oldest) = guard.iter().next()
41 {
42 guard.remove(&oldest);
43 }
44 guard.insert(conv_id);
45 }
46}
47
48pub fn is_ccr_active(conv_id: u64) -> bool {
50 active_sessions().lock().is_ok_and(|g| g.contains(&conv_id))
51}
52
53#[derive(Debug, Clone, Default, PartialEq, Eq)]
57pub(crate) struct HandleUse {
58 expand: Option<String>,
66 list_read: bool,
69 correlation: Option<Correlation>,
73}
74
75#[derive(Debug, Clone, Default, PartialEq, Eq)]
78struct Correlation {
79 listed: Vec<String>,
81 by_call: BTreeMap<String, String>,
86}
87
88#[cfg(feature = "http-server")]
89impl Correlation {
90 fn of(listed: Vec<String>, doc: Option<&Value>) -> Self {
91 let by_call = doc
92 .map(producing_calls)
93 .unwrap_or_default()
94 .into_iter()
95 .filter_map(|(call_id, producer)| {
96 Some((call_id.to_owned(), expand_of_producer(producer?, &listed)?))
97 })
98 .collect();
99 Self { listed, by_call }
100 }
101}
102
103#[cfg(feature = "http-server")]
116fn producing_calls(doc: &Value) -> BTreeMap<&str, Option<&str>> {
117 fn note<'doc>(
118 calls: &mut BTreeMap<&'doc str, Option<&'doc str>>,
119 call_id: Option<&'doc Value>,
120 name: Option<&'doc Value>,
121 ) {
122 let (Some(call_id), Some(name)) = (
123 call_id.and_then(Value::as_str),
124 name.and_then(Value::as_str),
125 ) else {
126 return;
127 };
128 calls
129 .entry(call_id)
130 .and_modify(|known| {
131 if *known != Some(name) {
132 *known = None;
133 }
134 })
135 .or_insert(Some(name));
136 }
137 fn items<'doc>(doc: &'doc Value, key: &str) -> &'doc [Value] {
138 doc.get(key)
139 .and_then(Value::as_array)
140 .map_or(&[][..], Vec::as_slice)
141 }
142 let mut calls = BTreeMap::new();
143 for message in items(doc, "messages") {
144 let blocks = message.get("content").and_then(Value::as_array);
145 for block in blocks.into_iter().flatten() {
146 if block.get("type").and_then(Value::as_str) == Some("tool_use") {
147 note(&mut calls, block.get("id"), block.get("name"));
148 }
149 }
150 let tool_calls = message.get("tool_calls").and_then(Value::as_array);
151 for call in tool_calls.into_iter().flatten() {
152 let name = call
153 .get("function")
154 .and_then(|function| function.get("name"));
155 note(&mut calls, call.get("id"), name);
156 }
157 }
158 for item in items(doc, "input") {
159 if item.get("type").and_then(Value::as_str) == Some("function_call") {
160 note(&mut calls, item.get("call_id"), item.get("name"));
161 }
162 }
163 calls
164}
165
166#[cfg(feature = "http-server")]
168const WRAPPER_TOOL: &str = "ctx_call";
169
170#[cfg(feature = "http-server")]
182fn expand_of_producer(producer: &str, listed: &[String]) -> Option<String> {
183 let bare = bare_tool_name(producer);
184 if !bare.starts_with("ctx_") || bare == WRAPPER_TOOL {
185 return None;
186 }
187 let prefix = producer.strip_suffix(bare)?;
188 listed
189 .iter()
190 .find(|tool| tool.strip_suffix(EXPAND_TOOL_NAME) == Some(prefix))
191 .cloned()
192}
193
194#[cfg(feature = "http-server")]
195impl HandleUse {
196 pub(crate) fn for_request(doc: Option<&Value>, route_can_follow: bool) -> Self {
208 let listed = doc
209 .filter(|doc| route_can_follow && can_carry(doc))
210 .and_then(|doc| {
211 Shape::ALL
212 .iter()
213 .map(|shape| shape.listed_expand(doc))
214 .find(|listed| !listed.is_empty())
215 })
216 .unwrap_or_default();
217 Self::knowing(listed, doc)
218 }
219
220 fn knowing(listed: Vec<String>, doc: Option<&Value>) -> Self {
223 let protected = super::compress::protected();
224 let Some(first) = listed
225 .first()
226 .cloned()
227 .filter(|_| !protected || super::store_binding::host_store_bound())
228 else {
229 return Self {
230 list_read: true,
231 ..Self::default()
232 };
233 };
234 Self {
235 expand: Some(first),
236 list_read: true,
237 correlation: protected.then(|| Correlation::of(listed, doc)),
238 }
239 }
240
241 pub(crate) fn follows(&self, doc: &Value) -> bool {
247 let Some(name) = self.expand.as_deref() else {
248 return false;
249 };
250 can_carry(doc)
251 && Shape::ALL
252 .iter()
253 .any(|shape| match self.correlation.as_ref() {
254 Some(correlation) => shape.listed_expand(doc) == correlation.listed,
255 None => shape.registered_expand(doc).as_deref() == Some(name),
256 })
257 }
258}
259
260#[cfg(feature = "http-server")]
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub(crate) enum Shape {
264 Messages,
266 Chat,
268 Responses,
271 Gemini,
273}
274
275#[cfg(feature = "http-server")]
276impl Shape {
277 const ALL: [Self; 4] = [Self::Messages, Self::Chat, Self::Responses, Self::Gemini];
280
281 fn registered_expand(self, doc: &Value) -> Option<String> {
294 self.listed_expand(doc).into_iter().next()
295 }
296
297 fn listed_expand(self, doc: &Value) -> Vec<String> {
305 let mut listed: Vec<String> = Vec::new();
306 let tools = doc.get("tools").and_then(Value::as_array);
307 for tool in tools.into_iter().flatten() {
308 if tool.get("defer_loading").and_then(Value::as_bool) == Some(true) {
309 continue;
310 }
311 let names: Vec<String> = match self {
312 Self::Messages | Self::Responses => expand_name_of(tool).into_iter().collect(),
313 Self::Chat => tool
314 .get("function")
315 .and_then(expand_name_of)
316 .into_iter()
317 .collect(),
318 Self::Gemini => GEMINI_DECLARATIONS
319 .iter()
320 .filter_map(|key| tool.get(*key)?.as_array())
321 .flatten()
322 .filter_map(expand_name_of)
323 .collect(),
324 };
325 for name in names {
326 if !listed.contains(&name) {
327 listed.push(name);
328 }
329 }
330 }
331 listed
332 }
333}
334
335#[cfg(feature = "http-server")]
337fn expand_name_of(entry: &Value) -> Option<String> {
338 entry
339 .get("name")
340 .and_then(Value::as_str)
341 .filter(|name| expand_tool_name(name))
342 .map(str::to_owned)
343}
344
345#[cfg(feature = "http-server")]
347const GEMINI_DECLARATIONS: [&str; 2] = ["functionDeclarations", "function_declarations"];
348
349#[cfg(feature = "http-server")]
362fn can_carry(doc: &Value) -> bool {
363 let named = |value: Option<&Value>, name: &str| {
364 value
365 .and_then(Value::as_str)
366 .is_some_and(|given| given.eq_ignore_ascii_case(name))
367 };
368 let choice = doc.get("tool_choice");
369 let choice_type = choice.and_then(|choice| choice.get("type"));
370 let calling = doc
371 .get("toolConfig")
372 .or_else(|| doc.get("tool_config"))
373 .and_then(|config| {
374 config
375 .get("functionCallingConfig")
376 .or_else(|| config.get("function_calling_config"))
377 });
378 let narrowed = calling
379 .and_then(|calling| {
380 calling
381 .get("allowedFunctionNames")
382 .or_else(|| calling.get("allowed_function_names"))
383 })
384 .and_then(Value::as_array)
385 .is_some_and(|names| !names.is_empty());
386 let forbidden = named(choice, "none")
387 || named(choice_type, "none")
388 || named(choice_type, "allowed_tools")
389 || named(calling.and_then(|calling| calling.get("mode")), "none")
390 || narrowed
391 || [
392 "functions",
393 "function_call",
394 "cachedContent",
395 "cached_content",
396 ]
397 .iter()
398 .any(|key| doc.get(*key).is_some_and(|value| !value.is_null()));
399 if forbidden {
400 return false;
401 }
402 match doc.get("tools") {
403 None => true,
404 Some(Value::Array(tools)) => {
405 doc.get("contents").is_none()
406 || tools.is_empty()
407 || tools.iter().any(|tool| {
408 GEMINI_DECLARATIONS
409 .iter()
410 .any(|key| tool.get(*key).is_some())
411 })
412 }
413 Some(_) => false,
414 }
415}
416
417thread_local! {
418 static PASS: RefCell<Option<HandleUse>> = const { RefCell::new(None) };
421}
422
423pub(crate) fn within<T>(so_far: HandleUse, pass: impl FnOnce() -> T) -> (T, HandleUse) {
428 struct Restore(Option<HandleUse>);
429 impl Drop for Restore {
430 fn drop(&mut self) {
431 let outer = self.0.take();
432 PASS.with(|slot| *slot.borrow_mut() = outer);
433 }
434 }
435 let restore = Restore(PASS.with(|slot| slot.replace(Some(so_far))));
436 let result = pass();
437 let known = PASS
438 .with(|slot| slot.borrow_mut().take())
439 .unwrap_or_default();
440 drop(restore);
441 (result, known)
442}
443
444pub(crate) fn in_pass<T>(pass: impl FnOnce() -> T) -> T {
447 if PASS.with(|slot| slot.borrow().is_some()) {
448 pass()
449 } else {
450 within(HandleUse::default(), pass).0
451 }
452}
453
454#[cfg(feature = "http-server")]
457fn update(change: impl FnOnce(&mut HandleUse)) {
458 PASS.with(|slot| {
459 if let Some(known) = slot.borrow_mut().as_mut() {
460 change(known);
461 }
462 });
463}
464
465#[cfg(feature = "http-server")]
475pub(crate) fn begin(shape: Shape, doc: &Value) {
476 update(|known| {
477 let listed = if can_carry(doc) {
478 shape.listed_expand(doc)
479 } else {
480 Vec::new()
481 };
482 if !known.list_read {
483 *known = HandleUse::knowing(listed, Some(doc));
484 return;
485 }
486 let unchanged = match known.correlation.as_ref() {
487 Some(correlation) => correlation.listed == listed,
488 None => known.expand.as_ref() == listed.first(),
489 };
490 if !unchanged {
491 known.expand = None;
492 known.correlation = None;
493 }
494 });
495}
496
497thread_local! {
498 static BLOCK: RefCell<Option<String>> = const { RefCell::new(None) };
501}
502
503#[must_use = "the block ends where this is dropped"]
505pub(crate) struct BlockScope {
506 outer: Option<String>,
507}
508
509impl Drop for BlockScope {
510 fn drop(&mut self) {
511 let outer = self.outer.take();
512 BLOCK.with(|slot| *slot.borrow_mut() = outer);
513 }
514}
515
516pub(crate) fn block(call_id: Option<&str>) -> BlockScope {
525 let call_id = call_id.map(str::to_owned);
526 BlockScope {
527 outer: BLOCK.with(|slot| slot.replace(call_id)),
528 }
529}
530
531fn expand_tool() -> Option<String> {
553 PASS.with(|slot| {
554 let slot = slot.borrow();
555 let Some(known) = slot.as_ref() else {
556 return Some(EXPAND_TOOL_NAME.to_owned());
557 };
558 let first = known.expand.as_ref()?;
559 match known.correlation.as_ref() {
560 Some(correlation) => BLOCK.with(|block| {
561 let call_id = block.borrow();
562 correlation.by_call.get(call_id.as_deref()?).cloned()
563 }),
564 None if super::compress::protected() => None,
565 None => Some(first.clone()),
566 }
567 })
568}
569
570pub(crate) fn expand_tool_for(original: &str) -> Option<String> {
643 let tool = expand_tool()?;
644 let correlated = PASS.with(|slot| {
645 slot.borrow()
646 .as_ref()
647 .is_some_and(|known| known.correlation.is_some())
648 });
649 if !correlated {
650 return Some(tool);
651 }
652 let call_id = BLOCK.with(|slot| slot.borrow().clone())?;
653 super::store_binding::delivered_for_call(original, &call_id).then_some(tool)
654}
655
656fn expand_tool_definition() -> Value {
657 json!({
658 "name": EXPAND_TOOL_NAME,
659 "description": "Retrieve the original uncompressed content of a compressed tool result by its tee handle or hash.",
660 "input_schema": {
661 "type": "object",
662 "properties": {
663 "id": {
664 "type": "string",
665 "description": "The tee path or hash handle from the compressed output."
666 }
667 },
668 "required": ["id"]
669 }
670 })
671}
672
673pub fn ensure_tool_present(conv_id: u64, doc: &mut Value) -> bool {
676 if !is_ccr_active(conv_id) {
677 return false;
678 }
679
680 let tools = match doc.get_mut("tools") {
681 Some(Value::Array(arr)) => arr,
682 Some(_) => return false,
683 None => {
684 doc["tools"] = Value::Array(Vec::new());
685 doc["tools"]
686 .as_array_mut()
687 .expect("tools just initialized as array")
688 }
689 };
690
691 let already_present = tools.iter().any(|t| {
692 t.get("name")
693 .and_then(Value::as_str)
694 .is_some_and(|n| n == EXPAND_TOOL_NAME)
695 });
696
697 if already_present {
698 return false;
699 }
700
701 tools.push(expand_tool_definition());
702 true
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708 use serde_json::json;
709
710 const NO_CCR: u64 = 0xAA01;
714 const INJECT: u64 = 0xAA02;
715 const DEDUP: u64 = 0xAA03;
716 const STABLE: u64 = 0xAA04;
717 const STICKY: u64 = 0xAA05;
718
719 #[test]
720 fn tool_not_injected_without_ccr() {
721 let mut doc = json!({"tools": [], "messages": []});
722 assert!(!ensure_tool_present(NO_CCR, &mut doc));
723 {
724 let found = &doc["tools"].as_array().unwrap();
725 assert!(found.is_empty(), "{found:?}");
726 }
727 }
728
729 #[test]
730 fn tool_injected_after_ccr_activation() {
731 mark_ccr_active(INJECT);
732 assert!(is_ccr_active(INJECT));
733 let mut doc = json!({"tools": [], "messages": []});
734 assert!(ensure_tool_present(INJECT, &mut doc));
735 assert_eq!(doc["tools"].as_array().unwrap().len(), 1);
736 assert_eq!(doc["tools"][0]["name"], "ctx_expand");
737 }
738
739 #[test]
740 fn tool_not_duplicated() {
741 mark_ccr_active(DEDUP);
742 let mut doc = json!({"tools": [expand_tool_definition()], "messages": []});
743 assert!(!ensure_tool_present(DEDUP, &mut doc));
744 assert_eq!(doc["tools"].as_array().unwrap().len(), 1);
745 }
746
747 #[test]
748 fn tools_array_stays_stable_after_ccr_activation() {
749 mark_ccr_active(STABLE);
750 let existing = json!({"name": "other_tool", "description": "test"});
751
752 let mut doc1 = json!({"tools": [existing.clone()], "messages": []});
753 ensure_tool_present(STABLE, &mut doc1);
754 let snap1 = serde_json::to_string(&doc1["tools"]).unwrap();
755
756 let mut doc2 = json!({"tools": [existing], "messages": []});
757 ensure_tool_present(STABLE, &mut doc2);
758 let snap2 = serde_json::to_string(&doc2["tools"]).unwrap();
759
760 assert_eq!(
761 snap1, snap2,
762 "tool list must be byte-identical across turns"
763 );
764 }
765
766 #[test]
767 fn sticky_survives_turn_without_markers() {
768 mark_ccr_active(STICKY);
769 assert!(is_ccr_active(STICKY));
770 let mut doc = json!({"tools": [], "messages": []});
771 assert!(ensure_tool_present(STICKY, &mut doc));
772 }
773
774 #[test]
775 fn max_tracked_does_not_panic() {
776 for i in 0..(MAX_TRACKED + 100) {
777 mark_ccr_active(i as u64);
778 }
779 assert!(active_sessions().lock().unwrap().len() <= MAX_TRACKED);
780 }
781
782 #[cfg(feature = "http-server")]
783 fn developer_mode() {
786 assert!(!crate::compress::protected(), "developer mode");
787 }
788
789 #[cfg(feature = "http-server")]
790 fn governed_project(root: &std::path::Path) {
792 let config = lean_ctx::core::paths::config_dir_read_only().expect("config directory");
793 std::fs::create_dir_all(&config).expect("config directory");
794 std::fs::write(
795 config.join("config.toml"),
796 format!(
797 "[context_gateway]\nmode = \"governed\"\n\n[proxy]\nproject_root = '{}'\n",
798 root.display()
799 ),
800 )
801 .expect("gateway config");
802 assert!(crate::compress::protected(), "governed mode");
803 }
804
805 #[cfg(feature = "http-server")]
806 #[test]
812 fn passes_of_a_request_carry_the_expand_tool_and_nest() {
813 let _data = lean_ctx::core::data_dir::isolated_data_dir();
814 let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
815 developer_mode();
816 let prefixed = "mcp__lean-ctx__ctx_expand";
817 let doc = json!({"tools": [{"name": prefixed}]});
818 let known = HandleUse::for_request(Some(&doc), true);
819
820 let (first, known) = within(known, expand_tool);
822 assert_eq!(first.as_deref(), Some(prefixed));
823 let (later, known) = within(known, expand_tool);
824 assert_eq!(later.as_deref(), Some(prefixed));
825 assert_eq!(known, HandleUse::for_request(Some(&doc), true));
826
827 let (joined, known) = within(known, || {
829 in_pass(|| {
830 begin(Shape::Messages, &doc);
831 expand_tool()
832 })
833 });
834 assert_eq!(joined.as_deref(), Some(prefixed));
835 assert_eq!(known.expand.as_deref(), Some(prefixed));
836 let alone = in_pass(|| {
839 begin(Shape::Messages, &json!({"tools": [{"name": "bash"}]}));
840 expand_tool()
841 });
842 assert_eq!(alone, None);
843 assert_eq!(expand_tool().as_deref(), Some("ctx_expand"), "no pass");
844
845 let ((), outer) = within(known, || {
847 let (inner, _) = within(HandleUse::for_request(None, true), expand_tool);
848 assert_eq!(inner, None);
849 assert_eq!(expand_tool().as_deref(), Some(prefixed), "outer pass again");
850 });
851 assert_eq!(outer.expand.as_deref(), Some(prefixed));
852 }
853
854 #[cfg(feature = "http-server")]
855 #[test]
862 fn under_protection_a_listed_tool_is_named_only_for_a_correlated_block() {
863 use std::net::{IpAddr, Ipv4Addr};
864
865 use crate::store_binding::{bound_host, engine_delivers, listening_on};
866
867 let _data = lean_ctx::core::data_dir::isolated_data_dir();
868 let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
869 let directory = tempfile::tempdir().expect("project");
870 let root = directory.path().canonicalize().expect("canonical root");
871 governed_project(&root);
872 let prefixed = "mcp__lean-ctx__ctx_expand";
873 let other = "mcp__other__ctx_expand";
874 let use_of = |id: &str, name: &str| json!({"type": "tool_use", "id": id, "name": name});
875 let doc = json!({"tools": [{"name": prefixed}, {"name": other}], "messages": [
876 {"role": "assistant", "content": [
877 use_of("toolu_X", "mcp__lean-ctx__ctx_read"),
878 use_of("toolu_O", "mcp__other__ctx_shell"),
879 use_of("toolu_B", "Bash"),
880 use_of("toolu_W", "mcp__lean-ctx__ctx_call"),
881 use_of("toolu_T", "mcp__third__ctx_read"),
882 use_of("toolu_D", "mcp__lean-ctx__ctx_read"),
883 use_of("toolu_D", "mcp__other__ctx_read"),
884 ]},
885 ]});
886 let alone = || {
887 in_pass(|| {
888 begin(Shape::Messages, &doc);
889 expand_tool()
890 })
891 };
892 let named = |known: &HandleUse, id: Option<&str>| {
894 within(known.clone(), || {
895 let _block = block(id);
896 expand_tool()
897 })
898 .0
899 };
900
901 let unbound = HandleUse::for_request(Some(&doc), true);
903 assert_eq!(unbound.expand, None);
904 assert_eq!(named(&unbound, Some("toolu_X")), None);
905 assert_eq!(alone(), None);
906 {
907 let _loopback = listening_on(IpAddr::V4(Ipv4Addr::LOCALHOST));
909 assert_eq!(HandleUse::for_request(Some(&doc), true).expand, None);
910 assert_eq!(alone(), None);
911 }
912 {
913 let _bound = bound_host(&root);
917 let known = HandleUse::for_request(Some(&doc), true);
918 assert_eq!(known.expand.as_deref(), Some(prefixed));
919 assert!(known.follows(&doc));
920 assert!(
921 !known.follows(&json!({"tools": [{"name": prefixed}]})),
922 "a stub may name either listed tool: both have to leave listed"
923 );
924 assert_eq!(alone(), None, "no block, no name");
925 assert_eq!(named(&known, None), None);
926 assert_eq!(named(&known, Some("toolu_X")).as_deref(), Some(prefixed));
927 assert_eq!(named(&known, Some("toolu_O")).as_deref(), Some(other));
928 assert_eq!(named(&known, Some("toolu_B")), None, "a native tool");
929 assert_eq!(named(&known, Some("toolu_W")), None, "the wrapper");
930 assert_eq!(named(&known, Some("toolu_T")), None, "prefix not listed");
931 assert_eq!(named(&known, Some("toolu_D")), None, "one id, two tools");
932 assert_eq!(named(&known, Some("toolu_unknown")), None);
933 let ((), _) = within(known.clone(), || {
935 let outer = block(Some("toolu_X"));
936 {
937 let _inner = block(Some("toolu_O"));
938 assert_eq!(expand_tool().as_deref(), Some(other));
939 }
940 assert_eq!(expand_tool().as_deref(), Some(prefixed));
941 drop(outer);
942 assert_eq!(expand_tool(), None);
943 });
944
945 let text = "pub fn kept() {}\n";
948 let decided = |id: &str, text: &str| {
949 within(known.clone(), || {
950 let _block = block(Some(id));
951 expand_tool_for(text)
952 })
953 .0
954 };
955 assert_eq!(decided("toolu_X", text), None, "nothing on record");
956 engine_delivers(&root, "other.rs", text, text, None);
957 assert_eq!(decided("toolu_X", text), None, "a digest hit alone");
958 engine_delivers(&root, "kept.rs", text, text, Some("toolu_X"));
959 assert_eq!(decided("toolu_X", text).as_deref(), Some(prefixed));
960 assert_eq!(decided("toolu_X", "other bytes"), None);
961 assert_eq!(decided("toolu_O", text), None, "another call");
962 assert_eq!(decided("toolu_B", text), None, "a native tool");
963 let (narrowed, _) = within(known.clone(), || {
965 begin(Shape::Messages, &json!({"tools": [{"name": prefixed}]}));
966 let _block = block(Some("toolu_X"));
967 expand_tool_for(text)
968 });
969 assert_eq!(narrowed, None);
970 let unprotected = HandleUse {
973 correlation: None,
974 ..known.clone()
975 };
976 assert_eq!(named(&unprotected, Some("toolu_X")), None);
977 }
978 {
980 let _reachable = listening_on(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
981 let known = HandleUse::for_request(Some(&doc), true);
982 assert_eq!(known.expand, None);
983 assert_eq!(named(&known, Some("toolu_X")), None);
984 assert_eq!(alone(), None);
985 }
986 assert_eq!(HandleUse::for_request(Some(&doc), true).expand, None);
988 }
989
990 #[cfg(feature = "http-server")]
991 #[test]
995 fn the_producing_call_and_its_servers_expand_tool_are_read_per_shape() {
996 let messages = json!({"messages": [
997 {"role": "user", "content": "go"},
998 {"role": "assistant", "content": [
999 {"type": "text", "text": "reading"},
1000 {"type": "tool_use", "id": "toolu_1", "name": "mcp__a__ctx_read"},
1001 ]},
1002 ]});
1003 let chat = json!({"messages": [
1004 {"role": "assistant", "tool_calls": [
1005 {"id": "call_1", "type": "function", "function": {"name": "a.ctx_read"}},
1006 {"id": "call_2", "type": "function", "function": {}},
1007 ]},
1008 ]});
1009 let responses = json!({"input": [
1010 {"type": "function_call", "call_id": "c1", "name": "ctx_read"},
1011 {"type": "function_call_output", "call_id": "c1", "output": "text"},
1012 {"type": "message", "call_id": "c2", "name": "ctx_read"},
1013 ]});
1014 let gemini = json!({"contents": [
1015 {"role": "model", "parts": [{"functionCall": {"id": "g1", "name": "ctx_read"}}]},
1016 ]});
1017 assert_eq!(
1018 producing_calls(&messages),
1019 BTreeMap::from([("toolu_1", Some("mcp__a__ctx_read"))])
1020 );
1021 assert_eq!(
1022 producing_calls(&chat),
1023 BTreeMap::from([("call_1", Some("a.ctx_read"))])
1024 );
1025 assert_eq!(
1026 producing_calls(&responses),
1027 BTreeMap::from([("c1", Some("ctx_read"))])
1028 );
1029 assert!(producing_calls(&gemini).is_empty(), "no call id is read");
1030 assert!(producing_calls(&json!({"messages": "none"})).is_empty());
1031
1032 let listed = [
1033 "mcp__a__ctx_expand".to_owned(),
1034 "a.ctx_expand".to_owned(),
1035 "ctx_expand".to_owned(),
1036 ];
1037 for (producer, expand) in [
1038 ("mcp__a__ctx_read", Some("mcp__a__ctx_expand")),
1039 ("mcp__a__ctx_expand", Some("mcp__a__ctx_expand")),
1040 ("a.ctx_shell", Some("a.ctx_expand")),
1041 ("ctx_read", Some("ctx_expand")),
1042 ("mcp__b__ctx_read", None),
1043 ("b.ctx_read", None),
1044 ("mcp__a__ctx_call", None),
1045 ("ctx_call", None),
1046 ("ctx", None),
1047 ("Bash", None),
1048 ("mcp__a__read", None),
1049 ("mcp__a__", None),
1050 ("", None),
1051 ] {
1052 assert_eq!(
1053 expand_of_producer(producer, &listed).as_deref(),
1054 expand,
1055 "{producer}"
1056 );
1057 }
1058 assert_eq!(expand_of_producer("ctx_read", &listed[..2]), None);
1059
1060 let doc = json!({"tools": [
1062 {"name": "bash"},
1063 {"name": "mcp__a__ctx_expand"},
1064 {"name": "mcp__b__ctx_expand", "defer_loading": true},
1065 {"name": "mcp__c__ctx_expand"},
1066 {"name": "mcp__a__ctx_expand"},
1067 ]});
1068 assert_eq!(
1069 Shape::Messages.listed_expand(&doc),
1070 ["mcp__a__ctx_expand", "mcp__c__ctx_expand"]
1071 );
1072 assert_eq!(
1073 Shape::Messages.registered_expand(&doc).as_deref(),
1074 Some("mcp__a__ctx_expand")
1075 );
1076 }
1077
1078 #[cfg(feature = "http-server")]
1079 #[test]
1085 fn the_expand_tool_is_only_what_the_hosts_own_list_names() {
1086 let _data = lean_ctx::core::data_dir::isolated_data_dir();
1087 let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
1088 developer_mode();
1089 let prefixed = "mcp__lean-ctx__ctx_expand";
1090 let listed = [
1091 (Shape::Messages, json!({"tools": [{"name": prefixed}]})),
1092 (
1093 Shape::Chat,
1094 json!({"tools": [{"type": "function", "function": {"name": prefixed}}]}),
1095 ),
1096 (
1097 Shape::Responses,
1098 json!({"tools": [{"type": "function", "name": prefixed}]}),
1099 ),
1100 (
1101 Shape::Gemini,
1102 json!({"contents": [], "tools": [{"functionDeclarations": [{"name": prefixed}]}]}),
1103 ),
1104 (
1105 Shape::Gemini,
1106 json!({"contents": [], "tools": [{"function_declarations": [{"name": prefixed}]}]}),
1107 ),
1108 ];
1109 for (shape, doc) in &listed {
1110 assert_eq!(shape.registered_expand(doc).as_deref(), Some(prefixed));
1111 let known = HandleUse::for_request(Some(doc), true);
1112 assert_eq!(known.expand.as_deref(), Some(prefixed), "{doc}");
1113 assert!(known.follows(doc));
1114 assert!(!known.follows(&json!({"tools": []})), "no longer listed");
1115 assert_eq!(
1116 HandleUse::for_request(Some(doc), false).expand,
1117 None,
1118 "a route whose body is not read"
1119 );
1120 let ((), confirmed) = within(known, || begin(*shape, doc));
1123 assert_eq!(confirmed.expand.as_deref(), Some(prefixed));
1124 assert_eq!(
1125 in_pass(|| {
1126 begin(*shape, doc);
1127 expand_tool()
1128 })
1129 .as_deref(),
1130 Some(prefixed)
1131 );
1132 }
1133 assert_eq!(Shape::Chat.registered_expand(&listed[0].1), None);
1135 assert_eq!(Shape::Messages.registered_expand(&listed[1].1), None);
1136
1137 for name in [
1138 "ctx_expand",
1139 "lean-ctx:ctx_expand",
1140 "lean-ctx/ctx_expand",
1141 "a.ctx_expand",
1142 ] {
1143 let doc = json!({"tools": [{"name": "other"}, {"name": name}]});
1144 assert_eq!(
1145 Shape::Messages.registered_expand(&doc).as_deref(),
1146 Some(name)
1147 );
1148 }
1149
1150 let long = format!("{}__ctx_expand", "a".repeat(128));
1151 for doc in [
1152 json!({"messages": []}),
1153 json!({"tools": []}),
1154 json!({"tools": [{"name": "ctx_call"}, {"name": "ctx"}, {"name": "ctx_expander"}]}),
1155 json!({"tools": [{"name": "mcp__lean ctx__ctx_expand"}]}),
1156 json!({"tools": [{"name": "x]\n__ctx_expand"}]}),
1157 json!({"tools": [{"name": long}]}),
1158 json!({"tools": [{"name": prefixed, "defer_loading": true}]}),
1159 json!({"tools": [{"type": "mcp", "server_label": "lean-ctx"}]}),
1160 json!({"tools": [{"name": prefixed}], "tool_choice": {"type": "none"}}),
1161 json!({"tools": [{"name": prefixed}], "tool_choice": "none"}),
1162 json!({"tools": {"name": prefixed}}),
1163 ] {
1164 let known = HandleUse::for_request(Some(&doc), true);
1165 assert_eq!(known.expand, None, "{doc}");
1166 assert!(!known.follows(&doc));
1167 let (tool, _) = within(known, || {
1168 begin(Shape::Messages, &doc);
1169 expand_tool()
1170 });
1171 assert_eq!(tool, None, "{doc}");
1172 }
1173 assert_eq!(HandleUse::for_request(None, true).expand, None);
1174
1175 let ((), narrowed) = within(HandleUse::for_request(Some(&listed[0].1), true), || {
1177 begin(Shape::Messages, &json!({"tools": [{"name": "ctx_expand"}]}));
1178 });
1179 assert_eq!(narrowed.expand, None);
1180
1181 assert_eq!(expand_tool().as_deref(), Some("ctx_expand"));
1183 }
1184}