Reject ask user question tool in Execute and Custom (#9560)

## Summary
- Keep `request_user_input` in the tool list but reject it at runtime in
Execute/Custom modes with a clear model-facing error.
- Add a session accessor for current collaboration mode and enforce the
gate in the request_user_input handler.
- Update core/app-server tests to use Plan mode for success and add
Execute/Custom rejection coverage.
This commit is contained in:
charley-oai
2026-01-20 18:32:17 -08:00
committed by GitHub
parent 531748a080
commit 0523a259c8
4 changed files with 161 additions and 1 deletions

View File

@@ -7,7 +7,9 @@ use codex_core::protocol::AskForApproval;
use codex_core::protocol::EventMsg;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::Settings;
use codex_protocol::request_user_input::RequestUserInputAnswer;
use codex_protocol::request_user_input::RequestUserInputResponse;
use codex_protocol::user_input::UserInput;
@@ -45,6 +47,27 @@ fn call_output(req: &ResponsesRequest, call_id: &str) -> String {
}
}
fn call_output_content_and_success(
req: &ResponsesRequest,
call_id: &str,
) -> (String, Option<bool>) {
let raw = req.function_call_output(call_id);
assert_eq!(
raw.get("call_id").and_then(Value::as_str),
Some(call_id),
"mismatched call_id in function_call_output"
);
let (content_opt, success) = match req.function_call_output_content_and_success(call_id) {
Some(values) => values,
None => panic!("function_call_output present"),
};
let content = match content_opt {
Some(content) => content,
None => panic!("function_call_output content present"),
};
(content, success)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
@@ -109,7 +132,11 @@ async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()>
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
collaboration_mode: None,
collaboration_mode: Some(CollaborationMode::Plan(Settings {
model: session_configured.model.clone(),
reasoning_effort: None,
developer_instructions: None,
})),
})
.await?;
@@ -153,3 +180,112 @@ async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()>
Ok(())
}
async fn assert_request_user_input_rejected<F>(mode_name: &str, build_mode: F) -> anyhow::Result<()>
where
F: FnOnce(String) -> CollaborationMode,
{
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let builder = test_codex();
let TestCodex {
codex,
cwd,
session_configured,
..
} = builder
.with_config(|config| {
config.features.enable(Feature::CollaborationModes);
})
.build(&server)
.await?;
let mode_slug = mode_name.to_lowercase();
let call_id = format!("user-input-{mode_slug}-call");
let request_args = json!({
"questions": [{
"id": "confirm_path",
"header": "Confirm",
"question": "Proceed with the plan?",
"options": [{
"label": "Yes (Recommended)",
"description": "Continue the current plan."
}, {
"label": "No",
"description": "Stop and revisit the approach."
}]
}]
})
.to_string();
let first_response = sse(vec![
ev_response_created("resp-1"),
ev_function_call(&call_id, "request_user_input", &request_args),
ev_completed("resp-1"),
]);
responses::mount_sse_once(&server, first_response).await;
let second_response = sse(vec![
ev_assistant_message("msg-1", "thanks"),
ev_completed("resp-2"),
]);
let second_mock = responses::mount_sse_once(&server, second_response).await;
let session_model = session_configured.model.clone();
let collaboration_mode = build_mode(session_model.clone());
codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "please confirm".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
collaboration_mode: Some(collaboration_mode),
})
.await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
let req = second_mock.single_request();
let (output, success) = call_output_content_and_success(&req, &call_id);
assert_eq!(success, None);
assert_eq!(
output,
format!("request_user_input is unavailable in {mode_name} mode")
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn request_user_input_rejected_in_execute_mode() -> anyhow::Result<()> {
assert_request_user_input_rejected("Execute", |model| {
CollaborationMode::Execute(Settings {
model,
reasoning_effort: None,
developer_instructions: None,
})
})
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn request_user_input_rejected_in_custom_mode() -> anyhow::Result<()> {
assert_request_user_input_rejected("Custom", |model| {
CollaborationMode::Custom(Settings {
model,
reasoning_effort: None,
developer_instructions: None,
})
})
.await
}