mirror of
https://github.com/openai/codex.git
synced 2026-04-29 08:56:38 +00:00
Add request permissions tool (#13092)
Adds a built-in `request_permissions` tool and wires it through the Codex core, protocol, and app-server layers so a running turn can ask the client for additional permissions instead of relying on a static session policy. The new flow emits a `RequestPermissions` event from core, tracks the pending request by call ID, forwards it through app-server v2 as an `item/permissions/requestApproval` request, and resumes the tool call once the client returns an approved subset of the requested permission profile.
This commit is contained in:
@@ -837,6 +837,15 @@ impl From<CoreFileSystemPermissions> for AdditionalFileSystemPermissions {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdditionalFileSystemPermissions> for CoreFileSystemPermissions {
|
||||
fn from(value: AdditionalFileSystemPermissions) -> Self {
|
||||
Self {
|
||||
read: value.read,
|
||||
write: value.write,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -858,6 +867,17 @@ impl From<CoreMacOsSeatbeltProfileExtensions> for AdditionalMacOsPermissions {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdditionalMacOsPermissions> for CoreMacOsSeatbeltProfileExtensions {
|
||||
fn from(value: AdditionalMacOsPermissions) -> Self {
|
||||
Self {
|
||||
macos_preferences: value.preferences,
|
||||
macos_automation: value.automations,
|
||||
macos_accessibility: value.accessibility,
|
||||
macos_calendar: value.calendar,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -873,6 +893,14 @@ impl From<CoreNetworkPermissions> for AdditionalNetworkPermissions {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdditionalNetworkPermissions> for CoreNetworkPermissions {
|
||||
fn from(value: AdditionalNetworkPermissions) -> Self {
|
||||
Self {
|
||||
enabled: value.enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -892,6 +920,86 @@ impl From<CorePermissionProfile> for AdditionalPermissionProfile {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdditionalPermissionProfile> for CorePermissionProfile {
|
||||
fn from(value: AdditionalPermissionProfile) -> Self {
|
||||
Self {
|
||||
network: value.network.map(CoreNetworkPermissions::from),
|
||||
file_system: value.file_system.map(CoreFileSystemPermissions::from),
|
||||
macos: value.macos.map(CoreMacOsSeatbeltProfileExtensions::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct GrantedMacOsPermissions {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub preferences: Option<CoreMacOsPreferencesPermission>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub automations: Option<CoreMacOsAutomationPermission>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub accessibility: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub calendar: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<GrantedMacOsPermissions> for CoreMacOsSeatbeltProfileExtensions {
|
||||
fn from(value: GrantedMacOsPermissions) -> Self {
|
||||
Self {
|
||||
macos_preferences: value
|
||||
.preferences
|
||||
.unwrap_or(CoreMacOsPreferencesPermission::None),
|
||||
macos_automation: value
|
||||
.automations
|
||||
.unwrap_or(CoreMacOsAutomationPermission::None),
|
||||
macos_accessibility: value.accessibility.unwrap_or(false),
|
||||
macos_calendar: value.calendar.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct GrantedPermissionProfile {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub network: Option<AdditionalNetworkPermissions>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub file_system: Option<AdditionalFileSystemPermissions>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub macos: Option<GrantedMacOsPermissions>,
|
||||
}
|
||||
|
||||
impl From<GrantedPermissionProfile> for CorePermissionProfile {
|
||||
fn from(value: GrantedPermissionProfile) -> Self {
|
||||
let macos = value.macos.and_then(|macos| {
|
||||
if macos.preferences.is_none()
|
||||
&& macos.automations.is_none()
|
||||
&& macos.accessibility.is_none()
|
||||
&& macos.calendar.is_none()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(CoreMacOsSeatbeltProfileExtensions::from(macos))
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
network: value.network.map(CoreNetworkPermissions::from),
|
||||
file_system: value.file_system.map(CoreFileSystemPermissions::from),
|
||||
macos,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -4852,6 +4960,24 @@ pub struct DynamicToolCallParams {
|
||||
pub arguments: JsonValue,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PermissionsRequestApprovalParams {
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
pub item_id: String,
|
||||
pub reason: Option<String>,
|
||||
pub permissions: AdditionalPermissionProfile,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PermissionsRequestApprovalResponse {
|
||||
pub permissions: GrantedPermissionProfile,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -5203,6 +5329,128 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_request_approval_response_accepts_partial_macos_grants() {
|
||||
let cases = vec![
|
||||
(json!({}), Some(GrantedMacOsPermissions::default()), None),
|
||||
(
|
||||
json!({
|
||||
"preferences": "read_only",
|
||||
}),
|
||||
Some(GrantedMacOsPermissions {
|
||||
preferences: Some(CoreMacOsPreferencesPermission::ReadOnly),
|
||||
..Default::default()
|
||||
}),
|
||||
Some(CoreMacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: CoreMacOsPreferencesPermission::ReadOnly,
|
||||
macos_automation: CoreMacOsAutomationPermission::None,
|
||||
macos_accessibility: false,
|
||||
macos_calendar: false,
|
||||
}),
|
||||
),
|
||||
(
|
||||
json!({
|
||||
"automations": {
|
||||
"bundle_ids": ["com.apple.Notes"],
|
||||
},
|
||||
}),
|
||||
Some(GrantedMacOsPermissions {
|
||||
automations: Some(CoreMacOsAutomationPermission::BundleIds(vec![
|
||||
"com.apple.Notes".to_string(),
|
||||
])),
|
||||
..Default::default()
|
||||
}),
|
||||
Some(CoreMacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: CoreMacOsPreferencesPermission::None,
|
||||
macos_automation: CoreMacOsAutomationPermission::BundleIds(vec![
|
||||
"com.apple.Notes".to_string(),
|
||||
]),
|
||||
macos_accessibility: false,
|
||||
macos_calendar: false,
|
||||
}),
|
||||
),
|
||||
(
|
||||
json!({
|
||||
"accessibility": true,
|
||||
}),
|
||||
Some(GrantedMacOsPermissions {
|
||||
accessibility: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
Some(CoreMacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: CoreMacOsPreferencesPermission::None,
|
||||
macos_automation: CoreMacOsAutomationPermission::None,
|
||||
macos_accessibility: true,
|
||||
macos_calendar: false,
|
||||
}),
|
||||
),
|
||||
(
|
||||
json!({
|
||||
"calendar": true,
|
||||
}),
|
||||
Some(GrantedMacOsPermissions {
|
||||
calendar: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
Some(CoreMacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: CoreMacOsPreferencesPermission::None,
|
||||
macos_automation: CoreMacOsAutomationPermission::None,
|
||||
macos_accessibility: false,
|
||||
macos_calendar: true,
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (macos_json, expected_granted_macos, expected_core_macos) in cases {
|
||||
let response = serde_json::from_value::<PermissionsRequestApprovalResponse>(json!({
|
||||
"permissions": {
|
||||
"macos": macos_json,
|
||||
},
|
||||
}))
|
||||
.expect("partial macos permissions response should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
response.permissions,
|
||||
GrantedPermissionProfile {
|
||||
macos: expected_granted_macos,
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
CorePermissionProfile::from(response.permissions),
|
||||
CorePermissionProfile {
|
||||
macos: expected_core_macos,
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_request_approval_response_omits_ungranted_macos_keys_when_serialized() {
|
||||
let response = PermissionsRequestApprovalResponse {
|
||||
permissions: GrantedPermissionProfile {
|
||||
macos: Some(GrantedMacOsPermissions {
|
||||
accessibility: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(response).expect("response should serialize"),
|
||||
json!({
|
||||
"permissions": {
|
||||
"macos": {
|
||||
"accessibility": true,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_exec_params_default_optional_streaming_flags() {
|
||||
let params = serde_json::from_value::<CommandExecParams>(json!({
|
||||
|
||||
Reference in New Issue
Block a user