Skip to main content

lean_ctx_proxy/
sticky_tools.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Sticky CCR tool injection for provider prefix-cache stability.
3//!
4//! Once a conversation has seen at least one CCR handle, the `ctx_expand` tool
5//! definition stays in `tools[]` for the remainder of the session, even on turns
6//! with no active markers. This prevents the tool-list change from busting the
7//! provider prompt-cache prefix.
8//!
9//! That injection is the developer-mode path only. Under protection the proxy
10//! declares nothing: a declaration it adds is no evidence that the host can
11//! execute the call. There a recovery stub is issued only per block, for a
12//! tool result whose very call is on record in this proxy's store as answered
13//! by an Engine of this store, and it names the expand tool the host listed
14//! under the same prefix as the tool that produced the block
15//! (`expand_tool_for`). Nothing marks a conversation under protection: the
16//! mark is read by the injection only.
17
18use 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
35/// Mark a conversation as having used CCR.
36pub 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
48/// Returns `true` if this conversation has ever used CCR.
49pub fn is_ccr_active(conv_id: u64) -> bool {
50    active_sessions().lock().is_ok_and(|g| g.contains(&conv_id))
51}
52
53/// What the passes of ONE request know about the recovery stubs they hand
54/// to the model: under which name the host of the request listed the
55/// Engine's expand tool. Without one no stub is issued.
56#[derive(Debug, Clone, Default, PartialEq, Eq)]
57pub(crate) struct HandleUse {
58    /// The first expand tool exactly as the host registered it in this
59    /// request's own tool list. `None`: the list does not name one, the
60    /// request does not let its model call it, or — under protection — the
61    /// necessary store conditions do not hold
62    /// (`store_binding::host_store_bound`). No block may leave with a stub
63    /// then. Unprotected it is the name a pass holds; under protection the
64    /// name of a stub is decided per block from `correlation`.
65    expand: Option<String>,
66    /// Whether the tool list was read for this request. Until then `expand`
67    /// says nothing; the provider compressor reads it ([`begin`]).
68    list_read: bool,
69    /// Under protection, for a request that may issue a stub at all: what
70    /// the per-block decision reads ([`expand_tool_for`]). `None` when the
71    /// request was read unprotected, or when it issues no stub.
72    correlation: Option<Correlation>,
73}
74
75/// What a protected request holds for the per-block decision, read once
76/// from the request document as the host sent it.
77#[derive(Debug, Clone, Default, PartialEq, Eq)]
78struct Correlation {
79    /// Every expand tool the host listed, in list order, each once.
80    listed: Vec<String>,
81    /// Call id of a tool call in the request's own history → the listed
82    /// expand tool that has the same host prefix as the tool that call
83    /// named. Only calls of a `ctx_*` Engine tool with such a listed tool
84    /// are in it ([`expand_of_producer`]).
85    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/// The tool calls in the history of a request document, by call id, with
104/// the tool name each one named. Read at the place of every shape (the
105/// places do not collide):
106///
107/// - Messages: `messages[].content[]` blocks `{type: "tool_use", id, name}`;
108/// - Chat Completions: `messages[].tool_calls[]` `{id, function: {name}}`;
109/// - Responses (HTTP and WebSocket): `input[]` items
110///   `{type: "function_call", call_id, name}`.
111///
112/// Gemini is not read: `google.rs` reads no id of a `functionCall` /
113/// `functionResponse` pair, so a Gemini block has no call id here. An id
114/// that two calls with different tool names carry names no tool (`None`).
115#[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/// The Engine's wrapper tool: the tool that really ran is in its arguments.
167#[cfg(feature = "http-server")]
168const WRAPPER_TOOL: &str = "ctx_call";
169
170/// The listed expand tool that belongs to the same MCP server, in the host's
171/// own naming, as the tool `producer` — the name a tool call of the request
172/// named: `producer` is split into the host's prefix and the bare tool
173/// segment (`compress::bare_tool_name`); the bare segment has to be a
174/// `ctx_*` Engine tool, and the result is the listed tool that is exactly
175/// that prefix followed by `ctx_expand`. `None`: a native or foreign tool
176/// (`Bash`, `Read`), or no expand tool listed under that prefix.
177///
178/// The wrapper form is NOT correlated: a block produced through `ctx_call`
179/// (or the bare `ctx` surface) names the wrapper, the tool that ran is in
180/// its arguments, and nothing here reads them — such a block gets no stub.
181#[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    /// What a request knows before its first pass. `doc` is the caller's
197    /// document as the host sent it, before any change of the proxy;
198    /// `route_can_follow` is false for a route whose body is not one of the
199    /// shapes read here (Bedrock: the provider's own format). An expand
200    /// tool is known only if that document lists it and lets the model call
201    /// it; a missing document, an unknown shape or a list that does not name
202    /// the tool all leave it unknown — for every pass of the request. So do,
203    /// under protection, the necessary store conditions when they fail
204    /// (`store_binding::host_store_bound`): read once here, and only for a
205    /// request that lists the tool. Under protection the request's tool
206    /// calls are read here as well, once ([`Correlation`]).
207    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    /// A request whose own list names `listed` (already narrowed to what
221    /// the request lets its model call) and whose document is `doc`.
222    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    /// Whether a stub of this request may leave in `doc`, the document that
242    /// is about to be sent: the request knows its expand tool, and `doc`
243    /// still lets the model call it and still lists it under that very name
244    /// — under protection: still lists exactly the expand tools the request
245    /// listed, since a stub may name any of them.
246    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/// The request shapes whose tool list is read, each in its own format.
261#[cfg(feature = "http-server")]
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub(crate) enum Shape {
264    /// Anthropic Messages: `tools[] = {name, description, input_schema}`.
265    Messages,
266    /// OpenAI Chat Completions: `tools[] = {type: "function", function}`.
267    Chat,
268    /// OpenAI Responses, HTTP and WebSocket: `tools[] = {type: "function",
269    /// name, description, parameters}`.
270    Responses,
271    /// Gemini: `tools[] = {functionDeclarations: [..]}`.
272    Gemini,
273}
274
275#[cfg(feature = "http-server")]
276impl Shape {
277    /// Every shape. Their places for a tool name do not collide, so a
278    /// document of unknown shape can be read with each of them.
279    const ALL: [Self; 4] = [Self::Messages, Self::Chat, Self::Responses, Self::Gemini];
280
281    /// The Engine's expand tool as the host listed it in `doc`: the exact
282    /// name of the first entry of the request's own `tools` that
283    /// [`expand_tool_name`] accepts, read at the place this shape names a
284    /// tool. `None` whenever the list does not show, with certainty, a tool
285    /// the model can call by that name in THIS request:
286    ///
287    /// - no `tools` list, or the name is not in it — including tools the
288    ///   provider resolves elsewhere (a stored prompt, a remote MCP server,
289    ///   a toolset referenced by id), which carry no name here;
290    /// - the entry is deferred (`defer_loading`): the model does not hold
291    ///   it until a tool search returns it;
292    /// - the name is outside what a stub can carry.
293    fn registered_expand(self, doc: &Value) -> Option<String> {
294        self.listed_expand(doc).into_iter().next()
295    }
296
297    /// Every entry of the request's own `tools` that [`registered_expand`]
298    /// would accept, in list order, each name once. A host that runs several
299    /// Engines lists one expand tool per MCP server, each under that
300    /// server's prefix; which of them a stub names is decided per block
301    /// ([`expand_of_producer`]).
302    ///
303    /// [`registered_expand`]: Self::registered_expand
304    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/// The `name` of a tool entry, if it is the Engine's expand tool.
336#[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/// The two spellings of a Gemini tool's function list.
346#[cfg(feature = "http-server")]
347const GEMINI_DECLARATIONS: [&str; 2] = ["functionDeclarations", "function_declarations"];
348
349/// Whether the settings of a request let its model call a tool the request
350/// lists. Read from the document alone, whatever its shape (the keys of the
351/// shapes do not collide):
352///
353/// - tool use switched off or narrowed to a list for this request
354///   (`tool_choice` `none` / `allowed_tools`; Gemini's function-calling mode
355///   `NONE` or `allowedFunctionNames`);
356/// - the legacy function-calling fields of Chat Completions, which do not
357///   mix with `tools`;
358/// - Gemini cached content, which carries its own tools and refuses others;
359/// - a `tools` value that is no list, or a Gemini list of built-in tools
360///   only, which does not mix with function declarations.
361#[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    /// The request pass running on this thread. Every pass is synchronous
419    /// from the route down to the place that writes a stub.
420    static PASS: RefCell<Option<HandleUse>> = const { RefCell::new(None) };
421}
422
423/// Run one synchronous pass of a request that already knows `so_far`, and
424/// return what the request knows after it. A request runs several passes
425/// with awaits between them (possibly on different threads); the caller
426/// carries the value from one pass into the next.
427pub(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
444/// [`within`] for a provider compressor: joins the pass of the request that
445/// called it, or is its own pass when it runs alone.
446pub(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/// Change what the pass on this thread knows. Outside a pass there is
455/// nothing to change.
456#[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/// A provider compressor of `shape` starts on `doc`, before it touches any
466/// block. A compressor that runs alone reads the request here as
467/// [`HandleUse::for_request`] does, at this shape's place: its tool list,
468/// and under protection the necessary store conditions and the request's
469/// tool calls — once, and only if the list names the tool. Inside a request
470/// that read them on the caller's document, what it knows only holds if the
471/// compressor's document still lists the same expand tool(s) at this
472/// shape's place and still lets the model call them; otherwise no block of
473/// the pass leaves with a stub.
474#[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    /// The call id of the tool-result block the compressor on this thread
499    /// is working on. `None` outside such a block.
500    static BLOCK: RefCell<Option<String>> = const { RefCell::new(None) };
501}
502
503/// The tool-result block a compressor is working on, until dropped.
504#[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
516/// A compressor starts on ONE tool-result block whose call id, as the
517/// request carries it, is `call_id` (Messages `tool_result.tool_use_id`,
518/// Chat `tool_call_id`, Responses `function_call_output.call_id`). Every
519/// shortening decided on this thread until the returned value is dropped is
520/// a shortening of text of that block. A site that shortens text which is
521/// not one tool-result block (a summary, joined text, system or user prose,
522/// a cut message) opens no block: it has no call id, and under protection
523/// its text stays whole.
524pub(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
531/// The name a recovery stub of the pass on this thread would give the
532/// Engine's expand tool, before this proxy's store is asked about the block
533/// ([`expand_tool_for`] is the decision):
534///
535/// - No pass on this thread (a direct call of the compressor or the gate):
536///   nothing is known against the bare name `ctx_expand`. No product route
537///   is in this case: the proxy's model-facing routes run in a pass
538///   (`/v1/compress` runs none, and its stubs name a route, not a tool —
539///   this name is never asked for there), and the host-side shaping route of the `ctx_shape` tool
540///   (`compress::shape_command_output`, `compress::shape_tool_output`) —
541///   no request, no tool list — runs as a pass that knows no tool
542///   ([`in_pass`]), so under protection it issues no stub and its text
543///   stays whole (`recovery.handle_not_followable`). What remains outside
544///   a pass are direct calls in tests.
545/// - A pass that knows no tool: `None`.
546/// - Unprotected: the first expand tool as the host listed it.
547/// - Under protection: the listed expand tool with the host prefix of the
548///   tool that produced the current block ([`block`], [`Correlation`]);
549///   `None` without a current block, without a producing call of that id in
550///   the request, or for a pass that was read unprotected (the mode turned
551///   protected inside the request).
552fn 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
570/// The followability decision for `original`, the exact text a site is
571/// about to replace by a shorter form with a recovery stub: the name the
572/// stub has to give the Engine's expand tool, or `None` — the text then
573/// stays whole (`recovery.handle_not_followable`) and nothing is stored.
574///
575/// Unprotected, and outside any pass, this is [`expand_tool`] and nothing
576/// else. Under protection, in a pass, a stub is issued only if ALL of this
577/// is shown. A stub's id resolves only in an Engine that uses this proxy's
578/// data directory and project (`tools/ctx_expand.rs`), and neither a listed
579/// tool NAME nor a digest hit shows that: two Engines that read the same
580/// file deliver the same bytes. So the proof ties THIS tool call of THIS
581/// host to an Engine that writes THIS store:
582///
583/// 1. Once per request: the host's own list names an expand tool and lets
584///    the model call it; a live MCP Engine of this installation is
585///    registered for the proxy's project; the listener is loopback
586///    (`store_binding::host_store_bound` — necessary, not a proof).
587/// 2. `original` is text of ONE tool-result block that carries a call id
588///    ([`block`]).
589/// 3. This store's delivered register lists that very call id on the entry
590///    of exactly `original`'s bytes for the proxy's project
591///    (`store_binding::delivered_for_call`). Only an Engine writing this
592///    store records that, at the moment it answers a call for which the
593///    host named this id.
594/// 4. The request's own history holds the tool call with that id, and it
595///    named a `ctx_*` Engine tool ([`producing_calls`]).
596/// 5. The host listed an expand tool under the same prefix as that tool
597///    ([`expand_of_producer`]); the stub names exactly that one.
598///
599/// Who gets NO compression under protection, plainly:
600///
601/// - every host that does not send its call id to the Engine with the MCP
602///   call (`server/call_identity.rs`; one `_meta` key, taken from host
603///   behaviour and not verified by source) — today that is every host but
604///   the one that key belongs to, and that one only once it is confirmed;
605/// - every block the Engine delivered outside an MCP call: hook-delivered
606///   `lean-ctx read` output and other CLI deliveries carry no call id;
607/// - every block delivered before call ids were recorded, served from the
608///   Engine's response cache or a replay (no delivery record is made for
609///   those), or recorded without the tool-delivery entry (text with a
610///   caller-presented lineage);
611/// - Gemini (no call id is read in that shape) and Bedrock;
612/// - a surface that only lists the wrapper (`ctx_call`), and every block
613///   produced through it;
614/// - a Responses turn that does not carry the producing `function_call`
615///   item itself (a turn continued by `previous_response_id`, most
616///   WebSocket turns);
617/// - a block whose text is a JSON payload shortened string by string, a cut
618///   chat message, and every text site without a block.
619///
620/// The live compressor leaves the results of `ctx_*` tools untouched in
621/// every mode (`compress::compress_inner`, #479), and the result of any
622/// other tool has no correlation. Under protection the live compressor
623/// therefore issues no stub at all; what remains is the pruning of old
624/// `ctx_*` results (`history_prune`, `prune_responses_input`).
625///
626/// What the correlation does NOT prove:
627///
628/// - The id is chosen by the host or its provider, not by this
629///   installation. A replayed id helps a caller only if this store's Engine
630///   wrote exactly this digest for it; a caller that has both was served by
631///   that Engine, or copies one that was.
632/// - A deliberate collision — another local process of the same OS user
633///   that sends a known id with the same bytes and lists a different
634///   Engine's expand tool under the same prefix — is outside what this
635///   closes. It gets a handle its own Engine refuses; nothing is disclosed.
636/// - That the host routes the prefixed `ctx_expand` to the same MCP server
637///   as the producing `ctx_*` call rests on the host's own naming (same
638///   prefix = same server). Nothing in a request shows the routing.
639/// - The mode is read when the request starts. A request read unprotected
640///   issues no stub once the mode is protected ([`expand_tool`]); the
641///   opposite change inside one request is not covered.
642pub(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
673/// Ensure `ctx_expand` is present in the `tools` array when CCR is active.
674/// Returns `true` if the tool was injected.
675pub 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    // High conv IDs reserved for these unit tests. Parallel tests in compress.rs
711    // and history_prune.rs call mark_ccr_active(0); clearing the global set
712    // races with them, so each test marks its own ID instead.
713    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    /// Developer mode (the default of an isolated data directory): the tool
784    /// list is read as it is, no store binding is asked.
785    fn developer_mode() {
786        assert!(!crate::compress::protected(), "developer mode");
787    }
788
789    #[cfg(feature = "http-server")]
790    /// Governed mode with `root` as the proxy's configured project root.
791    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    /// What a request knows is carried from one pass into the next, a
807    /// compressor joins the pass of the request that called it and is its
808    /// own pass when it runs alone, and nothing of a pass leaks into the
809    /// next request on the thread. No pass marks a conversation: the mark
810    /// belongs to the developer-mode tee sites alone.
811    #[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        // First pass, an await, a later pass: the name is still held.
821        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        // A compressor joins the pass of the request that called it …
828        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        // … and is its own pass when it runs alone: it knows nothing of the
837        // request before it, and leaves nothing behind.
838        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        // A pass inside a pass gives the outer one back what it knew.
846        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    /// Under protection a listed expand tool is not held on the list alone,
856    /// and the necessary store conditions (a live MCP Engine registered for
857    /// the proxy's project, behind a loopback listener) only let the request
858    /// know its tools: a NAME is given per block — to the block of a call
859    /// that named a `ctx_*` tool of the same prefix — and the decision
860    /// additionally needs this store's record of that very call.
861    #[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        // The name a pass of the request gives the block of the call `id`.
893        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        // Listed, but nothing shows which store the tool's Engine reads.
902        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            // A loopback listener alone: no Engine on record.
908            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            // A bound standard host: the request knows its expand tools,
914            // and names one only for the block of a `ctx_*` call of the
915            // same prefix.
916            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            // The block scope ends where its guard is dropped, and nests.
934            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            // The decision asks this store about the block's exact bytes
946            // and its call: a name alone decides nothing.
947            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            // The compressor's own document no longer lists both tools.
964            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            // A pass that was read unprotected names nothing once the mode
971            // is protected.
972            let unprotected = HandleUse {
973                correlation: None,
974                ..known.clone()
975            };
976            assert_eq!(named(&unprotected, Some("toolu_X")), None);
977        }
978        // The Engine is still on record; the listener is not loopback.
979        {
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        // Unknown listener again: not bound.
987        assert_eq!(HandleUse::for_request(Some(&doc), true).expand, None);
988    }
989
990    #[cfg(feature = "http-server")]
991    /// The producing call of a result block is read at each shape's place,
992    /// and the expand tool of its server is the listed one with exactly the
993    /// same prefix.
994    #[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        // Every listed expand tool is read, each once, deferred ones not.
1061        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    /// The expand tool of a request is what the host's own list names, read
1080    /// at each shape's place, under exactly the registered name — and
1081    /// nothing else: a deferred entry, a name a stub cannot carry, a list
1082    /// without the tool and a request that forbids tool calls name none.
1083    /// Read in developer mode, where the list alone decides.
1084    #[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            // Inside the pass the compressor confirms the name; alone it
1121            // reads it.
1122            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        // A name is read at its own shape's place only.
1134        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        // The compressor's document no longer lists what the caller's did.
1176        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        // Outside any pass nothing is known against the bare name.
1182        assert_eq!(expand_tool().as_deref(), Some("ctx_expand"));
1183    }
1184}