Skip to content

Commit fdb8e3b

Browse files
committed
Changed: Redact user files in history
1 parent d1f8de6 commit fdb8e3b

1 file changed

Lines changed: 74 additions & 5 deletions

File tree

‎src/reloaded-code-serdesai/src/agent_runtime/stream_events.rs‎

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ use futures::{Stream, StreamExt};
2727
use reloaded_code_core::hooks::{
2828
RunEvent, RunMessage, RunMessageRole, RunToolCallSummary, RunToolResultSummary,
2929
};
30-
use serdes_ai::core::messages::{RetryContent, ToolCallArgs, ToolReturnContent};
30+
use serdes_ai::core::messages::{
31+
AudioContent, DocumentContent, FileContent, ImageContent, RetryContent, ToolCallArgs,
32+
ToolReturnContent, VideoContent,
33+
};
3134
use serdes_ai::core::{
32-
ModelRequest, ModelRequestPart, ModelResponse, ModelResponsePart, UserContent,
35+
ModelRequest, ModelRequestPart, ModelResponse, ModelResponsePart, UserContent, UserContentPart,
3336
};
3437
use serdes_ai::{AgentStream, AgentStreamEvent};
3538
use std::pin::Pin;
@@ -301,17 +304,48 @@ fn tool_result_message(tool_call_id: Option<String>, output: String) -> RunMessa
301304

302305
/// Renders user prompt content as audit text.
303306
///
304-
/// Plain text passes through; multi-part prompts are serialized so image
305-
/// and mixed content stay observable in the transcript.
307+
/// Plain text passes through; multi-part prompts are serialized so
308+
/// image and mixed content stay observable in the transcript. Inline
309+
/// binary parts collapse to a media type plus byte length placeholder
310+
/// so payloads never bloat the transcript.
306311
fn user_content_text(content: UserContent) -> String {
307312
match content {
308313
UserContent::Text(text) => text,
309314
UserContent::Parts(parts) => {
310-
serde_json::to_string(&parts).unwrap_or_else(|_| format!("{parts:?}"))
315+
// After the map: binary parts are placeholders; text and
316+
// URL parts serialize exactly as before.
317+
let redacted: Vec<_> = parts.iter().map(redact_binary_part).collect();
318+
serde_json::to_string(&redacted).unwrap_or_else(|_| format!("{redacted:?}"))
311319
}
312320
}
313321
}
314322

323+
/// Serializes one prompt part for the audit trail.
324+
///
325+
/// Text and URL parts keep their JSON shape; binary parts become a
326+
/// placeholder carrying their media type and byte length.
327+
fn redact_binary_part(part: &UserContentPart) -> serde_json::Value {
328+
let placeholder = |kind: &str, media_type: &str, bytes: usize| serde_json::json!({ "type": kind, "media_type": media_type, "bytes": bytes });
329+
match part {
330+
UserContentPart::Image {
331+
image: ImageContent::Binary(binary),
332+
} => placeholder("image", binary.media_type.mime_type(), binary.data.len()),
333+
UserContentPart::Audio {
334+
audio: AudioContent::Binary(binary),
335+
} => placeholder("audio", binary.media_type.mime_type(), binary.data.len()),
336+
UserContentPart::Video {
337+
video: VideoContent::Binary(binary),
338+
} => placeholder("video", binary.media_type.mime_type(), binary.data.len()),
339+
UserContentPart::Document {
340+
document: DocumentContent::Binary(binary),
341+
} => placeholder("document", binary.media_type.mime_type(), binary.data.len()),
342+
UserContentPart::File {
343+
file: FileContent::Binary(binary),
344+
} => placeholder("file", binary.mime_type.as_str(), binary.data.len()),
345+
other => serde_json::to_value(other).unwrap_or(serde_json::Value::Null),
346+
}
347+
}
348+
315349
#[cfg(test)]
316350
mod tests {
317351
use super::*;
@@ -591,6 +625,41 @@ mod tests {
591625
);
592626
}
593627

628+
#[test]
629+
fn user_content_text_redacts_binary_but_keeps_text_and_urls() {
630+
use serdes_ai::core::messages::ImageMediaType;
631+
632+
// Base64 of the binary bytes must never reach the audit text.
633+
let content = UserContent::parts(vec![
634+
UserContentPart::text("look at this"),
635+
UserContentPart::image_url("https://example.invalid/image.png"),
636+
UserContentPart::image_binary(vec![1, 2, 3, 4], ImageMediaType::Png),
637+
UserContentPart::File {
638+
file: FileContent::binary(vec![9, 9], "application/pdf"),
639+
},
640+
]);
641+
642+
let rendered = user_content_text(content);
643+
644+
assert!(
645+
rendered.contains("look at this")
646+
&& rendered.contains("https://example.invalid/image.png"),
647+
"text and URL parts keep their serialization: {rendered}"
648+
);
649+
assert!(
650+
rendered.contains(r#""type":"image""#)
651+
&& rendered.contains(r#""media_type":"image/png""#)
652+
&& rendered.contains(r#""bytes":4"#)
653+
&& rendered.contains(r#""media_type":"application/pdf""#)
654+
&& rendered.contains(r#""bytes":2"#),
655+
"binary parts render as media type plus length: {rendered}"
656+
);
657+
assert!(
658+
!rendered.contains("AQIDBA==") && !rendered.contains("CQk="),
659+
"base64 payloads must not leak: {rendered}"
660+
);
661+
}
662+
594663
// ========================================================================
595664
// HookedAgent::run_stream integration
596665
// ========================================================================

0 commit comments

Comments
 (0)