Compare commits

...

2 Commits

Author SHA1 Message Date
Dylan Hurd
993274057e Add request_permissions UI support
Co-authored-by: Codex <noreply@openai.com>
2026-03-22 14:35:19 -07:00
Dylan Hurd
3b52dd7b7d Add request_permissions profile persistence core support
Co-authored-by: Codex <noreply@openai.com>
2026-03-22 14:35:19 -07:00
15 changed files with 415 additions and 39 deletions

View File

@@ -84,6 +84,7 @@ use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource;
use codex_protocol::protocol::TokenUsage as CoreTokenUsage;
use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo;
use codex_protocol::request_permissions::PermissionGrantScope as CorePermissionGrantScope;
use codex_protocol::request_permissions::PermissionProfilePersistence as CorePermissionProfilePersistence;
use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile;
use codex_protocol::user_input::ByteRange as CoreByteRange;
use codex_protocol::user_input::TextElement as CoreTextElement;
@@ -1129,6 +1130,21 @@ pub struct RequestPermissionProfile {
pub file_system: Option<AdditionalFileSystemPermissions>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct PermissionProfilePersistence {
pub profile_name: String,
}
impl From<CorePermissionProfilePersistence> for PermissionProfilePersistence {
fn from(value: CorePermissionProfilePersistence) -> Self {
Self {
profile_name: value.profile_name,
}
}
}
impl From<CoreRequestPermissionProfile> for RequestPermissionProfile {
fn from(value: CoreRequestPermissionProfile) -> Self {
Self {
@@ -5681,6 +5697,9 @@ pub struct PermissionsRequestApprovalParams {
pub item_id: String,
pub reason: Option<String>,
pub permissions: RequestPermissionProfile,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub permissions_profile_persistence: Option<PermissionProfilePersistence>,
}
v2_enum_from_core!(
@@ -5699,6 +5718,8 @@ pub struct PermissionsRequestApprovalResponse {
pub permissions: GrantedPermissionProfile,
#[serde(default)]
pub scope: PermissionGrantScope,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub persist_to_profile: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]

View File

@@ -126,12 +126,14 @@ use codex_protocol::protocol::GuardianAssessmentEvent;
use codex_protocol::protocol::McpToolCallBeginEvent;
use codex_protocol::protocol::McpToolCallEndEvent;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::PersistPermissionProfileAction;
use codex_protocol::protocol::RealtimeEvent;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::ReviewOutputEvent;
use codex_protocol::protocol::TokenCountEvent;
use codex_protocol::protocol::TurnDiffEvent;
use codex_protocol::request_permissions::PermissionGrantScope as CorePermissionGrantScope;
use codex_protocol::request_permissions::PermissionProfilePersistence as CorePermissionProfilePersistence;
use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile;
use codex_protocol::request_permissions::RequestPermissionsResponse as CoreRequestPermissionsResponse;
use codex_protocol::request_user_input::RequestUserInputAnswer as CoreRequestUserInputAnswer;
@@ -859,6 +861,10 @@ pub(crate) async fn apply_bespoke_event_handling(
item_id: request.call_id.clone(),
reason: request.reason,
permissions: request.permissions.into(),
permissions_profile_persistence: request
.permissions_profile_persistence
.clone()
.map(codex_app_server_protocol::PermissionProfilePersistence::from),
};
let (pending_request_id, rx) = outgoing
.send_request(ServerRequestPayload::PermissionsRequestApproval(params))
@@ -867,6 +873,7 @@ pub(crate) async fn apply_bespoke_event_handling(
on_request_permissions_response(
request.call_id,
requested_permissions,
request.permissions_profile_persistence,
pending_request_id,
rx,
conversation,
@@ -888,6 +895,7 @@ pub(crate) async fn apply_bespoke_event_handling(
.submit(Op::RequestPermissionsResponse {
id: request.call_id,
response: empty,
persist_permissions: None,
})
.await
{
@@ -2445,6 +2453,7 @@ fn mcp_server_elicitation_response_from_client_result(
async fn on_request_permissions_response(
call_id: String,
requested_permissions: CoreRequestPermissionProfile,
permissions_profile_persistence: Option<CorePermissionProfilePersistence>,
pending_request_id: RequestId,
receiver: oneshot::Receiver<ClientRequestResult>,
conversation: Arc<CodexThread>,
@@ -2454,9 +2463,11 @@ async fn on_request_permissions_response(
let response = receiver.await;
resolve_server_request_on_thread_listener(&thread_state, pending_request_id).await;
drop(request_permissions_guard);
let Some(response) =
request_permissions_response_from_client_result(requested_permissions, response)
else {
let Some((response, persist_permissions)) = request_permissions_response_from_client_result(
requested_permissions,
permissions_profile_persistence,
response,
) else {
return;
};
@@ -2464,6 +2475,7 @@ async fn on_request_permissions_response(
.submit(Op::RequestPermissionsResponse {
id: call_id,
response,
persist_permissions,
})
.await
{
@@ -2473,24 +2485,34 @@ async fn on_request_permissions_response(
fn request_permissions_response_from_client_result(
requested_permissions: CoreRequestPermissionProfile,
permissions_profile_persistence: Option<CorePermissionProfilePersistence>,
response: std::result::Result<ClientRequestResult, oneshot::error::RecvError>,
) -> Option<CoreRequestPermissionsResponse> {
) -> Option<(
CoreRequestPermissionsResponse,
Option<PersistPermissionProfileAction>,
)> {
let value = match response {
Ok(Ok(value)) => value,
Ok(Err(err)) if is_turn_transition_server_request_error(&err) => return None,
Ok(Err(err)) => {
error!("request failed with client error: {err:?}");
return Some(CoreRequestPermissionsResponse {
permissions: Default::default(),
scope: CorePermissionGrantScope::Turn,
});
return Some((
CoreRequestPermissionsResponse {
permissions: Default::default(),
scope: CorePermissionGrantScope::Turn,
},
None,
));
}
Err(err) => {
error!("request failed: {err:?}");
return Some(CoreRequestPermissionsResponse {
permissions: Default::default(),
scope: CorePermissionGrantScope::Turn,
});
return Some((
CoreRequestPermissionsResponse {
permissions: Default::default(),
scope: CorePermissionGrantScope::Turn,
},
None,
));
}
};
@@ -2500,16 +2522,26 @@ fn request_permissions_response_from_client_result(
PermissionsRequestApprovalResponse {
permissions: V2GrantedPermissionProfile::default(),
scope: codex_app_server_protocol::PermissionGrantScope::Turn,
persist_to_profile: false,
}
});
Some(CoreRequestPermissionsResponse {
permissions: intersect_permission_profiles(
requested_permissions.into(),
response.permissions.into(),
)
.into(),
scope: response.scope.to_core(),
})
let permissions: codex_protocol::models::PermissionProfile =
intersect_permission_profiles(requested_permissions.into(), response.permissions.into());
let persist_permissions = if response.persist_to_profile {
permissions_profile_persistence.map(|target| PersistPermissionProfileAction {
profile_name: target.profile_name,
permissions: permissions.clone(),
})
} else {
None
};
Some((
CoreRequestPermissionsResponse {
permissions: permissions.into(),
scope: response.scope.to_core(),
},
persist_permissions,
))
}
const REVIEW_FALLBACK_MESSAGE: &str = "Reviewer failed to output a response.";
@@ -3058,6 +3090,7 @@ mod tests {
let response = request_permissions_response_from_client_result(
CoreRequestPermissionProfile::default(),
None,
Ok(Err(error)),
);
@@ -3148,6 +3181,7 @@ mod tests {
for (granted_permissions, expected_permissions) in cases {
let response = request_permissions_response_from_client_result(
requested_permissions.clone(),
None,
Ok(Ok(serde_json::json!({
"permissions": granted_permissions,
}))),
@@ -3156,10 +3190,13 @@ mod tests {
assert_eq!(
response,
CoreRequestPermissionsResponse {
permissions: expected_permissions,
scope: CorePermissionGrantScope::Turn,
}
(
CoreRequestPermissionsResponse {
permissions: expected_permissions,
scope: CorePermissionGrantScope::Turn,
},
None,
)
);
}
}
@@ -3168,6 +3205,7 @@ mod tests {
fn request_permissions_response_preserves_session_scope() {
let response = request_permissions_response_from_client_result(
CoreRequestPermissionProfile::default(),
None,
Ok(Ok(serde_json::json!({
"scope": "session",
"permissions": {},
@@ -3177,10 +3215,13 @@ mod tests {
assert_eq!(
response,
CoreRequestPermissionsResponse {
permissions: CoreRequestPermissionProfile::default(),
scope: CorePermissionGrantScope::Session,
}
(
CoreRequestPermissionsResponse {
permissions: CoreRequestPermissionProfile::default(),
scope: CorePermissionGrantScope::Session,
},
None,
)
);
}

View File

@@ -96,6 +96,7 @@ async fn request_permissions_round_trip() -> Result<()> {
}),
},
scope: PermissionGrantScope::Turn,
persist_to_profile: false,
})?,
)
.await?;

View File

@@ -33,6 +33,7 @@ use crate::models_manager::manager::ModelsManager;
use crate::models_manager::manager::RefreshStrategy;
use crate::parse_command::parse_command;
use crate::parse_turn_item;
use crate::permission_profile_persistence::persistence_target_for_permissions;
use crate::realtime_conversation::RealtimeConversationManager;
use crate::realtime_conversation::handle_audio as handle_realtime_conversation_audio;
use crate::realtime_conversation::handle_close as handle_realtime_conversation_close;
@@ -2998,7 +2999,11 @@ impl Session {
call_id,
turn_id: turn_context.sub_id.clone(),
reason: args.reason,
permissions: args.permissions,
permissions: args.permissions.clone(),
permissions_profile_persistence: persistence_target_for_permissions(
turn_context.config.as_ref(),
&args.permissions.into(),
),
});
self.send_event(turn_context, event).await;
rx_response.await.ok()
@@ -4253,8 +4258,18 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
handlers::request_user_input_response(&sess, id, response).await;
false
}
Op::RequestPermissionsResponse { id, response } => {
handlers::request_permissions_response(&sess, id, response).await;
Op::RequestPermissionsResponse {
id,
response,
persist_permissions,
} => {
handlers::request_permissions_response(
&sess,
id,
response,
persist_permissions,
)
.await;
false
}
Op::DynamicToolResponse { id, response } => {
@@ -4400,6 +4415,7 @@ mod handlers {
use crate::codex::spawn_review_thread;
use crate::config::Config;
use crate::permission_profile_persistence::persist_permissions_for_profile;
use crate::mcp::auth::compute_auth_statuses;
use crate::mcp::collect_mcp_snapshot_from_manager;
@@ -4420,6 +4436,7 @@ mod handlers {
use codex_protocol::protocol::ListSkillsResponseEvent;
use codex_protocol::protocol::McpServerRefreshConfig;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::PersistPermissionProfileAction;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::ReviewRequest;
use codex_protocol::protocol::RolloutItem;
@@ -4681,7 +4698,19 @@ mod handlers {
sess: &Arc<Session>,
id: String,
response: RequestPermissionsResponse,
persist_permissions: Option<PersistPermissionProfileAction>,
) {
if let Some(action) = persist_permissions.as_ref()
&& let Err(err) = persist_permissions_for_profile(sess.as_ref(), action).await
{
let message = format!("Failed to update permissions profile: {err}");
tracing::warn!("{message}");
sess.send_event_raw(Event {
id: id.clone(),
msg: EventMsg::Warning(WarningEvent { message }),
})
.await;
}
sess.notify_request_permissions_response(&id, response)
.await;
}

View File

@@ -764,6 +764,7 @@ async fn handle_request_permissions(
.submit(Op::RequestPermissionsResponse {
id: call_id,
response,
persist_permissions: None,
})
.await;
}

View File

@@ -200,6 +200,7 @@ async fn handle_request_permissions_uses_tool_call_id_for_round_trip() {
}),
..RequestPermissionProfile::default()
},
permissions_profile_persistence: None,
},
&cancel_token,
)
@@ -234,6 +235,7 @@ async fn handle_request_permissions_uses_tool_call_id_for_round_trip() {
Op::RequestPermissionsResponse {
id: call_id,
response: expected_response,
persist_permissions: None,
}
);
}

View File

@@ -54,6 +54,7 @@ mod network_policy_decision;
pub mod network_proxy_loader;
mod original_image_detail;
mod packages;
mod permission_profile_persistence;
pub use mcp_connection_manager::MCP_SANDBOX_STATE_CAPABILITY;
pub use mcp_connection_manager::MCP_SANDBOX_STATE_METHOD;
pub use mcp_connection_manager::SandboxState;

View File

@@ -0,0 +1,179 @@
use std::collections::BTreeMap;
use std::io;
use toml_edit::value;
use crate::codex::Session;
use crate::config::Config;
use crate::config::deserialize_config_toml_with_base;
use crate::config::edit::ConfigEdit;
use crate::config::edit::ConfigEditsBuilder;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::request_permissions::PermissionProfilePersistence;
pub(crate) fn persistence_target_for_permissions(
config: &Config,
permissions: &PermissionProfile,
) -> Option<PermissionProfilePersistence> {
if !is_supported_filesystem_only_request(permissions) {
return None;
}
let user_layer = config.config_layer_stack.get_user_layer()?;
let user_config =
deserialize_config_toml_with_base(user_layer.config.clone(), &config.codex_home).ok()?;
let profile_name = user_config.default_permissions?;
let permissions = user_config.permissions?;
permissions
.entries
.contains_key(profile_name.as_str())
.then_some(PermissionProfilePersistence { profile_name })
}
pub(crate) async fn persist_permissions_for_profile(
sess: &Session,
action: &codex_protocol::protocol::PersistPermissionProfileAction,
) -> io::Result<()> {
let codex_home = sess.codex_home().await;
let edits = filesystem_permission_edits(
action.profile_name.as_str(),
action.permissions.file_system.as_ref(),
);
if edits.is_empty() {
return Ok(());
}
ConfigEditsBuilder::new(&codex_home)
.with_edits(edits)
.apply()
.await
.map_err(|err| io::Error::other(format!("failed to persist permission profile: {err}")))?;
sess.reload_user_config_layer().await;
Ok(())
}
fn is_supported_filesystem_only_request(permissions: &PermissionProfile) -> bool {
let Some(file_system) = permissions.file_system.as_ref() else {
return false;
};
if file_system.is_empty() {
return false;
}
if permissions
.network
.as_ref()
.and_then(|network| network.enabled)
.unwrap_or(false)
{
return false;
}
permissions.macos.is_none()
}
fn filesystem_permission_edits(
profile_name: &str,
file_system: Option<&FileSystemPermissions>,
) -> Vec<ConfigEdit> {
let Some(file_system) = file_system else {
return Vec::new();
};
let mut path_access = BTreeMap::new();
if let Some(read_roots) = file_system.read.as_ref() {
for path in read_roots {
path_access
.entry(path.display().to_string())
.or_insert(FileSystemAccessMode::Read);
}
}
if let Some(write_roots) = file_system.write.as_ref() {
for path in write_roots {
path_access.insert(path.display().to_string(), FileSystemAccessMode::Write);
}
}
path_access
.into_iter()
.map(|(path, access)| ConfigEdit::SetPath {
segments: vec![
"permissions".to_string(),
profile_name.to_string(),
"filesystem".to_string(),
path,
],
value: value(access.to_string()),
})
.collect()
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
use codex_utils_absolute_path::AbsolutePathBuf;
fn absolute_path(path: &str) -> AbsolutePathBuf {
AbsolutePathBuf::from_absolute_path(path).expect("absolute path")
}
#[test]
fn filesystem_permission_edits_upgrade_write_access() {
let edits = filesystem_permission_edits(
"workspace",
Some(&FileSystemPermissions {
read: Some(vec![
absolute_path("/tmp/read"),
absolute_path("/tmp/write"),
]),
write: Some(vec![absolute_path("/tmp/write")]),
}),
);
assert_eq!(edits.len(), 2);
match &edits[0] {
ConfigEdit::SetPath { segments, value } => {
assert_eq!(
segments,
&[
"permissions".to_string(),
"workspace".to_string(),
"filesystem".to_string(),
"/tmp/read".to_string(),
]
);
assert_eq!(
value.as_value().and_then(toml_edit::Value::as_str),
Some("read")
);
}
other => panic!("unexpected edit: {other:?}"),
}
match &edits[1] {
ConfigEdit::SetPath { segments, value } => {
assert_eq!(
segments,
&[
"permissions".to_string(),
"workspace".to_string(),
"filesystem".to_string(),
"/tmp/write".to_string(),
]
);
assert_eq!(
value.as_value().and_then(toml_edit::Value::as_str),
Some("write")
);
}
other => panic!("unexpected edit: {other:?}"),
}
}
}

View File

@@ -1087,6 +1087,7 @@ async fn request_permissions_grants_apply_to_later_exec_command_calls() -> Resul
permissions: normalized_requested_permissions.clone(),
scope: PermissionGrantScope::Turn,
},
persist_permissions: None,
})
.await?;
@@ -1201,6 +1202,7 @@ async fn request_permissions_preapprove_explicit_exec_permissions_outside_on_req
permissions: normalized_requested_permissions,
scope: PermissionGrantScope::Turn,
},
persist_permissions: None,
})
.await?;
@@ -1314,6 +1316,7 @@ async fn request_permissions_grants_apply_to_later_shell_command_calls() -> Resu
permissions: normalized_requested_permissions.clone(),
scope: PermissionGrantScope::Turn,
},
persist_permissions: None,
})
.await?;
@@ -1423,6 +1426,7 @@ async fn request_permissions_grants_apply_to_later_shell_command_calls_without_i
permissions: normalized_requested_permissions.clone(),
scope: PermissionGrantScope::Turn,
},
persist_permissions: None,
})
.await?;
@@ -1569,6 +1573,7 @@ async fn partial_request_permissions_grants_do_not_preapprove_new_permissions()
permissions: granted_permissions.clone(),
scope: PermissionGrantScope::Turn,
},
persist_permissions: None,
})
.await?;
@@ -1687,6 +1692,7 @@ async fn request_permissions_grants_do_not_carry_across_turns() -> Result<()> {
permissions: normalized_requested_permissions,
scope: PermissionGrantScope::Turn,
},
persist_permissions: None,
})
.await?;
wait_for_completion(&test).await;
@@ -1804,6 +1810,7 @@ async fn request_permissions_session_grants_carry_across_turns() -> Result<()> {
permissions: normalized_requested_permissions,
scope: PermissionGrantScope::Session,
},
persist_permissions: None,
})
.await?;
wait_for_completion(&test).await;

View File

@@ -261,6 +261,7 @@ async fn approved_folder_write_request_permissions_unblocks_later_exec_without_s
permissions: normalized_requested_permissions,
scope: PermissionGrantScope::Turn,
},
persist_permissions: None,
})
.await?;
@@ -380,6 +381,7 @@ async fn approved_folder_write_request_permissions_unblocks_later_apply_patch_wi
permissions: normalized_requested_permissions,
scope: PermissionGrantScope::Turn,
},
persist_permissions: None,
})
.await?;

View File

@@ -403,6 +403,9 @@ pub enum Op {
id: String,
/// User-granted permissions.
response: RequestPermissionsResponse,
/// Optional permission-profile mutation to persist alongside the grant.
#[serde(default, skip_serializing_if = "Option::is_none")]
persist_permissions: Option<PersistPermissionProfileAction>,
},
/// Resolve a dynamic tool call request.
@@ -3211,6 +3214,12 @@ impl ReviewDecision {
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct PersistPermissionProfileAction {
pub profile_name: String,
pub permissions: crate::models::PermissionProfile,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
#[ts(tag = "type")]

View File

@@ -14,6 +14,11 @@ pub enum PermissionGrantScope {
Session,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct PermissionProfilePersistence {
pub profile_name: String,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(deny_unknown_fields)]
pub struct RequestPermissionProfile {
@@ -71,4 +76,7 @@ pub struct RequestPermissionsEvent {
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
pub permissions: RequestPermissionProfile,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub permissions_profile_persistence: Option<PermissionProfilePersistence>,
}

View File

@@ -1658,6 +1658,7 @@ impl App {
call_id: ev.call_id.clone(),
reason: ev.reason.clone(),
permissions: ev.permissions.clone(),
permissions_profile_persistence: ev.permissions_profile_persistence.clone(),
},
)),
_ => None,

View File

@@ -28,8 +28,10 @@ use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::NetworkApprovalContext;
use codex_protocol::protocol::NetworkPolicyRuleAction;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::PersistPermissionProfileAction;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::PermissionProfilePersistence;
use codex_protocol::request_permissions::RequestPermissionProfile;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
@@ -62,6 +64,7 @@ pub(crate) enum ApprovalRequest {
call_id: String,
reason: Option<String>,
permissions: RequestPermissionProfile,
permissions_profile_persistence: Option<PermissionProfilePersistence>,
},
ApplyPatch {
thread_id: ThreadId,
@@ -168,8 +171,11 @@ impl ApprovalOverlay {
},
),
),
ApprovalRequest::Permissions { .. } => (
permissions_options(),
ApprovalRequest::Permissions {
permissions_profile_persistence,
..
} => (
permissions_options(permissions_profile_persistence.as_ref()),
"Would you like to grant these permissions?".to_string(),
),
ApprovalRequest::ApplyPatch { .. } => (
@@ -226,10 +232,16 @@ impl ApprovalOverlay {
ApprovalRequest::Permissions {
call_id,
permissions,
permissions_profile_persistence,
..
},
ApprovalDecision::Review(decision),
) => self.handle_permissions_decision(call_id, permissions, decision.clone()),
) => self.handle_permissions_decision(
call_id,
permissions,
permissions_profile_persistence.as_ref(),
decision.clone(),
),
(ApprovalRequest::ApplyPatch { id, .. }, ApprovalDecision::Review(decision)) => {
self.handle_patch_decision(id, decision.clone());
}
@@ -278,13 +290,16 @@ impl ApprovalOverlay {
&self,
call_id: &str,
permissions: &RequestPermissionProfile,
permissions_profile_persistence: Option<&PermissionProfilePersistence>,
decision: ReviewDecision,
) {
let Some(request) = self.current_request.as_ref() else {
return;
};
let granted_permissions = match decision {
ReviewDecision::Approved | ReviewDecision::ApprovedForSession => permissions.clone(),
ReviewDecision::Approved
| ReviewDecision::ApprovedForSession
| ReviewDecision::ApprovedPersistToProfile => permissions.clone(),
ReviewDecision::Denied | ReviewDecision::Abort => Default::default(),
ReviewDecision::ApprovedExecpolicyAmendment { .. }
| ReviewDecision::NetworkPolicyAmendment { .. } => Default::default(),
@@ -307,6 +322,11 @@ impl ApprovalOverlay {
)));
}
let thread_id = request.thread_id();
let persist_permissions = persist_permissions_for_permissions_decision(
&decision,
permissions_profile_persistence,
&granted_permissions,
);
self.app_event_tx.send(AppEvent::SubmitThreadOp {
thread_id,
op: Op::RequestPermissionsResponse {
@@ -315,6 +335,7 @@ impl ApprovalOverlay {
permissions: granted_permissions,
scope,
},
persist_permissions,
},
});
}
@@ -443,9 +464,15 @@ impl BottomPaneView for ApprovalOverlay {
ApprovalRequest::Permissions {
call_id,
permissions,
permissions_profile_persistence,
..
} => {
self.handle_permissions_decision(call_id, permissions, ReviewDecision::Abort);
self.handle_permissions_decision(
call_id,
permissions,
permissions_profile_persistence.as_ref(),
ReviewDecision::Abort,
);
}
ApprovalRequest::ApplyPatch { id, .. } => {
self.handle_patch_decision(id, ReviewDecision::Abort);
@@ -855,8 +882,10 @@ fn patch_options() -> Vec<ApprovalOption> {
]
}
fn permissions_options() -> Vec<ApprovalOption> {
vec![
fn permissions_options(
permissions_profile_persistence: Option<&PermissionProfilePersistence>,
) -> Vec<ApprovalOption> {
let mut options = vec![
ApprovalOption {
label: "Yes, grant these permissions".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::Approved),
@@ -875,7 +904,50 @@ fn permissions_options() -> Vec<ApprovalOption> {
display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))],
},
]
];
if permissions_profile_persistence.is_some() {
options.insert(
1,
ApprovalOption {
label: "Yes, always allow these permissions".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::ApprovedPersistToProfile),
display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('p'))],
},
);
}
options
}
fn persist_permissions_for_exec_decision(
decision: &ReviewDecision,
additional_permissions: Option<&PermissionProfile>,
permissions_profile_persistence: Option<&PermissionProfilePersistence>,
) -> Option<PersistPermissionProfileAction> {
if !matches!(decision, ReviewDecision::ApprovedPersistToProfile) {
return None;
}
let permissions = additional_permissions?.clone();
let profile_name = permissions_profile_persistence?.profile_name.clone();
Some(PersistPermissionProfileAction {
profile_name,
permissions,
})
}
fn persist_permissions_for_permissions_decision(
decision: &ReviewDecision,
permissions_profile_persistence: Option<&PermissionProfilePersistence>,
granted_permissions: &RequestPermissionProfile,
) -> Option<PersistPermissionProfileAction> {
if !matches!(decision, ReviewDecision::ApprovedPersistToProfile) {
return None;
}
let profile_name = permissions_profile_persistence?.profile_name.clone();
Some(PersistPermissionProfileAction {
profile_name,
permissions: granted_permissions.clone().into(),
})
}
fn elicitation_options() -> Vec<ApprovalOption> {
@@ -1336,6 +1408,7 @@ mod tests {
}),
..Default::default()
}),
permissions_profile_persistence: None,
};
let view = ApprovalOverlay::new(exec_request, tx, Features::with_defaults());

View File

@@ -3445,6 +3445,7 @@ impl ChatWidget {
call_id: ev.call_id,
reason: ev.reason,
permissions: ev.permissions,
permissions_profile_persistence: ev.permissions_profile_persistence,
};
self.bottom_pane
.push_approval_request(request, &self.config.features);