mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
Restrict Guardian native multi-agent history to the OpenAI provider (#59737)
## What changed Require the `openai` provider ID as well as `GuardianNativeMultiAgentHistory` to enable native agent history and review of multi-agent calls. Apply the same check to Guardian prompt construction, collaboration tool registration, and Guardian v2 configuration. ## Testing Add coverage for fresh and resumed sessions with the feature enabled and disabled on OpenAI, and enabled on Bedrock and LiteLLM. Verify that only OpenAI sessions with the feature enabled request native review and include the agent call as evidence. GitOrigin-RevId: bfbe547b0544e66763dca7d1e1376f805994bcb2
This commit is contained in:
@@ -97,6 +97,7 @@ use codex_model_provider_info::GCP_PROVIDER_ID;
|
||||
use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::OLLAMA_CHAT_PROVIDER_REMOVED_ERROR;
|
||||
use codex_model_provider_info::OPENAI_PROVIDER_ID;
|
||||
use codex_model_provider_info::built_in_model_providers;
|
||||
use codex_model_provider_info::merge_configured_model_providers;
|
||||
use codex_models_manager::ModelsManagerConfig;
|
||||
@@ -1711,6 +1712,14 @@ impl Config {
|
||||
.unwrap_or(model_messages.auto_review().policy)
|
||||
}
|
||||
|
||||
/// Native Guardian agent history requires the reviewed thread's OpenAI provider.
|
||||
pub fn guardian_native_multi_agent_history_enabled(&self) -> bool {
|
||||
self.model_provider_id == OPENAI_PROVIDER_ID
|
||||
&& self
|
||||
.features
|
||||
.enabled(Feature::GuardianNativeMultiAgentHistory)
|
||||
}
|
||||
|
||||
pub(crate) fn multi_agent_version_override(&self) -> Option<MultiAgentVersion> {
|
||||
if self.features.enabled(Feature::MultiAgentV2) {
|
||||
Some(MultiAgentVersion::V2)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use codex_extension_api::ConversationHistorySnapshot;
|
||||
use codex_features::Feature;
|
||||
use codex_guardian_context::CollectedContext;
|
||||
use codex_guardian_context::ContextProfile;
|
||||
use codex_guardian_context::GuardianRootMessage;
|
||||
@@ -145,9 +144,7 @@ pub(super) async fn collect_guardian_prompt_items_with_parent_turn(
|
||||
profile.transcript.options.native_multi_agent_namespace = parent_context
|
||||
.is_some_and(|context| {
|
||||
let turn = context.turn();
|
||||
turn.config
|
||||
.features
|
||||
.enabled(Feature::GuardianNativeMultiAgentHistory)
|
||||
turn.config.guardian_native_multi_agent_history_enabled()
|
||||
&& turn.multi_agent_version == codex_protocol::protocol::MultiAgentVersion::V2
|
||||
})
|
||||
.then(|| {
|
||||
|
||||
@@ -1500,8 +1500,7 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, registry: &mut Too
|
||||
let tool_namespace = turn_context.config.multi_agent_v2.tool_namespace.as_deref();
|
||||
let native_review = if turn_context
|
||||
.config
|
||||
.features
|
||||
.enabled(Feature::GuardianNativeMultiAgentHistory)
|
||||
.guardian_native_multi_agent_history_enabled()
|
||||
{
|
||||
NativeReview::Required
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Guardian retains native encrypted messages and historical calls across incremental reviews.
|
||||
//! OpenAI-only native agent review preserves encrypted history across reviews and resumes.
|
||||
|
||||
use codex_core::config::Constrained;
|
||||
use codex_features::Feature;
|
||||
@@ -21,6 +21,102 @@ use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use test_case::test_case;
|
||||
|
||||
#[test_case("openai", true, true; "openai_treatment")]
|
||||
#[test_case("openai", false, false; "openai_control")]
|
||||
#[test_case("amazon-bedrock-runtime", true, false; "bedrock")]
|
||||
#[test_case("litellm-hybrid", true, false; "litellm")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn native_agent_review_requires_openai_on_start_and_resume(
|
||||
provider_id: &'static str,
|
||||
requested: bool,
|
||||
expected_review: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = responses::start_mock_server().await;
|
||||
let mut previous = None;
|
||||
for phase in ["fresh", "resumed"] {
|
||||
let mut builder = test_codex()
|
||||
.with_model("gpt-5.5")
|
||||
.with_config(move |config| {
|
||||
super::configure_scenario_catalog(config);
|
||||
config.model_provider_id = provider_id.to_owned();
|
||||
// Keep the mock Responses transport and OpenAI display name for every provider ID.
|
||||
config
|
||||
.model_providers
|
||||
.insert(provider_id.to_owned(), config.model_provider.clone());
|
||||
for feature in [Feature::Collab, Feature::MultiAgentV2] {
|
||||
config
|
||||
.features
|
||||
.enable(feature)
|
||||
.expect("enable multi-agent feature");
|
||||
}
|
||||
config
|
||||
.features
|
||||
.set_enabled(Feature::GuardianNativeMultiAgentHistory, requested)
|
||||
.expect("set native agent review flag");
|
||||
config.multi_agent_v2.tool_namespace = Some("collaboration".to_owned());
|
||||
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
|
||||
config.permissions.approval_policy =
|
||||
Constrained::allow_any(AskForApproval::OnRequest);
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(PermissionProfile::read_only())
|
||||
.expect("set read-only permissions");
|
||||
});
|
||||
let test = if let Some(previous) = &previous {
|
||||
builder.restart_with_auto_env(&server, previous).await?
|
||||
} else {
|
||||
builder.build_with_auto_env(&server).await?
|
||||
};
|
||||
let call_id = format!("send-{phase}");
|
||||
let mut call = responses::ev_function_call_with_namespace(
|
||||
&call_id,
|
||||
"collaboration",
|
||||
"send_message",
|
||||
r#"{"target":"/root/missing","message":"opaque-message"}"#,
|
||||
);
|
||||
call["item"]["encrypted_content"] = json!("opaque-replay");
|
||||
call["item"]["encrypted_function_args"] = json!(["message"]);
|
||||
let mut stream = vec![responses::sse(vec![call, responses::ev_completed("send")])];
|
||||
if expected_review {
|
||||
stream.push(responses::sse(vec![
|
||||
responses::ev_assistant_message("assessment", r#"{"outcome":"allow","rationale":"Mock approval.","risk_level":"low","user_authorization":"high"}"#),
|
||||
responses::ev_completed("review"),
|
||||
]));
|
||||
}
|
||||
stream.push(responses::sse(vec![
|
||||
responses::ev_assistant_message("done", "The worker is unavailable."),
|
||||
responses::ev_completed("done"),
|
||||
]));
|
||||
let mock = responses::mount_sse_sequence(&server, stream).await;
|
||||
test.submit_text_turn("Ask the missing worker to confirm receipt.")
|
||||
.await?;
|
||||
let requests = mock.requests();
|
||||
let reviews = requests
|
||||
.iter()
|
||||
.filter(|request| {
|
||||
request.body_json()["client_metadata"]["x-openai-subagent"] == "guardian"
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
reviews.len(),
|
||||
usize::from(expected_review),
|
||||
"{phase}: {provider_id}"
|
||||
);
|
||||
if let Some(review) = reviews.first() {
|
||||
assert!(
|
||||
review
|
||||
.inputs_of_type("function_call")
|
||||
.iter()
|
||||
.any(|call| call["call_id"] == call_id),
|
||||
"{phase}: native review evidence"
|
||||
);
|
||||
}
|
||||
previous = Some(test);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), test_case(None; "agent_reply"))]
|
||||
#[test_case(Some("functions"); "native_default_namespace")]
|
||||
#[test_case(Some("team"); "native_custom_namespace")]
|
||||
|
||||
@@ -80,9 +80,7 @@ impl GuardianV2Config {
|
||||
..configured
|
||||
});
|
||||
resolved.policy = GuardianPolicyLoader::new(Some(&legacy));
|
||||
resolved.review_multi_agent_calls = config
|
||||
.features
|
||||
.enabled(codex_features::Feature::GuardianNativeMultiAgentHistory);
|
||||
resolved.review_multi_agent_calls = config.guardian_native_multi_agent_history_enabled();
|
||||
resolved.multi_agent_namespace = config.multi_agent_v2.tool_namespace.clone();
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user