mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
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:
@@ -12,6 +12,8 @@ use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use tokio::time::timeout;
|
||||
|
||||
@@ -52,6 +54,11 @@ async fn request_user_input_round_trip() -> Result<()> {
|
||||
}],
|
||||
model: Some("mock-model".to_string()),
|
||||
effort: Some(ReasoningEffort::Medium),
|
||||
collaboration_mode: Some(CollaborationMode::Plan(Settings {
|
||||
model: "mock-model".to_string(),
|
||||
reasoning_effort: Some(ReasoningEffort::Medium),
|
||||
developer_instructions: None,
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -1531,6 +1531,11 @@ impl Session {
|
||||
self.features.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn collaboration_mode(&self) -> CollaborationMode {
|
||||
let state = self.state.lock().await;
|
||||
state.session_configuration.collaboration_mode.clone()
|
||||
}
|
||||
|
||||
async fn send_raw_response_items(&self, turn_context: &TurnContext, items: &[ResponseItem]) {
|
||||
for item in items {
|
||||
self.send_event(
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
|
||||
pub struct RequestUserInputHandler;
|
||||
@@ -35,6 +36,17 @@ impl ToolHandler for RequestUserInputHandler {
|
||||
}
|
||||
};
|
||||
|
||||
let disallowed_mode = match session.collaboration_mode().await {
|
||||
CollaborationMode::Execute(_) => Some("Execute"),
|
||||
CollaborationMode::Custom(_) => Some("Custom"),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(mode_name) = disallowed_mode {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"request_user_input is unavailable in {mode_name} mode"
|
||||
)));
|
||||
}
|
||||
|
||||
let args: RequestUserInputArgs = parse_arguments(&arguments)?;
|
||||
let response = session
|
||||
.request_user_input(turn.as_ref(), call_id, args)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user