permissions: start using PermissionProfile as the canonical runtime model

This commit is contained in:
Michael Bolin
2026-04-15 15:48:28 -07:00
parent f2a4925f63
commit 20dfdc8a87
41 changed files with 2422 additions and 421 deletions
+11
View File
@@ -1456,6 +1456,12 @@ impl Session {
per_turn_config.service_tier = session_configuration.service_tier;
per_turn_config.personality = session_configuration.personality;
per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer;
per_turn_config.permissions.approval_policy = session_configuration.approval_policy.clone();
per_turn_config.permissions.sandbox_policy = session_configuration.sandbox_policy.clone();
per_turn_config.permissions.file_system_sandbox_policy =
session_configuration.file_system_sandbox_policy.clone();
per_turn_config.permissions.network_sandbox_policy =
session_configuration.network_sandbox_policy;
let resolved_web_search_mode = resolve_web_search_mode_for_turn(
&per_turn_config.web_search_mode,
session_configuration.sandbox_policy.get(),
@@ -3557,6 +3563,11 @@ impl Session {
ts.granted_permissions()
}
pub(crate) async fn cwd(&self) -> AbsolutePathBuf {
let state = self.state.lock().await;
state.session_configuration.cwd.clone()
}
pub(crate) async fn granted_session_permissions(&self) -> Option<PermissionProfile> {
let state = self.state.lock().await;
state.granted_permissions()
+4
View File
@@ -79,6 +79,10 @@ impl CodexThread {
self.codex.shutdown_and_wait().await
}
pub async fn cwd(&self) -> AbsolutePathBuf {
self.codex.session.cwd().await
}
#[doc(hidden)]
pub async fn ensure_rollout_materialized(&self) {
self.codex.session.ensure_rollout_materialized().await;
+16
View File
@@ -48,6 +48,7 @@ use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID;
use codex_model_provider_info::WireApi;
use codex_models_manager::bundled_models_response;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
@@ -627,6 +628,13 @@ fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::io::Re
},
]),
);
assert_eq!(
config.permissions.runtime_permission_profile(),
PermissionProfile::from_runtime_permissions(
&config.permissions.file_system_sandbox_policy,
config.permissions.network_sandbox_policy,
)
);
assert_eq!(
config.permissions.sandbox_policy.get(),
&SandboxPolicy::WorkspaceWrite {
@@ -1245,6 +1253,14 @@ exclude_slash_tmp = true
NetworkSandboxPolicy::from(sandbox_policy),
"case `{name}` should preserve network semantics from legacy config"
);
assert_eq!(
config.permissions.runtime_permission_profile(),
PermissionProfile::from_runtime_permissions(
&config.permissions.file_system_sandbox_policy,
config.permissions.network_sandbox_policy,
),
"case `{name}` should populate canonical permission profile from runtime policies"
);
assert_eq!(
config
.permissions
+12
View File
@@ -73,6 +73,8 @@ use codex_protocol::config_types::Verbosity;
use codex_protocol::config_types::WebSearchConfig;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::config_types::WindowsSandboxLevel;
#[cfg(test)]
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::permissions::FileSystemSandboxPolicy;
@@ -212,6 +214,16 @@ pub struct Permissions {
pub windows_sandbox_private_desktop: bool,
}
impl Permissions {
#[cfg(test)]
pub(crate) fn runtime_permission_profile(&self) -> PermissionProfile {
PermissionProfile::from_runtime_permissions(
&self.file_system_sandbox_policy,
self.network_sandbox_policy,
)
}
}
/// Application configuration loaded from disk and merged with overrides.
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
@@ -82,10 +82,10 @@ fn write_permissions_for_paths(
.ok()?;
let permissions = (!write_paths.is_empty()).then_some(PermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(write_paths),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(write_paths),
)),
..Default::default()
})?;
@@ -85,7 +85,13 @@ fn write_permissions_for_paths_keep_dirs_outside_workspace_root() {
dunce::simplified(&outside.canonicalize().expect("canonicalize outside dir")).abs();
assert_eq!(
permissions.and_then(|profile| profile.file_system.and_then(|fs| fs.write)),
Some(vec![expected_outside])
permissions,
Some(PermissionProfile {
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![expected_outside]),
)),
..Default::default()
})
);
}
+61 -13
View File
@@ -18,9 +18,9 @@ mod tool_suggest;
pub(crate) mod unified_exec;
mod view_image;
use codex_sandboxing::policy_transforms::intersect_permission_profiles;
use codex_sandboxing::policy_transforms::merge_permission_profiles;
use codex_sandboxing::policy_transforms::normalize_additional_permissions;
use codex_sandboxing::policy_transforms::permission_profile_is_preapproved;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use serde::Deserialize;
@@ -183,17 +183,12 @@ pub(super) async fn apply_granted_turn_permissions(
granted_session_permissions.as_ref(),
granted_turn_permissions.as_ref(),
);
let effective_permissions = merge_permission_profiles(
let session_cwd = session.cwd().await;
let (effective_permissions, permissions_preapproved) = effective_permissions_for_granted_turn(
additional_permissions.as_ref(),
granted_permissions.as_ref(),
session_cwd.as_path(),
);
let permissions_preapproved = match (effective_permissions.as_ref(), granted_permissions) {
(Some(effective_permissions), Some(granted_permissions)) => {
intersect_permission_profiles(effective_permissions.clone(), granted_permissions)
== *effective_permissions
}
_ => false,
};
let sandbox_permissions =
if effective_permissions.is_some() && !sandbox_permissions.uses_additional_permissions() {
@@ -209,9 +204,30 @@ pub(super) async fn apply_granted_turn_permissions(
}
}
fn effective_permissions_for_granted_turn(
additional_permissions: Option<&PermissionProfile>,
granted_permissions: Option<&PermissionProfile>,
cwd: &Path,
) -> (Option<PermissionProfile>, bool) {
let effective_permissions =
merge_permission_profiles(additional_permissions, granted_permissions);
let permissions_preapproved = match (effective_permissions.as_ref(), granted_permissions) {
(Some(effective_permissions), Some(granted_permissions)) => {
permission_profile_is_preapproved(effective_permissions, granted_permissions, cwd)
}
_ => false,
};
if permissions_preapproved {
(granted_permissions.cloned(), true)
} else {
(effective_permissions, false)
}
}
#[cfg(test)]
mod tests {
use super::EffectiveAdditionalPermissions;
use super::effective_permissions_for_granted_turn;
use super::implicit_granted_permissions;
use super::normalize_and_validate_additional_permissions;
use crate::sandboxing::SandboxPermissions;
@@ -235,12 +251,24 @@ mod tests {
fn file_system_permissions(path: &std::path::Path) -> PermissionProfile {
PermissionProfile {
file_system: Some(FileSystemPermissions {
read: None,
write: Some(vec![
file_system: Some(FileSystemPermissions::from_read_write_roots(
/*read*/ None,
Some(vec![
AbsolutePathBuf::from_absolute_path(path).expect("absolute path"),
]),
}),
)),
..Default::default()
}
}
fn read_file_system_permissions(path: &std::path::Path) -> PermissionProfile {
PermissionProfile {
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![
AbsolutePathBuf::from_absolute_path(path).expect("absolute path"),
]),
/*write*/ None,
)),
..Default::default()
}
}
@@ -322,4 +350,24 @@ mod tests {
assert_eq!(implicit_permissions, None);
}
#[test]
fn preapproved_sticky_grants_do_not_get_narrowed_by_child_request() {
let cwd = tempdir().expect("tempdir");
let child_path = cwd.path().join("child");
let requested_permissions = read_file_system_permissions(&child_path);
let granted_permissions = file_system_permissions(cwd.path());
let (effective_permissions, permissions_preapproved) =
effective_permissions_for_granted_turn(
Some(&requested_permissions),
Some(&granted_permissions),
cwd.path(),
);
assert_eq!(
(effective_permissions, permissions_preapproved),
(Some(granted_permissions), true)
);
}
}
@@ -188,10 +188,10 @@ fn exec_command_args_resolve_relative_additional_permissions_against_workdir() -
assert_eq!(
args.additional_permissions,
Some(PermissionProfile {
file_system: Some(FileSystemPermissions {
read: None,
write: Some(vec![expected_write.abs()]),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
/*read*/ None,
Some(vec![expected_write.abs()]),
)),
..Default::default()
})
);
@@ -82,10 +82,10 @@ fn file_system_sandbox_context_uses_active_attempt() {
.abs();
let additional_permissions = PermissionProfile {
network: None,
file_system: Some(FileSystemPermissions {
read: Some(vec![path.clone()]),
write: Some(Vec::new()),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![path.clone()]),
Some(Vec::new()),
)),
};
let req = ApplyPatchRequest {
action: ApplyPatchAction::new_add_for_test(&path, "hello".to_string()),
@@ -250,12 +250,12 @@ fn map_exec_result_preserves_stdout_and_stderr() {
#[test]
fn shell_request_escalation_execution_is_explicit() {
let requested_permissions = PermissionProfile {
file_system: Some(FileSystemPermissions {
read: None,
write: Some(vec![
file_system: Some(FileSystemPermissions::from_read_write_roots(
/*read*/ None,
Some(vec![
AbsolutePathBuf::from_absolute_path("/tmp/output").unwrap(),
]),
}),
)),
..Default::default()
};
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
@@ -7,6 +7,7 @@ use codex_features::Feature;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecApprovalRequestEvent;
@@ -293,20 +294,20 @@ fn workspace_write_excluding_tmp() -> SandboxPolicy {
fn requested_directory_write_permissions(path: &Path) -> RequestPermissionProfile {
RequestPermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(vec![absolute_path(path)]),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![absolute_path(path)]),
)),
..RequestPermissionProfile::default()
}
}
fn normalized_directory_write_permissions(path: &Path) -> Result<RequestPermissionProfile> {
Ok(RequestPermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(vec![AbsolutePathBuf::try_from(path.canonicalize()?)?]),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![AbsolutePathBuf::try_from(path.canonicalize()?)?]),
)),
..RequestPermissionProfile::default()
})
}
@@ -343,10 +344,10 @@ async fn with_additional_permissions_requires_approval_under_on_request() -> Res
let call_id = "request_permissions_skip_approval";
let command = "touch requested-dir/requested-but-unused.txt";
let requested_permissions = PermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(vec![absolute_path(&requested_dir_canonical)]),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![absolute_path(&requested_dir_canonical)]),
)),
..Default::default()
};
let event = shell_event_with_request_permissions(call_id, command, &requested_permissions)?;
@@ -521,10 +522,10 @@ async fn relative_additional_permissions_resolve_against_tool_workdir() -> Resul
let call_id = "request_permissions_relative_workdir";
let command = "touch relative-write.txt";
let expected_permissions = PermissionProfile {
file_system: Some(FileSystemPermissions {
read: None,
write: Some(vec![absolute_path(&nested_dir_canonical)]),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
/*read*/ None,
Some(vec![absolute_path(&nested_dir_canonical)]),
)),
..Default::default()
};
let event = shell_event_with_raw_request_permissions(
@@ -624,10 +625,10 @@ async fn read_only_with_additional_permissions_does_not_widen_to_unrequested_cwd
"cwd-widened", unrequested_write, unrequested_write
);
let requested_permissions = PermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(vec![absolute_path(&requested_write)]),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![absolute_path(&requested_write)]),
)),
..Default::default()
};
let event = shell_event_with_request_permissions(call_id, &command, &requested_permissions)?;
@@ -725,10 +726,10 @@ async fn read_only_with_additional_permissions_does_not_widen_to_unrequested_tmp
"tmp-widened", tmp_write, tmp_write
);
let requested_permissions = PermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(vec![absolute_path(&requested_write)]),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![absolute_path(&requested_write)]),
)),
..Default::default()
};
let event = shell_event_with_request_permissions(call_id, &command, &requested_permissions)?;
@@ -824,19 +825,19 @@ async fn workspace_write_with_additional_permissions_can_write_outside_cwd() ->
"outside-cwd-ok", outside_write, outside_write
);
let requested_permissions = RequestPermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(vec![absolute_path(outside_dir.path())]),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![absolute_path(outside_dir.path())]),
)),
..RequestPermissionProfile::default()
};
let normalized_requested_permissions = RequestPermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(vec![AbsolutePathBuf::try_from(
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![AbsolutePathBuf::try_from(
outside_dir.path().canonicalize()?,
)?]),
}),
)),
..RequestPermissionProfile::default()
};
let event = shell_event_with_request_permissions(call_id, &command, &requested_permissions)?;
@@ -926,19 +927,19 @@ async fn with_additional_permissions_denied_approval_blocks_execution() -> Resul
"should-not-write", outside_write, outside_write
);
let requested_permissions = PermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(vec![absolute_path(outside_dir.path())]),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![absolute_path(outside_dir.path())]),
)),
..Default::default()
};
let normalized_requested_permissions = PermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(vec![AbsolutePathBuf::try_from(
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![AbsolutePathBuf::try_from(
outside_dir.path().canonicalize()?,
)?]),
}),
)),
..Default::default()
};
let event = shell_event_with_request_permissions(call_id, &command, &requested_permissions)?;
@@ -1028,19 +1029,19 @@ async fn request_permissions_grants_apply_to_later_exec_command_calls() -> Resul
"sticky-grant-ok", outside_write, outside_write
);
let requested_permissions = RequestPermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(vec![absolute_path(outside_dir.path())]),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![absolute_path(outside_dir.path())]),
)),
..Default::default()
};
let normalized_requested_permissions = RequestPermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(vec![AbsolutePathBuf::try_from(
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![AbsolutePathBuf::try_from(
outside_dir.path().canonicalize()?,
)?]),
}),
)),
..Default::default()
};
let responses = mount_sse_sequence(
@@ -1492,35 +1493,35 @@ async fn partial_request_permissions_grants_do_not_preapprove_new_permissions()
);
let requested_permissions = RequestPermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(vec![
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![
absolute_path(first_dir.path()),
absolute_path(second_dir.path()),
]),
}),
)),
..RequestPermissionProfile::default()
};
let normalized_requested_permissions = RequestPermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(vec![
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![
AbsolutePathBuf::try_from(first_dir.path().canonicalize()?)?,
AbsolutePathBuf::try_from(second_dir.path().canonicalize()?)?,
]),
}),
)),
..RequestPermissionProfile::default()
};
let granted_permissions = normalized_directory_write_permissions(first_dir.path())?;
let second_dir_permissions = requested_directory_write_permissions(second_dir.path());
let merged_permissions = PermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(vec![
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![
AbsolutePathBuf::try_from(first_dir.path().canonicalize()?)?,
AbsolutePathBuf::try_from(second_dir.path().canonicalize()?)?,
]),
}),
)),
..Default::default()
};
@@ -1584,16 +1585,31 @@ async fn partial_request_permissions_grants_do_not_preapprove_new_permissions()
let approval_file_system = approval_permissions
.file_system
.unwrap_or_else(|| panic!("expected filesystem permissions"));
assert!(approval_file_system.read.as_ref().is_none_or(Vec::is_empty));
let mut approval_writes = approval_file_system.write.unwrap_or_default();
assert_eq!(
approval_file_system
.explicit_path_entries()
.filter(|(_, access)| *access == FileSystemAccessMode::Read)
.count(),
0
);
let mut approval_writes = approval_file_system
.explicit_path_entries()
.filter_map(|(path, access)| {
(access == FileSystemAccessMode::Write).then_some(path.clone())
})
.collect::<Vec<_>>();
approval_writes.sort_by_key(|path| path.display().to_string());
let mut expected_writes = merged_permissions
.file_system
.unwrap_or_else(|| panic!("expected merged filesystem permissions"))
.write
.unwrap_or_default();
.explicit_path_entries()
.filter_map(|(path, access)| {
(access == FileSystemAccessMode::Write).then_some(path.clone())
})
.collect::<Vec<_>>();
expected_writes.sort_by_key(|path| path.display().to_string());
assert_eq!(approval_writes, expected_writes);
@@ -81,20 +81,20 @@ fn workspace_write_excluding_tmp() -> SandboxPolicy {
fn requested_directory_write_permissions(path: &Path) -> RequestPermissionProfile {
RequestPermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(vec![absolute_path(path)]),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![absolute_path(path)]),
)),
..RequestPermissionProfile::default()
}
}
fn normalized_directory_write_permissions(path: &Path) -> Result<RequestPermissionProfile> {
Ok(RequestPermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(vec![AbsolutePathBuf::try_from(path.canonicalize()?)?]),
}),
file_system: Some(FileSystemPermissions::from_read_write_roots(
Some(vec![]),
Some(vec![AbsolutePathBuf::try_from(path.canonicalize()?)?]),
)),
..RequestPermissionProfile::default()
})
}