Skip to main content

codetether_agent/session/helper/
archive.rs

1//! S3/R2 archival of session event streams.
2//!
3//! Sessions that run with `CODETETHER_EVENT_STREAM_PATH` set append
4//! structured JSONL events to a per-session directory. When S3/R2 credentials
5//! are configured, those files are archived after each prompt for immutable
6//! compliance logging (SOC 2, FedRAMP, ATO).
7
8use std::path::PathBuf;
9
10use crate::event_stream::s3_sink::S3Sink;
11
12/// Resolve the event-stream base directory from the
13/// `CODETETHER_EVENT_STREAM_PATH` environment variable.
14pub(crate) fn event_stream_path() -> Option<PathBuf> {
15    std::env::var("CODETETHER_EVENT_STREAM_PATH")
16        .ok()
17        .map(PathBuf::from)
18}
19
20/// Spawn a background task that uploads every `*.jsonl` event file under the
21/// given session's event-stream directory to S3/R2.
22///
23/// This is best-effort: missing configuration, a missing directory, or an S3
24/// sink construction failure are all treated as no-ops so they never block
25/// the prompt from returning.
26pub(crate) async fn archive_event_stream_to_s3(session_id: &str, base_dir: Option<PathBuf>) {
27    if !S3Sink::is_configured() {
28        return;
29    }
30
31    let Some(base_dir) = base_dir else {
32        return;
33    };
34
35    let session_event_dir = base_dir.join(session_id);
36    if !session_event_dir.exists() {
37        return;
38    }
39
40    let Ok(sink) = S3Sink::from_env().await else {
41        tracing::warn!("Failed to create S3 sink for archival");
42        return;
43    };
44
45    let session_id = session_id.to_string();
46    tokio::spawn(async move {
47        let Ok(mut entries) = tokio::fs::read_dir(&session_event_dir).await else {
48            return;
49        };
50        while let Ok(Some(entry)) = entries.next_entry().await {
51            let path = entry.path();
52            if path.extension().is_none_or(|e| e != "jsonl") {
53                continue;
54            }
55            match sink.upload_file(&path, &session_id).await {
56                Ok(url) => tracing::info!(url = %url, "Archived event stream to S3/R2"),
57                Err(e) => tracing::warn!(error = %e, "Failed to archive event file to S3"),
58            }
59        }
60    });
61}