From aa905bb9625d777a2f64b20408e5ddedbf173395 Mon Sep 17 00:00:00 2001 From: jif Date: Fri, 14 Aug 2026 12:11:52 +0000 Subject: [PATCH] Store security risk scores as a snapshot (#38567) ## What changed - Replace the single `category` and `score` fields in `SecurityRiskScore` with a deterministic map of category names to scores. - Validate the full Guardian V2 classifier response before storing it as one thread extension value and, for non-ephemeral threads, one rollout item. - Cover serialization and rollout loading with snapshots containing multiple score categories. GitOrigin-RevId: 24525de691f96bd34ccfe04acab2f01aca309837 --- .../app-server-exports-stable.json.zst | Bin 133494 -> 133502 bytes .../thread_history_projection_tests.rs | 4 +-- .../session/rollout_reconstruction_tests.rs | 4 +-- codex-rs/ext/guardian-v2/src/extension.rs | 33 +++++++++--------- .../ext/guardian-v2/src/extension_tests.rs | 4 +-- .../src/sessions/append_tests.rs | 4 +-- codex-rs/history/src/tests.rs | 7 +++- codex-rs/memories/write/src/phase1.rs | 4 +-- codex-rs/protocol/src/security_risk.rs | 6 ++-- codex-rs/rollout/src/recorder_tests.rs | 7 ++-- 10 files changed, 41 insertions(+), 32 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index d5e25f6099c6538f0cfb7c66eab4466b496b3371..5e41454f63308f80d7e5b37c5d3b4cee42d67ee7 100644 GIT binary patch delta 5323 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 delta 5316 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 W=anylJhrY`LlX$z>()?; + let score = SecurityRiskScore { scores }; + if !ephemeral { + thread + .append_rollout_items(&[RolloutItem::SecurityRiskScore(score.clone())]) + .await + .map_err(|error| error.to_string())?; } + thread.thread_extension_data().insert(score); Ok(()) } .await; diff --git a/codex-rs/ext/guardian-v2/src/extension_tests.rs b/codex-rs/ext/guardian-v2/src/extension_tests.rs index fdcadbbd98..0168a41413 100644 --- a/codex-rs/ext/guardian-v2/src/extension_tests.rs +++ b/codex-rs/ext/guardian-v2/src/extension_tests.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::sync::Arc; use std::time::Duration; @@ -275,8 +276,7 @@ async fn contributor_samples_tool_calls_with_the_existing_luna_pool() -> Result< assert_eq!( score.as_ref(), &SecurityRiskScore { - category: "action_risk".to_string(), - score: 0.25, + scores: BTreeMap::from([("action_risk".to_string(), 0.25)]), } ); test.codex.ensure_rollout_materialized().await; diff --git a/codex-rs/external-agent-migration/src/sessions/append_tests.rs b/codex-rs/external-agent-migration/src/sessions/append_tests.rs index 2a64e1a2cc..23de57961d 100644 --- a/codex-rs/external-agent-migration/src/sessions/append_tests.rs +++ b/codex-rs/external-agent-migration/src/sessions/append_tests.rs @@ -9,6 +9,7 @@ use codex_protocol::protocol::ContextCompactedEvent; use codex_protocol::protocol::ThreadRolledBackEvent; use codex_protocol::security_risk::SecurityRiskScore; use pretty_assertions::assert_eq; +use std::collections::BTreeMap; #[test] fn returns_the_missing_suffix_from_its_visible_boundary() { @@ -64,8 +65,7 @@ fn requires_a_strict_nonempty_model_prefix() { } } let security_risk = RolloutItem::SecurityRiskScore(SecurityRiskScore { - category: "action_risk".to_string(), - score: 0.92, + scores: BTreeMap::from([("action_risk".to_string(), 0.92)]), }); metadata_changed.push(security_risk.clone()); assert!(model_transcripts_match(&history, &metadata_changed)); diff --git a/codex-rs/history/src/tests.rs b/codex-rs/history/src/tests.rs index 056bad02f9..94f08c0ad8 100644 --- a/codex-rs/history/src/tests.rs +++ b/codex-rs/history/src/tests.rs @@ -354,7 +354,12 @@ fn rollout_item_variants_preserve_existing_payload_shapes() -> Result<()> { }), json!({ "type": "security_risk_score", - "payload": { "category": "action_risk", "score": 0.92 }, + "payload": { + "scores": { + "action_risk": 0.92, + "data_exfiltration": 0.31, + }, + }, }), json!({ "type": "event_msg", diff --git a/codex-rs/memories/write/src/phase1.rs b/codex-rs/memories/write/src/phase1.rs index f7caefbeda..954eb6b3a8 100644 --- a/codex-rs/memories/write/src/phase1.rs +++ b/codex-rs/memories/write/src/phase1.rs @@ -675,6 +675,7 @@ mod tests { use codex_protocol::protocol::InterAgentCommunication; use codex_protocol::security_risk::SecurityRiskScore; use pretty_assertions::assert_eq; + use std::collections::BTreeMap; #[test] fn serializes_memory_rollout_with_agents_removed_but_environment_kept() { @@ -725,8 +726,7 @@ mod tests { RolloutItem::ResponseItem(mixed_contextual_message.into()), RolloutItem::ResponseItem(skill_message.into()), RolloutItem::SecurityRiskScore(SecurityRiskScore { - category: "action_risk".to_string(), - score: 0.92, + scores: BTreeMap::from([("action_risk".to_string(), 0.92)]), }), RolloutItem::ResponseItem(subagent_message.clone().into()), ]) diff --git a/codex-rs/protocol/src/security_risk.rs b/codex-rs/protocol/src/security_risk.rs index 73565eff82..70c9a83d64 100644 --- a/codex-rs/protocol/src/security_risk.rs +++ b/codex-rs/protocol/src/security_risk.rs @@ -1,13 +1,13 @@ use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; +use std::collections::BTreeMap; -/// A durable, thread-owned security risk classifier score. +/// A durable, thread-owned snapshot of security risk classifier scores. /// /// These records belong to rollout history only and must not enter model-visible /// conversation context or user-visible thread item projections. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct SecurityRiskScore { - pub category: String, - pub score: f64, + pub scores: BTreeMap, } diff --git a/codex-rs/rollout/src/recorder_tests.rs b/codex-rs/rollout/src/recorder_tests.rs index 3c64aa6179..a6f1237a89 100644 --- a/codex-rs/rollout/src/recorder_tests.rs +++ b/codex-rs/rollout/src/recorder_tests.rs @@ -23,6 +23,7 @@ use codex_protocol::protocol::UserMessageEvent; use codex_protocol::security_risk::SecurityRiskScore; use codex_utils_absolute_path::test_support::PathExt; use pretty_assertions::assert_eq; +use std::collections::BTreeMap; use std::fs; use std::fs::File; use std::io::Write; @@ -438,8 +439,10 @@ async fn load_rollout_items_preserves_security_risk_scores() -> std::io::Result< let rollout_path = home.path().join("rollout.jsonl"); let thread_id = ThreadId::new(); let security_risk = SecurityRiskScore { - category: "action_risk".to_string(), - score: 0.76, + scores: BTreeMap::from([ + ("action_risk".to_string(), 0.76), + ("data_exfiltration".to_string(), 0.31), + ]), }; let security_risk_item = RolloutItem::SecurityRiskScore(security_risk.clone()); for history_mode in [ThreadHistoryMode::Legacy, ThreadHistoryMode::Paginated] {