Include Node REPL policy in turn metadata (#38241)

## What changed

- Parse `node_repl_auto_review_required` and `node_repl_disabled` from model catalog entries and include their values in each turn's Responses API metadata.
- Treat both fields as reserved metadata so client-provided values cannot override the selected model's policy.
- Use the selected review model's catalog entry when building metadata for review turns.

## Testing

- Cover metadata propagation across model switches, review turns, Responses API requests, and MCP tool calls.

GitOrigin-RevId: c1f63426eeb8e559abff439fbc618716a4908aff
This commit is contained in:
Ankush Gupta
2026-08-12 19:37:51 +00:00
committed by copyberry
parent d6eefb26a6
commit 74004b5397
23 changed files with 223 additions and 27 deletions
@@ -65,6 +65,8 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,
@@ -103,6 +103,8 @@ async fn models_client_hits_models_endpoint() {
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: true,
node_repl_disabled: true,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,
@@ -136,6 +138,8 @@ async fn models_client_hits_models_endpoint() {
assert_eq!(models.len(), 1);
assert_eq!(models[0].slug, "gpt-test");
assert!(models[0].node_repl_auto_review_required);
assert!(models[0].node_repl_disabled);
let received = server
.received_requests()
+27 -15
View File
@@ -97,6 +97,13 @@ fn mcp_turn_metadata_context(turn_context: &TurnContext) -> McpTurnMetadataConte
}
}
fn expected_mcp_turn_metadata(turn_context: &TurnContext) -> serde_json::Value {
turn_context
.turn_metadata_state
.current_meta_value_for_mcp_request(mcp_turn_metadata_context(turn_context))
.expect("turn metadata")
}
fn write_sample_plugin_mcp(codex_home: &std::path::Path) {
let plugin_root = codex_home.join("plugins/cache/test/sample/local");
std::fs::create_dir_all(plugin_root.join(".codex-plugin")).expect("create plugin manifest dir");
@@ -1004,10 +1011,16 @@ fn truncate_mcp_tool_result_for_event_bounds_large_error() {
#[tokio::test]
async fn mcp_tool_call_request_meta_includes_turn_metadata_for_custom_server() {
let (_, turn_context) = make_session_and_context().await;
let expected_turn_metadata = turn_context
turn_context
.turn_metadata_state
.current_meta_value_for_mcp_request(mcp_turn_metadata_context(&turn_context))
.expect("turn metadata");
.set_responsesapi_client_metadata(HashMap::from([
(
"node_repl_auto_review_required".to_string(),
"true".to_string(),
),
("node_repl_disabled".to_string(), "true".to_string()),
]));
let expected_turn_metadata = expected_mcp_turn_metadata(&turn_context);
let meta = build_mcp_tool_call_request_meta(
&turn_context,
@@ -1026,6 +1039,14 @@ async fn mcp_tool_call_request_meta_includes_turn_metadata_for_custom_server() {
.and_then(serde_json::Value::as_str),
Some(turn_context.model_info.slug.as_str())
);
assert_eq!(
turn_metadata["node_repl_auto_review_required"],
serde_json::Value::Bool(turn_context.model_info.node_repl_auto_review_required),
);
assert_eq!(
turn_metadata["node_repl_disabled"],
serde_json::Value::Bool(turn_context.model_info.node_repl_disabled),
);
assert_eq!(
turn_metadata
.get("reasoning_effort")
@@ -1122,10 +1143,7 @@ async fn mcp_sandbox_cwd_is_none_for_unselected_server_environment() -> anyhow::
#[tokio::test]
async fn plugin_mcp_tool_call_request_meta_includes_plugin_id() {
let (_, turn_context) = make_session_and_context().await;
let expected_turn_metadata = turn_context
.turn_metadata_state
.current_meta_value_for_mcp_request(mcp_turn_metadata_context(&turn_context))
.expect("turn metadata");
let expected_turn_metadata = expected_mcp_turn_metadata(&turn_context);
let mut metadata = approval_metadata(
/*connector_id*/ None, /*connector_name*/ None,
/*connector_description*/ None, /*tool_title*/ None,
@@ -1246,10 +1264,7 @@ async fn mcp_tool_call_item_includes_app_identity() {
#[tokio::test]
async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps_meta() {
let (_, turn_context) = make_session_and_context().await;
let expected_turn_metadata = turn_context
.turn_metadata_state
.current_meta_value_for_mcp_request(mcp_turn_metadata_context(&turn_context))
.expect("turn metadata");
let expected_turn_metadata = expected_mcp_turn_metadata(&turn_context);
let metadata = McpToolApprovalMetadata {
annotations: None,
connector_id: Some("calendar".to_string()),
@@ -1297,10 +1312,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps
#[tokio::test]
async fn codex_apps_tool_call_request_meta_includes_call_id_without_existing_codex_apps_meta() {
let (_, turn_context) = make_session_and_context().await;
let expected_turn_metadata = turn_context
.turn_metadata_state
.current_meta_value_for_mcp_request(mcp_turn_metadata_context(&turn_context))
.expect("turn metadata");
let expected_turn_metadata = expected_mcp_turn_metadata(&turn_context);
assert_eq!(
build_mcp_tool_call_request_meta(
+14
View File
@@ -44,6 +44,8 @@ pub(crate) const THREAD_SOURCE_KEY: &str = "thread_source";
pub(crate) const SANDBOX_KEY: &str = "sandbox";
pub(crate) const SANDBOX_MODE_KEY: &str = "sandbox_mode";
pub(crate) const AUTO_REVIEW_ENABLED_KEY: &str = "auto_review_enabled";
pub(crate) const NODE_REPL_AUTO_REVIEW_REQUIRED_KEY: &str = "node_repl_auto_review_required";
pub(crate) const NODE_REPL_DISABLED_KEY: &str = "node_repl_disabled";
pub(crate) const WORKSPACES_KEY: &str = "workspaces";
// App-server clients can specify additional metadata in the `responsesapi_client_metadata` param
@@ -73,6 +75,8 @@ const RESERVED_METADATA_KEYS: &[&str] = &[
SANDBOX_KEY,
SANDBOX_MODE_KEY,
AUTO_REVIEW_ENABLED_KEY,
NODE_REPL_AUTO_REVIEW_REQUIRED_KEY,
NODE_REPL_DISABLED_KEY,
WORKSPACES_KEY,
];
const MAX_EXTRA_METADATA_ENTRIES: usize = 16;
@@ -213,6 +217,8 @@ pub struct CodexResponsesMetadata {
pub(crate) sandbox: Option<String>,
pub(crate) sandbox_mode: Option<String>,
pub(crate) auto_review_enabled: Option<bool>,
pub(crate) node_repl_auto_review_required: Option<bool>,
pub(crate) node_repl_disabled: Option<bool>,
pub(crate) workspaces: BTreeMap<String, TurnMetadataWorkspace>,
pub(crate) tool_namespaces_info: Option<TurnToolNamespacesInfo>,
pub(crate) turn_started_at_unix_ms: Option<i64>,
@@ -244,6 +250,8 @@ impl CodexResponsesMetadata {
sandbox: None,
sandbox_mode: None,
auto_review_enabled: None,
node_repl_auto_review_required: None,
node_repl_disabled: None,
workspaces: BTreeMap::new(),
tool_namespaces_info: None,
turn_started_at_unix_ms: None,
@@ -360,6 +368,8 @@ impl CodexResponsesMetadata {
sandbox: self.sandbox.as_deref(),
sandbox_mode: self.sandbox_mode.as_deref(),
auto_review_enabled: self.auto_review_enabled,
node_repl_auto_review_required: self.node_repl_auto_review_required,
node_repl_disabled: self.node_repl_disabled,
workspaces: non_empty_workspaces(&self.workspaces),
tool_namespaces_info: self.tool_namespaces_info.as_ref(),
turn_started_at_unix_ms: self.turn_started_at_unix_ms,
@@ -488,6 +498,10 @@ struct CodexTurnMetadataPayload<'a> {
#[serde(default, skip_serializing_if = "Option::is_none")]
auto_review_enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
node_repl_auto_review_required: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
node_repl_disabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
workspaces: Option<&'a BTreeMap<String, TurnMetadataWorkspace>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
tool_namespaces_info: Option<&'a TurnToolNamespacesInfo>,
+9 -8
View File
@@ -13,6 +13,14 @@ pub(super) async fn spawn_review_thread(
.review_model
.clone()
.unwrap_or_else(|| parent_turn_context.model_info.slug.clone());
let available_models = sess
.services
.models_manager
.list_models(
RefreshStrategy::OnlineIfUncached,
config.http_client_factory(),
)
.await;
let review_model_info = sess
.services
.models_manager
@@ -24,14 +32,6 @@ pub(super) async fn spawn_review_thread(
let _ = review_features.disable(Feature::WebSearchCached);
let _ = review_features.disable(Feature::Goals);
let review_web_search_mode = WebSearchMode::Disabled;
let available_models = sess
.services
.models_manager
.list_models(
RefreshStrategy::OnlineIfUncached,
config.http_client_factory(),
)
.await;
let unified_exec_shell_mode = UnifiedExecShellMode::for_session(
codex_tools::unified_exec_feature_mode_for_features(review_features.get()),
crate::tools::tool_user_shell_type(sess.services.user_shell.as_ref()),
@@ -120,6 +120,7 @@ pub(super) async fn spawn_review_thread(
parent_turn_context.windows_sandbox_level,
parent_turn_context.network.is_some(),
auto_review_enabled,
&model_info,
));
if turn_metadata_state.can_start_root_turn(&session_source) {
turn_metadata_state.set_root_turn_id(review_turn_id.clone());
@@ -605,6 +605,7 @@ impl Session {
session_configuration.windows_sandbox_level,
network.is_some(),
auto_review_enabled,
&model_info,
));
turn_metadata_state
.set_responses_api_metadata(per_turn_config.responses_api_metadata.clone());
+8
View File
@@ -30,6 +30,7 @@ use codex_git_utils::get_head_commit_hash;
use codex_protocol::ThreadId;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::ThreadSource;
@@ -110,6 +111,8 @@ pub(crate) struct TurnMetadataState {
sandbox: Option<String>,
sandbox_mode: Option<String>,
auto_review_enabled: bool,
node_repl_auto_review_required: bool,
node_repl_disabled: bool,
enriched_workspaces: RwLock<Option<BTreeMap<String, TurnMetadataWorkspace>>>,
tool_namespaces_info: RwLock<Option<TurnToolNamespacesInfo>>,
turn_started_at_unix_ms: RwLock<Option<i64>>,
@@ -135,6 +138,7 @@ impl TurnMetadataState {
windows_sandbox_level: WindowsSandboxLevel,
enforce_managed_network: bool,
auto_review_enabled: bool,
model_info: &ModelInfo,
) -> Self {
let repo_root = get_git_repo_root(&cwd);
let sandbox = Some(
@@ -163,6 +167,8 @@ impl TurnMetadataState {
sandbox,
sandbox_mode,
auto_review_enabled,
node_repl_auto_review_required: model_info.node_repl_auto_review_required,
node_repl_disabled: model_info.node_repl_disabled,
enriched_workspaces: RwLock::new(None),
tool_namespaces_info: RwLock::new(None),
turn_started_at_unix_ms: RwLock::new(None),
@@ -347,6 +353,8 @@ impl TurnMetadataState {
sandbox: self.sandbox.clone(),
sandbox_mode: self.sandbox_mode.clone(),
auto_review_enabled: Some(self.auto_review_enabled),
node_repl_auto_review_required: Some(self.node_repl_auto_review_required),
node_repl_disabled: Some(self.node_repl_disabled),
workspaces: self.current_workspaces(),
tool_namespaces_info: self
.tool_namespaces_info
+28
View File
@@ -5,6 +5,8 @@ use crate::responses_metadata::CodexResponsesRequestKind;
use crate::responses_metadata::CompactionTurnMetadata;
use crate::responses_metadata::INSTALLATION_ID_KEY;
use crate::responses_metadata::LEGACY_CODE_MODE_TOOL_NAMES_KEY;
use crate::responses_metadata::NODE_REPL_AUTO_REVIEW_REQUIRED_KEY;
use crate::responses_metadata::NODE_REPL_DISABLED_KEY;
use crate::responses_metadata::PARENT_TURN_ID_KEY;
use crate::responses_metadata::ROOT_TURN_ID_KEY;
use crate::responses_metadata::SANDBOX_MODE_KEY;
@@ -19,6 +21,7 @@ use codex_analytics::CompactionImplementation;
use codex_analytics::CompactionPhase;
use codex_analytics::CompactionReason;
use codex_analytics::CompactionTrigger;
use codex_models_manager::model_info::model_info_from_slug;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::protocol::SessionSource;
@@ -235,6 +238,7 @@ fn turn_metadata_state_includes_sandbox_metadata() {
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
/*auto_review_enabled*/ true,
&model_info_from_slug("gpt-5.4"),
);
let header = test_turn_metadata_header(&state);
@@ -283,6 +287,7 @@ fn turn_metadata_state_includes_root_fork_lineage() {
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
/*auto_review_enabled*/ false,
&model_info_from_slug("gpt-5.4"),
);
let header = test_turn_metadata_header(&state);
@@ -323,6 +328,7 @@ fn turn_metadata_state_includes_thread_spawn_subagent_parent_without_fork() {
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
/*auto_review_enabled*/ false,
&model_info_from_slug("gpt-5.4"),
);
let header = test_turn_metadata_header(&state);
@@ -363,6 +369,7 @@ fn turn_metadata_state_includes_forked_thread_spawn_subagent_lineage() {
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
/*auto_review_enabled*/ false,
&model_info_from_slug("gpt-5.4"),
);
let header = test_turn_metadata_header(&state);
@@ -405,6 +412,7 @@ fn turn_metadata_state_includes_known_parent_for_non_thread_spawn_subagents_with
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
/*auto_review_enabled*/ false,
&model_info_from_slug("gpt-5.4"),
);
let header = test_turn_metadata_header(&state);
@@ -438,6 +446,7 @@ fn turn_metadata_state_includes_turn_started_at_unix_ms_after_start() {
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
/*auto_review_enabled*/ false,
&model_info_from_slug("gpt-5.4"),
);
state.set_turn_started_at_unix_ms(/*turn_started_at_unix_ms*/ 1_700_000_000_123);
@@ -469,6 +478,7 @@ fn turn_metadata_state_includes_model_and_reasoning_effort_only_in_request_meta(
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
/*auto_review_enabled*/ false,
&model_info_from_slug("gpt-5.4"),
);
let header = test_turn_metadata_header(&state);
@@ -519,6 +529,7 @@ fn turn_metadata_state_marks_user_input_requested_during_turn_only_for_mcp_reque
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
/*auto_review_enabled*/ false,
&model_info_from_slug("gpt-5.4"),
);
let header = test_turn_metadata_header(&state);
@@ -573,6 +584,7 @@ fn turn_metadata_state_ignores_client_reserved_metadata_before_start() {
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
/*auto_review_enabled*/ false,
&model_info_from_slug("gpt-5.4"),
);
state.set_responsesapi_client_metadata(HashMap::from([
(
@@ -603,6 +615,11 @@ fn turn_metadata_state_ignores_client_reserved_metadata_before_start() {
"danger-full-access".to_string(),
),
(AUTO_REVIEW_ENABLED_KEY.to_string(), "true".to_string()),
(
NODE_REPL_AUTO_REVIEW_REQUIRED_KEY.to_string(),
"true".to_string(),
),
(NODE_REPL_DISABLED_KEY.to_string(), "true".to_string()),
]));
let header = test_turn_metadata_header(&state);
@@ -618,6 +635,11 @@ fn turn_metadata_state_ignores_client_reserved_metadata_before_start() {
assert!(json.get("subagent_kind").is_none());
assert_eq!(json[SANDBOX_MODE_KEY].as_str(), Some("read-only"));
assert_eq!(json[AUTO_REVIEW_ENABLED_KEY].as_bool(), Some(false));
assert_eq!(
json[NODE_REPL_AUTO_REVIEW_REQUIRED_KEY].as_bool(),
Some(false)
);
assert_eq!(json[NODE_REPL_DISABLED_KEY].as_bool(), Some(false));
}
#[test]
@@ -649,6 +671,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
/*auto_review_enabled*/ false,
&model_info_from_slug("gpt-5.4"),
);
state.set_responses_api_metadata(BTreeMap::from([(
"codex_security_surface".to_string(),
@@ -873,6 +896,7 @@ fn turn_metadata_state_overlays_compaction_only_on_compaction_requests() {
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
/*auto_review_enabled*/ false,
&model_info_from_slug("gpt-5.4"),
);
state.set_responses_api_metadata(BTreeMap::from([(
"codex_security_surface".to_string(),
@@ -953,6 +977,7 @@ async fn turn_metadata_state_preserves_lineage_after_git_enrichment() {
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
/*auto_review_enabled*/ false,
&model_info_from_slug("gpt-5.4"),
));
state.spawn_git_enrichment_task();
@@ -996,6 +1021,7 @@ async fn turn_metadata_state_coalesces_concurrent_git_enrichment() {
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
/*auto_review_enabled*/ false,
&model_info_from_slug("gpt-5.4"),
));
let barrier = Arc::new(tokio::sync::Barrier::new(8));
let tasks = (0..8)
@@ -1050,6 +1076,7 @@ async fn turn_metadata_state_git_enrichment_cancellation_is_retryable_and_errors
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
/*auto_review_enabled*/ false,
&model_info_from_slug("gpt-5.4"),
));
state.spawn_git_enrichment_task();
state.cancel_git_enrichment_task();
@@ -1084,6 +1111,7 @@ async fn turn_metadata_state_git_enrichment_cancellation_is_retryable_and_errors
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
/*auto_review_enabled*/ false,
&model_info_from_slug("gpt-5.4"),
));
invalid_state.spawn_git_enrichment_task();
tokio::time::timeout(Duration::from_secs(2), async {
@@ -36,10 +36,12 @@ async fn websocket_model_switch_to_responses_lite_omits_top_level_tools() -> Res
let mut builder = test_codex()
.with_model_info_override("gpt-5.2", |model_info| {
model_info.tool_mode = Some(ToolMode::CodeMode);
model_info.node_repl_auto_review_required = true;
})
.with_model_info_override("gpt-5.4", |model_info| {
model_info.use_responses_lite = true;
model_info.tool_mode = Some(ToolMode::CodeMode);
model_info.node_repl_disabled = true;
})
.with_model("gpt-5.2");
let test = builder.build_with_websocket_server(&server).await?;
@@ -79,6 +81,20 @@ async fn websocket_model_switch_to_responses_lite_omits_top_level_tools() -> Res
assert_eq!(non_lite_turn["model"].as_str(), Some("gpt-5.2"));
assert_eq!(lite_turn["model"].as_str(), Some("gpt-5.4"));
for (request, auto_review_required, disabled) in
[(&non_lite_turn, true, false), (&lite_turn, false, true)]
{
let metadata: Value = serde_json::from_str(
request["client_metadata"]["x-codex-turn-metadata"]
.as_str()
.expect("websocket request should include turn metadata"),
)?;
assert_eq!(
metadata["node_repl_auto_review_required"],
auto_review_required
);
assert_eq!(metadata["node_repl_disabled"], disabled);
}
assert!(
non_lite_turn
.get("tools")
+2
View File
@@ -260,6 +260,8 @@ fn remote_model_with_auto_review_override(slug: &str, review_model: &str) -> Mod
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: Some(review_model.to_string()),
model_specialty: None,
tool_mode: None,
@@ -117,6 +117,8 @@ fn test_model_info(
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,
@@ -1187,6 +1189,8 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,
@@ -534,6 +534,8 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,
+4
View File
@@ -669,6 +669,8 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,
@@ -792,6 +794,8 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,
@@ -527,6 +527,8 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,
@@ -783,6 +785,8 @@ async fn remote_models_apply_legacy_instructions() -> Result<()> {
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,
@@ -1368,6 +1372,8 @@ fn test_remote_model_with_policy(
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,
+24
View File
@@ -7,6 +7,7 @@ use codex_exec_server::CreateDirectoryOptions;
use codex_features::Feature;
use codex_history::RolloutItem;
use codex_history::RolloutLine;
use codex_login::CodexAuth;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::items::TurnItem;
@@ -774,6 +775,7 @@ async fn review_uses_custom_review_model_from_config() {
let codex_home = Arc::new(TempDir::new().unwrap());
let test = test_codex()
.with_home(Arc::clone(&codex_home))
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(|config| {
config.model = Some("gpt-4.1".to_string());
config.review_model = Some("custom-review-model".to_string());
@@ -783,6 +785,19 @@ async fn review_uses_custom_review_model_from_config() {
.await
.expect("custom review conversation should be created");
let codex = Arc::clone(&test.codex);
std::fs::remove_file(codex_home.path().join("models_cache.json"))
.expect("initial empty model catalog should be cached");
let mut models = codex_models_manager::bundled_models_response()
.expect("bundled model catalog should parse");
let model = models
.models
.iter_mut()
.find(|model| model.slug == "gpt-5.6-sol")
.expect("bundled model should exist");
model.slug = "custom-review-model".to_string();
model.node_repl_auto_review_required = true;
model.node_repl_disabled = true;
let models_mock = responses::mount_models_once(&server, models).await;
codex
.submit(Op::Review {
@@ -816,6 +831,15 @@ async fn review_uses_custom_review_model_from_config() {
let body = request.body_json();
assert_eq!(body["model"].as_str().unwrap(), "custom-review-model");
assert_eq!(body["reasoning"]["effort"].as_str(), Some("max"));
let turn_metadata: serde_json::Value = serde_json::from_str(
&request
.header("x-codex-turn-metadata")
.expect("review request turn metadata"),
)
.expect("review request turn metadata json");
assert_eq!(turn_metadata["node_repl_auto_review_required"], true);
assert_eq!(turn_metadata["node_repl_disabled"], true);
assert_eq!(models_mock.requests().len(), 1);
let _codex_home_guard = codex_home;
server.verify().await;
+54 -2
View File
@@ -1344,8 +1344,15 @@ async fn local_stdio_server_uses_runtime_fallback_cwd_when_config_omits_cwd() ->
Ok(())
}
#[test_case(false, false; "both disabled")]
#[test_case(true, false; "auto review required")]
#[test_case(false, true; "disabled")]
#[test_case(true, true; "both enabled")]
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()> {
async fn stdio_mcp_tool_call_includes_sandbox_state_meta(
node_repl_auto_review_required: bool,
node_repl_disabled: bool,
) -> anyhow::Result<()> {
// TODO(anp): Remove after packaging a Windows stdio test server for Wine exec.
skip_if_wine_exec!(
Ok(()),
@@ -1358,8 +1365,17 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()>
let call_id = "sandbox-meta-call";
let server_name = "rmcp";
let namespace = format!("mcp__{server_name}");
let mut models = codex_models_manager::bundled_models_response()?;
let model = models
.models
.iter_mut()
.find(|model| model.slug == "gpt-5.4")
.expect("bundled model should exist");
model.node_repl_auto_review_required = node_repl_auto_review_required;
model.node_repl_disabled = node_repl_disabled;
let models_mock = mount_models_once(&server, models).await;
mount_sse_once(
let initial_mock = mount_sse_once(
&server,
responses::sse(vec![
responses::ev_response_created("resp-1"),
@@ -1379,6 +1395,8 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()>
let rmcp_test_server_bin = remote_aware_stdio_server_bin()?;
let fixture = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_model("gpt-5.4")
.with_config(move |config| {
insert_mcp_server(
config,
@@ -1394,6 +1412,15 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()>
.await?;
wait_for_mcp_server(&fixture.codex, server_name).await?;
fixture
.thread_manager
.get_models_manager()
.list_models(
RefreshStrategy::Online,
codex_core::test_support::default_http_client_factory(),
)
.await;
assert_eq!(models_mock.requests().len(), 1);
fixture
.submit_turn_with_permission_profile(
@@ -1402,6 +1429,21 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()>
)
.await?;
let initial_request = initial_mock.single_request().body_json();
let response_metadata: Value = serde_json::from_str(
initial_request["client_metadata"]["x-codex-turn-metadata"]
.as_str()
.expect("responses request should include turn metadata"),
)?;
assert_eq!(
response_metadata["node_repl_auto_review_required"],
json!(node_repl_auto_review_required),
);
assert_eq!(
response_metadata["node_repl_disabled"],
json!(node_repl_disabled)
);
let output_item = final_mock.single_request().function_call_output(call_id);
let output_text = output_item
.get("output")
@@ -1413,6 +1455,14 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()>
let meta = output_json
.as_object()
.expect("sandbox_meta should return metadata object");
assert_eq!(
output_json.pointer("/x-codex-turn-metadata/node_repl_auto_review_required"),
Some(&json!(node_repl_auto_review_required))
);
assert_eq!(
output_json.pointer("/x-codex-turn-metadata/node_repl_disabled"),
Some(&json!(node_repl_disabled))
);
let sandbox_meta = meta
.get(MCP_SANDBOX_STATE_META_CAPABILITY)
@@ -2240,6 +2290,8 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,
@@ -12,7 +12,7 @@ Scenario: After five varied API-key-auth turns, remote manual compaction omits s
- "thread_id": "<UUID>",
- "turn_id": "<UUID>",
- "x-codex-installation-id": "<UUID>",
- "x-codex-turn-metadata": "{\"installation_id\":\"<UUID>\",\"session_id\":\"<UUID>\",\"thread_id\":\"<UUID>\",\"turn_id\":\"<UUID>\",\"window_id\":\"<UUID>:0\",\"request_kind\":\"turn\",\"root_turn_id\":\"<UUID>\",\"sandbox\":\"<SANDBOX>\",\"sandbox_mode\":\"read-only\",\"auto_review_enabled\":false,\"turn_started_at_unix_ms\":<UNIX_MS>}",
- "x-codex-turn-metadata": "{\"installation_id\":\"<UUID>\",\"session_id\":\"<UUID>\",\"thread_id\":\"<UUID>\",\"turn_id\":\"<UUID>\",\"window_id\":\"<UUID>:0\",\"request_kind\":\"turn\",\"root_turn_id\":\"<UUID>\",\"sandbox\":\"<SANDBOX>\",\"sandbox_mode\":\"read-only\",\"auto_review_enabled\":false,\"node_repl_auto_review_required\":false,\"node_repl_disabled\":false,\"turn_started_at_unix_ms\":<UNIX_MS>}",
- "x-codex-window-id": "<UUID>:0"
- },
- "include": [
@@ -12,7 +12,7 @@ Scenario: After five varied ChatGPT-auth turns, remote manual compaction reuses
- "thread_id": "<UUID>",
- "turn_id": "<UUID>",
- "x-codex-installation-id": "<UUID>",
- "x-codex-turn-metadata": "{\"installation_id\":\"<UUID>\",\"session_id\":\"<UUID>\",\"thread_id\":\"<UUID>\",\"turn_id\":\"<UUID>\",\"window_id\":\"<UUID>:0\",\"request_kind\":\"turn\",\"root_turn_id\":\"<UUID>\",\"sandbox\":\"<SANDBOX>\",\"sandbox_mode\":\"read-only\",\"auto_review_enabled\":false,\"turn_started_at_unix_ms\":<UNIX_MS>}",
- "x-codex-turn-metadata": "{\"installation_id\":\"<UUID>\",\"session_id\":\"<UUID>\",\"thread_id\":\"<UUID>\",\"turn_id\":\"<UUID>\",\"window_id\":\"<UUID>:0\",\"request_kind\":\"turn\",\"root_turn_id\":\"<UUID>\",\"sandbox\":\"<SANDBOX>\",\"sandbox_mode\":\"read-only\",\"auto_review_enabled\":false,\"node_repl_auto_review_required\":false,\"node_repl_disabled\":false,\"turn_started_at_unix_ms\":<UNIX_MS>}",
- "x-codex-window-id": "<UUID>:0"
- },
- "include": [
@@ -74,6 +74,8 @@ fn test_model_info(
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,
+2
View File
@@ -1620,6 +1620,8 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,
@@ -175,6 +175,8 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo {
used_fallback_model_metadata: true, // this is the fallback model metadata
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,
+8
View File
@@ -442,6 +442,10 @@ pub struct ModelInfo {
pub supports_search_tool: bool,
#[serde(default)]
pub use_responses_lite: bool,
#[serde(default)]
pub node_repl_auto_review_required: bool,
#[serde(default)]
pub node_repl_disabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_review_model_override: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -855,6 +859,8 @@ mod tests {
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,
@@ -1435,6 +1441,8 @@ mod tests {
assert_eq!(model.web_search_tool_type, WebSearchToolType::Text);
assert!(!model.supports_search_tool);
assert!(!model.use_responses_lite);
assert!(!model.node_repl_auto_review_required);
assert!(!model.node_repl_disabled);
assert_eq!(model.comp_hash, None);
assert_eq!(model.auto_review_model_override, None);
assert_eq!(model.tool_mode, None);
+2
View File
@@ -48,6 +48,8 @@ fn model_with_shell_type(shell_type: ConfigShellToolType) -> ModelInfo {
used_fallback_model_metadata: false,
supports_search_tool: false,
use_responses_lite: false,
node_repl_auto_review_required: false,
node_repl_disabled: false,
auto_review_model_override: None,
model_specialty: None,
tool_mode: None,