1use std::path::{Path, PathBuf};
28use std::sync::atomic::{AtomicU64, Ordering};
29use std::time::{SystemTime, UNIX_EPOCH};
30
31pub const EXPAND_OPEN: &str = "<lc_expand:";
33pub const EXPAND_CLOSE: char = '>';
35
36pub const MIN_TEE_BYTES: usize = 512;
39
40const CLEANUP_INTERVAL_SECS: u64 = 600;
43
44const TEE_HASH_LEN: usize = 16;
46const SHELL_TEE_HASH_LEN: usize = 8;
48
49fn 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
62fn 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 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
82pub fn persist(content: &str) -> Option<String> {
88 persist_with(content, "proxy")
89}
90
91pub fn persist_once(content: &str) -> Option<String> {
96 if content.len() < MIN_TEE_BYTES
97 || crate::core::context_admission::stored::source_authority_required()
98 {
99 return None;
100 }
101 let path = tee_path(content, "proxy")?;
102 if std::fs::symlink_metadata(&path).is_ok_and(|meta| meta.file_type().is_file()) {
103 return Some(path.to_string_lossy().to_string());
104 }
105 persist(content)
106}
107
108#[cfg_attr(not(test), allow(dead_code))] pub fn persist_conversation(content: &str) -> Option<String> {
115 let path = persist_with_min(content, "conv", 1)?;
116 Path::new(&path)
117 .file_name()
118 .and_then(|name| name.to_str())
119 .map(str::to_owned)
120}
121
122pub fn persist_json(content: &str) -> Option<String> {
129 persist_with(content, "json")
130}
131
132pub fn persist_tabular(content: &str) -> Option<String> {
138 persist_with(content, "tbl")
139}
140
141pub fn persist_yaml(content: &str) -> Option<String> {
147 persist_with(content, "yaml")
148}
149
150pub fn persist_html(content: &str) -> Option<String> {
156 persist_with(content, "html")
157}
158
159#[cfg(feature = "http-server")]
165pub fn persist_tool_definition(content: &str) -> Option<String> {
166 let path = persist_with_min(content, "tool", 1)?;
167 Path::new(&path)
168 .file_name()
169 .and_then(|name| name.to_str())
170 .map(str::to_owned)
171}
172
173fn persist_with(content: &str, prefix: &str) -> Option<String> {
174 persist_with_min(content, prefix, MIN_TEE_BYTES)
175}
176
177fn persist_with_min(content: &str, prefix: &str, min_bytes: usize) -> Option<String> {
178 debug_assert!(
179 TEE_PREFIXES.contains(&prefix),
180 "tee prefix {prefix} must be resolvable (add it to TEE_PREFIXES)"
181 );
182 if content.len() < min_bytes {
183 return None;
184 }
185 let path = tee_path(content, prefix)?;
186 let handle = path.to_string_lossy().to_string();
187 storage::write(&path, content)?;
188 if let Some(dir) = path.parent() {
189 maybe_cleanup(dir);
190 }
191 if path.is_file() {
192 let source_tool = match prefix {
193 "json" | "tbl" | "yaml" => "ctx_read",
194 "html" => "ctx_shell",
195 _ => "proxy",
196 };
197 crate::core::relevance_tracker::register_compressed(
198 handle.clone(),
199 content,
200 source_tool,
201 crate::core::tokens::count_tokens(content),
202 0,
203 );
204 }
205 Some(handle)
206}
207
208fn is_hex(s: &str, len: usize) -> bool {
209 s.len() == len && s.bytes().all(|b| b.is_ascii_hexdigit())
210}
211
212const TEE_PREFIXES: &[&str] = &["proxy", "conv", "json", "tbl", "yaml", "html", "tool"];
216
217fn canonical_tee_name(name: &str) -> Option<String> {
221 let stem = name.strip_suffix(".log").unwrap_or(name);
222 for prefix in TEE_PREFIXES {
223 if let Some(hash) = stem
224 .strip_prefix(prefix)
225 .and_then(|rest| rest.strip_prefix('_'))
226 {
227 return is_hex(hash, TEE_HASH_LEN).then(|| format!("{prefix}_{hash}.log"));
228 }
229 }
230 is_hex(stem, TEE_HASH_LEN).then(|| format!("proxy_{stem}.log"))
231}
232
233fn is_shell_tee_name(name: &str) -> bool {
238 let Some(stem) = name.strip_suffix(".log") else {
239 return false;
240 };
241 if !stem
242 .bytes()
243 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
244 {
245 return false;
246 }
247 match stem.rsplit_once('_') {
248 Some((slug, hash)) => !slug.is_empty() && is_hex(hash, SHELL_TEE_HASH_LEN),
249 None => false,
250 }
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum TeeResolveError {
256 Malformed,
257 Missing,
258 Refused(&'static str),
259}
260
261pub fn resolve_tee_checked(id: &str) -> Result<PathBuf, TeeResolveError> {
264 let name = Path::new(id)
265 .file_name()
266 .and_then(|n| n.to_str())
267 .unwrap_or(id);
268 let canon = canonical_tee_name(name)
269 .or_else(|| is_shell_tee_name(name).then(|| name.to_string()))
270 .ok_or(TeeResolveError::Malformed)?;
271 let state_dir = crate::core::paths::state_dir().map_err(|_| TeeResolveError::Missing)?;
272 let tee_dir = state_dir.join("tee");
273 let path = tee_dir.join(canon);
274 if std::fs::symlink_metadata(&tee_dir).is_ok_and(|m| m.file_type().is_symlink()) {
277 return Err(TeeResolveError::Refused(
278 "tee store is a symlink to another location",
279 ));
280 }
281
282 let link_metadata = std::fs::symlink_metadata(&path).map_err(|error| {
283 if error.kind() == std::io::ErrorKind::PermissionDenied {
284 TeeResolveError::Refused("tee metadata read refused by filesystem policy")
285 } else {
286 TeeResolveError::Missing
287 }
288 })?;
289 if link_metadata.file_type().is_symlink() {
290 return Err(TeeResolveError::Refused(
291 "tee handles cannot resolve through symlinks",
292 ));
293 }
294 if !link_metadata.is_file() {
295 return Err(TeeResolveError::Missing);
296 }
297
298 let canonical_state = std::fs::canonicalize(&state_dir).map_err(|error| {
299 if error.kind() == std::io::ErrorKind::PermissionDenied {
300 TeeResolveError::Refused("tee store access refused by filesystem policy")
301 } else {
302 TeeResolveError::Missing
303 }
304 })?;
305 let canonical_tee = std::fs::canonicalize(&tee_dir).map_err(|error| {
306 if error.kind() == std::io::ErrorKind::PermissionDenied {
307 TeeResolveError::Refused("tee store access refused by filesystem policy")
308 } else {
309 TeeResolveError::Missing
310 }
311 })?;
312 if canonical_tee != canonical_state.join("tee") {
313 return Err(TeeResolveError::Refused(
314 "tee store resolves outside the state directory",
315 ));
316 }
317
318 let canonical_file = std::fs::canonicalize(&path).map_err(|error| {
319 if error.kind() == std::io::ErrorKind::PermissionDenied {
320 TeeResolveError::Refused("tee read refused by filesystem policy")
321 } else {
322 TeeResolveError::Missing
323 }
324 })?;
325 if !canonical_file.starts_with(&canonical_tee) {
326 return Err(TeeResolveError::Refused(
327 "tee handle resolves outside the tee store",
328 ));
329 }
330 if !canonical_file.is_file() {
331 return Err(TeeResolveError::Missing);
332 }
333 Ok(path)
334}
335
336pub fn resolve_tee(id: &str) -> Option<PathBuf> {
338 resolve_tee_checked(id).ok()
339}
340
341pub fn inband_marker(handle: &str) -> Option<String> {
348 let name = Path::new(handle).file_name().and_then(|n| n.to_str())?;
349 let hash = name.strip_prefix("proxy_")?.strip_suffix(".log")?;
350 (hash.len() == 16 && hash.bytes().all(|b| b.is_ascii_hexdigit()))
351 .then(|| format!("{EXPAND_OPEN}{hash}{EXPAND_CLOSE}"))
352}
353
354pub fn inband_locator(handle: &str) -> Option<String> {
362 crate::core::config::Config::load()
363 .proxy
364 .ccr_inband_enabled()
365 .then(|| inband_marker(handle))
366 .flatten()
367}
368
369pub fn read_tee_file(path: &Path) -> Option<String> {
373 read_checked_tee(path)
374}
375
376pub fn read_tee(id: &str) -> Option<String> {
378 read_tee_file(&resolve_tee(id)?)
379}
380
381pub use storage::{
382 read as read_checked_tee, read_detailed as read_tee_detailed, write as write_tee,
383};
384
385mod storage;
386
387pub const LITELLM_HASH_LEN: usize = 24;
391
392pub fn litellm_hash(content: &str) -> String {
399 blake3::hash(content.as_bytes()).to_hex()[..LITELLM_HASH_LEN].to_string()
400}
401
402#[cfg(feature = "http-server")]
408pub fn retrieve_litellm(hash: &str) -> Option<String> {
409 if hash.len() != LITELLM_HASH_LEN
410 || !hash
411 .bytes()
412 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
413 {
414 return None;
415 }
416 read_tee(&hash[..TEE_HASH_LEN])
417}
418
419#[cfg(any(test, feature = "test-support"))]
420pub mod test_support {
422 pub fn big(seed: &str) -> String {
424 format!("{seed}\n").repeat(40)
425 }
426}
427
428#[cfg(test)]
429pub(crate) mod tests {
430 use super::*;
431
432 pub(crate) use super::test_support::big;
433
434 fn write_gateway_mode(mode: &str) {
435 let config = crate::core::paths::config_dir_read_only().expect("config directory");
436 std::fs::create_dir_all(&config).expect("config directory");
437 std::fs::write(
438 config.join("config.toml"),
439 format!("[context_gateway]\nmode = \"{mode}\"\n"),
440 )
441 .expect("gateway config");
442 }
443
444 #[test]
449 fn governed_mode_without_a_pack_neither_serves_nor_writes_an_originless_tee() {
450 let _data = crate::core::data_dir::isolated_data_dir();
451 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
452 write_gateway_mode("developer");
453 let content = big("deploy log line with the marker tee-governed-regression");
454 let handle = persist(&content).expect("developer mode keeps a recovery copy");
455 let path = Path::new(&handle);
456 assert!(
457 read_tee_detailed(path)
458 .expect("developer mode serves it")
459 .contains("tee-governed-regression")
460 );
461
462 write_gateway_mode("governed");
463 let refusal = read_tee_detailed(path).expect_err("governed mode must not serve it");
464 assert!(refusal.contains("no source authority"), "{refusal}");
465 assert!(!refusal.contains("tee-governed-regression"));
466 assert!(read_tee(&handle).is_none());
467 assert!(
468 persist(&big("another originless log line")).is_none(),
469 "governed mode writes no originless copy"
470 );
471
472 write_gateway_mode("developer");
473 assert!(read_tee_detailed(path).is_ok(), "developer mode unchanged");
474 }
475
476 #[test]
477 fn handle_is_content_addressed_and_deterministic() {
478 let _lock = crate::core::data_dir::test_env_lock();
479 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
480 let content = big("file body line");
481 let a = persist(&content).expect("persisted");
482 let b = persist(&content).expect("persisted again");
483 assert_eq!(
484 a, b,
485 "same content must map to the same handle (cache-safe)"
486 );
487 assert!(a.contains("proxy_"), "handle is a proxy tee path: {a}");
488
489 let other = persist(&big("different body")).expect("persisted");
490 assert_ne!(a, other, "different content must get a different handle");
491 }
492
493 #[test]
494 fn persisted_original_is_recoverable() {
495 let _lock = crate::core::data_dir::test_env_lock();
496 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
497 let content = big("recoverable verbatim line");
498 let handle = persist(&content).expect("persisted");
499 let on_disk = std::fs::read_to_string(&handle).expect("tee file readable");
500 assert!(
501 on_disk.contains("recoverable verbatim line"),
502 "the verbatim original must be retrievable from the handle"
503 );
504 }
505
506 #[test]
507 fn small_content_gets_no_handle() {
508 let _lock = crate::core::data_dir::test_env_lock();
509 assert!(
510 persist("too small to bother").is_none(),
511 "below MIN_TEE_BYTES there is no handle (the caller keeps its plain stub)"
512 );
513 }
514
515 #[test]
516 #[allow(clippy::case_sensitive_file_extension_comparisons)]
517 fn conversation_handle_is_compact_and_deterministic() {
518 let _lock = crate::core::data_dir::test_env_lock();
519 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
520 let a = persist_conversation("{\"role\":\"user\",\"content\":\"hello\"}")
521 .expect("conversation handle");
522 let b = persist_conversation("{\"role\":\"user\",\"content\":\"hello\"}")
523 .expect("conversation handle");
524 assert_eq!(a, b);
525 assert!(a.starts_with("conv_") && a.ends_with(".log"));
526 }
527
528 #[test]
529 fn conversation_handle_resolves_short_messages() {
530 let _lock = crate::core::data_dir::test_env_lock();
531 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
532 let handle = persist_conversation("short conversation message").expect("handle");
533 assert!(resolve_tee(&handle).is_some());
534 }
535
536 #[test]
537 fn conversation_original_is_recoverable() {
538 let _lock = crate::core::data_dir::test_env_lock();
539 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
540 let original = "{\"role\":\"tool\",\"content\":\"recover me\"}";
541 let handle = persist_conversation(original).expect("handle");
542 let path = resolve_tee(&handle).expect("resolved handle");
543 assert_eq!(std::fs::read_to_string(path).unwrap(), original);
544 }
545
546 #[test]
547 fn resolve_tee_accepts_every_stub_form() {
548 let _lock = crate::core::data_dir::test_env_lock();
549 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
550 let content = big("resolvable tee body");
551 let handle = persist(&content).expect("persisted");
552 let hash = crate::core::hasher::hash_short(&content);
553
554 for form in [
557 handle.clone(),
558 format!("proxy_{hash}.log"),
559 format!("proxy_{hash}"),
560 hash.clone(),
561 ] {
562 let resolved = resolve_tee(&form).unwrap_or_else(|| panic!("must resolve {form}"));
563 assert_eq!(
564 resolved.to_string_lossy(),
565 handle,
566 "form {form} -> {handle}"
567 );
568 }
569 }
570
571 #[test]
574 fn persist_html_handle_resolves_in_every_form() {
575 let _lock = crate::core::data_dir::test_env_lock();
576 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
577 let content = big("<p>html crush original</p>");
578 let handle = persist_html(&content).expect("html persisted");
579 let hash = crate::core::hasher::hash_short(&content);
580 for form in [
581 handle.clone(),
582 format!("html_{hash}.log"),
583 format!("html_{hash}"),
584 ] {
585 let resolved = resolve_tee(&form).unwrap_or_else(|| panic!("must resolve {form}"));
586 assert_eq!(resolved.to_string_lossy(), handle, "form {form}");
587 }
588 assert!(
589 std::fs::read_to_string(&handle)
590 .unwrap()
591 .contains("html crush original")
592 );
593 }
594
595 #[test]
598 fn canonical_tee_name_covers_every_producer_prefix() {
599 let hash = "0123456789abcdef";
600 for prefix in TEE_PREFIXES {
601 assert_eq!(
602 canonical_tee_name(&format!("{prefix}_{hash}.log")),
603 Some(format!("{prefix}_{hash}.log"))
604 );
605 }
606 assert_eq!(canonical_tee_name(&format!("html{hash}")), None);
607 assert_eq!(canonical_tee_name("html_nothex000000000"), None);
608 }
609
610 #[test]
611 fn resolve_tee_rejects_nontee_and_traversal_ids() {
612 let _lock = crate::core::data_dir::test_env_lock();
613 assert!(resolve_tee("/etc/passwd").is_none());
616 assert!(resolve_tee("../../secret").is_none());
617 assert!(resolve_tee("proxy_nothex0000000.log").is_none());
618 assert!(resolve_tee("deadbeefdeadbeef").is_none());
620 }
621
622 #[test]
623 fn persist_json_is_distinct_prefix_and_resolvable() {
624 let _lock = crate::core::data_dir::test_env_lock();
625 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
626 let content = big("json crusher original");
627 let proxy = persist(&content).expect("proxy persisted");
628 let json = persist_json(&content).expect("json persisted");
629 assert!(
630 json.contains("json_"),
631 "json handle uses json_ prefix: {json}"
632 );
633 assert_ne!(
634 proxy, json,
635 "same content gets distinct files per producer prefix"
636 );
637
638 let hash = crate::core::hasher::hash_short(&content);
641 for form in [
642 json.clone(),
643 format!("json_{hash}.log"),
644 format!("json_{hash}"),
645 ] {
646 assert_eq!(
647 resolve_tee(&form)
648 .expect("json form resolves")
649 .to_string_lossy(),
650 json,
651 "json form {form} -> {json}"
652 );
653 }
654 }
655
656 #[test]
657 fn persist_tabular_is_distinct_prefix_and_resolvable() {
658 let _lock = crate::core::data_dir::test_env_lock();
659 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
660 let content = big("tabular crusher original");
661 let json = persist_json(&content).expect("json persisted");
662 let tbl = persist_tabular(&content).expect("tbl persisted");
663 assert!(
664 tbl.contains("tbl_"),
665 "tabular handle uses tbl_ prefix: {tbl}"
666 );
667 assert_ne!(
668 json, tbl,
669 "same content gets distinct files per producer prefix"
670 );
671
672 let hash = crate::core::hasher::hash_short(&content);
673 for form in [
674 tbl.clone(),
675 format!("tbl_{hash}.log"),
676 format!("tbl_{hash}"),
677 ] {
678 assert_eq!(
679 resolve_tee(&form)
680 .expect("tbl form resolves")
681 .to_string_lossy(),
682 tbl,
683 "tbl form {form} -> {tbl}"
684 );
685 }
686 }
687
688 #[test]
689 fn persist_yaml_is_distinct_prefix_and_resolvable() {
690 let _lock = crate::core::data_dir::test_env_lock();
691 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
692 let content = big("yaml crusher original");
693 let tbl = persist_tabular(&content).expect("tbl persisted");
694 let yaml = persist_yaml(&content).expect("yaml persisted");
695 assert!(
696 yaml.contains("yaml_"),
697 "yaml handle uses yaml_ prefix: {yaml}"
698 );
699 assert_ne!(
700 tbl, yaml,
701 "same content gets distinct files per producer prefix"
702 );
703
704 let hash = crate::core::hasher::hash_short(&content);
705 for form in [
706 yaml.clone(),
707 format!("yaml_{hash}.log"),
708 format!("yaml_{hash}"),
709 ] {
710 assert_eq!(
711 resolve_tee(&form)
712 .expect("yaml form resolves")
713 .to_string_lossy(),
714 yaml,
715 "yaml form {form} -> {yaml}"
716 );
717 }
718 }
719
720 #[test]
721 fn resolve_tee_resolves_shell_tee_with_underscored_slug() {
722 let _lock = crate::core::data_dir::test_env_lock();
723 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
724 let path = crate::shell::save_tee("gh api /repos/foo/bar", &big("api row"))
728 .expect("shell tee saved");
729 let name = std::path::Path::new(&path)
730 .file_name()
731 .and_then(|n| n.to_str())
732 .unwrap()
733 .to_string();
734 assert!(
735 is_shell_tee_name(&name),
736 "save_tee name must be recognized as a shell tee: {name}"
737 );
738 for form in [path.clone(), name] {
739 assert_eq!(
740 resolve_tee(&form)
741 .expect("shell tee form resolves")
742 .to_string_lossy(),
743 path,
744 "shell tee form -> {path}"
745 );
746 }
747 }
748
749 #[test]
750 fn resolve_tee_does_not_capture_reference_ids() {
751 let _lock = crate::core::data_dir::test_env_lock();
752 assert!(resolve_tee("ref_deadbeefcafef00d").is_none());
756 assert!(resolve_tee("0123456789abcdef").is_none());
758 }
759
760 #[test]
761 fn litellm_hash_is_24_lowercase_hex_and_extends_tee_hash() {
762 let content = big("gateway retrieval body");
763 let hash = litellm_hash(&content);
764 assert_eq!(hash.len(), LITELLM_HASH_LEN);
765 assert!(
766 hash.bytes()
767 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)),
768 "must match LiteLLM's [a-f0-9]{{24}} class: {hash}"
769 );
770 assert_eq!(hash[..16], crate::core::hasher::hash_short(&content));
773 assert_eq!(hash, litellm_hash(&content));
775 }
776
777 #[cfg(feature = "http-server")]
778 #[test]
779 fn retrieve_litellm_resolves_persisted_content() {
780 let _lock = crate::core::data_dir::test_env_lock();
781 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
782 let content = big("litellm retrievable original");
783 persist(&content).expect("persisted");
784 let recovered =
785 retrieve_litellm(&litellm_hash(&content)).expect("24-hex hash must resolve");
786 assert!(recovered.contains("litellm retrievable original"));
787 }
788
789 #[cfg(feature = "http-server")]
790 #[test]
791 fn retrieve_litellm_is_shape_locked_to_the_guardrail_regex() {
792 let _lock = crate::core::data_dir::test_env_lock();
793 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
794 let content = big("shape locked body");
795 persist(&content).expect("persisted");
796 let hash = litellm_hash(&content);
797
798 assert!(retrieve_litellm(&hash[..16]).is_none(), "16-hex rejected");
801 assert!(retrieve_litellm(&hash[..23]).is_none(), "23-hex rejected");
802 assert!(
803 retrieve_litellm(&format!("{hash}0")).is_none(),
804 "25-hex rejected"
805 );
806 assert!(
807 retrieve_litellm(&hash.to_uppercase()).is_none(),
808 "uppercase rejected (regex class is [a-f0-9])"
809 );
810 assert!(
811 retrieve_litellm("zzzzzzzzzzzzzzzzzzzzzzzz").is_none(),
812 "non-hex rejected"
813 );
814 assert!(retrieve_litellm("../../etc/passwd00000000").is_none());
816 assert!(retrieve_litellm("0123456789abcdef01234567").is_none());
818 }
819
820 #[test]
821 fn inband_marker_is_derived_from_handle() {
822 let _lock = crate::core::data_dir::test_env_lock();
823 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
824 let content = big("inband marker body");
825 let handle = persist(&content).expect("persisted");
826 let hash = crate::core::hasher::hash_short(&content);
827 assert_eq!(inband_marker(&handle), Some(format!("<lc_expand:{hash}>")));
830 assert!(inband_marker("/tmp/not-a-tee.txt").is_none());
832 }
833}