permissions: make SessionConfigured profile-only

This commit is contained in:
Michael Bolin
2026-04-27 16:12:15 -07:00
parent 80686c3dfe
commit 1ec30342a1
17 changed files with 192 additions and 161 deletions
+2 -2
View File
@@ -556,6 +556,7 @@ mod tests {
use crate::app::test_support::app_enabled_in_effective_config;
use crate::app::test_support::make_test_app;
use crate::test_support::PathBufExt;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::SessionConfiguredEvent;
@@ -653,8 +654,7 @@ mod tests {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: next_cwd.clone().abs(),
reasoning_effort: None,
history_log_id: 0,
+6 -12
View File
@@ -3568,8 +3568,7 @@ async fn render_clear_ui_header_after_long_transcript_for_snapshot() -> String {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/tmp/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::High),
history_log_id: 0,
@@ -4319,8 +4318,7 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -4383,8 +4381,7 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -4477,8 +4474,7 @@ async fn backtrack_resubmit_preserves_data_image_urls_in_user_turn() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -4861,8 +4857,7 @@ async fn new_session_requests_shutdown_for_previous_conversation() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -4974,8 +4969,7 @@ async fn clear_only_ui_reset_preserves_chat_session_state() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/tmp/project").abs(),
reasoning_effort: None,
history_log_id: 0,
+6 -29
View File
@@ -142,12 +142,8 @@ use codex_protocol::items::AgentMessageContent;
use codex_protocol::items::AgentMessageItem;
use codex_protocol::items::UserMessageItem;
use codex_protocol::models::MessagePhase;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxEnforcement;
use codex_protocol::models::local_image_label_text;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::plan_tool::PlanItemArg as UpdatePlanItemArg;
use codex_protocol::plan_tool::StepStatus as UpdatePlanItemStatus;
#[cfg(test)]
@@ -1618,8 +1614,7 @@ fn thread_session_state_to_legacy_event(
service_tier: session.service_tier,
approval_policy: session.approval_policy,
approvals_reviewer: session.approvals_reviewer,
sandbox_policy: session.sandbox_policy,
permission_profile: Some(session.permission_profile),
permission_profile: session.permission_profile,
cwd: session.cwd,
reasoning_effort: session.reasoning_effort,
history_log_id: session.history_log_id,
@@ -2347,32 +2342,14 @@ impl ChatWidget {
self.config.permissions.approval_policy =
Constrained::allow_only(event.approval_policy);
}
let permission_sync = match event.permission_profile.clone() {
Some(permission_profile) => self
.config
.permissions
.set_permission_profile(permission_profile),
None => self
.config
.permissions
.set_legacy_sandbox_policy(event.sandbox_policy.clone(), event.cwd.as_path()),
};
let permission_sync = self
.config
.permissions
.set_permission_profile(event.permission_profile.clone());
if let Err(err) = permission_sync {
tracing::warn!(%err, "failed to sync permissions from SessionConfigured");
let permission_profile = event.permission_profile.clone().unwrap_or_else(|| {
let file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&event.sandbox_policy,
event.cwd.as_path(),
);
PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(&event.sandbox_policy),
&file_system_sandbox_policy,
NetworkSandboxPolicy::from(&event.sandbox_policy),
)
});
self.config.permissions.permission_profile =
Constrained::allow_only(permission_profile);
Constrained::allow_only(event.permission_profile.clone());
}
self.config.approvals_reviewer = event.approvals_reviewer;
self.status_line_project_root_name_cache = None;
@@ -16,8 +16,7 @@ async fn submission_preserves_text_elements_and_local_images() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -121,8 +120,7 @@ async fn submission_includes_configured_permission_profile() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: Some(expected_permission_profile.clone()),
permission_profile: expected_permission_profile.clone(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -172,10 +170,7 @@ async fn submission_keeps_profile_when_legacy_projection_is_external() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::ExternalSandbox {
network_access: codex_protocol::protocol::NetworkAccess::Restricted,
},
permission_profile: Some(expected_permission_profile.clone()),
permission_profile: expected_permission_profile.clone(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -218,8 +213,7 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -314,8 +308,7 @@ async fn enter_with_only_remote_images_submits_user_turn() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -380,8 +373,7 @@ async fn shift_enter_with_only_remote_images_does_not_submit_user_turn() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -421,8 +413,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -462,8 +453,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -506,8 +496,7 @@ async fn submission_prefers_selected_duplicate_skill_path() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -1004,8 +1004,7 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -24,8 +24,7 @@ async fn resumed_initial_messages_render_history() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -138,8 +137,7 @@ async fn replayed_user_message_preserves_text_elements_and_local_images() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -200,8 +198,7 @@ async fn replayed_user_message_preserves_remote_image_urls() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -256,7 +253,6 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
.expect("set sandbox policy");
chat.config.cwd = test_path_buf("/home/user/main").abs();
let legacy_fallback_sandbox = SandboxPolicy::new_read_only_policy();
let expected_cwd = test_path_buf("/home/user/sub-agent").abs();
let expected_file_system_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
@@ -289,8 +285,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: legacy_fallback_sandbox,
permission_profile: Some(expected_permission_profile.clone()),
permission_profile: expected_permission_profile.clone(),
cwd: expected_cwd.clone(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -355,8 +350,9 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: expected_sandbox.clone(),
permission_profile: None,
permission_profile: codex_protocol::models::PermissionProfile::External {
network: NetworkSandboxPolicy::Restricted,
},
cwd: test_path_buf("/home/user/external").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -405,8 +401,7 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -459,8 +454,7 @@ async fn replayed_user_message_with_only_local_images_does_not_render_history_ce
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -781,8 +775,7 @@ async fn replayed_reasoning_item_hides_raw_reasoning_when_disabled() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
cwd: test_project_path().abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -829,8 +822,7 @@ async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
cwd: test_project_path().abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -483,8 +483,7 @@ async fn permissions_selection_marks_auto_review_current_after_session_configure
service_tier: None,
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::AutoReview,
sandbox_policy: SandboxPolicy::new_workspace_write_policy(),
permission_profile: None,
permission_profile: PermissionProfile::workspace_write(),
cwd: test_project_path().abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -519,6 +518,16 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w
.set_enabled(Feature::GuardianApproval, /*enabled*/ true);
let extra_root = test_path_buf("/tmp/guardian-approvals-extra").abs();
let cwd = test_project_path().abs();
let permission_profile = PermissionProfile::from_legacy_sandbox_policy_for_cwd(
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![extra_root],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
cwd.as_path(),
);
chat.handle_codex_event(Event {
id: "session-configured-custom-workspace".to_string(),
@@ -531,14 +540,8 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w
service_tier: None,
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::AutoReview,
sandbox_policy: SandboxPolicy::WorkspaceWrite {
writable_roots: vec![extra_root],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
permission_profile: None,
cwd: test_project_path().abs(),
permission_profile,
cwd,
reasoning_effort: None,
history_log_id: 0,
history_entry_count: 0,
@@ -1059,8 +1059,7 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -1305,8 +1304,7 @@ async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -1741,8 +1741,7 @@ async fn session_configured_clears_goal_status_footer() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
+32 -11
View File
@@ -54,6 +54,8 @@ use codex_protocol::account::PlanType;
use codex_protocol::mcp::Resource;
#[cfg(test)]
use codex_protocol::mcp::ResourceTemplate;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::WebSearchAction;
use codex_protocol::models::local_image_label_text;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
@@ -64,7 +66,6 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::McpAuthStatus;
use codex_protocol::protocol::McpInvocation;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionConfiguredEvent;
use codex_protocol::request_user_input::RequestUserInputAnswer;
use codex_protocol::request_user_input::RequestUserInputQuestion;
@@ -1240,7 +1241,7 @@ pub(crate) fn new_session_info(
model,
reasoning_effort,
approval_policy,
sandbox_policy,
permission_profile,
..
} = event;
// Header box rendered as history (so it appears at the very top)
@@ -1251,7 +1252,7 @@ pub(crate) fn new_session_info(
config.cwd.to_path_buf(),
CODEX_CLI_VERSION,
)
.with_yolo_mode(has_yolo_permissions(approval_policy, &sandbox_policy));
.with_yolo_mode(has_yolo_permissions(approval_policy, &permission_profile));
let mut parts: Vec<Box<dyn HistoryCell>> = vec![Box::new(header)];
if is_first_event {
@@ -1313,14 +1314,23 @@ pub(crate) fn new_session_info(
pub(crate) fn is_yolo_mode(config: &Config) -> bool {
has_yolo_permissions(
config.permissions.approval_policy.value(),
&config
.permissions
.legacy_sandbox_policy(config.cwd.as_path()),
&config.permissions.permission_profile(),
)
}
fn has_yolo_permissions(approval_policy: AskForApproval, sandbox_policy: &SandboxPolicy) -> bool {
approval_policy == AskForApproval::Never && *sandbox_policy == SandboxPolicy::DangerFullAccess
fn has_yolo_permissions(
approval_policy: AskForApproval,
permission_profile: &PermissionProfile,
) -> bool {
approval_policy == AskForApproval::Never
&& matches!(
permission_profile,
PermissionProfile::Disabled
| PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: codex_protocol::protocol::NetworkSandboxPolicy::Enabled,
}
)
}
pub(crate) fn new_user_prompt(
@@ -2993,7 +3003,6 @@ mod tests {
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::McpAuthStatus;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionConfiguredEvent;
use dirs::home_dir;
use pretty_assertions::assert_eq;
@@ -3184,8 +3193,7 @@ mod tests {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/tmp/project").abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -4205,6 +4213,19 @@ mod tests {
insta::assert_snapshot!(rendered);
}
#[test]
fn yolo_mode_includes_managed_full_access_profiles() {
let permission_profile = PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: codex_protocol::protocol::NetworkSandboxPolicy::Enabled,
};
assert!(has_yolo_permissions(
AskForApproval::Never,
&permission_profile
));
}
#[test]
fn session_header_directory_center_truncates() {
let mut dir = home_dir().expect("home directory");