mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Improve sandboxing, MCP errors, and cached approvals (#41196)
Expand Windows deny-read globs robustly across protected directories while preserving filesystem enforcement. Preserve structured MCP tool and resource errors through app-server responses. Bind cached Guardian classifications to current local and root authorization state so stale scores cannot approve actions. Add regression coverage for the sandbox resolver, structured protocol errors, and authorization changes. GitOrigin-RevId: 4b80ed724d869afeca79204222d8465fa99d3a24
This commit is contained in:
1 parent
0182ff3480
commit
035295b46e
18 files changed
+1285
-81
No files matched your search
@@ -0,0 +1,23 @@
|
||||
//! Binds cached classifier results to the user authorization they evaluated.
|
||||
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::GuardianAuthorizationVersion;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(super) struct ScoreAuthorization {
|
||||
pub(super) local: GuardianAuthorizationVersion,
|
||||
pub(super) root: Option<GuardianAuthorizationVersion>,
|
||||
}
|
||||
|
||||
impl ScoreAuthorization {
|
||||
pub(super) async fn current(thread: &CodexThread) -> Self {
|
||||
let root = thread
|
||||
.guardian_root_snapshot()
|
||||
.await
|
||||
.map(|snapshot| snapshot.authorization_version);
|
||||
Self {
|
||||
local: thread.guardian_authorization_version().await,
|
||||
root,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::Weak;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -44,6 +45,7 @@ use codex_protocol::security_risk::SecurityRiskScore;
|
||||
|
||||
use super::action::GuardianAction;
|
||||
use super::action::RenderedAction;
|
||||
use super::authorization::ScoreAuthorization;
|
||||
use super::config::GuardianV2Config;
|
||||
use super::config::GuardianV2ReviewScope;
|
||||
use super::metrics::REVIEW_FALLBACK_METRIC;
|
||||
@@ -113,6 +115,8 @@ struct GuardianV2ScoreProgress {
|
||||
latest_tool_call: AtomicUsize,
|
||||
latest_scored_tool_call: AtomicUsize,
|
||||
latest_failed_tool_call: AtomicUsize,
|
||||
// Serialize successful score publication with its authorization metadata.
|
||||
authorization: Mutex<Option<ScoreAuthorization>>,
|
||||
metrics: Option<Arc<dyn ExtensionMetrics>>,
|
||||
}
|
||||
|
||||
@@ -262,6 +266,17 @@ impl ApprovalReviewContributor for GuardianV2Extension {
|
||||
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 current_authorization = ScoreAuthorization::current(&thread).await;
|
||||
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);
|
||||
@@ -306,6 +321,15 @@ impl ApprovalReviewContributor for GuardianV2Extension {
|
||||
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);
|
||||
}
|
||||
@@ -515,6 +539,10 @@ impl GuardianV2Extension {
|
||||
let root_conversation = root_snapshot.map(|snapshot| snapshot.messages);
|
||||
let authorization_version =
|
||||
guardian_evidence.authorization_version(conversation_history.as_ref());
|
||||
let score_authorization = ScoreAuthorization {
|
||||
local: authorization_version,
|
||||
root: root_authorization_version,
|
||||
};
|
||||
let trusted_user_inputs =
|
||||
guardian_evidence.user_input_fragments(conversation_history.as_ref());
|
||||
let transcript = guardian_config
|
||||
@@ -658,12 +686,26 @@ impl GuardianV2Extension {
|
||||
),
|
||||
sampled_at: Some(sampled_at.into()),
|
||||
};
|
||||
let accepted =
|
||||
thread
|
||||
.thread_extension_data()
|
||||
.insert_if(score.clone(), |previous| {
|
||||
previous.is_none_or(|previous| previous.sampled_at < score.sampled_at)
|
||||
});
|
||||
if score_authorization != ScoreAuthorization::current(&thread).await {
|
||||
return Ok(ClassificationOutcome::Superseded);
|
||||
}
|
||||
let accepted = {
|
||||
let mut scored_authorization = score_progress
|
||||
.authorization
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let accepted =
|
||||
thread
|
||||
.thread_extension_data()
|
||||
.insert_if(score.clone(), |previous| {
|
||||
previous
|
||||
.is_none_or(|previous| previous.sampled_at < score.sampled_at)
|
||||
});
|
||||
if accepted {
|
||||
*scored_authorization = Some(score_authorization);
|
||||
}
|
||||
accepted
|
||||
};
|
||||
tracing::info!(
|
||||
%thread_id,
|
||||
%turn_id,
|
||||
|
||||
@@ -937,6 +937,13 @@ async fn sample_configured_conversation_history_with_source(
|
||||
let tool_payload = ToolPayload::Function {
|
||||
arguments: arguments.to_owned(),
|
||||
};
|
||||
if !conversation_history.is_empty() {
|
||||
Box::pin(
|
||||
test.codex
|
||||
.inject_response_items(conversation_history.clone()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let conversation_history = TestConversationHistory(conversation_history);
|
||||
|
||||
registry.tool_lifecycle_contributors()[0]
|
||||
@@ -2153,6 +2160,11 @@ async fn contributor_skips_required_models_in_standard_scope() -> Result<()> {
|
||||
.await;
|
||||
model_info.slug = "protected-model".to_owned();
|
||||
thread_store.insert(model_info);
|
||||
let authorization = super::ScoreAuthorization::current(&test.codex).await;
|
||||
let progress = thread_store
|
||||
.get::<GuardianV2ScoreProgress>()
|
||||
.expect("Guardian v2 should track score progress per thread");
|
||||
*progress.authorization.lock().unwrap() = Some(authorization);
|
||||
thread_store.insert(SecurityRiskScore {
|
||||
scores: BTreeMap::from([("action_risk".to_owned(), 0.25)]),
|
||||
call_id: None,
|
||||
@@ -2597,7 +2609,6 @@ async fn contributor_reuses_the_latest_compatible_parent_compaction() -> Result<
|
||||
let session_store = ExtensionData::new("session-1");
|
||||
let thread_store = test.codex.thread_extension_data();
|
||||
let metrics = Arc::new(RecordingMetrics::default());
|
||||
thread_store.insert(parent_model);
|
||||
registry.thread_lifecycle_contributors()[0]
|
||||
.on_thread_start(ThreadStartInput {
|
||||
config: &config,
|
||||
@@ -2643,6 +2654,13 @@ async fn contributor_reuses_the_latest_compatible_parent_compaction() -> Result<
|
||||
},
|
||||
]);
|
||||
|
||||
Box::pin(
|
||||
test.codex
|
||||
.inject_response_items(conversation_history.0.clone()),
|
||||
)
|
||||
.await?;
|
||||
thread_store.insert(parent_model);
|
||||
|
||||
registry.tool_lifecycle_contributors()[0]
|
||||
.on_tool_start(ToolStartInput {
|
||||
session_store: &session_store,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod action;
|
||||
mod authorization;
|
||||
mod config;
|
||||
mod extension;
|
||||
mod metrics;
|
||||
|
||||
Reference in new issue
Block a user