Forward model confirmation policies to actor MCP tools (#41072)

## What changed

- Add optional Browser Use and Computer Use confirmation-policy Markdown to model catalog messages.
- Send the configured documents verbatim in `openai/confirmation_policies` metadata for `node_repl` and `cua_repl` tool calls. Send an empty object when the issuing model has no policies so runtime defaults are cleared.
- Use the issuing step's model snapshot across approval waits, and omit the metadata for other MCP servers and Guardian sessions.

## Testing

- Cover policy serialization, configuration overrides, model changes, approval waits, partial or empty policies, eligible server filtering, and Guardian exclusion.

GitOrigin-RevId: 038c4c262a31fc71986e4a2a947fe9bcbd407acc
This commit is contained in:
Ankush Gupta
2026-08-27 06:58:07 +00:00
committed by copyberry
parent 218a3e50ae
commit b592a0bfed
16 changed files with 434 additions and 7 deletions
@@ -43,6 +43,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
}),
include_skills_usage_instructions: false,
@@ -1921,6 +1921,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
};
@@ -1960,6 +1961,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
};
@@ -2007,6 +2009,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
};
+42
View File
@@ -45,7 +45,9 @@ use codex_protocol::items::McpToolCallError;
use codex_protocol::items::McpToolCallItem;
use codex_protocol::items::McpToolCallStatus;
use codex_protocol::items::TurnItem;
use codex_protocol::mcp::CONFIRMATION_POLICIES_META_KEY;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::mcp::is_node_repl_backed_server;
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_KEY as MCP_TOOL_APPROVAL_KIND_KEY;
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_MCP_TOOL_CALL as MCP_TOOL_APPROVAL_KIND_MCP_TOOL_CALL;
use codex_protocol::mcp_approval_meta::CONNECTOR_DESCRIPTION_KEY as MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY;
@@ -1234,9 +1236,49 @@ fn build_mcp_tool_call_request_meta(
);
}
if let Some(policies) = build_confirmation_policies_request_meta(step_context, server) {
request_meta.insert(CONFIRMATION_POLICIES_META_KEY.to_string(), policies);
}
(!request_meta.is_empty()).then_some(serde_json::Value::Object(request_meta))
}
/// Builds confirmation-policy metadata for eligible actor MCP calls.
///
/// Policies follow the issuing step's model snapshot, including across approval
/// waits. Only `node_repl`/`cua_repl` receive them; Guardian sessions are excluded.
/// Eligible calls get an empty object when no policies are configured, clearing
/// startup defaults. Text stays verbatim so runtimes own blank-value fallback.
fn build_confirmation_policies_request_meta(
step_context: &StepContext,
server: &str,
) -> Option<serde_json::Value> {
if !is_node_repl_backed_server(server)
|| crate::guardian::is_basic_session_source(&step_context.turn.session_source)
{
return None;
}
let mut policies = serde_json::Map::new();
if let Some(confirmation_policies) = step_context
.settings
.model_info
.model_messages
.as_ref()
.and_then(|messages| messages.confirmation_policies.as_ref())
{
for (name, policy) in [
("browser_use", confirmation_policies.browser_use.as_ref()),
("computer_use", confirmation_policies.computer_use.as_ref()),
] {
if let Some(policy) = policy {
policies.insert(name.to_string(), serde_json::Value::String(policy.clone()));
}
}
}
Some(serde_json::Value::Object(policies))
}
fn with_mcp_tool_call_ids_meta(
meta: Option<serde_json::Value>,
thread_id: &str,
+48
View File
@@ -8,6 +8,7 @@ use crate::session::tests::make_session_and_context;
use crate::session::tests::make_session_and_context_with_rx;
use crate::session::tests::mcp_config_for_test;
use crate::session::tests::update_selected_settings_for_test;
use crate::session::tests::update_turn_settings_for_test;
use crate::session::turn_context::TurnEnvironment;
use crate::state::ActiveTurn;
use crate::test_support::models_manager_with_provider;
@@ -32,7 +33,10 @@ use codex_protocol::protocol::EnvironmentConfig;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::InternalSessionSource;
use codex_protocol::protocol::McpInvocation;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_utils_path_uri::PathUri;
use core_test_support::hooks::trusted_config_layer_stack;
@@ -1110,6 +1114,7 @@ async fn mcp_tool_call_request_meta_uses_the_issuing_step(
Some(serde_json::json!({
"callId": "call-b",
crate::X_CODEX_TURN_METADATA_HEADER: expected,
CONFIRMATION_POLICIES_META_KEY: {},
})),
);
assert_eq!(
@@ -1130,6 +1135,49 @@ async fn mcp_tool_call_request_meta_uses_the_issuing_step(
);
}
#[tokio::test]
async fn guardian_mcp_tool_call_request_meta_excludes_actor_confirmation_policy() {
for session_source in [
SessionSource::Internal(InternalSessionSource::Guardian),
SessionSource::SubAgent(SubAgentSource::Other(
crate::guardian::GUARDIAN_REVIEWER_NAME.to_string(),
)),
] {
let (_, mut turn_context) = make_session_and_context().await;
turn_context.session_source = session_source;
update_turn_settings_for_test(&mut turn_context, |settings| {
Arc::make_mut(&mut settings.model_info).model_messages = Some(
serde_json::from_value(serde_json::json!({
"confirmation_policies": {
"browser_use": "actor-only raw Markdown",
"computer_use": "actor-only native Markdown",
},
}))
.expect("confirmation policy fixture should deserialize"),
);
});
let expected = Some(serde_json::json!({
"callId": "call-guardian",
crate::X_CODEX_TURN_METADATA_HEADER: expected_mcp_turn_metadata(&turn_context),
}));
let step_context = StepContext::for_test(Arc::new(turn_context));
for server in ["node_repl", "cua_repl"] {
assert_eq!(
build_mcp_tool_call_request_meta(
&step_context,
server,
"call-guardian",
/*metadata*/ None,
),
expected,
"{server}: {:?}",
step_context.turn.session_source,
);
}
}
}
#[tokio::test]
async fn mcp_tool_call_request_meta_includes_turn_started_at_unix_ms() {
let (_, turn_context) = make_session_and_context().await;
@@ -44,6 +44,7 @@ async fn catalog_permission_message_loaded_from_remote_models_is_sent() -> Resul
}),
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
});
let models_mock = mount_models_once(
@@ -84,6 +84,7 @@ fn model_with_collaboration_messages(
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
});
model_messages.collaboration_modes = Some(CollaborationModeMessages {
@@ -513,6 +513,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
}),
include_skills_usage_instructions: false,
@@ -61,6 +61,7 @@ fn model_with_approval_messages(
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
});
model
@@ -81,6 +82,7 @@ fn model_with_permission_messages(
permissions: Some(permissions),
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
});
model
@@ -236,6 +238,7 @@ async fn catalog_non_on_request_approval_messages_are_sent_in_initial_permission
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
});
let mut builder = test_codex()
+2
View File
@@ -646,6 +646,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
}),
include_skills_usage_instructions: false,
@@ -773,6 +774,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
}),
include_skills_usage_instructions: false,
@@ -124,6 +124,7 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<(
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
}),
..specific
@@ -140,6 +141,7 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<(
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
}),
..generic
@@ -415,6 +417,7 @@ async fn remote_models_long_model_slug_is_sent_with_supported_reasoning(
multi_agent: None,
token_budget: None,
guardian_v2: None,
confirmation_policies: None,
});
mount_models_once(
&server,
@@ -854,6 +857,7 @@ async fn remote_models_apply_legacy_instructions() -> Result<()> {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
}),
include_skills_usage_instructions: false,
+62 -7
View File
@@ -55,6 +55,7 @@ use codex_protocol::models::PermissionProfile;
use codex_protocol::models::PermissionProfileSnapshot;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::ConfirmationPolicies;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelVisibility;
@@ -104,6 +105,7 @@ use image::DynamicImage;
use image::GenericImageView;
use image::ImageBuffer;
use image::Rgba;
use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::json;
use serial_test::serial;
@@ -1633,16 +1635,28 @@ async fn local_stdio_server_uses_runtime_fallback_cwd_when_config_omits_cwd() ->
Ok(())
}
#[test_case(false, false, false; "both disabled")]
#[test_case(true, false, false; "auto review required")]
#[test_case(false, true, false; "disabled")]
#[test_case(true, true, false; "both enabled")]
#[test_case(false, false, true; "attachment-owned permissions preserve foreign workspace roots")]
#[test_case("rmcp", false, false, false, Some("catalog policy"), Some("native catalog policy"); "both disabled")]
#[test_case("rmcp", true, false, false, Some("catalog policy"), Some("native catalog policy"); "auto review required")]
#[test_case("rmcp", false, true, false, Some("catalog policy"), Some("native catalog policy"); "disabled")]
#[test_case("rmcp", true, true, false, Some("catalog policy"), Some("native catalog policy"); "both enabled")]
#[test_case("rmcp", false, false, true, Some("catalog policy"), Some("native catalog policy"); "attachment-owned permissions preserve foreign workspace roots")]
#[test_case("node_repl", false, false, false, Some(" # Policy A\r\n{literal} <raw> & café\n"), Some("\t# Native A\n{{literal}} & desktop\r\n"); "node repl raw policy")]
#[test_case("cua_repl", false, false, false, Some("\t# Policy B\n${literal} </policy>\r\n "), Some(" # Native B\r\n<computer> ${native}\n "); "cua repl raw policy")]
#[test_case("node_repl", false, false, false, None, None; "node repl missing policy")]
#[test_case("cua_repl", false, false, false, Some(""), Some("native retained"); "cua repl empty policy")]
#[test_case("node_repl", false, false, false, Some(" \r\n\t"), Some("native retained"); "node repl blank policy")]
#[test_case("node_repl", false, false, false, None, Some("native retained"); "node repl missing browser policy")]
#[test_case("cua_repl", false, false, false, Some("browser retained"), None; "cua repl missing computer policy")]
#[test_case("node_repl", false, false, false, Some("browser retained"), Some(""); "node repl empty computer policy")]
#[test_case("cua_repl", false, false, false, Some("browser retained"), Some(" \r\n\t"); "cua repl blank computer policy")]
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn stdio_mcp_tool_call_includes_sandbox_state_meta(
server_name: &'static str,
node_repl_auto_review_required: bool,
node_repl_disabled: bool,
attachment_owned_permissions: bool,
browser_policy: Option<&str>,
computer_policy: Option<&str>,
) -> anyhow::Result<()> {
// TODO(anp): Remove after packaging a Windows stdio test server for Wine exec.
skip_if_wine_exec!(
@@ -1655,7 +1669,6 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta(
let call_id = "sandbox-meta-call";
let restricted_call_id = "owner-restricted-call";
let server_name = "rmcp";
let namespace = format!("mcp__{server_name}");
let mut models = codex_models_manager::bundled_models_response()?;
let model = models
@@ -1665,11 +1678,33 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta(
.expect("bundled model should exist");
model.node_repl_auto_review_required = node_repl_auto_review_required;
model.node_repl_disabled = node_repl_disabled;
let messages = model
.model_messages
.as_mut()
.expect("bundled model messages");
messages.confirmation_policies = Some(ConfirmationPolicies {
browser_use: browser_policy.map(str::to_owned),
computer_use: computer_policy.map(str::to_owned),
});
let models_mock = mount_models_once(&server, models).await;
let mut response_events = vec![
responses::ev_response_created("resp-1"),
responses::ev_function_call_with_namespace(call_id, &namespace, "sandbox_meta", "{}"),
responses::ev_function_call_with_namespace(
call_id,
&namespace,
"sandbox_meta",
&json!({
"_meta": {
"openai/confirmation_policies": {
"browser_use": "forged argument policy",
"computer_use": "forged computer policy",
},
"threadId": "forged-thread",
},
})
.to_string(),
),
];
if attachment_owned_permissions {
response_events.push(responses::ev_function_call_with_namespace(
@@ -1859,6 +1894,26 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta(
output_json.pointer("/x-codex-turn-metadata/node_repl_disabled"),
Some(&json!(node_repl_disabled))
);
let expected_policies = match server_name {
"node_repl" | "cua_repl" => Some(match (browser_policy, computer_policy) {
(Some(browser), Some(computer)) => {
json!({"browser_use": browser, "computer_use": computer})
}
(Some(browser), None) => json!({"browser_use": browser}),
(None, Some(computer)) => json!({"computer_use": computer}),
(None, None) => json!({}),
}),
_ => None,
};
assert_eq!(
meta.get("openai/confirmation_policies"),
expected_policies.as_ref(),
);
assert_eq!(
output_json["threadId"],
json!(fixture.session_configured.thread_id.to_string()),
);
assert_eq!(output_json["callId"], json!(call_id));
let sandbox_meta = meta
.get(MCP_SANDBOX_STATE_META_CAPABILITY)
+211
View File
@@ -1,4 +1,5 @@
use anyhow::Result;
use codex_config::McpServerConfig;
use codex_core::CodexThread;
use codex_core::TurnInputRequest;
use codex_core::config::Constrained;
@@ -9,6 +10,7 @@ use codex_models_manager::manager::RefreshStrategy;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::openai_models::ConfirmationPolicies;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::ReasoningEffort;
@@ -24,9 +26,12 @@ use codex_protocol::request_user_input::RequestUserInputAnswer;
use codex_protocol::request_user_input::RequestUserInputEvent;
use codex_protocol::request_user_input::RequestUserInputResponse;
use codex_protocol::user_input::UserInput;
use core_test_support::apps_test_server::AppsTestServer;
use core_test_support::apps_test_server::recorded_apps_tool_calls;
use core_test_support::responses::ResponsesRequest;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_function_call;
use core_test_support::responses::ev_function_call_with_namespace;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_models_once;
use core_test_support::responses::mount_response_sequence;
@@ -40,6 +45,7 @@ use core_test_support::test_codex::TestCodexBuilder;
use core_test_support::test_codex::test_codex;
use core_test_support::wait_for_event;
use core_test_support::wait_for_event_match;
use core_test_support::wait_for_mcp_server;
use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::json;
@@ -335,6 +341,211 @@ async fn settings_updates_preserve_turn_identity_and_target(target: SettingsTarg
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn mcp_confirmation_policy_follows_step_model_changes() -> Result<()> {
skip_if_no_network!(Ok(()));
const BROWSER_POLICY_A: &str = " # Browser policy A\r\n{literal}\n";
const BROWSER_POLICY_B: &str = "\t# Browser policy B\n<raw> & café\r\n ";
const COMPUTER_POLICY_A: &str = "\t# Native policy A\n{{literal}}\r\n";
const COMPUTER_POLICY_B: &str = " # Native policy B\r\n<computer> ${native}\n ";
const BROWSER_ONLY_MODEL: &str = "policy-browser-only";
const COMPUTER_ONLY_MODEL: &str = "policy-computer-only";
let server = start_mock_server().await;
AppsTestServer::mount(&server).await?;
let policy_call = |response_id: &str, call_id: &str| {
sse(vec![
ev_response_created(response_id),
ev_function_call_with_namespace(
call_id,
"mcp__node_repl",
"calendar_list_events",
"{}",
),
ev_completed(response_id),
])
};
mount_sse_sequence(
&server,
vec![
policy_call("resp-1", "policy-a"),
sse_completed("resp-2"),
sse(vec![
ev_response_created("resp-3"),
ev_function_call_with_namespace(
"policy-a-pending",
"mcp__node_repl",
"calendar_create_event",
&json!({
"title": "Policy snapshot test",
"starts_at": "2026-08-26T12:00:00Z",
})
.to_string(),
),
ev_completed("resp-3"),
]),
policy_call("resp-4", "policy-b"),
sse_completed("resp-5"),
policy_call("resp-6", "browser-only"),
sse_completed("resp-7"),
policy_call("resp-8", "computer-only"),
sse_completed("resp-9"),
policy_call("resp-10", "no-policy"),
sse_completed("resp-11"),
],
)
.await;
let mcp_url = format!("{}/api/codex/ps/mcp", server.uri());
let test = step_settings_test()
.with_config(move |config| {
config
.features
.disable(Feature::ToolCallMcpElicitation)
.expect("disable MCP elicitation for the approval barrier");
let models = &mut config.model_catalog.as_mut().expect("test models").models;
for slug in [BROWSER_ONLY_MODEL, COMPUTER_ONLY_MODEL] {
let mut model = models[0].clone();
model.slug = slug.to_string();
models.push(model);
}
for model in models {
let messages = model
.model_messages
.as_mut()
.expect("bundled model messages");
messages.confirmation_policies = match model.slug.as_str() {
MODEL_A => Some(ConfirmationPolicies {
browser_use: Some(BROWSER_POLICY_A.to_string()),
computer_use: Some(COMPUTER_POLICY_A.to_string()),
}),
MODEL_B => Some(ConfirmationPolicies {
browser_use: Some(BROWSER_POLICY_B.to_string()),
computer_use: Some(COMPUTER_POLICY_B.to_string()),
}),
BROWSER_ONLY_MODEL => Some(ConfirmationPolicies {
browser_use: Some(BROWSER_POLICY_B.to_string()),
computer_use: None,
}),
COMPUTER_ONLY_MODEL => Some(ConfirmationPolicies {
browser_use: None,
computer_use: Some(COMPUTER_POLICY_B.to_string()),
}),
MODEL_C => None,
_ => unreachable!("unexpected test model"),
};
}
let node_repl: McpServerConfig = serde_json::from_value(json!({
"url": mcp_url,
"tools": {
"calendar_create_event": {
"approval_mode": "prompt",
},
},
}))
.expect("valid test MCP server");
config
.mcp_servers
.set(HashMap::from([("node_repl".to_string(), node_repl)]))
.expect("configure test MCP server");
})
.build_with_auto_env(&server)
.await?;
wait_for_mcp_server(&test.codex, "node_repl").await?;
test.submit_text_turn("call the tool with model A").await?;
let request = start_paused_turn(&test.codex).await?;
assert_eq!(request.call_id, "policy-a-pending");
// The pending call must retain model A's policies after this settings update.
assert_eq!(
submit_turn_settings(
&test.codex,
&request.turn_id,
TurnSettingsUpdate {
model: Some(MODEL_B.to_string()),
..Default::default()
},
)
.await?,
TurnSettingsUpdateOutcome::Applied,
);
test.codex
.submit(Op::UserInputAnswer {
id: request.turn_id.clone(),
response: RequestUserInputResponse {
answers: HashMap::from([(
request.questions[0].id.clone(),
RequestUserInputAnswer {
answers: vec!["Allow".to_string()],
},
)]),
},
})
.await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
for model in [BROWSER_ONLY_MODEL, COMPUTER_ONLY_MODEL, MODEL_C] {
core_test_support::submit_thread_settings(
&test.codex,
ThreadSettingsOverrides {
model: Some(model.to_string()),
..Default::default()
},
)
.await?;
test.submit_text_turn("call the tool").await?;
}
assert_eq!(
recorded_apps_tool_calls(&server)
.await
.into_iter()
.map(|call| {
let meta = &call["params"]["_meta"];
(
meta["callId"].clone(),
meta["openai/confirmation_policies"].clone(),
)
})
.collect::<Vec<_>>(),
vec![
(
json!("policy-a"),
json!({
"browser_use": BROWSER_POLICY_A,
"computer_use": COMPUTER_POLICY_A,
})
),
(
json!("policy-a-pending"),
json!({
"browser_use": BROWSER_POLICY_A,
"computer_use": COMPUTER_POLICY_A,
})
),
(
json!("policy-b"),
json!({
"browser_use": BROWSER_POLICY_B,
"computer_use": COMPUTER_POLICY_B,
})
),
(
json!("browser-only"),
json!({"browser_use": BROWSER_POLICY_B})
),
(
json!("computer-only"),
json!({"computer_use": COMPUTER_POLICY_B})
),
(json!("no-policy"), json!({})),
],
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn response_metadata_uses_the_captured_model_after_a_turn_update() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -60,6 +60,7 @@ pub fn with_config_overrides(mut model: ModelInfo, config: &ModelsManagerConfig)
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
});
model_messages.instructions_template = Some(base_instructions.clone());
@@ -204,6 +205,7 @@ fn local_model_messages_for_slug(slug: &str) -> ModelMessages {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
},
_ => ModelMessages {
@@ -216,6 +218,7 @@ fn local_model_messages_for_slug(slug: &str) -> ModelMessages {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
},
}
@@ -4,6 +4,7 @@ use codex_protocol::config_types::Personality;
use codex_protocol::openai_models::ApprovalMessages;
use codex_protocol::openai_models::AutoReviewMessages;
use codex_protocol::openai_models::CollaborationModeMessages;
use codex_protocol::openai_models::ConfirmationPolicies;
use codex_protocol::openai_models::GuardianV2ModelConfig;
use codex_protocol::openai_models::ModelTokenBudgetConfig;
use codex_protocol::openai_models::MultiAgentMessages;
@@ -67,6 +68,10 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
classifier_instructions: Some("Guardian experiment".to_string()),
..Default::default()
};
let confirmation_policies = ConfirmationPolicies {
browser_use: Some("# Browser policy\n\n{{literal_markdown}}\n".to_string()),
computer_use: Some(" # Native policy\r\n\n${native_markdown}\n".to_string()),
};
model.model_messages = Some(ModelMessages {
persistent_instructions: Some(persistent_instructions.to_string()),
instructions_template: Some("template".to_string()),
@@ -81,6 +86,7 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
permissions: Some(permissions.clone()),
multi_agent: Some(multi_agent.clone()),
token_budget: Some(token_budget.clone()),
confirmation_policies: Some(confirmation_policies.clone()),
guardian_v2: Some(guardian_v2.clone()),
});
let config = ModelsManagerConfig {
@@ -102,6 +108,7 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
permissions: Some(permissions),
multi_agent: Some(multi_agent),
token_budget: Some(token_budget),
confirmation_policies: Some(confirmation_policies),
guardian_v2: Some(guardian_v2),
})
);
@@ -134,6 +141,7 @@ fn disabled_personality_bakes_default_and_preserves_catalog_approval_messages()
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
});
let config = ModelsManagerConfig {
@@ -155,6 +163,7 @@ fn disabled_personality_bakes_default_and_preserves_catalog_approval_messages()
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
})
);
@@ -183,6 +192,7 @@ fn disabled_personality_uses_plain_base_instructions_for_local_personality_model
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
}),
"unexpected model messages for {slug}"
@@ -225,6 +235,7 @@ fn personality_none_strips_catalog_instruction_sources_through_the_next_h1() {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
});
+2
View File
@@ -29,6 +29,8 @@ pub const OPENAI_FORM_EXTENSION_ID: &str = "openai/form";
pub const OPENAI_STANDARD_FORM_INPUT_EXTENSION_ID: &str = "openai/standard-form-input";
/// Extension ID for MCP App UI rendering.
pub const MCP_APP_UI_EXTENSION_ID: &str = "io.modelcontextprotocol/ui";
/// Host-supplied confirmation-policy documents for Node REPL-backed actor calls.
pub const CONFIRMATION_POLICIES_META_KEY: &str = "openai/confirmation_policies";
/// Returns whether a raw MCP server name identifies a Node REPL-backed server.
pub fn is_node_repl_backed_server(server: &str) -> bool {
+39
View File
@@ -547,6 +547,20 @@ pub struct ModelMessages {
pub token_budget: Option<ModelTokenBudgetConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guardian_v2: Option<GuardianV2ModelConfig>,
/// Replacement confirmation-policy documents forwarded in actor MCP request metadata.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub confirmation_policies: Option<ConfirmationPolicies>,
}
/// Model-owned confirmation-policy Markdown, forwarded unchanged to actor tools.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct ConfirmationPolicies {
/// Replacement Markdown for the Browser Use confirmation-policy document.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub browser_use: Option<String>,
/// Replacement Markdown for the native Computer Use confirmation policy.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub computer_use: Option<String>,
}
/// Model-owned defaults for the context-window token-budget feature.
@@ -774,6 +788,7 @@ where
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
});
messages.instructions_template = Some(base_instructions);
@@ -981,6 +996,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
}
);
@@ -1130,6 +1146,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
}
);
@@ -1219,6 +1236,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
}));
@@ -1243,6 +1261,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
}));
assert_eq!(
@@ -1268,6 +1287,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
}));
assert_eq!(
@@ -1304,6 +1324,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
}));
@@ -1346,6 +1367,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
})
);
@@ -1399,6 +1421,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
}))],
};
@@ -1440,6 +1463,14 @@ mod tests {
}),
multi_agent: None,
token_budget: None,
confirmation_policies: Some(ConfirmationPolicies {
browser_use: Some(
"# Browser confirmations\n\nKeep {{literal_markdown}}.\n".to_string(),
),
computer_use: Some(
" # Native confirmations\r\n\nKeep ${native_markdown}.\n".to_string(),
),
}),
guardian_v2: Some(GuardianV2ModelConfig {
classifier_instructions: Some("Guardian classification".to_string()),
review_threshold_basis_points: Some(7_500),
@@ -1456,6 +1487,13 @@ mod tests {
models: vec![test_model(Some(messages.clone()))],
})
.expect("serialize models response");
assert_eq!(
value["models"][0]["model_messages"]["confirmation_policies"],
serde_json::json!({
"browser_use": "# Browser confirmations\n\nKeep {{literal_markdown}}.\n",
"computer_use": " # Native confirmations\r\n\nKeep ${native_markdown}.\n",
})
);
value["models"][0]["base_instructions"] = serde_json::json!("legacy instructions");
let response: ModelsResponse =
@@ -1474,6 +1512,7 @@ mod tests {
permissions: None,
multi_agent: None,
token_budget: None,
confirmation_policies: None,
guardian_v2: None,
};
let mut value = serde_json::to_value(ModelsResponse {