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:
felixxia-oai authored and copyberry committed 2026-08-03 21:37:17 +00:00
1 parent 51d4aa946c
commit 1bbfb5cfad
15 files changed
+539 -59

No files matched your search

+121
View File
@@ -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<()> {