mirror of
https://github.com/openai/codex.git
synced 2026-05-05 05:42:33 +03:00
permissions: make runtime config profile-backed (#19606)
## Why This supersedes #19391. During stack repair, GitHub marked #19391 as merged into a temporary stack branch rather than into `main`, so the runtime-config change needed a fresh PR. `PermissionProfile` is now the canonical permissions shape after #19231 because it can distinguish `Managed`, `Disabled`, and `External` enforcement while also carrying filesystem rules that legacy `SandboxPolicy` cannot represent cleanly. Core config and session state still needed to accept profile-backed permissions without forcing every profile through the strict legacy bridge, which rejected valid runtime profiles such as direct write roots. The unrelated CI/test hardening that previously rode along with this PR has been split into #19683 so this PR stays focused on the permissions model migration. ## What Changed - Adds `Permissions.permission_profile` and `SessionConfiguration.permission_profile` as constrained runtime state, while keeping `sandbox_policy` as a legacy compatibility projection. - Introduces profile setters that keep `PermissionProfile`, split filesystem/network policies, and legacy `SandboxPolicy` projections synchronized. - Uses a compatibility projection for requirement checks and legacy consumers instead of rejecting profiles that cannot round-trip through `SandboxPolicy` exactly. - Updates config loading, config overrides, session updates, turn context plumbing, prompt permission text, sandbox tags, and exec request construction to carry profile-backed runtime permissions. - Preserves configured deny-read entries and `glob_scan_max_depth` when command/session profiles are narrowed. - Adds `PermissionProfile::read_only()` and `PermissionProfile::workspace_write()` presets that match legacy defaults. ## Verification - `cargo test -p codex-core direct_write_roots` - `cargo test -p codex-core runtime_roots_to_legacy_projection` - `cargo test -p codex-app-server requested_permissions_trust_project_uses_permission_profile_intent` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19606). * #19395 * #19394 * #19393 * #19392 * __->__ #19606
This commit is contained in:
@@ -4,6 +4,7 @@ use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::de::Error as SerdeError;
|
||||
use std::borrow::Cow;
|
||||
use std::cell::RefCell;
|
||||
use std::path::Display;
|
||||
use std::path::Path;
|
||||
@@ -46,16 +47,23 @@ impl AbsolutePathBuf {
|
||||
base_path: B,
|
||||
) -> Self {
|
||||
let expanded = Self::maybe_expand_home_directory(path.as_ref());
|
||||
Self(absolutize::absolutize_from(&expanded, base_path.as_ref()))
|
||||
let expanded = normalize_path_for_platform(&expanded);
|
||||
let base_path = normalize_path_for_platform(base_path.as_ref());
|
||||
Self(absolutize::absolutize_from(
|
||||
expanded.as_ref(),
|
||||
base_path.as_ref(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn from_absolute_path<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
|
||||
let expanded = Self::maybe_expand_home_directory(path.as_ref());
|
||||
Ok(Self(absolutize::absolutize(&expanded)?))
|
||||
let expanded = normalize_path_for_platform(&expanded);
|
||||
Ok(Self(absolutize::absolutize(expanded.as_ref())?))
|
||||
}
|
||||
|
||||
pub fn from_absolute_path_checked<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
|
||||
let expanded = Self::maybe_expand_home_directory(path.as_ref());
|
||||
let expanded = normalize_path_for_platform(&expanded);
|
||||
if !expanded.is_absolute() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
@@ -63,15 +71,14 @@ impl AbsolutePathBuf {
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self(absolutize::absolutize_from(&expanded, Path::new("/"))))
|
||||
Ok(Self(absolutize::absolutize_from(
|
||||
expanded.as_ref(),
|
||||
Path::new("/"),
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn current_dir() -> std::io::Result<Self> {
|
||||
let current_dir = std::env::current_dir()?;
|
||||
Ok(Self(absolutize::absolutize_from(
|
||||
¤t_dir,
|
||||
¤t_dir,
|
||||
)))
|
||||
Self::from_absolute_path(std::env::current_dir()?)
|
||||
}
|
||||
|
||||
/// Construct an absolute path from `path`, resolving relative paths against
|
||||
@@ -132,6 +139,45 @@ impl AbsolutePathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_path_for_platform(path: &Path) -> Cow<'_, Path> {
|
||||
if cfg!(windows)
|
||||
&& let Some(path) = path.to_str()
|
||||
&& let Some(normalized) = normalize_windows_device_path(path)
|
||||
{
|
||||
return Cow::Owned(PathBuf::from(normalized));
|
||||
}
|
||||
|
||||
Cow::Borrowed(path)
|
||||
}
|
||||
|
||||
fn normalize_windows_device_path(path: &str) -> Option<String> {
|
||||
if let Some(unc) = path.strip_prefix(r"\\?\UNC\") {
|
||||
return Some(format!(r"\\{unc}"));
|
||||
}
|
||||
if let Some(unc) = path.strip_prefix(r"\\.\UNC\") {
|
||||
return Some(format!(r"\\{unc}"));
|
||||
}
|
||||
if let Some(path) = path.strip_prefix(r"\\?\")
|
||||
&& is_windows_drive_absolute_path(path)
|
||||
{
|
||||
return Some(path.to_string());
|
||||
}
|
||||
if let Some(path) = path.strip_prefix(r"\\.\")
|
||||
&& is_windows_drive_absolute_path(path)
|
||||
{
|
||||
return Some(path.to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn is_windows_drive_absolute_path(path: &str) -> bool {
|
||||
let bytes = path.as_bytes();
|
||||
bytes.len() >= 3
|
||||
&& bytes[0].is_ascii_alphabetic()
|
||||
&& bytes[1] == b':'
|
||||
&& matches!(bytes[2], b'\\' | b'/')
|
||||
}
|
||||
|
||||
/// Canonicalize a path when possible, but preserve the logical absolute path
|
||||
/// whenever canonicalization would rewrite it through a nested symlink.
|
||||
///
|
||||
@@ -391,6 +437,43 @@ mod tests {
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_windows_device_path_strips_supported_verbatim_prefixes() {
|
||||
assert_eq!(
|
||||
normalize_windows_device_path(r"\\?\D:\c\x\worktrees\2508\swift-base"),
|
||||
Some(r"D:\c\x\worktrees\2508\swift-base".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_windows_device_path(r"\\.\D:\c\x\worktrees\2508\swift-base"),
|
||||
Some(r"D:\c\x\worktrees\2508\swift-base".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_windows_device_path(r"\\?\UNC\server\share\workspace"),
|
||||
Some(r"\\server\share\workspace".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_windows_device_path(r"\\.\UNC\server\share\workspace"),
|
||||
Some(r"\\server\share\workspace".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_windows_device_path(r"\\?\GLOBALROOT\Device"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn from_absolute_path_strips_windows_verbatim_prefix() {
|
||||
let path =
|
||||
AbsolutePathBuf::from_absolute_path_checked(r"\\?\D:\c\x\worktrees\2508\swift-base")
|
||||
.expect("verbatim drive path should be absolute");
|
||||
|
||||
assert_eq!(
|
||||
path.as_path(),
|
||||
Path::new(r"D:\c\x\worktrees\2508\swift-base")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_path_is_resolved_against_base_path() {
|
||||
let temp_dir = tempdir().expect("base dir");
|
||||
|
||||
Reference in New Issue
Block a user