Redact secrets from pipeline event output (NDJSON, live.json, SSE)

Apply redact::redact_jsonl_line at all three serialization sites so
secrets (AWS keys, GitHub PATs, private keys, etc.) are scrubbed
before reaching disk or the network.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: e23c92750cd9
This commit is contained in:
Bryan Helmkamp 2026-02-25 16:02:06 -05:00
parent 5067a59f7b
commit d47c8e9048
4 changed files with 49 additions and 0 deletions

1
Cargo.lock generated
View file

@ -207,6 +207,7 @@ dependencies = [
"nom",
"predicates",
"rand 0.8.5",
"redact",
"serde",
"serde_json",
"tempfile",

View file

@ -27,6 +27,7 @@ dotenvy.workspace = true
agent = { path = "../agent" }
terminal = { path = "../terminal" }
llm = { path = "../llm" }
redact = { path = "../redact" }
thiserror.workspace = true
serde.workspace = true
serde_json.workspace = true

View file

@ -119,6 +119,7 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
});
// Append to progress.ndjson
if let Ok(line) = serde_json::to_string(&envelope) {
let line = redact::redact_jsonl_line(&line);
use std::io::Write;
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
@ -130,6 +131,7 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
}
// Overwrite live.json
if let Ok(pretty) = serde_json::to_string_pretty(&envelope) {
let pretty = redact::redact_jsonl_line(&pretty);
let _ = std::fs::write(&live_path, pretty);
}
});
@ -330,3 +332,47 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn redact_removes_aws_key_from_compact_json() {
let envelope = serde_json::json!({
"timestamp": "2025-01-01T00:00:00.000Z",
"run_id": "abc-123",
"event": {
"type": "agent",
"content": "My key is AKIAYRWQG5EJLPZLBYNP and secret is wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}
});
let compact = serde_json::to_string(&envelope).unwrap();
let redacted = redact::redact_jsonl_line(&compact);
assert!(!redacted.contains("AKIAYRWQG5EJLPZLBYNP"));
assert!(redacted.contains("REDACTED"));
let parsed: serde_json::Value = serde_json::from_str(&redacted).unwrap();
assert_eq!(parsed["run_id"], "abc-123");
assert_eq!(parsed["timestamp"], "2025-01-01T00:00:00.000Z");
}
#[test]
fn redact_removes_aws_key_from_pretty_json() {
let envelope = serde_json::json!({
"timestamp": "2025-01-01T00:00:00.000Z",
"run_id": "def-456",
"event": {
"type": "agent",
"content": "Credentials: AKIAYRWQG5EJLPZLBYNP"
}
});
let pretty = serde_json::to_string_pretty(&envelope).unwrap();
let redacted = redact::redact_jsonl_line(&pretty);
assert!(!redacted.contains("AKIAYRWQG5EJLPZLBYNP"));
assert!(redacted.contains("REDACTED"));
let parsed: serde_json::Value = serde_json::from_str(&redacted).unwrap();
assert_eq!(parsed["run_id"], "def-456");
}
}

View file

@ -366,6 +366,7 @@ async fn get_events(
let stream = BroadcastStream::new(rx).filter_map(|result| match result {
Ok(event) => {
let data = serde_json::to_string(&event).unwrap_or_default();
let data = redact::redact_jsonl_line(&data);
Some(Ok::<Event, std::convert::Infallible>(
Event::default().data(data),
))