Skip to main content

lean_ctx_proxy/
ccr.rs

1// SPDX-License-Identifier: Apache-2.0
2//! In-band CCR on the proxy wire (#493): `<lc_expand:HASH>` markers a model
3//! echoed are spliced back into the request body before it leaves.
4//!
5//! The content-addressed tee store itself (persist, resolve, bounded read)
6//! lives in [`lean_ctx::core::ccr`]; it is re-exported here for the proxy's
7//! callers.
8
9pub use lean_ctx::core::ccr::*;
10
11#[cfg(feature = "http-server")]
12use serde_json::Value;
13
14/// Recover the verbatim original for a 16-hex CCR `hash` from the local tee
15/// store, or `None` when the hash is malformed or the file is gone (past TTL).
16#[cfg(feature = "http-server")]
17fn recover(hash: &str) -> Option<String> {
18    if hash.len() != 16 || !hash.bytes().all(|b| b.is_ascii_hexdigit()) {
19        return None;
20    }
21    read_tee(hash)
22}
23
24/// Replace every `<lc_expand:HASH>` marker in `s` with the verbatim original
25/// recovered from the local tee store. Returns `Some(spliced)` only when at
26/// least one marker resolved, else `None` (so the caller leaves the string —
27/// and therefore the request bytes — untouched).
28///
29/// An unresolvable marker (bad hash, or a file dropped past the 24h TTL) is left
30/// in place verbatim: the model still sees its own marker rather than a silent
31/// deletion, and a later turn can retry once the operator restores the file.
32/// The spliced content is inserted **raw** (not `<lc_safe>`-wrapped): this runs
33/// on the recent assistant turn the model echoed the marker into, which no proxy
34/// compressor rewrites, and the proxy has no global `<lc_safe>` strip — wrapping
35/// would instead leak the markers to the provider.
36#[cfg(feature = "http-server")]
37fn splice_str(s: &str) -> Option<String> {
38    if !s.contains(EXPAND_OPEN) {
39        return None;
40    }
41    let mut out = String::with_capacity(s.len());
42    let mut rest = s;
43    let mut changed = false;
44    while let Some(pos) = rest.find(EXPAND_OPEN) {
45        let after = &rest[pos + EXPAND_OPEN.len()..];
46        match after.find(EXPAND_CLOSE) {
47            Some(end) => {
48                let hash = &after[..end];
49                if let Some(original) = recover(hash) {
50                    out.push_str(&rest[..pos]);
51                    out.push_str(&original);
52                    rest = &after[end + EXPAND_CLOSE.len_utf8()..];
53                    changed = true;
54                } else {
55                    // Keep the literal marker; resume scanning past this `<` so a
56                    // later valid marker in the same string is still spliced.
57                    out.push_str(&rest[..pos + EXPAND_OPEN.len()]);
58                    rest = after;
59                }
60            }
61            // No closing `>`: nothing more can match — keep the remainder verbatim.
62            None => break,
63        }
64    }
65    out.push_str(rest);
66    changed.then_some(out)
67}
68
69/// Splice in-band `<lc_expand:HASH>` markers throughout a parsed request body
70/// (#493), replacing each with the verbatim original recovered from the local
71/// tee store. Recurses over every JSON string (object values and array items).
72///
73/// Returns `true` iff at least one marker was spliced. A request with no marker
74/// is left **byte-identical** (the function never allocates a replacement), so a
75/// marker-less turn never perturbs the provider prompt-cache prefix — the splice
76/// only ever changes the bytes the model explicitly asked to expand.
77#[cfg(feature = "http-server")]
78pub(crate) fn splice_inband_in_place(value: &mut Value) -> bool {
79    match value {
80        Value::String(s) => {
81            if let Some(spliced) = splice_str(s) {
82                *s = spliced;
83                true
84            } else {
85                false
86            }
87        }
88        Value::Array(items) => {
89            let mut changed = false;
90            for item in items {
91                changed |= splice_inband_in_place(item);
92            }
93            changed
94        }
95        Value::Object(map) => {
96            let mut changed = false;
97            for (_, v) in map.iter_mut() {
98                changed |= splice_inband_in_place(v);
99            }
100            changed
101        }
102        _ => false,
103    }
104}
105
106#[cfg(all(test, feature = "http-server"))]
107mod tests {
108    use super::*;
109    use lean_ctx::core::ccr::test_support::big;
110    use lean_ctx::core::policy::content::protected_for_test as protected;
111
112    #[test]
113    fn splice_replaces_marker_with_verbatim_original() {
114        let _lock = lean_ctx::core::data_dir::test_env_lock();
115        let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
116        let content = big("the historical verbatim line");
117        let handle = persist(&content).expect("persisted");
118        let marker = inband_marker(&handle).expect("marker");
119
120        let mut doc = serde_json::json!({
121            "messages": [{ "role": "assistant", "content": format!("recall {marker} please") }]
122        });
123        assert!(splice_inband_in_place(&mut doc), "a marker must splice");
124        let spliced = doc["messages"][0]["content"].as_str().unwrap();
125        assert!(
126            spliced.contains("the historical verbatim line"),
127            "verbatim original must be spliced in: {spliced}"
128        );
129        assert!(
130            !spliced.contains("<lc_expand:"),
131            "the marker must be consumed, not left behind"
132        );
133    }
134
135    #[test]
136    fn splice_is_byte_identical_no_op_without_marker() {
137        let _lock = lean_ctx::core::data_dir::test_env_lock();
138        let mut doc = serde_json::json!({
139            "messages": [{ "role": "user", "content": "no marker here" }],
140            "system": "plain"
141        });
142        let before = doc.clone();
143        assert!(
144            !splice_inband_in_place(&mut doc),
145            "no marker → must report no change"
146        );
147        assert_eq!(
148            doc, before,
149            "marker-less body must stay byte-identical (cache-safe)"
150        );
151    }
152
153    #[test]
154    fn splice_keeps_unresolvable_marker_verbatim() {
155        let _lock = lean_ctx::core::data_dir::test_env_lock();
156        // Right shape, but no such file in the store → leave the model's marker in
157        // place rather than silently deleting it.
158        let mut doc = serde_json::json!({ "t": "before <lc_expand:deadbeefdeadbeef> after" });
159        assert!(!splice_inband_in_place(&mut doc));
160        assert_eq!(
161            doc["t"].as_str().unwrap(),
162            "before <lc_expand:deadbeefdeadbeef> after"
163        );
164    }
165
166    #[test]
167    fn splice_recurses_and_handles_multiple_markers() {
168        let _lock = lean_ctx::core::data_dir::test_env_lock();
169        let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
170        let a = big("first recovered body");
171        let b = big("second recovered body");
172        let ma = inband_marker(&persist(&a).unwrap()).unwrap();
173        let mb = inband_marker(&persist(&b).unwrap()).unwrap();
174
175        // Two markers in one nested string, plus a deeper array item.
176        let mut doc = serde_json::json!({
177            "contents": [
178                { "parts": [{ "text": format!("{ma} and {mb}") }] }
179            ]
180        });
181        assert!(splice_inband_in_place(&mut doc));
182        let text = doc["contents"][0]["parts"][0]["text"].as_str().unwrap();
183        assert!(text.contains("first recovered body"));
184        assert!(text.contains("second recovered body"));
185        assert!(!text.contains("<lc_expand:"));
186    }
187
188    /// Legacy originless history stays closed under a policy on every
189    /// recovery path, in-band splicing included.
190    #[test]
191    fn tee_selectors_and_inband_do_not_reopen_originless_history() {
192        let _data = lean_ctx::core::data_dir::isolated_data_dir();
193        let body = "visible marker\nordinary data\n".repeat(32) + "CONFIDENTIAL\n";
194        let handle = {
195            let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
196            let handle = lean_ctx::core::ccr::persist(&body).unwrap();
197            assert!(
198                lean_ctx_server::tools::ctx_expand::handle(
199                    &serde_json::json!({"id": handle, "head": 1})
200                )
201                .contains("visible marker")
202            );
203            handle
204        };
205        let marker = lean_ctx::core::ccr::inband_marker(&handle).unwrap();
206        let mut tracker =
207            lean_ctx::core::relevance_tracker::RelevanceTracker::with_config(200, 0.0);
208        tracker.register(handle.clone(), &body, "ctx_shell", 300, 0);
209        {
210            let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
211            assert!(
212                tracker
213                    .expand_if_relevant("visible")
214                    .unwrap()
215                    .contains("visible marker")
216            );
217        }
218        for rules in [
219            "[filters]\nclassification='block'",
220            "[redaction]\ncustomer='K-[0-9]{6}'",
221        ] {
222            let _policy = protected(rules);
223            assert!(tracker.expand_if_relevant("visible").is_none());
224            for selector in [
225                serde_json::json!({}),
226                serde_json::json!({"head":1}),
227                serde_json::json!({"tail":2}),
228                serde_json::json!({"search":"visible"}),
229                serde_json::json!({"start_line":1,"end_line":2}),
230                serde_json::json!({"json_keys":true}),
231                serde_json::json!({"json_path":"safe"}),
232            ] {
233                let mut args = selector;
234                args["id"] = handle.clone().into();
235                let result = lean_ctx_server::tools::ctx_expand::handle(&args);
236                assert!(result.starts_with("ERROR:"), "{result}");
237                assert!(!result.contains("visible marker"));
238            }
239            let mut request =
240                serde_json::json!({"messages":[{"role":"assistant","content":marker}]});
241            let before = request.clone();
242            assert!(!splice_inband_in_place(&mut request));
243            assert_eq!(request, before);
244            assert!(
245                lean_ctx::core::ccr::retrieve_litellm(&lean_ctx::core::ccr::litellm_hash(&body))
246                    .is_none()
247            );
248        }
249        // This is legacy Community history, not a protected bound record. It is
250        // preserved, not deleted, when the operator enables a policy.
251        let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
252        assert_eq!(
253            lean_ctx::core::ccr::read_checked_tee(std::path::Path::new(&handle)).unwrap(),
254            body
255        );
256    }
257}