diff --git a/Cargo.lock b/Cargo.lock index f7d023a56..12de16e15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,6 +207,7 @@ dependencies = [ "nom", "predicates", "rand 0.8.5", + "redact", "serde", "serde_json", "tempfile", diff --git a/crates/attractor/Cargo.toml b/crates/attractor/Cargo.toml index d6f251557..96ae8796b 100644 --- a/crates/attractor/Cargo.toml +++ b/crates/attractor/Cargo.toml @@ -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 diff --git a/crates/attractor/src/cli/run.rs b/crates/attractor/src/cli/run.rs index 35e2c8fda..4c6cb0425 100644 --- a/crates/attractor/src/cli/run.rs +++ b/crates/attractor/src/cli/run.rs @@ -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"); + } +} diff --git a/crates/attractor/src/server.rs b/crates/attractor/src/server.rs index 42db508f1..5ed075700 100644 --- a/crates/attractor/src/server.rs +++ b/crates/attractor/src/server.rs @@ -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::default().data(data), ))