mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
Remove legacy Guardian approval review paths (#43462)
## What changed - Remove the `fast_decision` and `full_review` extension hooks and `ApprovalAssessment`, leaving `ApprovalReviewContributor::decide` as the approval decision interface. - Remove the duplicate Guardian V2 fast-approval implementation and `StrictReviewReason` state. - Require an explicit `GuardianReviewReason` for synchronous reviews. ## Testing Migrate Guardian reviewer tests to the production `decide_approval` path and cached-score tests to the registry's decision interface. GitOrigin-RevId: 1f9092c0ff5bb1eed0c63fa557fb3807e2e71564
This commit is contained in:
1 parent
8260619cb6
commit
db0568dbbb
14 files changed
+342
-968
No files matched your search
@@ -150,15 +150,4 @@ impl GuardianPolicy {
|
||||
== Some("require_escalated")
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn review_scope(action: &serde_json::Value) -> Option<GuardianScope> {
|
||||
match action.get("tool").and_then(serde_json::Value::as_str)? {
|
||||
"mcp_tool_call" => action
|
||||
.get("server")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(GuardianScope::for_mcp_server),
|
||||
"network_access" => Some(GuardianScope::Network),
|
||||
tool => GuardianScope::for_tool(&ToolName::plain(tool)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ use codex_core::config::Config;
|
||||
use codex_core::context::GuardianContextMode;
|
||||
use codex_core::context::GuardianReviewEvidence;
|
||||
use codex_core::context::NodeReplReviewEvidence;
|
||||
use codex_extension_api::ApprovalReviewContributor;
|
||||
use codex_extension_api::ExtensionData;
|
||||
use codex_extension_api::ExtensionEventSink;
|
||||
use codex_extension_api::ExtensionFuture;
|
||||
@@ -40,10 +39,8 @@ use codex_model_provider::create_model_provider;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::mcp::is_node_repl_backed_server;
|
||||
use codex_protocol::openai_models::GuardianReviewMode;
|
||||
use codex_protocol::openai_models::GuardianScope;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::has_full_access;
|
||||
use codex_protocol::security_risk::SecurityRiskScore;
|
||||
|
||||
@@ -51,13 +48,9 @@ use super::action::GuardianAction;
|
||||
use super::action::RenderedAction;
|
||||
use super::authorization::ScoreAuthorization;
|
||||
use super::config::GuardianV2Config;
|
||||
use super::coverage::GuardianPolicy;
|
||||
use super::coverage::UnscoredAction;
|
||||
use super::metrics::REVIEW_FALLBACK_METRIC;
|
||||
use super::metrics::TOOL_CALL_LAG_METRIC;
|
||||
use super::metrics::record_classification;
|
||||
use super::metrics::record_classification_risk;
|
||||
use super::metrics::record_fast_decision;
|
||||
use super::metrics::sampler_failure_reason;
|
||||
use super::parent_compaction::ParentCompactionError;
|
||||
use super::parent_compaction::select_parent_compaction;
|
||||
@@ -72,14 +65,6 @@ use super::trusted_skills::TrustedSkillInvocations;
|
||||
use super::trusted_skills::TrustedSkillRoots;
|
||||
use super::trusted_tools::trusted_tool_context;
|
||||
|
||||
/// Explains why Guardian v2 requires synchronous approval review.
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum StrictReviewReason {
|
||||
ElevatedRisk,
|
||||
StaleScore,
|
||||
IncompatibleCompaction,
|
||||
}
|
||||
|
||||
enum ClassificationOutcome {
|
||||
Scored,
|
||||
Superseded,
|
||||
@@ -242,195 +227,6 @@ impl SkillInvocationContributor for GuardianV2Extension {
|
||||
}
|
||||
}
|
||||
|
||||
impl ApprovalReviewContributor for GuardianV2Extension {
|
||||
fn fast_decision<'a>(
|
||||
&'a self,
|
||||
_session_store: &'a ExtensionData,
|
||||
thread_store: &'a ExtensionData,
|
||||
prompt: &'a str,
|
||||
extension_metrics: Option<Arc<dyn ExtensionMetrics>>,
|
||||
) -> ExtensionFuture<'a, Option<ReviewDecision>> {
|
||||
Box::pin(async move {
|
||||
let model = thread_store.get::<ModelInfo>();
|
||||
let guardian_config = thread_store.get::<GuardianV2Config>()?;
|
||||
let policy = guardian_config.policy_for_model(model.as_deref());
|
||||
let action = serde_json::from_str::<serde_json::Value>(prompt).unwrap_or_default();
|
||||
let scope = GuardianPolicy::review_scope(&action);
|
||||
if scope.map_or(policy.other_tools, |scope| policy.mode(scope))
|
||||
!= GuardianReviewMode::Adaptive
|
||||
{
|
||||
record_fast_decision(extension_metrics.as_deref(), "deferred", "out_of_scope");
|
||||
return None;
|
||||
}
|
||||
let guardian_evidence = thread_store.get_or_init(GuardianReviewEvidence::default);
|
||||
let context_mode = guardian_evidence.context_mode();
|
||||
let mut initial_cua_call = false;
|
||||
if scope == Some(GuardianScope::ComputerUse) && policy.initial_cua_call {
|
||||
// Thread-owned context checks checkpoint compatibility before the first CUA allowance.
|
||||
initial_cua_call = action.get("tool_name").and_then(serde_json::Value::as_str)
|
||||
== Some("js")
|
||||
&& action
|
||||
.get("connector_id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== Some("node_repl")
|
||||
&& thread_store
|
||||
.get::<GuardianV2ScoreProgress>()?
|
||||
.js_executions
|
||||
.load(Ordering::Acquire)
|
||||
== 1;
|
||||
if initial_cua_call && context_mode == GuardianContextMode::Legacy {
|
||||
record_fast_decision(
|
||||
extension_metrics.as_deref(),
|
||||
"approved",
|
||||
"initial_cua_call",
|
||||
);
|
||||
return Some(ReviewDecision::Approved);
|
||||
}
|
||||
} else if thread_store.get::<ModelInfo>().is_some() {
|
||||
let manager = self.thread_manager.upgrade()?;
|
||||
let thread_id = ThreadId::from_string(thread_store.level_id()).ok()?;
|
||||
let Ok(thread) = manager.get_thread(thread_id).await else {
|
||||
record_fast_decision(
|
||||
extension_metrics.as_deref(),
|
||||
"deferred",
|
||||
"scoring_failure",
|
||||
);
|
||||
return None;
|
||||
};
|
||||
let config = thread.config().await;
|
||||
let model = thread_store.get::<ModelInfo>()?;
|
||||
if config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.auto_review_required_for_model(&model.slug)
|
||||
{
|
||||
record_fast_decision(
|
||||
extension_metrics.as_deref(),
|
||||
"deferred",
|
||||
"required_model",
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let Some(score_progress) = thread_store.get::<GuardianV2ScoreProgress>() else {
|
||||
record_fast_decision(extension_metrics.as_deref(), "deferred", "missing_score");
|
||||
return None;
|
||||
};
|
||||
let manager = self.thread_manager.upgrade()?;
|
||||
let thread_id = ThreadId::from_string(thread_store.level_id()).ok()?;
|
||||
let Ok(thread) = manager.get_thread(thread_id).await else {
|
||||
record_fast_decision(extension_metrics.as_deref(), "deferred", "scoring_failure");
|
||||
return None;
|
||||
};
|
||||
let history = thread.conversation_history_snapshot().await;
|
||||
if context_mode == GuardianContextMode::ThreadOwned {
|
||||
let sampler = thread_store.get::<LunaSampler>()?;
|
||||
if select_parent_compaction(
|
||||
context_mode,
|
||||
&guardian_config,
|
||||
history.as_ref(),
|
||||
&sampler,
|
||||
/*legacy_model_hash*/ None,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
thread_store.insert(StrictReviewReason::IncompatibleCompaction);
|
||||
record_fast_decision(
|
||||
extension_metrics.as_deref(),
|
||||
"deferred",
|
||||
"incompatible_compaction",
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
if initial_cua_call {
|
||||
record_fast_decision(extension_metrics.as_deref(), "approved", "initial_cua_call");
|
||||
return Some(ReviewDecision::Approved);
|
||||
}
|
||||
let current_authorization = ScoreAuthorization::current(&thread).await;
|
||||
if !current_authorization.local.retained_context_complete
|
||||
|| current_authorization
|
||||
.root
|
||||
.is_some_and(|root| !root.retained_context_complete)
|
||||
{
|
||||
record_fast_decision(
|
||||
extension_metrics.as_deref(),
|
||||
"deferred",
|
||||
"incomplete_authorization",
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let scored_authorization = score_progress
|
||||
.authorization
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let latest_scored_tool_call = score_progress
|
||||
.latest_scored_tool_call
|
||||
.load(Ordering::Acquire);
|
||||
let tool_call_lag = score_progress
|
||||
.latest_tool_call
|
||||
.load(Ordering::Acquire)
|
||||
.saturating_sub(latest_scored_tool_call);
|
||||
if let Some(metrics) = &extension_metrics {
|
||||
metrics.histogram(
|
||||
TOOL_CALL_LAG_METRIC,
|
||||
i64::try_from(tool_call_lag).unwrap_or(i64::MAX),
|
||||
&[],
|
||||
);
|
||||
}
|
||||
if tool_call_lag > guardian_config.max_tool_call_lag {
|
||||
thread_store.insert(StrictReviewReason::StaleScore);
|
||||
if let Some(metrics) = &extension_metrics {
|
||||
metrics.counter(
|
||||
REVIEW_FALLBACK_METRIC,
|
||||
/*inc*/ 1,
|
||||
&[("fallback_reason", "score_lag")],
|
||||
);
|
||||
}
|
||||
record_fast_decision(extension_metrics.as_deref(), "deferred", "stale_score");
|
||||
return None;
|
||||
}
|
||||
if score_progress
|
||||
.latest_failed_tool_call
|
||||
.load(Ordering::Acquire)
|
||||
> latest_scored_tool_call
|
||||
{
|
||||
thread_store.insert(StrictReviewReason::ElevatedRisk);
|
||||
record_fast_decision(extension_metrics.as_deref(), "deferred", "scoring_failure");
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(score) = thread_store
|
||||
.get::<SecurityRiskScore>()
|
||||
.and_then(|score| score.scores.get("action_risk").copied())
|
||||
else {
|
||||
record_fast_decision(extension_metrics.as_deref(), "deferred", "missing_score");
|
||||
return None;
|
||||
};
|
||||
if score < guardian_config.review_threshold {
|
||||
if scored_authorization.as_ref() != Some(¤t_authorization) {
|
||||
thread_store.insert(StrictReviewReason::StaleScore);
|
||||
record_fast_decision(
|
||||
extension_metrics.as_deref(),
|
||||
"deferred",
|
||||
"authorization_changed",
|
||||
);
|
||||
return None;
|
||||
}
|
||||
record_fast_decision(extension_metrics.as_deref(), "approved", "low_risk");
|
||||
return Some(ReviewDecision::Approved);
|
||||
}
|
||||
if score >= guardian_config.review_threshold {
|
||||
thread_store.insert(StrictReviewReason::ElevatedRisk);
|
||||
record_fast_decision(extension_metrics.as_deref(), "deferred", "elevated_risk");
|
||||
} else {
|
||||
record_fast_decision(extension_metrics.as_deref(), "deferred", "invalid_score");
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolLifecycleContributor for GuardianV2Extension {
|
||||
fn on_tool_start<'a>(&'a self, input: ToolStartInput<'a>) -> ToolLifecycleFuture<'a> {
|
||||
Box::pin(self.score_tool(input))
|
||||
@@ -971,13 +767,12 @@ pub fn install(
|
||||
let extension = Arc::new(GuardianV2Extension {
|
||||
auth_manager,
|
||||
event_sink: registry.event_sink(),
|
||||
thread_manager: thread_manager.clone(),
|
||||
thread_manager,
|
||||
});
|
||||
registry.thread_lifecycle_contributor(extension.clone());
|
||||
registry.approval_review_contributor(Arc::new(super::approval::GuardianApprovalReviewer {
|
||||
thread_manager,
|
||||
thread_manager: extension.thread_manager.clone(),
|
||||
}));
|
||||
registry.approval_review_contributor(extension.clone());
|
||||
registry.skill_invocation_contributor(extension.clone());
|
||||
registry.tool_lifecycle_contributor(extension);
|
||||
}
|
||||
|
||||
@@ -69,7 +69,6 @@ use serde_json::json;
|
||||
|
||||
use super::GuardianV2Extension;
|
||||
use super::GuardianV2ScoreProgress;
|
||||
use super::StrictReviewReason;
|
||||
|
||||
use crate::async_scorer::config::CLASSIFICATION_OUTPUT_INSTRUCTIONS;
|
||||
use crate::async_scorer::config::DEFAULT_MODEL_CONTEXT_ITEM_TOKENS;
|
||||
@@ -285,14 +284,13 @@ async fn installed_extension_uses_http_after_warm_socket_auth_expires() -> Resul
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
r#"{"tool":"mcp_tool_call","server":"node_repl"}"#,
|
||||
/*extension_metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
r#"{"tool":"mcp_tool_call","server":"node_repl"}"#,
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
Some(ReviewDecision::Approved)
|
||||
);
|
||||
}
|
||||
@@ -669,33 +667,29 @@ async fn computer_use_only_scores_cannot_approve_other_actions() -> Result<()> {
|
||||
] {
|
||||
let prompt = action.to_string();
|
||||
assert_eq!(
|
||||
fixture
|
||||
.registry
|
||||
.fast_approval_decision(
|
||||
&fixture.session_store,
|
||||
thread_store,
|
||||
&prompt,
|
||||
thread_store
|
||||
.get::<RecordingMetrics>()
|
||||
.map(|metrics| metrics as Arc<dyn ExtensionMetrics>),
|
||||
)
|
||||
.await,
|
||||
expected,
|
||||
"unexpected fast approval for {action}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
fixture
|
||||
.registry
|
||||
.fast_approval_decision(
|
||||
&fixture.session_store,
|
||||
cached_approval(
|
||||
&fixture.registry,
|
||||
thread_store,
|
||||
"not valid JSON",
|
||||
&prompt,
|
||||
thread_store
|
||||
.get::<RecordingMetrics>()
|
||||
.map(|metrics| metrics as Arc<dyn ExtensionMetrics>),
|
||||
)
|
||||
.await,
|
||||
expected,
|
||||
"unexpected fast approval for {action}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
cached_approval(
|
||||
&fixture.registry,
|
||||
thread_store,
|
||||
"not valid JSON",
|
||||
thread_store
|
||||
.get::<RecordingMetrics>()
|
||||
.map(|metrics| metrics as Arc<dyn ExtensionMetrics>),
|
||||
)
|
||||
.await,
|
||||
None,
|
||||
"malformed approval actions must not reuse a browser score"
|
||||
);
|
||||
@@ -711,16 +705,13 @@ async fn computer_use_only_scores_cannot_approve_other_actions() -> Result<()> {
|
||||
});
|
||||
thread_store.insert(changed_model);
|
||||
assert_eq!(
|
||||
fixture
|
||||
.registry
|
||||
.fast_approval_decision(
|
||||
&fixture.session_store,
|
||||
thread_store,
|
||||
&json!({"tool": "mcp_tool_call", "server": "node_repl", "tool_name": "js"})
|
||||
.to_string(),
|
||||
/*extension_metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
&fixture.registry,
|
||||
thread_store,
|
||||
&json!({"tool": "mcp_tool_call", "server": "node_repl", "tool_name": "js"}).to_string(),
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
None,
|
||||
"a score from the previous model policy must not approve a call"
|
||||
);
|
||||
@@ -772,15 +763,13 @@ async fn computer_use_only_scores_cannot_approve_other_actions() -> Result<()> {
|
||||
sampled_at: None,
|
||||
});
|
||||
assert_eq!(
|
||||
fixture
|
||||
.registry
|
||||
.fast_approval_decision(
|
||||
&fixture.session_store,
|
||||
thread_store,
|
||||
r#"{"tool":"mcp_tool_call","server":"node_repl","tool_name":"js"}"#,
|
||||
/*extension_metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
&fixture.registry,
|
||||
thread_store,
|
||||
r#"{"tool":"mcp_tool_call","server":"node_repl","tool_name":"js"}"#,
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
None,
|
||||
"switching back to a reviewed model must not revive a skipped score"
|
||||
);
|
||||
@@ -909,14 +898,13 @@ async fn sample_configured_conversation_history_with_source(
|
||||
thread_store.insert(parent_model);
|
||||
}
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*extension_metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
None
|
||||
);
|
||||
registry.thread_lifecycle_contributors()[0]
|
||||
@@ -1123,16 +1111,15 @@ impl GuardianFailureFixture {
|
||||
.await?;
|
||||
thread_store.insert(RecordingMetrics::default());
|
||||
assert_eq!(
|
||||
self.registry
|
||||
.fast_approval_decision(
|
||||
&self.session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
thread_store
|
||||
.get::<RecordingMetrics>()
|
||||
.map(|metrics| metrics as Arc<dyn ExtensionMetrics>),
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
&self.registry,
|
||||
thread_store,
|
||||
"review action",
|
||||
thread_store
|
||||
.get::<RecordingMetrics>()
|
||||
.map(|metrics| metrics as Arc<dyn ExtensionMetrics>),
|
||||
)
|
||||
.await,
|
||||
None
|
||||
);
|
||||
assert!(
|
||||
@@ -1434,7 +1421,6 @@ max_recent_non_user_entries = 8
|
||||
let retained_action_bytes = i64::try_from(serde_json::to_string_pretty(&action)?.len())?;
|
||||
assert_eq!(action["tool"], "read_file");
|
||||
|
||||
let session_store = ExtensionData::new("session-1");
|
||||
let thread_store = test.codex.thread_extension_data();
|
||||
let score_progress = thread_store
|
||||
.get::<GuardianV2ScoreProgress>()
|
||||
@@ -1451,7 +1437,6 @@ max_recent_non_user_entries = 8
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(thread_store.get::<StrictReviewReason>(), None);
|
||||
thread_store.insert(SecurityRiskScore {
|
||||
scores: BTreeMap::from([("action_risk".to_owned(), 0.65)]),
|
||||
call_id: None,
|
||||
@@ -1459,22 +1444,17 @@ max_recent_non_user_entries = 8
|
||||
sampled_at: None,
|
||||
});
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
thread_store
|
||||
.get::<RecordingMetrics>()
|
||||
.map(|metrics| metrics as Arc<dyn ExtensionMetrics>),
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
"review action",
|
||||
thread_store
|
||||
.get::<RecordingMetrics>()
|
||||
.map(|metrics| metrics as Arc<dyn ExtensionMetrics>),
|
||||
)
|
||||
.await,
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
thread_store.remove::<StrictReviewReason>().as_deref(),
|
||||
Some(&StrictReviewReason::ElevatedRisk)
|
||||
);
|
||||
thread_store.insert(SecurityRiskScore {
|
||||
scores: BTreeMap::from([("action_risk".to_owned(), 0.55)]),
|
||||
call_id: None,
|
||||
@@ -1482,16 +1462,15 @@ max_recent_non_user_entries = 8
|
||||
sampled_at: None,
|
||||
});
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
thread_store
|
||||
.get::<RecordingMetrics>()
|
||||
.map(|metrics| metrics as Arc<dyn ExtensionMetrics>),
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
"review action",
|
||||
thread_store
|
||||
.get::<RecordingMetrics>()
|
||||
.map(|metrics| metrics as Arc<dyn ExtensionMetrics>),
|
||||
)
|
||||
.await,
|
||||
Some(ReviewDecision::Approved)
|
||||
);
|
||||
|
||||
@@ -1505,16 +1484,15 @@ max_recent_non_user_entries = 8
|
||||
.latest_tool_call
|
||||
.store(/*val*/ 3, Ordering::Release);
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
thread_store
|
||||
.get::<RecordingMetrics>()
|
||||
.map(|metrics| metrics as Arc<dyn ExtensionMetrics>),
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
"review action",
|
||||
thread_store
|
||||
.get::<RecordingMetrics>()
|
||||
.map(|metrics| metrics as Arc<dyn ExtensionMetrics>),
|
||||
)
|
||||
.await,
|
||||
Some(ReviewDecision::Approved)
|
||||
);
|
||||
|
||||
@@ -1524,37 +1502,31 @@ max_recent_non_user_entries = 8
|
||||
.latest_tool_call
|
||||
.store(/*val*/ 4, Ordering::Release);
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
thread_store
|
||||
.get::<RecordingMetrics>()
|
||||
.map(|metrics| metrics as Arc<dyn ExtensionMetrics>),
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
"review action",
|
||||
thread_store
|
||||
.get::<RecordingMetrics>()
|
||||
.map(|metrics| metrics as Arc<dyn ExtensionMetrics>),
|
||||
)
|
||||
.await,
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
thread_store.remove::<StrictReviewReason>().as_deref(),
|
||||
Some(&StrictReviewReason::StaleScore)
|
||||
);
|
||||
|
||||
score_progress
|
||||
.latest_scored_tool_call
|
||||
.store(/*val*/ 2, Ordering::Release);
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
thread_store
|
||||
.get::<RecordingMetrics>()
|
||||
.map(|metrics| metrics as Arc<dyn ExtensionMetrics>),
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
"review action",
|
||||
thread_store
|
||||
.get::<RecordingMetrics>()
|
||||
.map(|metrics| metrics as Arc<dyn ExtensionMetrics>),
|
||||
)
|
||||
.await,
|
||||
Some(ReviewDecision::Approved)
|
||||
);
|
||||
|
||||
@@ -1822,7 +1794,6 @@ async fn contributor_uses_model_defaults_and_preserves_local_overrides() -> Resu
|
||||
.expect("planned action should be a text item");
|
||||
assert!(action.len() <= TruncationPolicy::Tokens(/*limit*/ 128).byte_budget());
|
||||
|
||||
let session_store = ExtensionData::new("session-1");
|
||||
let thread_store = test.codex.thread_extension_data();
|
||||
let guardian_config = thread_store
|
||||
.get::<crate::async_scorer::config::GuardianV2Config>()
|
||||
@@ -1857,14 +1828,13 @@ async fn contributor_uses_model_defaults_and_preserves_local_overrides() -> Resu
|
||||
sampled_at: None,
|
||||
});
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*extension_metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
Some(ReviewDecision::Approved)
|
||||
);
|
||||
|
||||
@@ -1931,7 +1901,6 @@ async fn assert_luna_pool_context(thread_context_enabled: bool) -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
let thread_id = test.session_configured.thread_id;
|
||||
let session_store = ExtensionData::new("session-1");
|
||||
let thread_store = test.codex.thread_extension_data();
|
||||
assert_eq!(request["model"], "gpt-5.6-luna");
|
||||
let classifier_thread_id = request["client_metadata"]["thread_id"]
|
||||
@@ -2043,14 +2012,13 @@ async fn assert_luna_pool_context(thread_context_enabled: bool) -> Result<()> {
|
||||
"risk scores should not be persisted unless explicitly enabled"
|
||||
);
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*extension_metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
None
|
||||
);
|
||||
thread_store.insert(SecurityRiskScore {
|
||||
@@ -2060,14 +2028,13 @@ async fn assert_luna_pool_context(thread_context_enabled: bool) -> Result<()> {
|
||||
sampled_at: None,
|
||||
});
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*extension_metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
None
|
||||
);
|
||||
|
||||
@@ -2078,14 +2045,13 @@ async fn assert_luna_pool_context(thread_context_enabled: bool) -> Result<()> {
|
||||
sampled_at: None,
|
||||
});
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*extension_metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
Some(ReviewDecision::Approved)
|
||||
);
|
||||
|
||||
@@ -2097,14 +2063,13 @@ async fn assert_luna_pool_context(thread_context_enabled: bool) -> Result<()> {
|
||||
sampled_at: None,
|
||||
});
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
&disabled_thread_store,
|
||||
"review action",
|
||||
/*extension_metrics*/ None
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
&disabled_thread_store,
|
||||
"review action",
|
||||
/*metrics*/ None
|
||||
)
|
||||
.await,
|
||||
None
|
||||
);
|
||||
|
||||
@@ -2254,14 +2219,13 @@ async fn contributor_skips_required_models_in_standard_scope() -> Result<()> {
|
||||
sampled_at: None,
|
||||
});
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
r#"{"tool":"mcp_tool_call","server":"node_repl"}"#,
|
||||
/*extension_metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
r#"{"tool":"mcp_tool_call","server":"node_repl"}"#,
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
None
|
||||
);
|
||||
|
||||
@@ -2316,7 +2280,6 @@ async fn cached_score_survives_compaction_and_internal_context_but_not_user_inpu
|
||||
/*model_defaults*/ None,
|
||||
)
|
||||
.await?;
|
||||
let session_store = ExtensionData::new("session-1");
|
||||
let thread_store = test.codex.thread_extension_data();
|
||||
let progress = thread_store.get::<GuardianV2ScoreProgress>().unwrap();
|
||||
tokio::time::timeout(ASYNC_TEST_TIMEOUT, async {
|
||||
@@ -2346,14 +2309,13 @@ async fn cached_score_survives_compaction_and_internal_context_but_not_user_inpu
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*extension_metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
Some(ReviewDecision::Approved),
|
||||
);
|
||||
|
||||
@@ -2369,14 +2331,13 @@ async fn cached_score_survives_compaction_and_internal_context_but_not_user_inpu
|
||||
}])
|
||||
.await?;
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*extension_metrics*/ None
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*metrics*/ None
|
||||
)
|
||||
.await,
|
||||
None,
|
||||
);
|
||||
Ok(())
|
||||
@@ -2408,15 +2369,13 @@ async fn assert_compaction_approval_policy(thread_context_enabled: bool) -> Resu
|
||||
sampled_at: None,
|
||||
});
|
||||
assert_eq!(
|
||||
fixture
|
||||
.registry
|
||||
.fast_approval_decision(
|
||||
&fixture.session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*extension_metrics*/ None
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
&fixture.registry,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*metrics*/ None
|
||||
)
|
||||
.await,
|
||||
Some(ReviewDecision::Approved)
|
||||
);
|
||||
let authorization = fixture.test.codex.guardian_authorization_version().await;
|
||||
@@ -2472,21 +2431,15 @@ async fn assert_compaction_approval_policy(thread_context_enabled: bool) -> Resu
|
||||
.js_executions
|
||||
.store(/*val*/ 1, Ordering::Release);
|
||||
assert_eq!(
|
||||
fixture
|
||||
.registry
|
||||
.fast_approval_decision(
|
||||
&fixture.session_store,
|
||||
thread_store,
|
||||
prompt,
|
||||
/*extension_metrics*/ None
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
&fixture.registry,
|
||||
thread_store,
|
||||
prompt,
|
||||
/*metrics*/ None
|
||||
)
|
||||
.await,
|
||||
(!thread_context_enabled).then_some(ReviewDecision::Approved)
|
||||
);
|
||||
assert_eq!(
|
||||
thread_store.remove::<StrictReviewReason>().as_deref(),
|
||||
thread_context_enabled.then_some(&StrictReviewReason::IncompatibleCompaction)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -2525,14 +2478,13 @@ async fn contributor_counts_failed_thread_lookups_toward_score_lag() -> Result<(
|
||||
sampled_at: None,
|
||||
});
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*extension_metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
Some(ReviewDecision::Approved)
|
||||
);
|
||||
|
||||
@@ -2563,14 +2515,13 @@ async fn contributor_counts_failed_thread_lookups_toward_score_lag() -> Result<(
|
||||
|
||||
assert_eq!(score_progress.latest_tool_call.load(Ordering::Acquire), 2);
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*extension_metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
None
|
||||
);
|
||||
|
||||
@@ -3098,14 +3049,13 @@ async fn assert_parent_compaction_reuse(thread_context_enabled: bool) -> Result<
|
||||
// Raw injection did not attach producer provenance to the live checkpoint.
|
||||
// The sample's mock snapshot cannot make that live checkpoint safe for approval.
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*extension_metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
(!thread_context_enabled).then_some(ReviewDecision::Approved),
|
||||
);
|
||||
|
||||
@@ -3149,14 +3099,13 @@ async fn assert_parent_compaction_reuse(thread_context_enabled: bool) -> Result<
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
registry
|
||||
.fast_approval_decision(
|
||||
&session_store,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*extension_metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
cached_approval(
|
||||
®istry,
|
||||
thread_store,
|
||||
"review action",
|
||||
/*metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -3289,3 +3238,68 @@ async fn contributor_bounds_oversized_actions_and_fairly_truncates_nested_fields
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct CacheMiss;
|
||||
impl codex_extension_api::SynchronousApprovalReviewer for CacheMiss {
|
||||
fn review(
|
||||
&self,
|
||||
_reason: codex_protocol::approvals::GuardianReviewReason,
|
||||
) -> codex_extension_api::ExtensionFuture<'_, ReviewDecision> {
|
||||
Box::pin(async { ReviewDecision::denied("cache miss") })
|
||||
}
|
||||
}
|
||||
|
||||
/// Exercises decision routing; a fresh review is observed as a cache miss.
|
||||
async fn cached_approval(
|
||||
registry: &codex_extension_api::ExtensionRegistry<Config>,
|
||||
store: &ExtensionData,
|
||||
action: &str,
|
||||
metrics: Option<Arc<dyn ExtensionMetrics>>,
|
||||
) -> Option<ReviewDecision> {
|
||||
let action = serde_json::from_str(action).unwrap_or(serde_json::Value::Null);
|
||||
let category = match review_scope(&action) {
|
||||
Some(category) => category,
|
||||
None if store.get::<super::GuardianV2Config>()?.policy.other_tools
|
||||
== codex_protocol::openai_models::GuardianReviewMode::Adaptive =>
|
||||
{
|
||||
codex_protocol::openai_models::GuardianScope::Shell
|
||||
}
|
||||
None => {
|
||||
super::super::metrics::record_fast_decision(
|
||||
metrics.as_deref(),
|
||||
"deferred",
|
||||
"out_of_scope",
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let input = codex_extension_api::ApprovalDecisionInput {
|
||||
approval_id: "cache-probe",
|
||||
action: &action,
|
||||
thread_id: codex_protocol::ThreadId::from_string(store.level_id()).unwrap(),
|
||||
thread_store: store,
|
||||
category,
|
||||
approval_policy: codex_protocol::protocol::AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::AutoReview,
|
||||
require_guardian: false,
|
||||
require_fresh_review: false,
|
||||
full_access: false,
|
||||
metrics,
|
||||
synchronous_reviewer: &CacheMiss,
|
||||
};
|
||||
match registry.decide_approval(&input).await {
|
||||
Some(codex_extension_api::ApprovalDecision::Allow) => Some(ReviewDecision::Approved),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn review_scope(action: &serde_json::Value) -> Option<GuardianScope> {
|
||||
match action.get("tool").and_then(serde_json::Value::as_str)? {
|
||||
"mcp_tool_call" => action
|
||||
.get("server")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(GuardianScope::for_mcp_server),
|
||||
"network_access" => Some(GuardianScope::Network),
|
||||
tool => GuardianScope::for_tool(&ToolName::plain(tool)),
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ mod trusted_tools;
|
||||
|
||||
pub(crate) use config::DEFAULT_MODEL_CONTEXT_ITEM_TOKENS;
|
||||
pub(crate) use config::GuardianV2Config;
|
||||
pub use extension::StrictReviewReason;
|
||||
pub(crate) use extension::install;
|
||||
pub(crate) use transcript::MAX_TOOL_ENTRY_TOKENS;
|
||||
pub(crate) use transcript::RenderedContext;
|
||||
|
||||
@@ -15,7 +15,6 @@ use codex_protocol::ThreadId;
|
||||
mod async_scorer;
|
||||
mod sync_reviewer;
|
||||
|
||||
pub use async_scorer::StrictReviewReason;
|
||||
pub use sync_reviewer::GuardianExtension as GuardianReviewerExtension;
|
||||
pub use sync_reviewer::GuardianThreadContext as GuardianReviewerThreadContext;
|
||||
|
||||
|
||||
Reference in new issue
Block a user