Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions codex-rs/core/src/config/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2075,7 +2075,7 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl
.await?;

assert!(config.explicit_permission_profile_mode);
assert!(config.custom_permission_profile_ids.is_empty());
assert!(config.custom_permission_profiles.is_empty());
let policy = config.permissions.file_system_sandbox_policy();
assert_eq!(
config
Expand Down Expand Up @@ -2723,7 +2723,7 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
entries: BTreeMap::from([(
"dev".to_string(),
PermissionProfileToml {
description: None,
description: Some("Workspace access.".to_string()),
extends: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
Expand All @@ -2748,8 +2748,11 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
.await?;

assert_eq!(
config.custom_permission_profile_ids,
vec!["dev".to_string()]
config.custom_permission_profiles,
vec![CustomPermissionProfileSummary {
id: "dev".to_string(),
description: Some("Workspace access.".to_string()),
}]
);
let memories_root = AbsolutePathBuf::from_absolute_path(std::fs::canonicalize(
codex_home.path().join("memories"),
Expand Down
23 changes: 18 additions & 5 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -585,8 +585,8 @@ pub struct Config {
/// of the legacy `sandbox_mode` syntax.
pub explicit_permission_profile_mode: bool,

/// User-defined permission profile IDs available from effective config.
pub custom_permission_profile_ids: Vec<String>,
/// User-defined permission profiles available from effective config.
pub custom_permission_profiles: Vec<CustomPermissionProfileSummary>,

/// Configures who approval requests are routed to for review once they have
/// been escalated. This does not disable separate safety checks such as
Expand Down Expand Up @@ -1889,6 +1889,12 @@ pub struct AgentRoleConfig {
pub nickname_candidates: Option<Vec<String>>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomPermissionProfileSummary {
pub id: String,
pub description: Option<String>,
}

fn resolve_tool_suggest_config(
config_toml: &ConfigToml,
config_layer_stack: &ConfigLayerStack,
Expand Down Expand Up @@ -2624,11 +2630,18 @@ impl Config {
permission_config_syntax,
Some(PermissionConfigSyntax::Profiles)
);
let custom_permission_profile_ids = cfg
let custom_permission_profiles = cfg
.permissions
.as_ref()
.map_or_else(Vec::new, |permissions| {
permissions.entries.keys().cloned().collect()
permissions
.entries
.iter()
.map(|(id, profile)| CustomPermissionProfileSummary {
id: id.clone(),
description: profile.description.clone(),
})
.collect()
});
let using_implicit_builtin_profile = permission_config_syntax.is_none()
&& effective_permission_selection.selected_profile_id.is_none();
Expand Down Expand Up @@ -3361,7 +3374,7 @@ impl Config {
windows_sandbox_private_desktop,
},
explicit_permission_profile_mode,
custom_permission_profile_ids,
custom_permission_profiles,
approvals_reviewer: constrained_approvals_reviewer.value(),
enforce_residency: enforce_residency.value,
notify: cfg.notify,
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/thread-manager-sample/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
Constrained::allow_any(PermissionProfile::read_only()),
)?,
explicit_permission_profile_mode: false,
custom_permission_profile_ids: Vec::new(),
custom_permission_profiles: Vec::new(),
approvals_reviewer: ApprovalsReviewer::User,
enforce_residency: Constrained::allow_any(/*initial_value*/ None),
hide_agent_reasoning: false,
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ mod model_popups;
mod notifications;
use self::notifications::Notification;
mod permission_popups;
mod permissions_menu;
mod protocol;
mod protocol_requests;
mod rate_limits;
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/tui/src/chatwidget/permission_popups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ impl ChatWidget {

/// Open a popup to choose the permissions mode.
pub(crate) fn open_permissions_popup(&mut self) {
if self.config.explicit_permission_profile_mode {
self.open_permission_profiles_popup();
return;
}

let include_read_only = cfg!(target_os = "windows");
let current_approval =
AskForApproval::from(self.config.permissions.approval_policy.value());
Expand Down
171 changes: 171 additions & 0 deletions codex-rs/tui/src/chatwidget/permissions_menu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use super::*;

impl ChatWidget {
pub(super) fn open_permission_profiles_popup(&mut self) {
let active_profile_id = self
.config
.permissions
.active_permission_profile()
.map(|profile| profile.id);
let presets = builtin_approval_presets();
let Some(read_only) = presets.iter().find(|preset| preset.id == "read-only") else {
self.add_error_message(
"Internal error: missing the 'read-only' approval preset.".to_string(),
);
return;
};
let Some(default) = presets.iter().find(|preset| preset.id == "auto") else {
self.add_error_message(
"Internal error: missing the 'auto' approval preset.".to_string(),
);
return;
};
let Some(full_access) = presets.iter().find(|preset| preset.id == "full-access") else {
self.add_error_message(
"Internal error: missing the 'full-access' approval preset.".to_string(),
);
return;
};
let mut items = vec![
self.builtin_permission_mode_selection_item(
default,
":workspace",
default
.description
.replace(" (Identical to Agent mode)", ""),
AskForApproval::from(default.approval),
ApprovalsReviewer::User,
),
];
if self.config.features.enabled(Feature::GuardianApproval) {
items.push(self.builtin_permission_mode_selection_item(
default,
":workspace",
AUTO_REVIEW_DESCRIPTION.to_string(),
AskForApproval::OnRequest,
ApprovalsReviewer::AutoReview,
));
}
items.push(self.builtin_permission_mode_selection_item(
full_access,
":danger-no-sandbox",
full_access.description.to_string(),
AskForApproval::from(full_access.approval),
ApprovalsReviewer::User,
));
items.push(self.builtin_permission_mode_selection_item(
read_only,
":read-only",
read_only.description.to_string(),
AskForApproval::from(read_only.approval),
ApprovalsReviewer::User,
));
items.extend(
self.config
.custom_permission_profiles
.iter()
.map(|profile| {
Self::permission_profile_selection_item(
&profile.id,
&profile.id,
profile
.description
.as_deref()
.unwrap_or("Configured permission profile."),
active_profile_id.as_deref(),
)
}),
);

self.bottom_pane.show_selection_view(SelectionViewParams {
title: Some("Update Model Permissions".to_string()),
footer_hint: Some(standard_popup_hint_line()),
items,
header: Box::new(()),
..Default::default()
});
}

fn builtin_permission_mode_selection_item(
&self,
preset: &ApprovalPreset,
id: &str,
description: String,
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
) -> SelectionItem {
let label = if approvals_reviewer == ApprovalsReviewer::AutoReview {
"Auto-review"
} else {
preset.label
};
let active_profile_id = self
.config
.permissions
.active_permission_profile()
.map(|profile| profile.id);
let current_approval =
AskForApproval::from(self.config.permissions.approval_policy.value());
let current_reviewer = self.config.approvals_reviewer;
let profile_id = id.to_string();
let selection = PermissionProfileSelection {
profile_id,
approval_policy: Some(approval_policy),
approvals_reviewer: Some(approvals_reviewer),
display_label: label.to_string(),
};
SelectionItem {
name: label.to_string(),
description: Some(description),
is_current: active_profile_id.as_deref() == Some(id)
&& current_approval == approval_policy
&& current_reviewer == approvals_reviewer,
actions: self.permission_mode_actions(
preset,
label.to_string(),
approvals_reviewer,
Some(selection),
/*return_to_permissions*/ true,
),
dismiss_on_select: true,
disabled_reason: self
.config
.permissions
.approval_policy
.can_set(&approval_policy.to_core())
.err()
.map(|err| err.to_string())
.or_else(|| {
self.config
.permissions
.can_set_permission_profile(&preset.permission_profile)
.err()
.map(|err| err.to_string())
}),
..Default::default()
}
}

fn permission_profile_selection_item(
label: &str,
id: &str,
description: &str,
active_profile_id: Option<&str>,
) -> SelectionItem {
let id_for_action = id.to_string();
let selection = PermissionProfileSelection {
profile_id: id_for_action.clone(),
approval_policy: None,
approvals_reviewer: None,
display_label: id_for_action,
};
SelectionItem {
name: label.to_string(),
description: Some(description.to_string()),
is_current: active_profile_id == Some(id),
actions: Self::permission_profile_selection_actions(selection),
dismiss_on_select: true,
..Default::default()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
source: tui/src/chatwidget/tests/permissions.rs
expression: "render_bottom_popup(&chat, 80)"
---
Update Model Permissions

› 1. Default (current) Codex can read and edit files in the current
workspace, and run commands. Approval is required to
access the internet or edit other files.
2. Auto-review Same workspace-write permissions as Default, but
eligible `on-request` approvals are routed through the
auto-reviewer subagent.
3. Full Access Codex can edit files outside this workspace and access
the internet without asking for approval. Exercise
caution when using.
4. Read Only Codex can read files in the current workspace.
Approval is required to edit files or access the
internet.

Press enter to confirm or esc to go back
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
source: tui/src/chatwidget/tests/permissions.rs
expression: "render_bottom_popup(&chat, 80)"
---
Update Model Permissions

1. Default Codex can read and edit files in the current
workspace, and run commands. Approval is required
to access the internet or edit other files.
2. Auto-review Same workspace-write permissions as Default, but
eligible `on-request` approvals are routed through
the auto-reviewer subagent.
3. Full Access Codex can edit files outside this workspace and
access the internet without asking for approval.
Exercise caution when using.
4. Read Only Codex can read files in the current workspace.
Approval is required to edit files or access the
internet.
› 5. locked-down (current) Inspect and patch only approved workspace files.
6. web-enabled Workspace profile with network access.

Press enter to confirm or esc to go back
Loading
Loading