mirror of
https://github.com/openai/codex.git
synced 2026-05-02 04:11:39 +03:00
feat(core) Introduce Feature::RequestPermissions (#11871)
## Summary Introduces the initial implementation of Feature::RequestPermissions. RequestPermissions allows the model to request that a command be run inside the sandbox, with additional permissions, like writing to a specific folder. Eventually this will include other rules as well, and the ability to persist these permissions, but this PR is already quite large - let's get the core flow working and go from there! <img width="1279" height="541" alt="Screenshot 2026-02-15 at 2 26 22 PM" src="https://github.com/user-attachments/assets/0ee3ec0f-02ec-4509-91a2-809ac80be368" /> ## Testing - [x] Added tests - [x] Tested locally - [x] Feature
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_utils_image::load_and_resize_to_fit;
|
||||
use serde::Deserialize;
|
||||
@@ -35,12 +36,34 @@ pub enum SandboxPermissions {
|
||||
UseDefault,
|
||||
/// Request to run outside the sandbox
|
||||
RequireEscalated,
|
||||
/// Request to run in the sandbox with additional per-command permissions.
|
||||
WithAdditionalPermissions,
|
||||
}
|
||||
|
||||
impl SandboxPermissions {
|
||||
/// True if SandboxPermissions requires full unsandboxed execution (i.e. RequireEscalated)
|
||||
pub fn requires_escalated_permissions(self) -> bool {
|
||||
matches!(self, SandboxPermissions::RequireEscalated)
|
||||
}
|
||||
|
||||
/// True if SandboxPermissions requires permissions beyond UseDefault
|
||||
pub fn requires_additional_permissions(self) -> bool {
|
||||
!matches!(self, SandboxPermissions::UseDefault)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
pub struct AdditionalPermissions {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub fs_read: Vec<PathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub fs_write: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl AdditionalPermissions {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.fs_read.is_empty() && self.fs_write.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
@@ -227,6 +250,8 @@ const APPROVAL_POLICY_ON_FAILURE: &str =
|
||||
include_str!("prompts/permissions/approval_policy/on_failure.md");
|
||||
const APPROVAL_POLICY_ON_REQUEST_RULE: &str =
|
||||
include_str!("prompts/permissions/approval_policy/on_request_rule.md");
|
||||
const APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION: &str =
|
||||
include_str!("prompts/permissions/approval_policy/on_request_rule_request_permission.md");
|
||||
|
||||
const SANDBOX_MODE_DANGER_FULL_ACCESS: &str =
|
||||
include_str!("prompts/permissions/sandbox_mode/danger_full_access.md");
|
||||
@@ -239,16 +264,25 @@ impl DeveloperInstructions {
|
||||
Self { text: text.into() }
|
||||
}
|
||||
|
||||
pub fn from(approval_policy: AskForApproval, exec_policy: &Policy) -> DeveloperInstructions {
|
||||
pub fn from(
|
||||
approval_policy: AskForApproval,
|
||||
exec_policy: &Policy,
|
||||
request_permission_enabled: bool,
|
||||
) -> DeveloperInstructions {
|
||||
let on_request_instructions = || {
|
||||
let on_request_rule = if request_permission_enabled {
|
||||
APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION
|
||||
} else {
|
||||
APPROVAL_POLICY_ON_REQUEST_RULE
|
||||
};
|
||||
let command_prefixes = format_allow_prefixes(exec_policy.get_allowed_prefixes());
|
||||
match command_prefixes {
|
||||
Some(prefixes) => {
|
||||
format!(
|
||||
"{APPROVAL_POLICY_ON_REQUEST_RULE}\n## Approved command prefixes\nThe following prefix rules have already been approved: {prefixes}"
|
||||
"{on_request_rule}\n## Approved command prefixes\nThe following prefix rules have already been approved: {prefixes}"
|
||||
)
|
||||
}
|
||||
None => APPROVAL_POLICY_ON_REQUEST_RULE.to_string(),
|
||||
None => on_request_rule.to_string(),
|
||||
}
|
||||
};
|
||||
let text = match approval_policy {
|
||||
@@ -306,6 +340,7 @@ impl DeveloperInstructions {
|
||||
approval_policy: AskForApproval,
|
||||
exec_policy: &Policy,
|
||||
cwd: &Path,
|
||||
request_permission_enabled: bool,
|
||||
) -> Self {
|
||||
let network_access = if sandbox_policy.has_full_network_access() {
|
||||
NetworkAccess::Enabled
|
||||
@@ -329,6 +364,7 @@ impl DeveloperInstructions {
|
||||
approval_policy,
|
||||
exec_policy,
|
||||
writable_roots,
|
||||
request_permission_enabled,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -352,6 +388,7 @@ impl DeveloperInstructions {
|
||||
approval_policy: AskForApproval,
|
||||
exec_policy: &Policy,
|
||||
writable_roots: Option<Vec<WritableRoot>>,
|
||||
request_permission_enabled: bool,
|
||||
) -> Self {
|
||||
let start_tag = DeveloperInstructions::new("<permissions instructions>");
|
||||
let end_tag = DeveloperInstructions::new("</permissions instructions>");
|
||||
@@ -360,7 +397,11 @@ impl DeveloperInstructions {
|
||||
sandbox_mode,
|
||||
network_access,
|
||||
))
|
||||
.concat(DeveloperInstructions::from(approval_policy, exec_policy))
|
||||
.concat(DeveloperInstructions::from(
|
||||
approval_policy,
|
||||
exec_policy,
|
||||
request_permission_enabled,
|
||||
))
|
||||
.concat(DeveloperInstructions::from_writable_roots(writable_roots))
|
||||
.concat(end_tag)
|
||||
}
|
||||
@@ -757,6 +798,9 @@ pub struct ShellToolCallParams {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub prefix_rule: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub additional_permissions: Option<AdditionalPermissions>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub justification: Option<String>,
|
||||
}
|
||||
@@ -780,6 +824,9 @@ pub struct ShellCommandToolCallParams {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub prefix_rule: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub additional_permissions: Option<AdditionalPermissions>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub justification: Option<String>,
|
||||
}
|
||||
@@ -1205,6 +1252,7 @@ mod tests {
|
||||
AskForApproval::OnRequest,
|
||||
&Policy::empty(),
|
||||
None,
|
||||
false,
|
||||
);
|
||||
|
||||
let text = instructions.into_text();
|
||||
@@ -1233,6 +1281,7 @@ mod tests {
|
||||
AskForApproval::UnlessTrusted,
|
||||
&Policy::empty(),
|
||||
&PathBuf::from("/tmp"),
|
||||
false,
|
||||
);
|
||||
let text = instructions.into_text();
|
||||
assert!(text.contains("Network access is enabled."));
|
||||
@@ -1254,6 +1303,7 @@ mod tests {
|
||||
AskForApproval::OnRequest,
|
||||
&exec_policy,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
|
||||
let text = instructions.into_text();
|
||||
@@ -1262,6 +1312,22 @@ mod tests {
|
||||
assert!(text.contains(r#"["git", "pull"]"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn includes_request_permission_rule_instructions_for_on_request_when_enabled() {
|
||||
let instructions = DeveloperInstructions::from_permissions_with_network(
|
||||
SandboxMode::WorkspaceWrite,
|
||||
NetworkAccess::Enabled,
|
||||
AskForApproval::OnRequest,
|
||||
&Policy::empty(),
|
||||
None,
|
||||
true,
|
||||
);
|
||||
|
||||
let text = instructions.into_text();
|
||||
assert!(text.contains("with_additional_permissions"));
|
||||
assert!(text.contains("additional_permissions"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_command_prefix_list_sorts_by_len_then_total_len_then_alphabetical() {
|
||||
let prefixes = vec![
|
||||
@@ -1572,6 +1638,7 @@ mod tests {
|
||||
timeout_ms: Some(1000),
|
||||
sandbox_permissions: None,
|
||||
prefix_rule: None,
|
||||
additional_permissions: None,
|
||||
justification: None,
|
||||
},
|
||||
params
|
||||
|
||||
Reference in New Issue
Block a user