mirror of
https://github.com/openai/codex.git
synced 2026-11-05 08:28:29 +00:00
Avoid reinjecting permissions after command approvals (#36800)
## What changed - Track approved command prefixes separately from the stable permissions instructions in world-state snapshots. - Emit only newly approved prefixes after an exec-policy amendment instead of appending the full permissions block again. - Preserve prefix updates when full permissions instructions are disabled and remain compatible with legacy world-state snapshots. ## Testing - Cover incremental prefix additions, removals, legacy snapshots, history rollback, and approval flows with permissions instructions enabled or disabled. GitOrigin-RevId: e6f68c6a91be82750e70e28456f2b7c58607fbd8
This commit is contained in:
1 parent
51d4aa946c
commit
1bbfb5cfad
15 files changed
+539
-59
No files matched your search
@@ -2139,6 +2139,127 @@ async fn approving_apply_patch_for_session_skips_future_prompts_for_same_file()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[cfg(unix)]
|
||||
async fn approving_execpolicy_amendment_does_not_reinject_permissions_instructions() -> Result<()> {
|
||||
assert_execpolicy_amendment_context(PermissionsInstructionsExpectation::Included).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[cfg(unix)]
|
||||
async fn approving_execpolicy_amendment_is_reported_when_permissions_are_disabled() -> Result<()> {
|
||||
assert_execpolicy_amendment_context(PermissionsInstructionsExpectation::Omitted).await
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum PermissionsInstructionsExpectation {
|
||||
Included,
|
||||
Omitted,
|
||||
}
|
||||
|
||||
async fn assert_execpolicy_amendment_context(
|
||||
expectation: PermissionsInstructionsExpectation,
|
||||
) -> Result<()> {
|
||||
let (include_permissions_instructions, expected_permissions_block_count) = match expectation {
|
||||
PermissionsInstructionsExpectation::Included => (true, 1),
|
||||
PermissionsInstructionsExpectation::Omitted => (false, 0),
|
||||
};
|
||||
let server = start_mock_server().await;
|
||||
let approval_policy = AskForApproval::OnRequest;
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let sandbox_policy_for_config = sandbox_policy.clone();
|
||||
let mut builder = test_codex()
|
||||
.with_model_info_override("gpt-5.4", |model_info| {
|
||||
model_info.model_messages = None;
|
||||
})
|
||||
.with_config(move |config| {
|
||||
config.include_permissions_instructions = include_permissions_instructions;
|
||||
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
|
||||
config
|
||||
.features
|
||||
.disable(Feature::DeferredExecutor)
|
||||
.expect("test config should allow feature update");
|
||||
config
|
||||
.set_legacy_sandbox_policy(sandbox_policy_for_config)
|
||||
.expect("set sandbox policy");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
let call_id = "permissions-prefix";
|
||||
let (event, expected_command) = ActionKind::RunCommand {
|
||||
command: "touch permissions-prefix.txt",
|
||||
}
|
||||
.prepare(
|
||||
&test,
|
||||
&server,
|
||||
call_id,
|
||||
SandboxPermissions::RequireEscalated,
|
||||
)
|
||||
.await?;
|
||||
let expected_command =
|
||||
expected_command.expect("execpolicy amendment scenario should produce a shell command");
|
||||
let expected_execpolicy_amendment = ExecPolicyAmendment::new(vec![
|
||||
"touch".to_string(),
|
||||
"permissions-prefix.txt".to_string(),
|
||||
]);
|
||||
|
||||
let _ = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-permissions-prefix-1"),
|
||||
event,
|
||||
ev_completed("resp-permissions-prefix-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let follow_up = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_assistant_message("msg-permissions-prefix", "done"),
|
||||
ev_completed("resp-permissions-prefix-2"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
submit_turn(&test, "permissions-prefix", approval_policy, sandbox_policy).await?;
|
||||
|
||||
let approval = expect_exec_approval(&test, expected_command.as_str()).await;
|
||||
assert_eq!(
|
||||
approval.proposed_execpolicy_amendment,
|
||||
Some(expected_execpolicy_amendment.clone())
|
||||
);
|
||||
test.codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::ApprovedExecpolicyAmendment {
|
||||
proposed_execpolicy_amendment: expected_execpolicy_amendment,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
|
||||
let developer_messages = follow_up.single_request().message_input_texts("developer");
|
||||
let permissions_block_count = developer_messages
|
||||
.iter()
|
||||
.map(|message| message.matches("<permissions instructions>").count())
|
||||
.sum::<usize>();
|
||||
assert_eq!(
|
||||
permissions_block_count, expected_permissions_block_count,
|
||||
"saving an execpolicy amendment must respect the full permissions rendering mode: \
|
||||
{developer_messages:#?}"
|
||||
);
|
||||
assert!(
|
||||
developer_messages
|
||||
.iter()
|
||||
.any(|message| message.contains("Approved command prefix saved:")
|
||||
&& message.contains(r#"["touch", "permissions-prefix.txt"]"#)),
|
||||
"expected developer message documenting saved rule, got: {developer_messages:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[cfg(unix)]
|
||||
async fn approving_execpolicy_amendment_persists_policy_and_skips_future_prompts() -> Result<()> {
|
||||
|
||||
Reference in new issue
Block a user