Skip to main content

lean_ctx/proxy/
ccr.rs

1//! Content-addressed recovery (CCR) for the proxy's lossy rewrites (#482).
2//!
3//! When the proxy prunes an old `tool_result` from conversation history, the
4//! lossy stub used to say *"re-read the file"* — which is stale-unsafe by
5//! construction: in an agent session files are edited or deleted between turns,
6//! so a re-read returns the *current* bytes (or fails), not the historical
7//! version the conversation actually showed. The model could then silently
8//! reason about the wrong content.
9//!
10//! CCR fixes this by persisting the **verbatim original** to the shared,
11//! content-addressed tee store (`{state}/tee/`, reused from the shell path) and
12//! embedding a **retrieval handle** — the absolute path of that file — in the
13//! stub. Retrieval is MCP-independent: the agent reads the path with its native
14//! file read; no lean-ctx tool has to be attached.
15//!
16//! ## Cache-safety (#448)
17//! The handle is the file path, and the path is a pure function of the content
18//! hash ([`crate::core::hasher::hash_short`]). For a fixed pruned message the
19//! handle is therefore byte-identical on every later turn, so the provider
20//! prompt-cache prefix is never invalidated. The on-disk *write* is best-effort
21//! and never affects the returned handle — only retrievability degrades if the
22//! write (or the 24h TTL cleanup) loses the file, so a stub can never become
23//! non-deterministic based on filesystem state.
24
25use std::path::{Path, PathBuf};
26use std::sync::atomic::{AtomicU64, Ordering};
27use std::time::{SystemTime, UNIX_EPOCH};
28
29use serde_json::Value;
30
31/// Opening delimiter of an in-band retrieval marker: `<lc_expand:HASH>` (#493).
32const EXPAND_OPEN: &str = "<lc_expand:";
33/// Closing delimiter of an in-band retrieval marker.
34const EXPAND_CLOSE: char = '>';
35
36/// Originals smaller than this are not worth a tee file + handle; the caller
37/// keeps its plain stub. Matches the spirit of the prune length thresholds.
38pub(crate) const MIN_TEE_BYTES: usize = 512;
39
40/// Throttle the O(dir) TTL cleanup so the prune hot path does at most one
41/// directory scan per this interval (the write itself is content-addressed and
42/// idempotent, so steady-state cost is a single `stat`).
43const CLEANUP_INTERVAL_SECS: u64 = 600;
44
45/// Length of the content hash in a proxy/json tee name ([`hash_short`]).
46const TEE_HASH_LEN: usize = 16;
47/// Length of the command hash in a shell tee name (`shell::redact::save_tee`).
48const SHELL_TEE_HASH_LEN: usize = 8;
49
50/// Deterministic tee path for `content`:
51/// `{state}/tee/{prefix}_{blake3(content)[..16]}.log`. Pure (no I/O) so a stub
52/// embedding it stays byte-stable regardless of filesystem state. `prefix`
53/// segregates the producer (`proxy` for history-prune / live-compression stubs,
54/// `json` for the JSON crusher's lossy originals, #936) yet keeps one shared
55/// store + one resolver ([`resolve_tee`]).
56fn tee_path(content: &str, prefix: &str) -> Option<PathBuf> {
57    let dir = crate::core::paths::state_dir().ok()?.join("tee");
58    let hash = crate::core::hasher::hash_short(content);
59    Some(dir.join(format!("{prefix}_{hash}.log")))
60}
61
62/// Run the shared 24h TTL cleanup at most once per [`CLEANUP_INTERVAL_SECS`].
63fn maybe_cleanup(tee_dir: &Path) {
64    static LAST: AtomicU64 = AtomicU64::new(0);
65    let Ok(now) = SystemTime::now().duration_since(UNIX_EPOCH) else {
66        return;
67    };
68    let now = now.as_secs();
69    let last = LAST.load(Ordering::Relaxed);
70    if now.saturating_sub(last) < CLEANUP_INTERVAL_SECS {
71        return;
72    }
73    // Only one thread wins the slot; the rest skip until the next interval.
74    if LAST
75        .compare_exchange(last, now, Ordering::Relaxed, Ordering::Relaxed)
76        .is_ok()
77    {
78        crate::shell::cleanup_old_tee_logs(tee_dir);
79    }
80}
81
82/// Persist `content` verbatim (best-effort, secret-redacted) to the
83/// content-addressed tee store and return its retrieval handle (the absolute
84/// path). Returns `None` only when `content` is below [`MIN_TEE_BYTES`] or the
85/// state dir can't be resolved — never because the *write* failed, so the
86/// returned handle is a pure function of the content and the embedding stub
87/// stays deterministic. Re-persisting identical content is idempotent: same
88/// content → same path → the existing file is left untouched.
89pub(crate) fn persist(content: &str) -> Option<String> {
90    persist_with(content, "proxy")
91}
92
93/// Persist a JSON crusher's verbatim original (#936) under the `json_` prefix and
94/// return its `{state}/tee/json_{hash}.log` handle. Used by the lossy crush stage
95/// so a dropped column is always recoverable out-of-band via [`resolve_tee`] /
96/// `ctx_expand`, never reconstructed from the (lossy) text. Shares the
97/// content-address and best-effort write contract of [`persist`], so the embedded
98/// handle stays deterministic (cache-safe).
99pub(crate) fn persist_json(content: &str) -> Option<String> {
100    persist_with(content, "json")
101}
102
103/// Persist a tabular (CSV/TSV) crusher's verbatim original (#982) under the
104/// `tbl_` prefix and return its `{state}/tee/tbl_{hash}.log` handle. Used by the
105/// lossy column-drop stage so a dropped column is always recoverable out-of-band
106/// via [`resolve_tee`] / `ctx_expand`, never reconstructed from the (lossy) text.
107/// Shares the content-address and best-effort write contract of [`persist`].
108pub(crate) fn persist_tabular(content: &str) -> Option<String> {
109    persist_with(content, "tbl")
110}
111
112/// Persist a YAML crusher's verbatim original (#985) under the `yaml_` prefix and
113/// return its `{state}/tee/yaml_{hash}.log` handle. Used by the lossy column-drop
114/// stage so a dropped column is always recoverable out-of-band via [`resolve_tee`]
115/// / `ctx_expand`, never reconstructed from the (lossy) text. Shares the
116/// content-address and best-effort write contract of [`persist`].
117pub(crate) fn persist_yaml(content: &str) -> Option<String> {
118    persist_with(content, "yaml")
119}
120
121fn persist_with(content: &str, prefix: &str) -> Option<String> {
122    if content.len() < MIN_TEE_BYTES {
123        return None;
124    }
125    let path = tee_path(content, prefix)?;
126    let handle = path.to_string_lossy().to_string();
127
128    if !path.exists() {
129        if let Some(dir) = path.parent()
130            && std::fs::create_dir_all(dir).is_ok()
131        {
132            maybe_cleanup(dir);
133        }
134        // Same redaction the shell tee applies, so a recovered original can never
135        // re-introduce a secret the live turn would also have masked.
136        let masked = crate::core::redaction::redact_text(content);
137        let (redacted, _) = crate::core::secret_detection::scan_and_redact_from_config(&masked);
138        if std::fs::write(&path, redacted).is_ok() {
139            #[cfg(unix)]
140            {
141                use std::os::unix::fs::PermissionsExt;
142                let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
143            }
144        }
145    }
146    Some(handle)
147}
148
149fn is_hex(s: &str, len: usize) -> bool {
150    s.len() == len && s.bytes().all(|b| b.is_ascii_hexdigit())
151}
152
153/// Canonical `{prefix}_{16hex}.log` name for a proxy / json / tbl / bare-hash id,
154/// or `None`. A bare 16-hex id defaults to the `proxy_` store (back-compat: that
155/// is the only form pre-#936 stubs carry).
156fn canonical_tee_name(name: &str) -> Option<String> {
157    let stem = name.strip_suffix(".log").unwrap_or(name);
158    if let Some(hash) = stem.strip_prefix("proxy_") {
159        return is_hex(hash, TEE_HASH_LEN).then(|| format!("proxy_{hash}.log"));
160    }
161    if let Some(hash) = stem.strip_prefix("json_") {
162        return is_hex(hash, TEE_HASH_LEN).then(|| format!("json_{hash}.log"));
163    }
164    if let Some(hash) = stem.strip_prefix("tbl_") {
165        return is_hex(hash, TEE_HASH_LEN).then(|| format!("tbl_{hash}.log"));
166    }
167    if let Some(hash) = stem.strip_prefix("yaml_") {
168        return is_hex(hash, TEE_HASH_LEN).then(|| format!("yaml_{hash}.log"));
169    }
170    is_hex(stem, TEE_HASH_LEN).then(|| format!("proxy_{stem}.log"))
171}
172
173/// True for a shell tee basename `<slug>_<8hex>.log` (`shell::redact::save_tee`):
174/// ends in `.log`, the whole basename is safe (`[A-Za-z0-9_-]`), and the **last**
175/// `_`-segment is exactly 8 hex. The slug itself may contain `_`, so the hash is
176/// matched as the suffix — never the first segment (the documented parsing trap).
177fn is_shell_tee_name(name: &str) -> bool {
178    let Some(stem) = name.strip_suffix(".log") else {
179        return false;
180    };
181    if !stem
182        .bytes()
183        .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
184    {
185        return false;
186    }
187    match stem.rsplit_once('_') {
188        Some((slug, hash)) => !slug.is_empty() && is_hex(hash, SHELL_TEE_HASH_LEN),
189        None => false,
190    }
191}
192
193/// Resolve a retrieval `id` back to a file in the shared `{state}/tee/` store.
194/// Accepts every handle form a stub or footer can carry, with a fixed precedence
195/// so the forms can never collide (#936):
196///
197/// 1. **Prefix forms** — `proxy_<16hex>(.log)`, `json_<16hex>(.log)`,
198///    `tbl_<16hex>(.log)`, `yaml_<16hex>(.log)`, or a bare `<16hex>` (→ `proxy_`,
199///    back-compat). The proxy history-prune / live stubs and the JSON / tabular /
200///    YAML crushers' lossy originals.
201/// 2. **Shell-tee form** — `<slug>_<8hex>.log` (`save_tee`), so every compressed
202///    shell command's already-teed verbatim output is surgically retrievable.
203///
204/// The 16-vs-8 hex length already disambiguates the two classes; the explicit
205/// order documents intent. Security: only the *file name* is trusted — the path
206/// is always rebuilt under `{state}/tee/`, so a crafted `id` can never escape the
207/// store (no path traversal) and a non-tee id resolves to `None`.
208pub(crate) fn resolve_tee(id: &str) -> Option<PathBuf> {
209    let name = Path::new(id)
210        .file_name()
211        .and_then(|n| n.to_str())
212        .unwrap_or(id);
213    let canon =
214        canonical_tee_name(name).or_else(|| is_shell_tee_name(name).then(|| name.to_string()))?;
215    let path = crate::core::paths::state_dir()
216        .ok()?
217        .join("tee")
218        .join(canon);
219    path.is_file().then_some(path)
220}
221
222/// The in-band retrieval marker `<lc_expand:HASH>` for a CCR `handle` (#493).
223///
224/// `HASH` is the content hash already embedded in the tee handle, so a model can
225/// echo the marker verbatim and the proxy can recover the original via
226/// [`resolve_tee`] on the next turn. Pure (no I/O, no config) so it is trivially
227/// testable; returns `None` for a handle that is not a canonical tee path.
228pub(crate) fn inband_marker(handle: &str) -> Option<String> {
229    let name = Path::new(handle).file_name().and_then(|n| n.to_str())?;
230    let hash = name.strip_prefix("proxy_")?.strip_suffix(".log")?;
231    (hash.len() == 16 && hash.bytes().all(|b| b.is_ascii_hexdigit()))
232        .then(|| format!("{EXPAND_OPEN}{hash}{EXPAND_CLOSE}"))
233}
234
235/// The in-band marker for `handle` **only when in-band CCR is enabled** (#493),
236/// else `None`. Stub sites use this to advertise an echo-able `<lc_expand:HASH>`
237/// solely in in-band mode: a normal (shared-filesystem) deployment keeps its
238/// path handle, so the model never sees a marker the proxy would not splice.
239///
240/// Reads the (process-cached) config; the surrounding stub path already does
241/// per-message tee I/O via [`persist`], so this adds no new I/O class.
242pub(crate) fn inband_locator(handle: &str) -> Option<String> {
243    crate::core::config::Config::load()
244        .proxy
245        .ccr_inband_enabled()
246        .then(|| inband_marker(handle))
247        .flatten()
248}
249
250/// Recover the verbatim original for a 16-hex CCR `hash` from the local tee
251/// store, or `None` when the hash is malformed or the file is gone (past TTL).
252fn recover(hash: &str) -> Option<String> {
253    if hash.len() != 16 || !hash.bytes().all(|b| b.is_ascii_hexdigit()) {
254        return None;
255    }
256    std::fs::read_to_string(resolve_tee(hash)?).ok()
257}
258
259/// Length of a LiteLLM gateway retrieval hash (#702): LiteLLM's headroom
260/// guardrail scans compressed text with `hash=([a-f0-9]{24})`
261/// (BerriAI/litellm#31681), so the marker must carry exactly 24 lowercase hex.
262pub(crate) const LITELLM_HASH_LEN: usize = 24;
263
264/// The 24-hex gateway retrieval hash for `content` (#702): the first 24 chars
265/// of the full blake3 hex. A strict extension of the 16-hex tee name
266/// ([`crate::core::hasher::hash_short`] is the same digest truncated to 16),
267/// so `hash[..16]` resolves the tee file while the extra 8 chars keep the
268/// marker collision-resistant on the gateway side. Pure — the marker embedding
269/// it stays byte-stable per content (#498).
270pub(crate) fn litellm_hash(content: &str) -> String {
271    blake3::hash(content.as_bytes()).to_hex()[..LITELLM_HASH_LEN].to_string()
272}
273
274/// Resolve a LiteLLM `hash=<24hex>` retrieval id (#702) back to the verbatim
275/// original in the tee store, for `GET /v1/retrieve/{hash}`. Shape-locked to
276/// LiteLLM's own regex (24 lowercase hex — uppercase or any other length is
277/// rejected, never coerced) so only a string the guardrail could actually have
278/// captured resolves; the first 16 chars are the `proxy_` tee content-address.
279pub(crate) fn retrieve_litellm(hash: &str) -> Option<String> {
280    if hash.len() != LITELLM_HASH_LEN
281        || !hash
282            .bytes()
283            .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
284    {
285        return None;
286    }
287    std::fs::read_to_string(resolve_tee(&hash[..TEE_HASH_LEN])?).ok()
288}
289
290/// Replace every `<lc_expand:HASH>` marker in `s` with the verbatim original
291/// recovered from the local tee store. Returns `Some(spliced)` only when at
292/// least one marker resolved, else `None` (so the caller leaves the string —
293/// and therefore the request bytes — untouched).
294///
295/// An unresolvable marker (bad hash, or a file dropped past the 24h TTL) is left
296/// in place verbatim: the model still sees its own marker rather than a silent
297/// deletion, and a later turn can retry once the operator restores the file.
298/// The spliced content is inserted **raw** (not `<lc_safe>`-wrapped): this runs
299/// on the recent assistant turn the model echoed the marker into, which no proxy
300/// compressor rewrites, and the proxy has no global `<lc_safe>` strip — wrapping
301/// would instead leak the markers to the provider.
302fn splice_str(s: &str) -> Option<String> {
303    if !s.contains(EXPAND_OPEN) {
304        return None;
305    }
306    let mut out = String::with_capacity(s.len());
307    let mut rest = s;
308    let mut changed = false;
309    while let Some(pos) = rest.find(EXPAND_OPEN) {
310        let after = &rest[pos + EXPAND_OPEN.len()..];
311        match after.find(EXPAND_CLOSE) {
312            Some(end) => {
313                let hash = &after[..end];
314                if let Some(original) = recover(hash) {
315                    out.push_str(&rest[..pos]);
316                    out.push_str(&original);
317                    rest = &after[end + EXPAND_CLOSE.len_utf8()..];
318                    changed = true;
319                } else {
320                    // Keep the literal marker; resume scanning past this `<` so a
321                    // later valid marker in the same string is still spliced.
322                    out.push_str(&rest[..pos + EXPAND_OPEN.len()]);
323                    rest = after;
324                }
325            }
326            // No closing `>`: nothing more can match — keep the remainder verbatim.
327            None => break,
328        }
329    }
330    out.push_str(rest);
331    changed.then_some(out)
332}
333
334/// Splice in-band `<lc_expand:HASH>` markers throughout a parsed request body
335/// (#493), replacing each with the verbatim original recovered from the local
336/// tee store. Recurses over every JSON string (object values and array items).
337///
338/// Returns `true` iff at least one marker was spliced. A request with no marker
339/// is left **byte-identical** (the function never allocates a replacement), so a
340/// marker-less turn never perturbs the provider prompt-cache prefix — the splice
341/// only ever changes the bytes the model explicitly asked to expand.
342pub(crate) fn splice_inband_in_place(value: &mut Value) -> bool {
343    match value {
344        Value::String(s) => {
345            if let Some(spliced) = splice_str(s) {
346                *s = spliced;
347                true
348            } else {
349                false
350            }
351        }
352        Value::Array(items) => {
353            let mut changed = false;
354            for item in items {
355                changed |= splice_inband_in_place(item);
356            }
357            changed
358        }
359        Value::Object(map) => {
360            let mut changed = false;
361            for (_, v) in map.iter_mut() {
362                changed |= splice_inband_in_place(v);
363            }
364            changed
365        }
366        _ => false,
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    fn big(seed: &str) -> String {
375        format!("{seed}\n").repeat(40)
376    }
377
378    #[test]
379    fn handle_is_content_addressed_and_deterministic() {
380        let _lock = crate::core::data_dir::test_env_lock();
381        let content = big("file body line");
382        let a = persist(&content).expect("persisted");
383        let b = persist(&content).expect("persisted again");
384        assert_eq!(
385            a, b,
386            "same content must map to the same handle (cache-safe)"
387        );
388        assert!(a.contains("proxy_"), "handle is a proxy tee path: {a}");
389
390        let other = persist(&big("different body")).expect("persisted");
391        assert_ne!(a, other, "different content must get a different handle");
392    }
393
394    #[test]
395    fn persisted_original_is_recoverable() {
396        let _lock = crate::core::data_dir::test_env_lock();
397        let content = big("recoverable verbatim line");
398        let handle = persist(&content).expect("persisted");
399        let on_disk = std::fs::read_to_string(&handle).expect("tee file readable");
400        assert!(
401            on_disk.contains("recoverable verbatim line"),
402            "the verbatim original must be retrievable from the handle"
403        );
404    }
405
406    #[test]
407    fn small_content_gets_no_handle() {
408        let _lock = crate::core::data_dir::test_env_lock();
409        assert!(
410            persist("too small to bother").is_none(),
411            "below MIN_TEE_BYTES there is no handle (the caller keeps its plain stub)"
412        );
413    }
414
415    #[test]
416    fn resolve_tee_accepts_every_stub_form() {
417        let _lock = crate::core::data_dir::test_env_lock();
418        let content = big("resolvable tee body");
419        let handle = persist(&content).expect("persisted");
420        let hash = crate::core::hasher::hash_short(&content);
421
422        // Full path, bare file name, proxy_<hash>, and bare <hash> all resolve to
423        // the same on-disk file — whatever the agent copied out of the stub.
424        for form in [
425            handle.clone(),
426            format!("proxy_{hash}.log"),
427            format!("proxy_{hash}"),
428            hash.clone(),
429        ] {
430            let resolved = resolve_tee(&form).unwrap_or_else(|| panic!("must resolve {form}"));
431            assert_eq!(
432                resolved.to_string_lossy(),
433                handle,
434                "form {form} -> {handle}"
435            );
436        }
437    }
438
439    #[test]
440    fn resolve_tee_rejects_nontee_and_traversal_ids() {
441        let _lock = crate::core::data_dir::test_env_lock();
442        // No FS escape: a crafted path is reduced to its file name, which is not a
443        // valid proxy tee name, so it resolves to None instead of reading it.
444        assert!(resolve_tee("/etc/passwd").is_none());
445        assert!(resolve_tee("../../secret").is_none());
446        assert!(resolve_tee("proxy_nothex0000000.log").is_none());
447        // Right shape but no such file in the store.
448        assert!(resolve_tee("deadbeefdeadbeef").is_none());
449    }
450
451    #[test]
452    fn persist_json_is_distinct_prefix_and_resolvable() {
453        let _lock = crate::core::data_dir::test_env_lock();
454        let content = big("json crusher original");
455        let proxy = persist(&content).expect("proxy persisted");
456        let json = persist_json(&content).expect("json persisted");
457        assert!(
458            json.contains("json_"),
459            "json handle uses json_ prefix: {json}"
460        );
461        assert_ne!(
462            proxy, json,
463            "same content gets distinct files per producer prefix"
464        );
465
466        // The json_ handle resolves through the unified resolver in every form a
467        // stub / footer can carry: full path, bare file name, and bare json_id.
468        let hash = crate::core::hasher::hash_short(&content);
469        for form in [
470            json.clone(),
471            format!("json_{hash}.log"),
472            format!("json_{hash}"),
473        ] {
474            assert_eq!(
475                resolve_tee(&form)
476                    .expect("json form resolves")
477                    .to_string_lossy(),
478                json,
479                "json form {form} -> {json}"
480            );
481        }
482    }
483
484    #[test]
485    fn persist_tabular_is_distinct_prefix_and_resolvable() {
486        let _lock = crate::core::data_dir::test_env_lock();
487        let content = big("tabular crusher original");
488        let json = persist_json(&content).expect("json persisted");
489        let tbl = persist_tabular(&content).expect("tbl persisted");
490        assert!(
491            tbl.contains("tbl_"),
492            "tabular handle uses tbl_ prefix: {tbl}"
493        );
494        assert_ne!(
495            json, tbl,
496            "same content gets distinct files per producer prefix"
497        );
498
499        let hash = crate::core::hasher::hash_short(&content);
500        for form in [
501            tbl.clone(),
502            format!("tbl_{hash}.log"),
503            format!("tbl_{hash}"),
504        ] {
505            assert_eq!(
506                resolve_tee(&form)
507                    .expect("tbl form resolves")
508                    .to_string_lossy(),
509                tbl,
510                "tbl form {form} -> {tbl}"
511            );
512        }
513    }
514
515    #[test]
516    fn persist_yaml_is_distinct_prefix_and_resolvable() {
517        let _lock = crate::core::data_dir::test_env_lock();
518        let content = big("yaml crusher original");
519        let tbl = persist_tabular(&content).expect("tbl persisted");
520        let yaml = persist_yaml(&content).expect("yaml persisted");
521        assert!(
522            yaml.contains("yaml_"),
523            "yaml handle uses yaml_ prefix: {yaml}"
524        );
525        assert_ne!(
526            tbl, yaml,
527            "same content gets distinct files per producer prefix"
528        );
529
530        let hash = crate::core::hasher::hash_short(&content);
531        for form in [
532            yaml.clone(),
533            format!("yaml_{hash}.log"),
534            format!("yaml_{hash}"),
535        ] {
536            assert_eq!(
537                resolve_tee(&form)
538                    .expect("yaml form resolves")
539                    .to_string_lossy(),
540                yaml,
541                "yaml form {form} -> {yaml}"
542            );
543        }
544    }
545
546    #[test]
547    fn resolve_tee_resolves_shell_tee_with_underscored_slug() {
548        let _lock = crate::core::data_dir::test_env_lock();
549        // A real shell command whose slug contains underscores: the hash is the
550        // last `_`-segment (8 hex), never the first — the parsing trap. The full
551        // verbatim output the shell already teed is now surgically retrievable.
552        let path = crate::shell::save_tee("gh api /repos/foo/bar", &big("api row"))
553            .expect("shell tee saved");
554        let name = std::path::Path::new(&path)
555            .file_name()
556            .and_then(|n| n.to_str())
557            .unwrap()
558            .to_string();
559        assert!(
560            is_shell_tee_name(&name),
561            "save_tee name must be recognized as a shell tee: {name}"
562        );
563        for form in [path.clone(), name] {
564            assert_eq!(
565                resolve_tee(&form)
566                    .expect("shell tee form resolves")
567                    .to_string_lossy(),
568                path,
569                "shell tee form -> {path}"
570            );
571        }
572    }
573
574    #[test]
575    fn resolve_tee_does_not_capture_reference_ids() {
576        let _lock = crate::core::data_dir::test_env_lock();
577        // A reference-store id (`ref_<16hex>`, no `.log`) must fall through the
578        // tee resolver so ctx_expand routes it to the reference store, not the
579        // tee store — the precedence guard for the unified retrieve ladder.
580        assert!(resolve_tee("ref_deadbeefcafef00d").is_none());
581        // A bare 16-hex archive id with no backing tee file also stays None.
582        assert!(resolve_tee("0123456789abcdef").is_none());
583    }
584
585    #[test]
586    fn litellm_hash_is_24_lowercase_hex_and_extends_tee_hash() {
587        let content = big("gateway retrieval body");
588        let hash = litellm_hash(&content);
589        assert_eq!(hash.len(), LITELLM_HASH_LEN);
590        assert!(
591            hash.bytes()
592                .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)),
593            "must match LiteLLM's [a-f0-9]{{24}} class: {hash}"
594        );
595        // Same blake3 digest as the tee name, longer prefix: the first 16 chars
596        // ARE the tee content-address, which is what makes retrieval work.
597        assert_eq!(hash[..16], crate::core::hasher::hash_short(&content));
598        // Pure function of content (#498): stable across calls.
599        assert_eq!(hash, litellm_hash(&content));
600    }
601
602    #[test]
603    fn retrieve_litellm_resolves_persisted_content() {
604        let _lock = crate::core::data_dir::test_env_lock();
605        let content = big("litellm retrievable original");
606        persist(&content).expect("persisted");
607        let recovered =
608            retrieve_litellm(&litellm_hash(&content)).expect("24-hex hash must resolve");
609        assert!(recovered.contains("litellm retrievable original"));
610    }
611
612    #[test]
613    fn retrieve_litellm_is_shape_locked_to_the_guardrail_regex() {
614        let _lock = crate::core::data_dir::test_env_lock();
615        let content = big("shape locked body");
616        persist(&content).expect("persisted");
617        let hash = litellm_hash(&content);
618
619        // 16-hex (our tee id), truncated, extended, uppercased, non-hex: all
620        // rejected — only the exact shape LiteLLM's regex captures resolves.
621        assert!(retrieve_litellm(&hash[..16]).is_none(), "16-hex rejected");
622        assert!(retrieve_litellm(&hash[..23]).is_none(), "23-hex rejected");
623        assert!(
624            retrieve_litellm(&format!("{hash}0")).is_none(),
625            "25-hex rejected"
626        );
627        assert!(
628            retrieve_litellm(&hash.to_uppercase()).is_none(),
629            "uppercase rejected (regex class is [a-f0-9])"
630        );
631        assert!(
632            retrieve_litellm("zzzzzzzzzzzzzzzzzzzzzzzz").is_none(),
633            "non-hex rejected"
634        );
635        // Traversal attempts die in resolve_tee's name canonicalization.
636        assert!(retrieve_litellm("../../etc/passwd00000000").is_none());
637        // Right shape, unknown content.
638        assert!(retrieve_litellm("0123456789abcdef01234567").is_none());
639    }
640
641    #[test]
642    fn inband_marker_is_derived_from_handle() {
643        let _lock = crate::core::data_dir::test_env_lock();
644        let content = big("inband marker body");
645        let handle = persist(&content).expect("persisted");
646        let hash = crate::core::hasher::hash_short(&content);
647        // The marker carries the same content hash the handle does, so a model can
648        // echo it and the proxy resolves it back to the very same tee file.
649        assert_eq!(inband_marker(&handle), Some(format!("<lc_expand:{hash}>")));
650        // A non-tee handle has no marker.
651        assert!(inband_marker("/tmp/not-a-tee.txt").is_none());
652    }
653
654    #[test]
655    fn splice_replaces_marker_with_verbatim_original() {
656        let _lock = crate::core::data_dir::test_env_lock();
657        let content = big("the historical verbatim line");
658        let handle = persist(&content).expect("persisted");
659        let marker = inband_marker(&handle).expect("marker");
660
661        let mut doc = serde_json::json!({
662            "messages": [{ "role": "assistant", "content": format!("recall {marker} please") }]
663        });
664        assert!(splice_inband_in_place(&mut doc), "a marker must splice");
665        let spliced = doc["messages"][0]["content"].as_str().unwrap();
666        assert!(
667            spliced.contains("the historical verbatim line"),
668            "verbatim original must be spliced in: {spliced}"
669        );
670        assert!(
671            !spliced.contains("<lc_expand:"),
672            "the marker must be consumed, not left behind"
673        );
674    }
675
676    #[test]
677    fn splice_is_byte_identical_no_op_without_marker() {
678        let _lock = crate::core::data_dir::test_env_lock();
679        let mut doc = serde_json::json!({
680            "messages": [{ "role": "user", "content": "no marker here" }],
681            "system": "plain"
682        });
683        let before = doc.clone();
684        assert!(
685            !splice_inband_in_place(&mut doc),
686            "no marker → must report no change"
687        );
688        assert_eq!(
689            doc, before,
690            "marker-less body must stay byte-identical (cache-safe)"
691        );
692    }
693
694    #[test]
695    fn splice_keeps_unresolvable_marker_verbatim() {
696        let _lock = crate::core::data_dir::test_env_lock();
697        // Right shape, but no such file in the store → leave the model's marker in
698        // place rather than silently deleting it.
699        let mut doc = serde_json::json!({ "t": "before <lc_expand:deadbeefdeadbeef> after" });
700        assert!(!splice_inband_in_place(&mut doc));
701        assert_eq!(
702            doc["t"].as_str().unwrap(),
703            "before <lc_expand:deadbeefdeadbeef> after"
704        );
705    }
706
707    #[test]
708    fn splice_recurses_and_handles_multiple_markers() {
709        let _lock = crate::core::data_dir::test_env_lock();
710        let a = big("first recovered body");
711        let b = big("second recovered body");
712        let ma = inband_marker(&persist(&a).unwrap()).unwrap();
713        let mb = inband_marker(&persist(&b).unwrap()).unwrap();
714
715        // Two markers in one nested string, plus a deeper array item.
716        let mut doc = serde_json::json!({
717            "contents": [
718                { "parts": [{ "text": format!("{ma} and {mb}") }] }
719            ]
720        });
721        assert!(splice_inband_in_place(&mut doc));
722        let text = doc["contents"][0]["parts"][0]["text"].as_str().unwrap();
723        assert!(text.contains("first recovered body"));
724        assert!(text.contains("second recovered body"));
725        assert!(!text.contains("<lc_expand:"));
726    }
727}