fix(taint): record taint.hosts once per phase, not once per sweep tick
deploy / build (push) Canceled after 0s
deploy / test (push) Canceled after 1m51s

The container drain revisits finished phases for 30 minutes; the other drains
are idempotent because they truncate, and the taint file deliberately is not.
The first live mission (01a0cb7e) recorded the same event four times. Record
only when no event for the phase already carries at least as many hosts.

Live result otherwise as designed: curl https://example.com tainted iana.org
(the page's link), not example.com (the agent's own target), and grep -rn curl
added nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-09-22 18:43:24 -05:00
co-authored by Claude Opus 5.5
parent 597e76b261
commit decc680bcc
2 changed files with 41 additions and 1 deletions
+18
View File
@@ -482,6 +482,24 @@ mod tests {
); );
} }
/// The taint file is never cleared, so its record needs its own
/// once-per-phase guard. Measured without one: the first live mission
/// recorded the same `taint.hosts` event four times, and the sweep that
/// revisits a finished phase for 30 minutes would have kept going.
#[test]
fn the_taint_record_is_written_once_per_phase() {
let runner = include_str!("phase_runner.rs");
let body = runner
.split("drain_taint(&docker, &container).await")
.nth(1)
.expect("the sweep drains the taint file");
let guard = body.find("SELECT EXISTS").expect("no once-per-phase guard");
let record = body.find("TAINT_HOSTS,\n").unwrap_or(usize::MAX).min(
body.find("MissionEvent::new").expect("the record"),
);
assert!(guard < record, "the guard must run before the record is written");
}
/// The drain must use the connector that honours DOCKER_HOST. /// The drain must use the connector that honours DOCKER_HOST.
/// ///
/// The server reaches Docker through a socket proxy, so /// The server reaches Docker through a socket proxy, so
+23 -1
View File
@@ -684,8 +684,30 @@ async fn drain_finished_container_phases(pool: &PgPool) -> Result<(), String> {
} }
// What fetched content named. Observed, not enforced — see // What fetched content named. Observed, not enforced — see
// docs/TASK-PERMISSION-AND-TAINT.md, piece 2, stage 1. // docs/TASK-PERMISSION-AND-TAINT.md, piece 2, stage 1.
//
// Once per phase. This sweep revisits every phase for 30 minutes, and
// the other drains are idempotent only because they truncate what they
// read; the taint file is deliberately never truncated, so without this
// check the first live run recorded the same event four times and would
// have gone on recording it every tick. A LARGER set is still recorded:
// that is new information.
let hosts = crate::container_tool_hooks::drain_taint(&docker, &container).await; let hosts = crate::container_tool_hooks::drain_taint(&docker, &container).await;
if !hosts.is_empty() { let already: bool = if hosts.is_empty() {
true
} else {
sqlx::query_scalar(
"SELECT EXISTS (SELECT 1 FROM mission_events
WHERE phase_id = $1 AND kind = $2
AND (detail->>'count')::int >= $3)",
)
.bind(phase_id)
.bind(crate::container_tool_hooks::TAINT_HOSTS)
.bind(hosts.len() as i32)
.fetch_one(pool)
.await
.unwrap_or(false)
};
if !already {
crate::mission_events::record( crate::mission_events::record(
pool, pool,
crate::mission_events::MissionEvent::new( crate::mission_events::MissionEvent::new(