From dec8060496d94d9fb85a3ed8602a21a000ae158c Mon Sep 17 00:00:00 2001 From: alex newman Date: Sun, 10 May 2026 15:13:44 -0400 Subject: [PATCH] Bound session replay responses --- src/noise_gateway/upstream.rs | 7 ++++++- src/sessiond.rs | 28 ++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/noise_gateway/upstream.rs b/src/noise_gateway/upstream.rs index 607e0ef..6e1279d 100644 --- a/src/noise_gateway/upstream.rs +++ b/src/noise_gateway/upstream.rs @@ -126,7 +126,12 @@ impl Sessiond { "shell.create_session" => self.post_json("/api/sessions", &session_body(req)).await, "shell.replay_session" => { let id = required_str(&req, "id")?; - self.get_json(&format!("/api/sessions/{id}/replay")).await + let path = if let Some(max_bytes) = req.get("max_bytes").and_then(|v| v.as_u64()) { + format!("/api/sessions/{id}/replay?max_bytes={max_bytes}") + } else { + format!("/api/sessions/{id}/replay") + }; + self.get_json(&path).await } "shell.resize_session" => { let id = required_str(&req, "id")?; diff --git a/src/sessiond.rs b/src/sessiond.rs index 51bbf9f..4a92be1 100644 --- a/src/sessiond.rs +++ b/src/sessiond.rs @@ -14,7 +14,7 @@ use std::process::Stdio; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use axum::extract::{Path as AxPath, State}; +use axum::extract::{Path as AxPath, Query, State}; use axum::http::StatusCode; use axum::routing::{get, post}; use axum::{Json, Router}; @@ -41,6 +41,8 @@ const DEFAULT_DIR: &str = "/var/lib/devopsdefender/sessiond"; const EE_DATA_MOUNT: &str = "/var/lib/easyenclave/data"; const DATA_MOUNT_WAIT_SECS: u64 = 60; const RING_LIMIT: usize = 256 * 1024; +const DEFAULT_REPLAY_MAX_BYTES: usize = 32 * 1024; +const ABSOLUTE_REPLAY_MAX_BYTES: usize = 32 * 1024; const CODEX_PODMAN_RECIPE: &str = r#"#!/var/lib/easyenclave/bin/busybox sh set -eu @@ -226,6 +228,12 @@ pub struct ResizeSession { pub struct ReplayResponse { pub id: String, pub bytes_b64: String, + pub truncated: bool, +} + +#[derive(Deserialize)] +struct ReplayQuery { + max_bytes: Option, } struct RecipeSeed { @@ -413,11 +421,17 @@ async fn create_session( async fn replay_session( State(app): State, AxPath(id): AxPath, + Query(query): Query, ) -> Result> { - let bytes = app.store.replay(&id).await?; + let max_bytes = query + .max_bytes + .unwrap_or(DEFAULT_REPLAY_MAX_BYTES) + .min(ABSOLUTE_REPLAY_MAX_BYTES); + let (bytes, truncated) = app.store.replay(&id, max_bytes).await?; Ok(Json(ReplayResponse { id, bytes_b64: base64::engine::general_purpose::STANDARD.encode(bytes), + truncated, })) } @@ -943,13 +957,14 @@ impl TranscriptStore { Ok(()) } - async fn replay(&self, id: &str) -> Result> { + async fn replay(&self, id: &str, max_bytes: usize) -> Result<(Vec, bool)> { let path = self.path(id); if !Path::new(&path).exists() { return Err(Error::NotFound); } let text = tokio::fs::read_to_string(path).await?; let mut out = Vec::new(); + let mut truncated = false; for line in text.lines().filter(|l| !l.trim().is_empty()) { let plain = self.decrypt_line(line)?; let record: TranscriptRecord = @@ -959,9 +974,14 @@ impl TranscriptStore { .decode(record.data_b64) .map_err(|e| Error::Internal(e.to_string()))?; out.extend_from_slice(&bytes); + if out.len() > max_bytes { + let drop_len = out.len() - max_bytes; + out.drain(..drop_len); + truncated = true; + } } } - Ok(out) + Ok((out, truncated)) } fn path(&self, id: &str) -> PathBuf {