mirror of
https://github.com/openai/codex.git
synced 2026-04-28 08:34:54 +00:00
Decouple request permissions feature and tool (#14426)
This commit is contained in:
@@ -10,6 +10,7 @@ use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecApprovalRequestEvent;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RejectConfig;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::request_permissions::PermissionGrantScope;
|
||||
@@ -395,6 +396,95 @@ async fn with_additional_permissions_requires_approval_under_on_request() -> Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn request_permissions_tool_is_auto_denied_when_reject_request_permissions_is_enabled()
|
||||
-> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let approval_policy = AskForApproval::Reject(RejectConfig {
|
||||
sandbox_approval: false,
|
||||
rules: false,
|
||||
skill_approval: false,
|
||||
request_permissions: true,
|
||||
mcp_elicitations: false,
|
||||
});
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let sandbox_policy_for_config = sandbox_policy.clone();
|
||||
|
||||
let mut builder = test_codex().with_config(move |config| {
|
||||
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
|
||||
config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config);
|
||||
config
|
||||
.features
|
||||
.enable(Feature::RequestPermissionsTool)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
let requested_dir = test.workspace_path("request-permissions-reject");
|
||||
fs::create_dir_all(&requested_dir)?;
|
||||
let requested_permissions = requested_directory_write_permissions(&requested_dir);
|
||||
let call_id = "request_permissions_reject_auto_denied";
|
||||
let event = request_permissions_tool_event(
|
||||
call_id,
|
||||
"Request access through the standalone tool",
|
||||
&requested_permissions,
|
||||
)?;
|
||||
|
||||
let _ = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-request-permissions-reject-1"),
|
||||
event,
|
||||
ev_completed("resp-request-permissions-reject-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let results = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_assistant_message("msg-request-permissions-reject-1", "done"),
|
||||
ev_completed("resp-request-permissions-reject-2"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
submit_turn(
|
||||
&test,
|
||||
"request permissions under reject.request_permissions",
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let event = wait_for_event(&test.codex, |event| {
|
||||
matches!(
|
||||
event,
|
||||
EventMsg::RequestPermissions(_) | EventMsg::TurnComplete(_)
|
||||
)
|
||||
})
|
||||
.await;
|
||||
assert!(
|
||||
matches!(event, EventMsg::TurnComplete(_)),
|
||||
"request_permissions should not emit a prompt when reject.request_permissions is set: {event:?}"
|
||||
);
|
||||
|
||||
let call_output = results.single_request().function_call_output(call_id);
|
||||
let result: RequestPermissionsResponse =
|
||||
serde_json::from_str(call_output["output"].as_str().unwrap_or_default())?;
|
||||
assert_eq!(
|
||||
result,
|
||||
RequestPermissionsResponse {
|
||||
permissions: PermissionProfile::default(),
|
||||
scope: PermissionGrantScope::Turn,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn relative_additional_permissions_resolve_against_tool_workdir() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -1254,6 +1344,118 @@ async fn request_permissions_grants_apply_to_later_shell_command_calls() -> Resu
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn request_permissions_grants_apply_to_later_shell_command_calls_without_inline_permission_feature()
|
||||
-> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let approval_policy = AskForApproval::OnRequest;
|
||||
let sandbox_policy = workspace_write_excluding_tmp();
|
||||
let sandbox_policy_for_config = sandbox_policy.clone();
|
||||
|
||||
let mut builder = test_codex().with_config(move |config| {
|
||||
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
|
||||
config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config);
|
||||
config
|
||||
.features
|
||||
.enable(Feature::RequestPermissionsTool)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
let outside_dir = tempfile::tempdir()?;
|
||||
let outside_write = outside_dir
|
||||
.path()
|
||||
.join("sticky-shell-feature-independent.txt");
|
||||
let command = format!(
|
||||
"printf {:?} > {:?} && cat {:?}",
|
||||
"sticky-shell-feature-independent-ok", outside_write, outside_write
|
||||
);
|
||||
let requested_permissions = requested_directory_write_permissions(outside_dir.path());
|
||||
let normalized_requested_permissions =
|
||||
normalized_directory_write_permissions(outside_dir.path())?;
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-sticky-shell-independent-1"),
|
||||
request_permissions_tool_event(
|
||||
"permissions-call",
|
||||
"Allow writing outside the workspace",
|
||||
&requested_permissions,
|
||||
)?,
|
||||
ev_completed("resp-sticky-shell-independent-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-sticky-shell-independent-2"),
|
||||
shell_command_event("shell-call", &command)?,
|
||||
ev_completed("resp-sticky-shell-independent-2"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-sticky-shell-independent-3"),
|
||||
ev_assistant_message("msg-sticky-shell-independent-1", "done"),
|
||||
ev_completed("resp-sticky-shell-independent-3"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
submit_turn(
|
||||
&test,
|
||||
"write outside the workspace without inline permission feature",
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let granted_permissions = expect_request_permissions_event(&test, "permissions-call").await;
|
||||
assert_eq!(
|
||||
granted_permissions,
|
||||
normalized_requested_permissions.clone()
|
||||
);
|
||||
test.codex
|
||||
.submit(Op::RequestPermissionsResponse {
|
||||
id: "permissions-call".to_string(),
|
||||
response: RequestPermissionsResponse {
|
||||
permissions: normalized_requested_permissions.clone(),
|
||||
scope: PermissionGrantScope::Turn,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
if let Some(approval) = wait_for_exec_approval_or_completion(&test).await {
|
||||
test.codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
}
|
||||
|
||||
let shell_output = responses
|
||||
.function_call_output_text("shell-call")
|
||||
.map(|output| json!({ "output": output }))
|
||||
.unwrap_or_else(|| panic!("expected shell-call output"));
|
||||
let result = parse_result(&shell_output);
|
||||
assert!(
|
||||
result.exit_code.is_none_or(|exit_code| exit_code == 0),
|
||||
"expected success output, got exit_code={:?}, stdout={:?}",
|
||||
result.exit_code,
|
||||
result.stdout
|
||||
);
|
||||
assert_eq!(result.stdout.trim(), "sticky-shell-feature-independent-ok");
|
||||
assert_eq!(
|
||||
fs::read_to_string(&outside_write)?,
|
||||
"sticky-shell-feature-independent-ok"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn partial_request_permissions_grants_do_not_preapprove_new_permissions() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
Reference in New Issue
Block a user