mirror of
https://github.com/openai/codex.git
synced 2026-05-01 01:47:18 +00:00
Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
235 lines
8.0 KiB
Rust
235 lines
8.0 KiB
Rust
use codex_core::config::Constrained;
|
|
use codex_core::protocol::AskForApproval;
|
|
use codex_core::protocol::EventMsg;
|
|
use codex_core::protocol::Op;
|
|
use codex_core::protocol::ReviewDecision;
|
|
use codex_core::protocol::ReviewRequest;
|
|
use codex_core::protocol::ReviewTarget;
|
|
use codex_core::protocol::SandboxPolicy;
|
|
use codex_core::sandboxing::SandboxPermissions;
|
|
use core_test_support::responses::ev_apply_patch_function_call;
|
|
use core_test_support::responses::ev_assistant_message;
|
|
use core_test_support::responses::ev_completed;
|
|
use core_test_support::responses::ev_function_call;
|
|
use core_test_support::responses::ev_reasoning_item_added;
|
|
use core_test_support::responses::ev_reasoning_summary_text_delta;
|
|
use core_test_support::responses::ev_response_created;
|
|
use core_test_support::responses::mount_sse_sequence;
|
|
use core_test_support::responses::sse;
|
|
use core_test_support::responses::start_mock_server;
|
|
use core_test_support::skip_if_no_network;
|
|
use core_test_support::test_codex::test_codex;
|
|
use core_test_support::wait_for_event;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
/// Delegate should surface ExecApprovalRequest from sub-agent and proceed
|
|
/// after parent submits an approval decision.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn codex_delegate_forwards_exec_approval_and_proceeds_on_approval() {
|
|
skip_if_no_network!();
|
|
|
|
// Sub-agent turn 1: emit a shell_command function_call requiring approval, then complete.
|
|
let call_id = "call-exec-1";
|
|
let args = serde_json::json!({
|
|
"command": "rm -rf delegated",
|
|
"timeout_ms": 1000,
|
|
"sandbox_permissions": SandboxPermissions::RequireEscalated,
|
|
})
|
|
.to_string();
|
|
let sse1 = sse(vec![
|
|
ev_response_created("resp-1"),
|
|
ev_function_call(call_id, "shell_command", &args),
|
|
ev_completed("resp-1"),
|
|
]);
|
|
|
|
// Sub-agent turn 2: return structured review output and complete.
|
|
let review_json = serde_json::json!({
|
|
"findings": [],
|
|
"overall_correctness": "ok",
|
|
"overall_explanation": "delegate approved exec",
|
|
"overall_confidence_score": 0.5
|
|
})
|
|
.to_string();
|
|
let sse2 = sse(vec![
|
|
ev_response_created("resp-2"),
|
|
ev_assistant_message("msg-1", &review_json),
|
|
ev_completed("resp-2"),
|
|
]);
|
|
|
|
let server = start_mock_server().await;
|
|
mount_sse_sequence(&server, vec![sse1, sse2]).await;
|
|
|
|
// Build a conversation configured to require approvals so the delegate
|
|
// routes ExecApprovalRequest via the parent.
|
|
let mut builder = test_codex().with_model("gpt-5.1").with_config(|config| {
|
|
config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
|
config.sandbox_policy = SandboxPolicy::ReadOnly;
|
|
});
|
|
let test = builder.build(&server).await.expect("build test codex");
|
|
|
|
// Kick off review (sub-agent starts internally).
|
|
test.codex
|
|
.submit(Op::Review {
|
|
review_request: ReviewRequest {
|
|
target: ReviewTarget::Custom {
|
|
instructions: "Please review".to_string(),
|
|
},
|
|
user_facing_hint: None,
|
|
},
|
|
})
|
|
.await
|
|
.expect("submit review");
|
|
|
|
// Lifecycle: Entered -> ExecApprovalRequest -> Exited(Some) -> TaskComplete.
|
|
wait_for_event(&test.codex, |ev| {
|
|
matches!(ev, EventMsg::EnteredReviewMode(_))
|
|
})
|
|
.await;
|
|
|
|
// Expect parent-side approval request (forwarded by delegate).
|
|
wait_for_event(&test.codex, |ev| {
|
|
matches!(ev, EventMsg::ExecApprovalRequest(_))
|
|
})
|
|
.await;
|
|
|
|
// Approve via parent; id "0" is the active sub_id in tests.
|
|
test.codex
|
|
.submit(Op::ExecApproval {
|
|
id: "0".into(),
|
|
decision: ReviewDecision::Approved,
|
|
})
|
|
.await
|
|
.expect("submit exec approval");
|
|
|
|
wait_for_event(&test.codex, |ev| {
|
|
matches!(ev, EventMsg::ExitedReviewMode(_))
|
|
})
|
|
.await;
|
|
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
|
|
}
|
|
|
|
/// Delegate should surface ApplyPatchApprovalRequest and honor parent decision
|
|
/// so the sub-agent can proceed to completion.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn codex_delegate_forwards_patch_approval_and_proceeds_on_decision() {
|
|
skip_if_no_network!();
|
|
|
|
let call_id = "call-patch-1";
|
|
let patch = "*** Begin Patch\n*** Add File: delegated.txt\n+hello\n*** End Patch\n";
|
|
let sse1 = sse(vec![
|
|
ev_response_created("resp-1"),
|
|
ev_apply_patch_function_call(call_id, patch),
|
|
ev_completed("resp-1"),
|
|
]);
|
|
let review_json = serde_json::json!({
|
|
"findings": [],
|
|
"overall_correctness": "ok",
|
|
"overall_explanation": "delegate patch handled",
|
|
"overall_confidence_score": 0.5
|
|
})
|
|
.to_string();
|
|
let sse2 = sse(vec![
|
|
ev_response_created("resp-2"),
|
|
ev_assistant_message("msg-1", &review_json),
|
|
ev_completed("resp-2"),
|
|
]);
|
|
|
|
let server = start_mock_server().await;
|
|
mount_sse_sequence(&server, vec![sse1, sse2]).await;
|
|
|
|
let mut builder = test_codex().with_model("gpt-5.1").with_config(|config| {
|
|
config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
|
// Use a restricted sandbox so patch approval is required
|
|
config.sandbox_policy = SandboxPolicy::ReadOnly;
|
|
config.include_apply_patch_tool = true;
|
|
});
|
|
let test = builder.build(&server).await.expect("build test codex");
|
|
|
|
test.codex
|
|
.submit(Op::Review {
|
|
review_request: ReviewRequest {
|
|
target: ReviewTarget::Custom {
|
|
instructions: "Please review".to_string(),
|
|
},
|
|
user_facing_hint: None,
|
|
},
|
|
})
|
|
.await
|
|
.expect("submit review");
|
|
|
|
wait_for_event(&test.codex, |ev| {
|
|
matches!(ev, EventMsg::EnteredReviewMode(_))
|
|
})
|
|
.await;
|
|
wait_for_event(&test.codex, |ev| {
|
|
matches!(ev, EventMsg::ApplyPatchApprovalRequest(_))
|
|
})
|
|
.await;
|
|
|
|
// Deny via parent so delegate can continue; id "0" is the active sub_id in tests.
|
|
test.codex
|
|
.submit(Op::PatchApproval {
|
|
id: "0".into(),
|
|
decision: ReviewDecision::Denied,
|
|
})
|
|
.await
|
|
.expect("submit patch approval");
|
|
|
|
wait_for_event(&test.codex, |ev| {
|
|
matches!(ev, EventMsg::ExitedReviewMode(_))
|
|
})
|
|
.await;
|
|
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn codex_delegate_ignores_legacy_deltas() {
|
|
skip_if_no_network!();
|
|
|
|
// Single response with reasoning summary deltas.
|
|
let sse_stream = sse(vec![
|
|
ev_response_created("resp-1"),
|
|
ev_reasoning_item_added("reason-1", &["initial"]),
|
|
ev_reasoning_summary_text_delta("think-1"),
|
|
ev_completed("resp-1"),
|
|
]);
|
|
|
|
let server = start_mock_server().await;
|
|
mount_sse_sequence(&server, vec![sse_stream]).await;
|
|
|
|
let mut builder = test_codex();
|
|
let test = builder.build(&server).await.expect("build test codex");
|
|
|
|
// Kick off review (delegated).
|
|
test.codex
|
|
.submit(Op::Review {
|
|
review_request: ReviewRequest {
|
|
target: ReviewTarget::Custom {
|
|
instructions: "Please review".to_string(),
|
|
},
|
|
user_facing_hint: None,
|
|
},
|
|
})
|
|
.await
|
|
.expect("submit review");
|
|
|
|
let mut reasoning_delta_count = 0;
|
|
let mut legacy_reasoning_delta_count = 0;
|
|
|
|
loop {
|
|
let ev = wait_for_event(&test.codex, |_| true).await;
|
|
match ev {
|
|
EventMsg::ReasoningContentDelta(_) => reasoning_delta_count += 1,
|
|
EventMsg::AgentReasoningDelta(_) => legacy_reasoning_delta_count += 1,
|
|
EventMsg::TaskComplete(_) => break,
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
assert_eq!(reasoning_delta_count, 1, "expected one new reasoning delta");
|
|
assert_eq!(
|
|
legacy_reasoning_delta_count, 1,
|
|
"expected one legacy reasoning delta"
|
|
);
|
|
}
|