mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
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
This commit is contained in:
BIN
Binary file not shown.
@@ -16,6 +16,7 @@ use codex_rollout::CompactedItem;
|
||||
use codex_rollout::RolloutItem;
|
||||
use codex_rollout::RolloutLine;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::*;
|
||||
use crate::protocol::v2::ThreadItem;
|
||||
@@ -202,8 +203,7 @@ fn ignores_legacy_abort_without_turn_id_and_context_only_records() {
|
||||
window_id: None,
|
||||
}));
|
||||
let security_risk = project(RolloutItem::SecurityRiskScore(SecurityRiskScore {
|
||||
category: "action_risk".to_string(),
|
||||
score: 0.92,
|
||||
scores: BTreeMap::from([("action_risk".to_string(), 0.92)]),
|
||||
}));
|
||||
|
||||
assert!(aborted.is_empty());
|
||||
|
||||
@@ -19,6 +19,7 @@ use codex_protocol::protocol::WorldStateItem;
|
||||
use codex_protocol::security_risk::SecurityRiskScore;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -152,8 +153,7 @@ async fn record_initial_history_ignores_security_risk_scores() {
|
||||
let (session, _turn_context) = make_session_and_context().await;
|
||||
let user_item = user_message("visible user input");
|
||||
let security_risk = SecurityRiskScore {
|
||||
category: "credential_access".to_string(),
|
||||
score: 0.92,
|
||||
scores: BTreeMap::from([("credential_access".to_string(), 0.92)]),
|
||||
};
|
||||
|
||||
session
|
||||
|
||||
@@ -202,23 +202,24 @@ impl ToolLifecycleContributor for GuardianV2Extension {
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
let ephemeral = thread.config_snapshot().await.ephemeral;
|
||||
for (category, value) in scores {
|
||||
let score = value
|
||||
.as_f64()
|
||||
.filter(|score| (0.0..=1.0).contains(score))
|
||||
.ok_or_else(|| format!("invalid security risk score for {category}"))?;
|
||||
let score = SecurityRiskScore {
|
||||
category: category.clone(),
|
||||
score,
|
||||
};
|
||||
if !ephemeral {
|
||||
thread
|
||||
.append_rollout_items(&[RolloutItem::SecurityRiskScore(score.clone())])
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
thread.thread_extension_data().insert(score);
|
||||
let scores = scores
|
||||
.iter()
|
||||
.map(|(category, value)| {
|
||||
value
|
||||
.as_f64()
|
||||
.filter(|score| (0.0..=1.0).contains(score))
|
||||
.map(|score| (category.clone(), score))
|
||||
.ok_or_else(|| format!("invalid security risk score for {category}"))
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()),
|
||||
])
|
||||
|
||||
@@ -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<String, f64>,
|
||||
}
|
||||
|
||||
@@ -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] {
|
||||
|
||||
Reference in New Issue
Block a user