From 489d56cb878abd4f75bf58ea673cb933380aa9b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=20Alexander=20=F0=9F=91=91?= Date: Tue, 25 Aug 2026 21:13:46 -0500 Subject: [PATCH] fix: fail closed on rejected memory activity Signed-off-by: Codex --- crates/worker/src/lib.rs | 55 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index 796a3e9..5945b57 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -1,6 +1,6 @@ //! Worker: pulls tasks from the queue, spawns coven-code sessions, streams progress. -use anyhow::Result; +use anyhow::{Context, Result}; use std::path::Path; use std::time::Duration; use tracing::{error, info, warn}; @@ -738,6 +738,45 @@ fn memory_activity_rows( rows } +/// Refuse the entire runtime result when it reports memory activity outside +/// the policy granted in its brief. The result may already have been +/// influenced by a rejected read, so filtering individual entries is not a +/// sufficient publication boundary. +fn enforce_memory_activity(rejections: &[memory::MemoryRejection]) -> Result<()> { + anyhow::ensure!( + rejections.is_empty(), + "runtime reported out-of-policy memory activity; refusing to publish result" + ); + Ok(()) +} + +#[cfg(test)] +mod memory_enforcement_tests { + use super::*; + + #[test] + fn rejected_memory_activity_blocks_the_runtime_result() { + let rejections = vec![memory::MemoryRejection { + op: memory::MemoryOp::Read, + target: "repo/OpenCoven/demo/secrets/api".to_string(), + reason: "memory has been revoked".to_string(), + }]; + + let error = enforce_memory_activity(&rejections) + .expect_err("a result influenced by revoked memory must not be published"); + + assert_eq!( + error.to_string(), + "runtime reported out-of-policy memory activity; refusing to publish result" + ); + } + + #[test] + fn accepted_memory_activity_allows_the_runtime_result() { + enforce_memory_activity(&[]).expect("in-policy memory activity should remain publishable"); + } +} + #[allow(clippy::too_many_arguments)] async fn run_and_publish( config: &Config, @@ -798,10 +837,12 @@ async fn run_and_publish( store .revocations_for(task.installation_id, &repo_full) .await - .unwrap_or_else(|e| { - warn!(task_id = %task.id, "failed to load memory revocations: {e:#}"); - Vec::new() - }) + .with_context(|| { + format!( + "failed to load memory revocations for installation {}", + task.installation_id + ) + })? } else { Vec::new() }; @@ -921,6 +962,10 @@ async fn run_and_publish( if let Err(e) = store.record_memory_activity(activity).await { warn!(task_id = %task.id, "failed to record memory activity: {e:#}"); } + // A rejected read may already have influenced every field in the + // runtime result. Audit it above, then fail closed before any result + // content can reach a PR, review, status comment, or Check Run. + enforce_memory_activity(&rejections)?; // Cite the reads the adapter accepted (not refused/revoked) so the // review discloses which memory influenced it (issue #6). cited_memory = used