mirror of
https://github.com/openai/codex.git
synced 2026-04-09 15:24:48 +00:00
Compare commits
3 Commits
dev/window
...
pr15472
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3df4c6a0a | ||
|
|
993274057e | ||
|
|
3b52dd7b7d |
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ async fn request_permissions_round_trip() -> Result<()> {
|
||||
}),
|
||||
},
|
||||
scope: PermissionGrantScope::Turn,
|
||||
persist_to_profile: false,
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -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;
|
||||
@@ -2886,12 +2887,17 @@ impl Session {
|
||||
},
|
||||
]
|
||||
});
|
||||
let permissions_profile_persistence =
|
||||
additional_permissions.as_ref().and_then(|permissions| {
|
||||
persistence_target_for_permissions(turn_context.config.as_ref(), permissions)
|
||||
});
|
||||
let available_decisions = available_decisions.unwrap_or_else(|| {
|
||||
ExecApprovalRequestEvent::default_available_decisions(
|
||||
network_approval_context.as_ref(),
|
||||
proposed_execpolicy_amendment.as_ref(),
|
||||
proposed_network_policy_amendments.as_deref(),
|
||||
additional_permissions.as_ref(),
|
||||
permissions_profile_persistence.as_ref(),
|
||||
)
|
||||
});
|
||||
let event = EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
|
||||
@@ -2905,6 +2911,7 @@ impl Session {
|
||||
proposed_execpolicy_amendment,
|
||||
proposed_network_policy_amendments,
|
||||
additional_permissions,
|
||||
permissions_profile_persistence,
|
||||
skill_metadata,
|
||||
available_decisions: Some(available_decisions),
|
||||
parsed_cmd,
|
||||
@@ -2998,7 +3005,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()
|
||||
@@ -4241,8 +4252,16 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
|
||||
id: approval_id,
|
||||
turn_id,
|
||||
decision,
|
||||
persist_permissions,
|
||||
} => {
|
||||
handlers::exec_approval(&sess, approval_id, turn_id, decision).await;
|
||||
handlers::exec_approval(
|
||||
&sess,
|
||||
approval_id,
|
||||
turn_id,
|
||||
decision,
|
||||
persist_permissions,
|
||||
)
|
||||
.await;
|
||||
false
|
||||
}
|
||||
Op::PatchApproval { id, decision } => {
|
||||
@@ -4253,8 +4272,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 +4429,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 +4450,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;
|
||||
@@ -4623,6 +4654,7 @@ mod handlers {
|
||||
approval_id: String,
|
||||
turn_id: Option<String>,
|
||||
decision: ReviewDecision,
|
||||
persist_permissions: Option<PersistPermissionProfileAction>,
|
||||
) {
|
||||
let event_turn_id = turn_id.unwrap_or_else(|| approval_id.clone());
|
||||
if let ReviewDecision::ApprovedExecpolicyAmendment {
|
||||
@@ -4652,10 +4684,28 @@ mod handlers {
|
||||
}
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
decision,
|
||||
ReviewDecision::ApprovedPersistToProfile | ReviewDecision::Approved
|
||||
) && 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: event_turn_id.clone(),
|
||||
msg: EventMsg::Warning(WarningEvent { message }),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
match decision {
|
||||
ReviewDecision::Abort => {
|
||||
sess.interrupt_task().await;
|
||||
}
|
||||
ReviewDecision::ApprovedPersistToProfile => {
|
||||
sess.notify_approval(&approval_id, ReviewDecision::Approved)
|
||||
.await;
|
||||
}
|
||||
other => sess.notify_approval(&approval_id, other).await,
|
||||
}
|
||||
}
|
||||
@@ -4681,7 +4731,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;
|
||||
}
|
||||
|
||||
@@ -493,6 +493,7 @@ async fn handle_exec_approval(
|
||||
id: approval_id_for_op,
|
||||
turn_id: Some(turn_id),
|
||||
decision,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
@@ -700,6 +701,7 @@ async fn maybe_auto_review_mcp_request_user_input(
|
||||
.map(|option| option.label.clone())
|
||||
.unwrap_or_else(|| MCP_TOOL_APPROVAL_ACCEPT.to_string()),
|
||||
ReviewDecision::Approved
|
||||
| ReviewDecision::ApprovedPersistToProfile
|
||||
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
|
||||
| ReviewDecision::NetworkPolicyAmendment { .. } => MCP_TOOL_APPROVAL_ACCEPT.to_string(),
|
||||
ReviewDecision::Denied | ReviewDecision::Abort => {
|
||||
@@ -764,6 +766,7 @@ async fn handle_request_permissions(
|
||||
.submit(Op::RequestPermissionsResponse {
|
||||
id: call_id,
|
||||
response,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -286,6 +288,7 @@ async fn handle_exec_approval_uses_call_id_for_guardian_review_and_approval_id_f
|
||||
proposed_execpolicy_amendment: None,
|
||||
proposed_network_policy_amendments: None,
|
||||
additional_permissions: None,
|
||||
permissions_profile_persistence: None,
|
||||
skill_metadata: None,
|
||||
available_decisions: Some(vec![
|
||||
ReviewDecision::Approved,
|
||||
@@ -343,6 +346,7 @@ async fn handle_exec_approval_uses_call_id_for_guardian_review_and_approval_id_f
|
||||
id: "callback-approval-1".to_string(),
|
||||
turn_id: Some("child-turn-1".to_string()),
|
||||
decision: ReviewDecision::Abort,
|
||||
persist_permissions: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -747,6 +747,7 @@ pub(crate) fn build_guardian_mcp_tool_review_request(
|
||||
fn mcp_tool_approval_decision_from_guardian(decision: ReviewDecision) -> McpToolApprovalDecision {
|
||||
match decision {
|
||||
ReviewDecision::Approved
|
||||
| ReviewDecision::ApprovedPersistToProfile
|
||||
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
|
||||
| ReviewDecision::NetworkPolicyAmendment { .. } => McpToolApprovalDecision::Accept,
|
||||
ReviewDecision::ApprovedForSession => McpToolApprovalDecision::AcceptForSession,
|
||||
|
||||
179
codex-rs/core/src/permission_profile_persistence.rs
Normal file
179
codex-rs/core/src/permission_profile_persistence.rs
Normal 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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -392,7 +392,9 @@ impl NetworkApprovalService {
|
||||
|
||||
let mut cache_session_deny = false;
|
||||
let resolved = match approval_decision {
|
||||
ReviewDecision::Approved | ReviewDecision::ApprovedExecpolicyAmendment { .. } => {
|
||||
ReviewDecision::Approved
|
||||
| ReviewDecision::ApprovedPersistToProfile
|
||||
| ReviewDecision::ApprovedExecpolicyAmendment { .. } => {
|
||||
PendingApprovalDecision::AllowOnce
|
||||
}
|
||||
ReviewDecision::ApprovedForSession => PendingApprovalDecision::AllowForSession,
|
||||
|
||||
@@ -155,6 +155,7 @@ impl ToolOrchestrator {
|
||||
return Err(ToolError::Rejected(reason));
|
||||
}
|
||||
ReviewDecision::Approved
|
||||
| ReviewDecision::ApprovedPersistToProfile
|
||||
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
|
||||
| ReviewDecision::ApprovedForSession => {}
|
||||
ReviewDecision::NetworkPolicyAmendment {
|
||||
@@ -309,6 +310,7 @@ impl ToolOrchestrator {
|
||||
return Err(ToolError::Rejected(reason));
|
||||
}
|
||||
ReviewDecision::Approved
|
||||
| ReviewDecision::ApprovedPersistToProfile
|
||||
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
|
||||
| ReviewDecision::ApprovedForSession => {}
|
||||
ReviewDecision::NetworkPolicyAmendment {
|
||||
|
||||
@@ -542,6 +542,7 @@ impl CoreShellActionProvider {
|
||||
.await?
|
||||
{
|
||||
ReviewDecision::Approved
|
||||
| ReviewDecision::ApprovedPersistToProfile
|
||||
| ReviewDecision::ApprovedExecpolicyAmendment { .. } => {
|
||||
if needs_escalation {
|
||||
EscalationDecision::escalate(escalation_execution.clone())
|
||||
|
||||
@@ -1715,6 +1715,7 @@ async fn run_scenario(scenario: &ScenarioSpec) -> Result<()> {
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: decision.clone(),
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -1941,6 +1942,7 @@ async fn approving_execpolicy_amendment_persists_policy_and_skips_future_prompts
|
||||
decision: ReviewDecision::ApprovedExecpolicyAmendment {
|
||||
proposed_execpolicy_amendment: expected_execpolicy_amendment.clone(),
|
||||
},
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -2180,6 +2182,7 @@ async fn spawned_subagent_execpolicy_amendment_propagates_to_parent_session() ->
|
||||
decision: ReviewDecision::ApprovedExecpolicyAmendment {
|
||||
proposed_execpolicy_amendment: expected_execpolicy_amendment,
|
||||
},
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -2405,6 +2408,7 @@ async fn approving_fallback_rule_for_compound_command_works() -> Result<()> {
|
||||
decision: ReviewDecision::ApprovedExecpolicyAmendment {
|
||||
proposed_execpolicy_amendment: amendment.clone(),
|
||||
},
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -2599,6 +2603,7 @@ allow_local_binding = true
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
@@ -2639,6 +2644,7 @@ allow_local_binding = true
|
||||
decision: ReviewDecision::NetworkPolicyAmendment {
|
||||
network_policy_amendment: deny_network_amendment.clone(),
|
||||
},
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -2742,6 +2748,7 @@ allow_local_binding = true
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
@@ -2826,6 +2833,7 @@ async fn compound_command_with_one_safe_command_still_requires_approval() -> Res
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Denied,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
|
||||
@@ -103,6 +103,7 @@ async fn codex_delegate_forwards_exec_approval_and_proceeds_on_approval() {
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await
|
||||
.expect("submit exec approval");
|
||||
|
||||
@@ -1159,6 +1159,7 @@ async fn handle_container_exec_user_approved_records_tool_decision() {
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1225,6 +1226,7 @@ async fn handle_container_exec_user_approved_for_session_records_tool_decision()
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::ApprovedForSession,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1291,6 +1293,7 @@ async fn handle_sandbox_error_user_approves_retry_records_tool_decision() {
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1357,6 +1360,7 @@ async fn handle_container_exec_user_denies_records_tool_decision() {
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Denied,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1423,6 +1427,7 @@ async fn handle_sandbox_error_user_approves_for_session_records_tool_decision()
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::ApprovedForSession,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1490,6 +1495,7 @@ async fn handle_sandbox_error_user_denies_records_tool_decision() {
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Denied,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -378,6 +378,7 @@ async fn with_additional_permissions_requires_approval_under_on_request() -> Res
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -566,6 +567,7 @@ async fn relative_additional_permissions_resolve_against_tool_workdir() -> Resul
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -660,6 +662,7 @@ async fn read_only_with_additional_permissions_does_not_widen_to_unrequested_cwd
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -761,6 +764,7 @@ async fn read_only_with_additional_permissions_does_not_widen_to_unrequested_tmp
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -869,6 +873,7 @@ async fn workspace_write_with_additional_permissions_can_write_outside_cwd() ->
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -971,6 +976,7 @@ async fn with_additional_permissions_denied_approval_blocks_execution() -> Resul
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Denied,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -1087,6 +1093,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?;
|
||||
|
||||
@@ -1100,6 +1107,7 @@ async fn request_permissions_grants_apply_to_later_exec_command_calls() -> Resul
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -1201,6 +1209,7 @@ async fn request_permissions_preapprove_explicit_exec_permissions_outside_on_req
|
||||
permissions: normalized_requested_permissions,
|
||||
scope: PermissionGrantScope::Turn,
|
||||
},
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -1210,6 +1219,7 @@ async fn request_permissions_preapprove_explicit_exec_permissions_outside_on_req
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -1314,6 +1324,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?;
|
||||
|
||||
@@ -1323,6 +1334,7 @@ async fn request_permissions_grants_apply_to_later_shell_command_calls() -> Resu
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -1423,6 +1435,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?;
|
||||
|
||||
@@ -1432,6 +1445,7 @@ async fn request_permissions_grants_apply_to_later_shell_command_calls_without_i
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -1569,6 +1583,7 @@ async fn partial_request_permissions_grants_do_not_preapprove_new_permissions()
|
||||
permissions: granted_permissions.clone(),
|
||||
scope: PermissionGrantScope::Turn,
|
||||
},
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -1601,6 +1616,7 @@ async fn partial_request_permissions_grants_do_not_preapprove_new_permissions()
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
@@ -1687,6 +1703,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 +1821,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;
|
||||
@@ -1846,6 +1864,7 @@ async fn request_permissions_session_grants_carry_across_turns() -> Result<()> {
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -277,6 +278,7 @@ async fn approved_folder_write_request_permissions_unblocks_later_exec_without_s
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
@@ -380,6 +382,7 @@ async fn approved_folder_write_request_permissions_unblocks_later_apply_patch_wi
|
||||
permissions: normalized_requested_permissions,
|
||||
scope: PermissionGrantScope::Turn,
|
||||
},
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -257,6 +257,7 @@ permissions:
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Denied,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -351,6 +352,7 @@ permissions:
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Denied,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -446,6 +448,7 @@ permissions:
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Denied,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -895,6 +898,7 @@ async fn shell_zsh_fork_skill_session_approval_enforces_skill_permissions() -> R
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::ApprovedForSession,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -230,6 +230,7 @@ async fn run_codex_tool_session_inner(
|
||||
additional_permissions: _,
|
||||
skill_metadata: _,
|
||||
available_decisions: _,
|
||||
permissions_profile_persistence: _,
|
||||
} = ev;
|
||||
handle_exec_approval_request(
|
||||
command,
|
||||
|
||||
@@ -139,6 +139,7 @@ async fn on_exec_approval_response(
|
||||
id: approval_id,
|
||||
turn_id: Some(event_id),
|
||||
decision: response.decision,
|
||||
persist_permissions: None,
|
||||
})
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::permissions::NetworkSandboxPolicy;
|
||||
use crate::protocol::FileChange;
|
||||
use crate::protocol::ReviewDecision;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::request_permissions::PermissionProfilePersistence;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -181,6 +182,11 @@ pub struct ExecApprovalRequestEvent {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub additional_permissions: Option<PermissionProfile>,
|
||||
/// Optional named permissions profile that can absorb the requested
|
||||
/// filesystem permissions for future approvals.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub permissions_profile_persistence: Option<PermissionProfilePersistence>,
|
||||
/// Optional skill metadata when the approval was triggered by a skill script.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -212,6 +218,7 @@ impl ExecApprovalRequestEvent {
|
||||
self.proposed_execpolicy_amendment.as_ref(),
|
||||
self.proposed_network_policy_amendments.as_deref(),
|
||||
self.additional_permissions.as_ref(),
|
||||
self.permissions_profile_persistence.as_ref(),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -221,6 +228,7 @@ impl ExecApprovalRequestEvent {
|
||||
proposed_execpolicy_amendment: Option<&ExecPolicyAmendment>,
|
||||
proposed_network_policy_amendments: Option<&[NetworkPolicyAmendment]>,
|
||||
additional_permissions: Option<&PermissionProfile>,
|
||||
permissions_profile_persistence: Option<&PermissionProfilePersistence>,
|
||||
) -> Vec<ReviewDecision> {
|
||||
if network_approval_context.is_some() {
|
||||
let mut decisions = vec![ReviewDecision::Approved, ReviewDecision::ApprovedForSession];
|
||||
@@ -238,7 +246,12 @@ impl ExecApprovalRequestEvent {
|
||||
}
|
||||
|
||||
if additional_permissions.is_some() {
|
||||
return vec![ReviewDecision::Approved, ReviewDecision::Abort];
|
||||
let mut decisions = vec![ReviewDecision::Approved];
|
||||
if permissions_profile_persistence.is_some() {
|
||||
decisions.push(ReviewDecision::ApprovedPersistToProfile);
|
||||
}
|
||||
decisions.push(ReviewDecision::Abort);
|
||||
return decisions;
|
||||
}
|
||||
|
||||
let mut decisions = vec![ReviewDecision::Approved];
|
||||
|
||||
@@ -362,6 +362,9 @@ pub enum Op {
|
||||
turn_id: Option<String>,
|
||||
/// The user's decision in response to the request.
|
||||
decision: ReviewDecision,
|
||||
/// Optional permission-profile mutation to persist alongside approval.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
persist_permissions: Option<PersistPermissionProfileAction>,
|
||||
},
|
||||
|
||||
/// Approve a code patch
|
||||
@@ -403,6 +406,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.
|
||||
@@ -3175,6 +3181,10 @@ pub enum ReviewDecision {
|
||||
/// remainder of the session.
|
||||
ApprovedForSession,
|
||||
|
||||
/// User has approved this request and wants the filesystem permissions to
|
||||
/// be persisted into the active named permissions profile.
|
||||
ApprovedPersistToProfile,
|
||||
|
||||
/// User chose to persist a network policy rule (allow/deny) for future
|
||||
/// requests to the same host.
|
||||
NetworkPolicyAmendment {
|
||||
@@ -3199,6 +3209,7 @@ impl ReviewDecision {
|
||||
ReviewDecision::Approved => "approved",
|
||||
ReviewDecision::ApprovedExecpolicyAmendment { .. } => "approved_with_amendment",
|
||||
ReviewDecision::ApprovedForSession => "approved_for_session",
|
||||
ReviewDecision::ApprovedPersistToProfile => "approved_persist_to_profile",
|
||||
ReviewDecision::NetworkPolicyAmendment {
|
||||
network_policy_amendment,
|
||||
} => match network_policy_amendment.action {
|
||||
@@ -3211,6 +3222,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")]
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user