mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
permissions: support workspace roots in profiles
This commit is contained in:
-33
@@ -5607,14 +5607,6 @@
|
||||
"id": {
|
||||
"description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
|
||||
"type": "string"
|
||||
},
|
||||
"modifications": {
|
||||
"default": [],
|
||||
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/ActivePermissionProfileModification"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -5622,31 +5614,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ActivePermissionProfileModification": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModification",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AddCreditsNudgeCreditType": {
|
||||
"enum": [
|
||||
"credits",
|
||||
|
||||
-33
@@ -143,14 +143,6 @@
|
||||
"id": {
|
||||
"description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
|
||||
"type": "string"
|
||||
},
|
||||
"modifications": {
|
||||
"default": [],
|
||||
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ActivePermissionProfileModification"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -158,31 +150,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ActivePermissionProfileModification": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModification",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AddCreditsNudgeCreditType": {
|
||||
"enum": [
|
||||
"credits",
|
||||
|
||||
@@ -18,14 +18,6 @@
|
||||
"id": {
|
||||
"description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
|
||||
"type": "string"
|
||||
},
|
||||
"modifications": {
|
||||
"default": [],
|
||||
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ActivePermissionProfileModification"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -33,31 +25,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ActivePermissionProfileModification": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModification",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -18,14 +18,6 @@
|
||||
"id": {
|
||||
"description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
|
||||
"type": "string"
|
||||
},
|
||||
"modifications": {
|
||||
"default": [],
|
||||
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ActivePermissionProfileModification"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -33,31 +25,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ActivePermissionProfileModification": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModification",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -18,14 +18,6 @@
|
||||
"id": {
|
||||
"description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
|
||||
"type": "string"
|
||||
},
|
||||
"modifications": {
|
||||
"default": [],
|
||||
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ActivePermissionProfileModification"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -33,31 +25,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ActivePermissionProfileModification": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Additional concrete directory that should be writable.",
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"additionalWritableRoot"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "AdditionalWritableRootActivePermissionProfileModification",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AgentPath": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+1
-7
@@ -1,7 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ActivePermissionProfileModification } from "./ActivePermissionProfileModification";
|
||||
|
||||
export type ActivePermissionProfile = {
|
||||
/**
|
||||
@@ -13,9 +12,4 @@ id: string,
|
||||
* Parent profile identifier once permissions profiles support
|
||||
* inheritance. This is currently always `null`.
|
||||
*/
|
||||
extends: string | null,
|
||||
/**
|
||||
* Bounded user-requested modifications applied on top of the named
|
||||
* profile, if any.
|
||||
*/
|
||||
modifications: Array<ActivePermissionProfileModification>, };
|
||||
extends: string | null, };
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
|
||||
export type ActivePermissionProfileModification = { "type": "additionalWritableRoot", path: AbsolutePathBuf, };
|
||||
@@ -5,7 +5,6 @@ export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedN
|
||||
export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification";
|
||||
export type { AccountUpdatedNotification } from "./AccountUpdatedNotification";
|
||||
export type { ActivePermissionProfile } from "./ActivePermissionProfile";
|
||||
export type { ActivePermissionProfileModification } from "./ActivePermissionProfileModification";
|
||||
export type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType";
|
||||
export type { AddCreditsNudgeEmailStatus } from "./AddCreditsNudgeEmailStatus";
|
||||
export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
|
||||
|
||||
@@ -5,7 +5,6 @@ use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalPro
|
||||
use codex_protocol::approvals::NetworkPolicyAmendment as CoreNetworkPolicyAmendment;
|
||||
use codex_protocol::approvals::NetworkPolicyRuleAction as CoreNetworkPolicyRuleAction;
|
||||
use codex_protocol::models::ActivePermissionProfile as CoreActivePermissionProfile;
|
||||
use codex_protocol::models::ActivePermissionProfileModification as CoreActivePermissionProfileModification;
|
||||
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
|
||||
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
|
||||
use codex_protocol::models::ManagedFileSystemPermissions as CoreManagedFileSystemPermissions;
|
||||
@@ -437,41 +436,6 @@ pub struct ActivePermissionProfile {
|
||||
/// inheritance. This is currently always `null`.
|
||||
#[serde(default)]
|
||||
pub extends: Option<String>,
|
||||
/// Bounded user-requested modifications applied on top of the named
|
||||
/// profile, if any.
|
||||
#[serde(default)]
|
||||
pub modifications: Vec<ActivePermissionProfileModification>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum ActivePermissionProfileModification {
|
||||
/// Additional concrete directory that should be writable.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
AdditionalWritableRoot { path: AbsolutePathBuf },
|
||||
}
|
||||
|
||||
impl From<CoreActivePermissionProfileModification> for ActivePermissionProfileModification {
|
||||
fn from(value: CoreActivePermissionProfileModification) -> Self {
|
||||
match value {
|
||||
CoreActivePermissionProfileModification::AdditionalWritableRoot { path } => {
|
||||
Self::AdditionalWritableRoot { path }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ActivePermissionProfileModification> for CoreActivePermissionProfileModification {
|
||||
fn from(value: ActivePermissionProfileModification) -> Self {
|
||||
match value {
|
||||
ActivePermissionProfileModification::AdditionalWritableRoot { path } => {
|
||||
Self::AdditionalWritableRoot { path }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CoreActivePermissionProfile> for ActivePermissionProfile {
|
||||
@@ -479,11 +443,6 @@ impl From<CoreActivePermissionProfile> for ActivePermissionProfile {
|
||||
Self {
|
||||
id: value.id,
|
||||
extends: value.extends,
|
||||
modifications: value
|
||||
.modifications
|
||||
.into_iter()
|
||||
.map(ActivePermissionProfileModification::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -493,11 +452,6 @@ impl From<ActivePermissionProfile> for CoreActivePermissionProfile {
|
||||
Self {
|
||||
id: value.id,
|
||||
extends: value.extends,
|
||||
modifications: value
|
||||
.modifications
|
||||
.into_iter()
|
||||
.map(CoreActivePermissionProfileModification::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,9 +588,9 @@ pub async fn run_main_with_transport_options(
|
||||
range: None,
|
||||
});
|
||||
}
|
||||
if let Some(warning) =
|
||||
codex_core::config::system_bwrap_warning(config.permissions.permission_profile.get())
|
||||
{
|
||||
if let Some(warning) = codex_core::config::system_bwrap_warning(
|
||||
config.permissions.permission_profile_constraint().get(),
|
||||
) {
|
||||
config_warnings.push(ConfigWarningNotification {
|
||||
summary: warning,
|
||||
details: None,
|
||||
|
||||
@@ -164,7 +164,10 @@ impl CommandExecRequestProcessor {
|
||||
let started_network_proxy = match self.config.permissions.network.as_ref() {
|
||||
Some(spec) => match spec
|
||||
.start_proxy(
|
||||
self.config.permissions.permission_profile.get(),
|
||||
self.config
|
||||
.permissions
|
||||
.permission_profile_constraint()
|
||||
.get(),
|
||||
/*policy_decider*/ None,
|
||||
/*blocked_request_observer*/ None,
|
||||
managed_network_requirements_enabled,
|
||||
@@ -243,7 +246,7 @@ impl CommandExecRequestProcessor {
|
||||
);
|
||||
self.config
|
||||
.permissions
|
||||
.permission_profile
|
||||
.permission_profile_constraint()
|
||||
.can_set(&effective_permission_profile)
|
||||
.map_err(|err| invalid_request(format!("invalid permission profile: {err}")))?;
|
||||
effective_permission_profile
|
||||
@@ -264,7 +267,7 @@ impl CommandExecRequestProcessor {
|
||||
);
|
||||
self.config
|
||||
.permissions
|
||||
.permission_profile
|
||||
.permission_profile_constraint()
|
||||
.can_set(&permission_profile)
|
||||
.map_err(|err| invalid_request(format!("invalid sandbox policy: {err}")))?;
|
||||
permission_profile
|
||||
|
||||
@@ -230,7 +230,7 @@ async fn run_command_under_sandbox(
|
||||
let network_proxy = match config.permissions.network.as_ref() {
|
||||
Some(spec) => Some(
|
||||
spec.start_proxy(
|
||||
config.permissions.permission_profile.get(),
|
||||
config.permissions.permission_profile_constraint().get(),
|
||||
/*policy_decider*/ None,
|
||||
/*blocked_request_observer*/ None,
|
||||
managed_network_requirements_enabled,
|
||||
|
||||
@@ -25,10 +25,25 @@ impl PermissionsToml {
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct PermissionProfileToml {
|
||||
pub workspace_roots: Option<WorkspaceRootsToml>,
|
||||
pub filesystem: Option<FilesystemPermissionsToml>,
|
||||
pub network: Option<NetworkToml>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
pub struct WorkspaceRootsToml {
|
||||
#[serde(flatten)]
|
||||
pub entries: BTreeMap<String, bool>,
|
||||
}
|
||||
|
||||
impl WorkspaceRootsToml {
|
||||
pub fn enabled_roots(&self) -> impl Iterator<Item = &String> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter_map(|(path, enabled)| (*enabled).then_some(path))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
pub struct FilesystemPermissionsToml {
|
||||
/// Optional maximum depth for expanding unreadable glob patterns on
|
||||
|
||||
@@ -1943,6 +1943,9 @@
|
||||
},
|
||||
"network": {
|
||||
"$ref": "#/definitions/NetworkToml"
|
||||
},
|
||||
"workspace_roots": {
|
||||
"$ref": "#/definitions/WorkspaceRootsToml"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -3930,6 +3933,9 @@
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"WorkspaceRootsToml": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"description": "Base config deserialized from ~/.codex/config.toml.",
|
||||
|
||||
@@ -29,6 +29,7 @@ use codex_config::permissions_toml::NetworkDomainPermissionsToml;
|
||||
use codex_config::permissions_toml::NetworkToml;
|
||||
use codex_config::permissions_toml::PermissionProfileToml;
|
||||
use codex_config::permissions_toml::PermissionsToml;
|
||||
use codex_config::permissions_toml::WorkspaceRootsToml;
|
||||
use codex_config::profile_toml::ConfigProfile;
|
||||
use codex_config::types::AppToolApproval;
|
||||
use codex_config::types::ApprovalsReviewer;
|
||||
@@ -69,7 +70,6 @@ use codex_model_provider_info::WireApi;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::ActivePermissionProfileModification;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
@@ -719,6 +719,10 @@ fn config_toml_deserializes_permission_profiles() {
|
||||
let toml = r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.workspace_roots]
|
||||
"~/code/openai" = true
|
||||
"~/code/ignored" = false
|
||||
|
||||
[permissions.workspace.filesystem]
|
||||
":minimal" = "read"
|
||||
|
||||
@@ -745,6 +749,12 @@ allow_upstream_proxy = false
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: Some(WorkspaceRootsToml {
|
||||
entries: BTreeMap::from([
|
||||
("~/code/ignored".to_string(), false),
|
||||
("~/code/openai".to_string(), true),
|
||||
]),
|
||||
}),
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([
|
||||
@@ -800,6 +810,7 @@ async fn permissions_profiles_proxy_policy_does_not_start_managed_network_proxy_
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -953,6 +964,7 @@ async fn network_proxy_feature_matrix_preserves_sandbox_network_semantics() -> s
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -1103,6 +1115,7 @@ async fn network_proxy_feature_uses_profile_network_proxy_settings() -> std::io:
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -1205,6 +1218,7 @@ enabled = false
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -1253,6 +1267,7 @@ async fn permissions_profiles_network_disabled_by_default_does_not_start_proxy()
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -1299,6 +1314,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([
|
||||
@@ -1332,6 +1348,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cwd_root = cwd.path().abs();
|
||||
let memories_root = codex_home.path().join("memories").abs();
|
||||
assert_eq!(
|
||||
config.permissions.file_system_sandbox_policy(),
|
||||
@@ -1343,14 +1360,14 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
|
||||
path: FileSystemPath::Path {
|
||||
path: cwd_root.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(Some("docs".into())),
|
||||
path: FileSystemPath::Path {
|
||||
path: cwd_root.join("docs"),
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
@@ -1371,6 +1388,12 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
|
||||
exclude_slash_tmp: true,
|
||||
}
|
||||
);
|
||||
assert!(
|
||||
!config
|
||||
.permissions
|
||||
.file_system_sandbox_policy()
|
||||
.can_write_path_with_cwd(&cwd.path().join(".git"), cwd.path())
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::Restricted
|
||||
@@ -1565,6 +1588,7 @@ async fn permission_profile_override_preserves_configured_network_policy_without
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -1610,7 +1634,9 @@ async fn permission_profile_override_preserves_configured_network_policy_without
|
||||
async fn workspace_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let extra_root = TempDir::new()?;
|
||||
tokio::fs::write(cwd.path().join(".git"), "gitdir: nowhere").await?;
|
||||
tokio::fs::write(extra_root.path().join(".git"), "gitdir: nowhere").await?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
@@ -1619,6 +1645,7 @@ async fn workspace_root_glob_none_compiles_to_filesystem_pattern_entry() -> std:
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: Some(2),
|
||||
entries: BTreeMap::from([(
|
||||
@@ -1637,6 +1664,7 @@ async fn workspace_root_glob_none_compiles_to_filesystem_pattern_entry() -> std:
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
additional_writable_roots: vec![extra_root.path().to_path_buf()],
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
@@ -1650,21 +1678,23 @@ async fn workspace_root_glob_none_compiles_to_filesystem_pattern_entry() -> std:
|
||||
.glob_scan_max_depth,
|
||||
Some(2)
|
||||
);
|
||||
let expected_pattern = AbsolutePathBuf::resolve_path_against_base("**/*.env", cwd.path())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
assert!(
|
||||
config
|
||||
.permissions
|
||||
.file_system_sandbox_policy()
|
||||
.entries
|
||||
.contains(&FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: expected_pattern,
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
})
|
||||
);
|
||||
for root in [cwd.path(), extra_root.path()] {
|
||||
let expected_pattern = AbsolutePathBuf::resolve_path_against_base("**/*.env", root)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
assert!(
|
||||
config
|
||||
.permissions
|
||||
.file_system_sandbox_policy()
|
||||
.entries
|
||||
.contains(&FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: expected_pattern,
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
})
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!config
|
||||
.permissions
|
||||
@@ -1694,6 +1724,7 @@ async fn permissions_profiles_require_default_permissions() -> std::io::Result<(
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -1764,8 +1795,7 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_permissions_read_only_applies_additional_writable_roots_as_modifications()
|
||||
-> std::io::Result<()> {
|
||||
async fn default_permissions_read_only_keeps_add_dir_read_only() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let extra_root = TempDir::new()?;
|
||||
@@ -1787,20 +1817,88 @@ async fn default_permissions_read_only_applies_additional_writable_roots_as_modi
|
||||
|
||||
let policy = config.permissions.file_system_sandbox_policy();
|
||||
assert!(
|
||||
policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()),
|
||||
"expected additional writable root to modify :read-only, policy: {policy:?}"
|
||||
!policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()),
|
||||
"expected :read-only to stay read-only for runtime workspace roots, policy: {policy:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.active_permission_profile(),
|
||||
Some(
|
||||
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_READ_ONLY).with_modifications(
|
||||
vec![
|
||||
ActivePermissionProfileModification::AdditionalWritableRoot {
|
||||
path: extra_root,
|
||||
Some(ActivePermissionProfile::new(
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
|
||||
))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_profile_applies_rules_to_runtime_and_profile_workspace_roots()
|
||||
-> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let codex_home = temp_dir.path().join("codex-home");
|
||||
let cwd = temp_dir.path().join("frontend");
|
||||
let runtime_root = temp_dir.path().join("backend");
|
||||
let profile_root = temp_dir.path().join("shared");
|
||||
for root in [&cwd, &runtime_root, &profile_root] {
|
||||
std::fs::create_dir_all(root.join(".git"))?;
|
||||
std::fs::create_dir_all(root.join(".codex"))?;
|
||||
}
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
default_permissions: Some("dev".to_string()),
|
||||
permissions: Some(PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"dev".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: Some(WorkspaceRootsToml {
|
||||
entries: BTreeMap::from([(
|
||||
profile_root.to_string_lossy().into_owned(),
|
||||
true,
|
||||
)]),
|
||||
}),
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
":workspace_roots".to_string(),
|
||||
FilesystemPermissionToml::Scoped(BTreeMap::from([
|
||||
(".".to_string(), FileSystemAccessMode::Write),
|
||||
(".git".to_string(), FileSystemAccessMode::Read),
|
||||
(".codex".to_string(), FileSystemAccessMode::Read),
|
||||
])),
|
||||
)]),
|
||||
}),
|
||||
network: None,
|
||||
},
|
||||
]
|
||||
)
|
||||
)
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.clone()),
|
||||
additional_writable_roots: vec![runtime_root.clone()],
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let policy = config.permissions.file_system_sandbox_policy();
|
||||
for root in [cwd.abs(), runtime_root.abs(), profile_root.abs()] {
|
||||
assert!(
|
||||
policy.can_write_path_with_cwd(root.as_path(), cwd.as_path()),
|
||||
"expected workspace root to be writable, policy: {policy:?}"
|
||||
);
|
||||
assert!(
|
||||
!policy.can_write_path_with_cwd(&root.join(".git"), cwd.as_path()),
|
||||
"expected .git carveout under {root:?}, policy: {policy:?}"
|
||||
);
|
||||
assert!(
|
||||
!policy.can_write_path_with_cwd(&root.join(".codex"), cwd.as_path()),
|
||||
"expected .codex carveout under {root:?}, policy: {policy:?}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
config.permissions.active_permission_profile(),
|
||||
Some(ActivePermissionProfile::new("dev"))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -2185,6 +2283,7 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -2241,6 +2340,7 @@ async fn permissions_profiles_reject_nested_entries_for_non_workspace_roots() ->
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -2301,6 +2401,7 @@ async fn load_workspace_permission_profile(
|
||||
#[tokio::test]
|
||||
async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<()> {
|
||||
let config = load_workspace_permission_profile(PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -2344,6 +2445,7 @@ async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<(
|
||||
async fn permissions_profiles_allow_unknown_special_paths_with_nested_entries()
|
||||
-> std::io::Result<()> {
|
||||
let config = load_workspace_permission_profile(PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -2380,6 +2482,7 @@ async fn permissions_profiles_allow_unknown_special_paths_with_nested_entries()
|
||||
#[tokio::test]
|
||||
async fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io::Result<()> {
|
||||
let config = load_workspace_permission_profile(PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: None,
|
||||
network: None,
|
||||
})
|
||||
@@ -2408,6 +2511,7 @@ async fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io
|
||||
#[tokio::test]
|
||||
async fn permissions_profiles_allow_empty_filesystem_with_warning() -> std::io::Result<()> {
|
||||
let config = load_workspace_permission_profile(PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::new(),
|
||||
@@ -2443,6 +2547,7 @@ async fn permissions_profiles_reject_workspace_root_parent_traversal() -> std::i
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -2489,6 +2594,7 @@ async fn permissions_profiles_allow_network_enablement() -> std::io::Result<()>
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
@@ -7356,6 +7462,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
active_permission_profile: Some(ActivePermissionProfile::new(
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
|
||||
)),
|
||||
workspace_roots: vec![fixture.cwd()],
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -7367,6 +7474,8 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
cwd: fixture.cwd(),
|
||||
workspace_roots: vec![fixture.cwd()],
|
||||
workspace_roots_explicit: false,
|
||||
cli_auth_credentials_store_mode: Default::default(),
|
||||
mcp_servers: Constrained::allow_any(HashMap::new()),
|
||||
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
|
||||
@@ -7805,6 +7914,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
active_permission_profile: Some(ActivePermissionProfile::new(
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
|
||||
)),
|
||||
workspace_roots: vec![fixture.cwd()],
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -7816,6 +7926,8 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
cwd: fixture.cwd(),
|
||||
workspace_roots: vec![fixture.cwd()],
|
||||
workspace_roots_explicit: false,
|
||||
cli_auth_credentials_store_mode: Default::default(),
|
||||
mcp_servers: Constrained::allow_any(HashMap::new()),
|
||||
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
|
||||
@@ -7968,6 +8080,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
active_permission_profile: Some(ActivePermissionProfile::new(
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
|
||||
)),
|
||||
workspace_roots: vec![fixture.cwd()],
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -7979,6 +8092,8 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
cwd: fixture.cwd(),
|
||||
workspace_roots: vec![fixture.cwd()],
|
||||
workspace_roots_explicit: false,
|
||||
cli_auth_credentials_store_mode: Default::default(),
|
||||
mcp_servers: Constrained::allow_any(HashMap::new()),
|
||||
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
|
||||
@@ -8116,6 +8231,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
active_permission_profile: Some(ActivePermissionProfile::new(
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
|
||||
)),
|
||||
workspace_roots: vec![fixture.cwd()],
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -8127,6 +8243,8 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
cwd: fixture.cwd(),
|
||||
workspace_roots: vec![fixture.cwd()],
|
||||
workspace_roots_explicit: false,
|
||||
cli_auth_credentials_store_mode: Default::default(),
|
||||
mcp_servers: Constrained::allow_any(HashMap::new()),
|
||||
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
|
||||
|
||||
+228
-79
@@ -89,7 +89,6 @@ use codex_protocol::config_types::WebSearchConfig;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::ActivePermissionProfileModification;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::SandboxEnforcement;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
@@ -117,6 +116,7 @@ use crate::config::permissions::BUILT_IN_WORKSPACE_PROFILE;
|
||||
use crate::config::permissions::apply_network_proxy_feature_config;
|
||||
use crate::config::permissions::builtin_permission_profile;
|
||||
use crate::config::permissions::compile_permission_profile_selection;
|
||||
use crate::config::permissions::compile_permission_profile_workspace_roots;
|
||||
use crate::config::permissions::default_builtin_permission_profile_name;
|
||||
use crate::config::permissions::get_readable_roots_required_for_codex_runtime;
|
||||
use crate::config::permissions::network_proxy_config_for_profile_selection;
|
||||
@@ -248,11 +248,14 @@ pub struct Permissions {
|
||||
/// Approval policy for executing commands.
|
||||
pub approval_policy: Constrained<AskForApproval>,
|
||||
/// Canonical effective runtime permissions after config requirements and
|
||||
/// runtime readable-root additions have been applied.
|
||||
pub permission_profile: Constrained<PermissionProfile>,
|
||||
/// runtime workspace-root materialization have been applied.
|
||||
permission_profile: Constrained<PermissionProfile>,
|
||||
/// Named or implicit built-in profile selected by config, rather than an
|
||||
/// ad-hoc override.
|
||||
pub active_permission_profile: Option<ActivePermissionProfile>,
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
/// Thread-scoped runtime workspace roots. Symbolic `:workspace_roots`
|
||||
/// entries in `permission_profile` are materialized against these roots.
|
||||
workspace_roots: Vec<AbsolutePathBuf>,
|
||||
/// Effective network configuration applied to all spawned processes.
|
||||
pub network: Option<NetworkProxySpec>,
|
||||
/// Whether the model may request a login shell for shell-based tools.
|
||||
@@ -274,10 +277,89 @@ pub struct Permissions {
|
||||
}
|
||||
|
||||
impl Permissions {
|
||||
/// Build permissions from the constrained values required for a minimal
|
||||
/// in-process configuration.
|
||||
pub fn from_approval_and_profile(
|
||||
approval_policy: Constrained<AskForApproval>,
|
||||
permission_profile: Constrained<PermissionProfile>,
|
||||
) -> Self {
|
||||
Self {
|
||||
approval_policy,
|
||||
permission_profile,
|
||||
active_permission_profile: None,
|
||||
workspace_roots: Vec::new(),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
windows_sandbox_private_desktop: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrow the constrained canonical profile. This preserves the raw
|
||||
/// symbolic `:workspace_roots` form for session/thread state.
|
||||
pub fn permission_profile_constraint(&self) -> &Constrained<PermissionProfile> {
|
||||
&self.permission_profile
|
||||
}
|
||||
|
||||
/// Replace the full constrained profile value and clear any profile-name
|
||||
/// sidecar because the new constraint may no longer match that name.
|
||||
pub fn replace_permission_profile_constraint(
|
||||
&mut self,
|
||||
permission_profile: Constrained<PermissionProfile>,
|
||||
) {
|
||||
self.permission_profile = permission_profile;
|
||||
self.active_permission_profile = None;
|
||||
}
|
||||
|
||||
/// Replace the full constrained profile value and preserve the active
|
||||
/// profile sidecar when the caller has already validated both together.
|
||||
pub fn replace_permission_profile_constraint_with_active_profile(
|
||||
&mut self,
|
||||
permission_profile: Constrained<PermissionProfile>,
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
) {
|
||||
self.permission_profile = permission_profile;
|
||||
self.active_permission_profile = active_permission_profile;
|
||||
}
|
||||
|
||||
/// Record the active profile id only if it still describes the current
|
||||
/// canonical profile.
|
||||
pub fn set_active_permission_profile_for_current_profile(
|
||||
&mut self,
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
expected_permission_profile: Option<&PermissionProfile>,
|
||||
) {
|
||||
self.active_permission_profile =
|
||||
match (active_permission_profile, expected_permission_profile) {
|
||||
(Some(active_permission_profile), Some(expected_permission_profile))
|
||||
if self.permission_profile.get() == expected_permission_profile =>
|
||||
{
|
||||
Some(active_permission_profile)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn set_workspace_roots(&mut self, workspace_roots: Vec<AbsolutePathBuf>) {
|
||||
self.workspace_roots = workspace_roots;
|
||||
}
|
||||
|
||||
pub fn workspace_roots(&self) -> &[AbsolutePathBuf] {
|
||||
&self.workspace_roots
|
||||
}
|
||||
|
||||
fn materialized_permission_profile(&self) -> PermissionProfile {
|
||||
self.permission_profile
|
||||
.get()
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(&self.workspace_roots)
|
||||
}
|
||||
|
||||
/// Effective runtime permissions after config requirements and runtime
|
||||
/// readable-root additions have been applied.
|
||||
/// workspace-root materialization have been applied.
|
||||
pub fn permission_profile(&self) -> PermissionProfile {
|
||||
self.permission_profile.get().clone()
|
||||
self.materialized_permission_profile()
|
||||
}
|
||||
|
||||
/// Named profile selected by config, if the current profile has one.
|
||||
@@ -287,7 +369,8 @@ impl Permissions {
|
||||
|
||||
/// Effective filesystem sandbox policy derived from the canonical profile.
|
||||
pub fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
|
||||
self.permission_profile.get().file_system_sandbox_policy()
|
||||
self.materialized_permission_profile()
|
||||
.file_system_sandbox_policy()
|
||||
}
|
||||
|
||||
/// Effective network sandbox policy derived from the canonical profile.
|
||||
@@ -297,10 +380,10 @@ impl Permissions {
|
||||
|
||||
/// Legacy compatibility projection derived from the canonical profile.
|
||||
pub fn legacy_sandbox_policy(&self, cwd: &Path) -> SandboxPolicy {
|
||||
let permission_profile = self.permission_profile.get();
|
||||
let permission_profile = self.materialized_permission_profile();
|
||||
let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
|
||||
compatibility_sandbox_policy_for_permission_profile(
|
||||
permission_profile,
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
permission_profile.network_sandbox_policy(),
|
||||
cwd,
|
||||
@@ -341,6 +424,26 @@ impl Permissions {
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
self.workspace_roots = match &sandbox_policy {
|
||||
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
|
||||
let mut workspace_roots = vec![
|
||||
AbsolutePathBuf::from_absolute_path(cwd)
|
||||
.unwrap_or_else(|_| AbsolutePathBuf::resolve_path_against_base(cwd, "/")),
|
||||
];
|
||||
for root in writable_roots {
|
||||
if !workspace_roots.iter().any(|existing| existing == root) {
|
||||
workspace_roots.push(root.clone());
|
||||
}
|
||||
}
|
||||
workspace_roots
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess
|
||||
| SandboxPolicy::ExternalSandbox { .. }
|
||||
| SandboxPolicy::ReadOnly { .. } => vec![
|
||||
AbsolutePathBuf::from_absolute_path(cwd)
|
||||
.unwrap_or_else(|_| AbsolutePathBuf::resolve_path_against_base(cwd, "/")),
|
||||
],
|
||||
};
|
||||
|
||||
self.permission_profile.set(permission_profile)?;
|
||||
self.active_permission_profile = None;
|
||||
@@ -577,6 +680,15 @@ pub struct Config {
|
||||
/// layer are resolved against this path.
|
||||
pub cwd: AbsolutePathBuf,
|
||||
|
||||
/// Absolute runtime workspace roots for the session. Symbolic
|
||||
/// `:workspace_roots` permission entries are materialized against these
|
||||
/// roots while profile-defined workspace roots remain encoded directly in
|
||||
/// the permission profile.
|
||||
pub workspace_roots: Vec<AbsolutePathBuf>,
|
||||
/// Whether runtime workspace roots were supplied explicitly by the caller
|
||||
/// or legacy config, rather than defaulting to `cwd`.
|
||||
pub workspace_roots_explicit: bool,
|
||||
|
||||
/// Preferred store for CLI auth credentials.
|
||||
/// file (default): Use a file in the Codex home directory.
|
||||
/// keyring: Use an OS-specific keyring service.
|
||||
@@ -1085,8 +1197,14 @@ impl Config {
|
||||
&mut self,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
) -> ConstraintResult<()> {
|
||||
self.workspace_roots_explicit = matches!(
|
||||
&sandbox_policy,
|
||||
SandboxPolicy::WorkspaceWrite { writable_roots, .. } if !writable_roots.is_empty()
|
||||
);
|
||||
self.permissions
|
||||
.set_legacy_sandbox_policy(sandbox_policy, self.cwd.as_path())
|
||||
.set_legacy_sandbox_policy(sandbox_policy, self.cwd.as_path())?;
|
||||
self.workspace_roots = self.permissions.workspace_roots().to_vec();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_models_manager_config(&self) -> ModelsManagerConfig {
|
||||
@@ -1935,6 +2053,14 @@ pub struct ConfigOverrides {
|
||||
pub bypass_hook_trust: Option<bool>,
|
||||
/// Additional directories that should be treated as writable roots for this session.
|
||||
pub additional_writable_roots: Vec<PathBuf>,
|
||||
/// Explicit runtime workspace roots for this session. When set, this is
|
||||
/// the full runtime root list rather than an additive override.
|
||||
pub workspace_roots: Option<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
fn dedupe_absolute_paths(paths: &mut Vec<AbsolutePathBuf>) {
|
||||
let mut seen = HashSet::new();
|
||||
paths.retain(|path| seen.insert(path.clone()));
|
||||
}
|
||||
|
||||
/// Resolves the OSS provider from CLI override, profile config, or global config.
|
||||
@@ -2248,6 +2374,7 @@ impl Config {
|
||||
ephemeral,
|
||||
bypass_hook_trust,
|
||||
additional_writable_roots,
|
||||
workspace_roots: workspace_roots_override,
|
||||
} = overrides;
|
||||
let bypass_hook_trust = bypass_hook_trust.unwrap_or_default();
|
||||
|
||||
@@ -2340,11 +2467,10 @@ impl Config {
|
||||
}
|
||||
}
|
||||
}))?;
|
||||
let mut additional_writable_roots: Vec<AbsolutePathBuf> = additional_writable_roots
|
||||
let requested_additional_writable_roots: Vec<AbsolutePathBuf> = additional_writable_roots
|
||||
.into_iter()
|
||||
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, resolved_cwd.as_path()))
|
||||
.collect();
|
||||
let requested_additional_writable_roots = additional_writable_roots.clone();
|
||||
let repo_root = resolve_root_git_project_for_trust(fs, &resolved_cwd).await;
|
||||
let active_project = cfg
|
||||
.get_active_project(
|
||||
@@ -2386,12 +2512,7 @@ impl Config {
|
||||
};
|
||||
let memories_root = memory_root(&codex_home);
|
||||
std::fs::create_dir_all(&memories_root)?;
|
||||
if !additional_writable_roots
|
||||
.iter()
|
||||
.any(|existing| existing == &memories_root)
|
||||
{
|
||||
additional_writable_roots.push(memories_root);
|
||||
}
|
||||
let internal_writable_roots = vec![memories_root];
|
||||
|
||||
let profiles_are_active = default_permissions_override.is_some()
|
||||
|| matches!(
|
||||
@@ -2401,6 +2522,40 @@ impl Config {
|
||||
|| permission_config_syntax.is_none();
|
||||
let using_implicit_builtin_profile =
|
||||
permission_config_syntax.is_none() && default_permissions.is_none();
|
||||
let should_seed_legacy_workspace_roots = default_permissions.is_none()
|
||||
&& matches!(
|
||||
permission_config_syntax,
|
||||
None | Some(PermissionConfigSyntax::Legacy)
|
||||
);
|
||||
let legacy_workspace_roots_explicit = should_seed_legacy_workspace_roots
|
||||
&& cfg
|
||||
.sandbox_workspace_write
|
||||
.as_ref()
|
||||
.is_some_and(|sandbox_workspace_write| {
|
||||
!sandbox_workspace_write.writable_roots.is_empty()
|
||||
});
|
||||
let workspace_roots_explicit = workspace_roots_override.is_some()
|
||||
|| !requested_additional_writable_roots.is_empty()
|
||||
|| legacy_workspace_roots_explicit;
|
||||
let mut workspace_roots = match workspace_roots_override {
|
||||
Some(workspace_roots) => workspace_roots
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
AbsolutePathBuf::resolve_path_against_base(path, resolved_cwd.as_path())
|
||||
})
|
||||
.collect(),
|
||||
None => {
|
||||
let mut workspace_roots = vec![resolved_cwd.clone()];
|
||||
workspace_roots.extend(requested_additional_writable_roots.clone());
|
||||
if should_seed_legacy_workspace_roots
|
||||
&& let Some(sandbox_workspace_write) = cfg.sandbox_workspace_write.as_ref()
|
||||
{
|
||||
workspace_roots.extend(sandbox_workspace_write.writable_roots.clone());
|
||||
}
|
||||
workspace_roots
|
||||
}
|
||||
};
|
||||
dedupe_absolute_paths(&mut workspace_roots);
|
||||
let (
|
||||
mut configured_network_proxy_config,
|
||||
permission_profile,
|
||||
@@ -2429,18 +2584,24 @@ impl Config {
|
||||
} else {
|
||||
NetworkProxyConfig::default()
|
||||
};
|
||||
let materialized_file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(&workspace_roots);
|
||||
let materialized_permission_profile =
|
||||
PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
permission_profile.enforcement(),
|
||||
&materialized_file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
&materialized_permission_profile,
|
||||
&materialized_file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
resolved_cwd.as_path(),
|
||||
);
|
||||
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.with_additional_writable_roots(
|
||||
resolved_cwd.as_path(),
|
||||
&additional_writable_roots,
|
||||
);
|
||||
.with_additional_legacy_workspace_writable_roots(&internal_writable_roots);
|
||||
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
permission_profile.enforcement(),
|
||||
&file_system_sandbox_policy,
|
||||
@@ -2474,6 +2635,20 @@ impl Config {
|
||||
resolved_cwd.as_path(),
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
let mut configured_workspace_roots = compile_permission_profile_workspace_roots(
|
||||
cfg.permissions.as_ref(),
|
||||
default_permissions,
|
||||
resolved_cwd.as_path(),
|
||||
)?;
|
||||
if using_implicit_builtin_profile
|
||||
&& default_permissions == BUILT_IN_WORKSPACE_PROFILE
|
||||
&& let Some(sandbox_workspace_write) = cfg.sandbox_workspace_write.as_ref()
|
||||
{
|
||||
configured_workspace_roots.extend(sandbox_workspace_write.writable_roots.clone());
|
||||
}
|
||||
dedupe_absolute_paths(&mut configured_workspace_roots);
|
||||
file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.with_materialized_project_roots_for_workspace_roots(&configured_workspace_roots);
|
||||
let mut permission_profile = if let Some(permission_profile) =
|
||||
builtin_permission_profile(default_permissions, builtin_workspace_write_settings)
|
||||
{
|
||||
@@ -2484,36 +2659,26 @@ impl Config {
|
||||
network_sandbox_policy,
|
||||
)
|
||||
};
|
||||
let materialized_file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(&workspace_roots);
|
||||
let materialized_permission_profile =
|
||||
PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
permission_profile.enforcement(),
|
||||
&materialized_file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
&materialized_permission_profile,
|
||||
&materialized_file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
resolved_cwd.as_path(),
|
||||
);
|
||||
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
file_system_sandbox_policy = if using_implicit_builtin_profile {
|
||||
file_system_sandbox_policy
|
||||
.with_additional_legacy_workspace_writable_roots(
|
||||
&additional_writable_roots,
|
||||
)
|
||||
} else {
|
||||
file_system_sandbox_policy.with_additional_writable_roots(
|
||||
resolved_cwd.as_path(),
|
||||
&additional_writable_roots,
|
||||
)
|
||||
};
|
||||
permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
} else if matches!(permission_profile, PermissionProfile::Managed { .. })
|
||||
&& !requested_additional_writable_roots.is_empty()
|
||||
{
|
||||
file_system_sandbox_policy = file_system_sandbox_policy.with_additional_writable_roots(
|
||||
resolved_cwd.as_path(),
|
||||
&requested_additional_writable_roots,
|
||||
);
|
||||
permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.with_additional_legacy_workspace_writable_roots(&internal_writable_roots);
|
||||
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
permission_profile.enforcement(),
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
@@ -2529,22 +2694,7 @@ impl Config {
|
||||
// when doing so would lose roots, network, or tmp settings.
|
||||
None
|
||||
} else {
|
||||
let active_permission_profile = if !requested_additional_writable_roots.is_empty()
|
||||
&& matches!(permission_profile, PermissionProfile::Managed { .. })
|
||||
{
|
||||
ActivePermissionProfile::new(default_permissions).with_modifications(
|
||||
requested_additional_writable_roots
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|path| {
|
||||
ActivePermissionProfileModification::AdditionalWritableRoot { path }
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
ActivePermissionProfile::new(default_permissions)
|
||||
};
|
||||
Some(active_permission_profile)
|
||||
Some(ActivePermissionProfile::new(default_permissions))
|
||||
};
|
||||
(
|
||||
configured_network_proxy_config,
|
||||
@@ -2583,25 +2733,21 @@ impl Config {
|
||||
}
|
||||
let (mut file_system_sandbox_policy, network_sandbox_policy) =
|
||||
permission_profile.to_runtime_permissions();
|
||||
// `additional_writable_roots` is a legacy workspace-write knob. It
|
||||
// only applies when the derived managed profile has workspace-style
|
||||
// write access to the project roots; read-only, disabled, external,
|
||||
// and future non-workspace profiles must not silently grow extra
|
||||
// write access.
|
||||
let materialized_file_system_sandbox_policy = permission_profile
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(&workspace_roots)
|
||||
.file_system_sandbox_policy();
|
||||
if matches!(permission_profile.enforcement(), SandboxEnforcement::Managed)
|
||||
&& file_system_sandbox_policy.can_write_path_with_cwd(
|
||||
&& materialized_file_system_sandbox_policy.can_write_path_with_cwd(
|
||||
resolved_cwd.as_path(),
|
||||
resolved_cwd.as_path(),
|
||||
)
|
||||
&& !file_system_sandbox_policy.has_full_disk_write_access()
|
||||
&& !materialized_file_system_sandbox_policy.has_full_disk_write_access()
|
||||
{
|
||||
// Keep legacy behavior for extra writable roots while storing
|
||||
// the result as the canonical permission profile. Explicit
|
||||
// extra roots are concrete paths, so their metadata carveouts
|
||||
// are also concrete rather than symbolic `:workspace_roots`
|
||||
// entries.
|
||||
// Keep Codex runtime write access while storing the runtime
|
||||
// workspace roots separately on the thread.
|
||||
file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.with_additional_legacy_workspace_writable_roots(&additional_writable_roots);
|
||||
.with_additional_legacy_workspace_writable_roots(&internal_writable_roots);
|
||||
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
permission_profile.enforcement(),
|
||||
&file_system_sandbox_policy,
|
||||
@@ -3114,11 +3260,14 @@ impl Config {
|
||||
model_provider_id,
|
||||
model_provider,
|
||||
cwd: resolved_cwd,
|
||||
workspace_roots: workspace_roots.clone(),
|
||||
workspace_roots_explicit,
|
||||
startup_warnings,
|
||||
permissions: Permissions {
|
||||
approval_policy: constrained_approval_policy.value,
|
||||
permission_profile: constrained_permission_profile.value,
|
||||
active_permission_profile,
|
||||
workspace_roots,
|
||||
network,
|
||||
allow_login_shell,
|
||||
shell_environment_policy,
|
||||
@@ -3404,7 +3553,7 @@ impl Config {
|
||||
|
||||
pub fn managed_network_requirements_enabled(&self) -> bool {
|
||||
!matches!(
|
||||
self.permissions.permission_profile.get(),
|
||||
self.permissions.permission_profile_constraint().get(),
|
||||
PermissionProfile::Disabled
|
||||
) && self
|
||||
.config_layer_stack
|
||||
|
||||
@@ -13,6 +13,7 @@ use codex_config::permissions_toml::NetworkUnixSocketPermissionToml;
|
||||
use codex_config::permissions_toml::NetworkUnixSocketPermissionsToml;
|
||||
use codex_config::permissions_toml::PermissionProfileToml;
|
||||
use codex_config::permissions_toml::PermissionsToml;
|
||||
use codex_config::permissions_toml::WorkspaceRootsToml;
|
||||
use codex_config::types::SandboxWorkspaceWrite;
|
||||
use codex_features::NetworkProxyConfigToml;
|
||||
use codex_features::NetworkProxyDomainPermissionToml;
|
||||
@@ -33,6 +34,7 @@ use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::permissions::project_roots_glob_pattern;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
use super::ProjectConfig;
|
||||
@@ -72,12 +74,12 @@ pub(crate) fn builtin_permission_profile(
|
||||
BUILT_IN_READ_ONLY_PROFILE => Some(PermissionProfile::read_only()),
|
||||
BUILT_IN_WORKSPACE_PROFILE => Some(match workspace_write {
|
||||
Some(SandboxWorkspaceWrite {
|
||||
writable_roots,
|
||||
writable_roots: _,
|
||||
network_access,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
}) => PermissionProfile::workspace_write_with(
|
||||
writable_roots,
|
||||
&[],
|
||||
if *network_access {
|
||||
NetworkSandboxPolicy::Enabled
|
||||
} else {
|
||||
@@ -303,6 +305,41 @@ pub(crate) fn compile_permission_profile_selection(
|
||||
compile_permission_profile(permissions, profile_name, policy_cwd, startup_warnings)
|
||||
}
|
||||
|
||||
pub(crate) fn compile_permission_profile_workspace_roots(
|
||||
permissions: Option<&PermissionsToml>,
|
||||
profile_name: &str,
|
||||
policy_cwd: &Path,
|
||||
) -> io::Result<Vec<AbsolutePathBuf>> {
|
||||
if is_builtin_permission_profile_name(profile_name) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
reject_unknown_builtin_permission_profile(profile_name)?;
|
||||
|
||||
let permissions = permissions.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"default_permissions requires a `[permissions]` table",
|
||||
)
|
||||
})?;
|
||||
let profile = resolve_permission_profile(permissions, profile_name)?;
|
||||
Ok(compile_workspace_roots(
|
||||
profile.workspace_roots.as_ref(),
|
||||
policy_cwd,
|
||||
))
|
||||
}
|
||||
|
||||
fn compile_workspace_roots(
|
||||
workspace_roots: Option<&WorkspaceRootsToml>,
|
||||
policy_cwd: &Path,
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
workspace_roots.map_or_else(Vec::new, |workspace_roots| {
|
||||
workspace_roots
|
||||
.enabled_roots()
|
||||
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, policy_cwd))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn reject_unknown_builtin_permission_profile(profile_name: &str) -> io::Result<()> {
|
||||
if profile_name.starts_with(':') {
|
||||
return Err(io::Error::new(
|
||||
@@ -478,7 +515,7 @@ fn compile_scoped_filesystem_pattern(
|
||||
path: &str,
|
||||
subpath: &str,
|
||||
access: FileSystemAccessMode,
|
||||
policy_cwd: &Path,
|
||||
_policy_cwd: &Path,
|
||||
) -> io::Result<String> {
|
||||
// Pattern entries currently mean deny-read only. Supporting broader access
|
||||
// modes here would imply glob-based read/write allow semantics that the
|
||||
@@ -493,15 +530,10 @@ fn compile_scoped_filesystem_pattern(
|
||||
|
||||
match parse_special_path(path) {
|
||||
Some(FileSystemSpecialPath::ProjectRoots { .. }) => {
|
||||
// `:workspace_roots` is represented as a special path, but current
|
||||
// filesystem-policy resolution defines it relative to the session
|
||||
// cwd. Use the same policy cwd here so glob entries and exact
|
||||
// scoped entries resolve consistently.
|
||||
Ok(
|
||||
AbsolutePathBuf::resolve_path_against_base(&subpath, policy_cwd)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
// Keep `:workspace_roots` glob patterns symbolic until the active
|
||||
// workspace roots are known, then materialize them for cwd and any
|
||||
// runtime/profile-added workspace roots together.
|
||||
Ok(project_roots_glob_pattern(&subpath))
|
||||
}
|
||||
Some(_) => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
|
||||
@@ -11,6 +11,7 @@ use codex_config::permissions_toml::NetworkUnixSocketPermissionToml;
|
||||
use codex_config::permissions_toml::NetworkUnixSocketPermissionsToml;
|
||||
use codex_config::permissions_toml::PermissionProfileToml;
|
||||
use codex_config::permissions_toml::PermissionsToml;
|
||||
use codex_config::permissions_toml::WorkspaceRootsToml;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
@@ -66,6 +67,7 @@ async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Resu
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::new(),
|
||||
@@ -275,6 +277,39 @@ fn profile_network_proxy_config_keeps_proxy_disabled_for_proxy_policy() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_permission_profile_workspace_roots_resolves_enabled_entries() -> std::io::Result<()> {
|
||||
let cwd = TempDir::new()?;
|
||||
let workspace_roots = compile_permission_profile_workspace_roots(
|
||||
Some(&PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: Some(WorkspaceRootsToml {
|
||||
entries: BTreeMap::from([
|
||||
("backend".to_string(), true),
|
||||
("disabled".to_string(), false),
|
||||
]),
|
||||
}),
|
||||
filesystem: None,
|
||||
network: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
"workspace",
|
||||
cwd.path(),
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
workspace_roots,
|
||||
vec![AbsolutePathBuf::resolve_path_against_base(
|
||||
"backend",
|
||||
cwd.path()
|
||||
)]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_write_glob_warnings_skip_supported_deny_read_globs_and_trailing_subpaths() {
|
||||
let filesystem = FilesystemPermissionsToml {
|
||||
@@ -359,6 +394,7 @@ fn read_write_trailing_glob_suffix_compiles_as_subpath() -> std::io::Result<()>
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
workspace_roots: None,
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
|
||||
@@ -896,9 +896,11 @@ pub(crate) fn build_guardian_review_session_config(
|
||||
guardian_config.developer_instructions = None;
|
||||
guardian_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
guardian_config.permissions.permission_profile = Constrained::allow_only(
|
||||
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy),
|
||||
);
|
||||
guardian_config
|
||||
.permissions
|
||||
.replace_permission_profile_constraint(Constrained::allow_only(
|
||||
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy),
|
||||
));
|
||||
guardian_config
|
||||
.permissions
|
||||
.set_legacy_sandbox_policy(sandbox_policy, guardian_config.cwd.as_path())
|
||||
@@ -924,7 +926,10 @@ pub(crate) fn build_guardian_review_session_config(
|
||||
guardian_config.permissions.network = Some(NetworkProxySpec::from_config_and_constraints(
|
||||
live_network_config,
|
||||
network_constraints,
|
||||
guardian_config.permissions.permission_profile.get(),
|
||||
guardian_config
|
||||
.permissions
|
||||
.permission_profile_constraint()
|
||||
.get(),
|
||||
)?);
|
||||
}
|
||||
for feature in [
|
||||
|
||||
@@ -2163,7 +2163,10 @@ async fn guardian_review_session_config_preserves_parent_network_proxy() {
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
parent_config.permissions.permission_profile.get(),
|
||||
parent_config
|
||||
.permissions
|
||||
.permission_profile_constraint()
|
||||
.get(),
|
||||
)
|
||||
.expect("network proxy spec");
|
||||
parent_config.permissions.network = Some(network.clone());
|
||||
@@ -2190,8 +2193,8 @@ async fn guardian_review_session_config_preserves_parent_network_proxy() {
|
||||
Constrained::allow_only(AskForApproval::Never)
|
||||
);
|
||||
assert_eq!(
|
||||
guardian_config.permissions.permission_profile,
|
||||
Constrained::allow_only(PermissionProfile::from_legacy_sandbox_policy(
|
||||
guardian_config.permissions.permission_profile_constraint(),
|
||||
&Constrained::allow_only(PermissionProfile::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
))
|
||||
);
|
||||
@@ -2230,7 +2233,10 @@ async fn guardian_review_session_config_uses_live_network_proxy_state() {
|
||||
NetworkProxySpec::from_config_and_constraints(
|
||||
parent_network,
|
||||
/*requirements*/ None,
|
||||
parent_config.permissions.permission_profile.get(),
|
||||
parent_config
|
||||
.permissions
|
||||
.permission_profile_constraint()
|
||||
.get(),
|
||||
)
|
||||
.expect("parent network proxy spec"),
|
||||
);
|
||||
|
||||
@@ -617,7 +617,7 @@ impl Codex {
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile_constraint().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
|
||||
@@ -769,7 +769,7 @@ impl Session {
|
||||
let (network_proxy, session_network_proxy) = Self::start_managed_network_proxy(
|
||||
spec,
|
||||
current_exec_policy.as_ref(),
|
||||
config.permissions.permission_profile.get(),
|
||||
config.permissions.permission_profile_constraint().get(),
|
||||
network_policy_decider.as_ref().map(Arc::clone),
|
||||
blocked_request_observer.as_ref().map(Arc::clone),
|
||||
managed_network_requirements_configured,
|
||||
@@ -833,7 +833,7 @@ impl Session {
|
||||
// setup is straightforward enough and performs well.
|
||||
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
|
||||
&config.permissions.approval_policy,
|
||||
&config.permissions.permission_profile,
|
||||
config.permissions.permission_profile_constraint(),
|
||||
))),
|
||||
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
|
||||
unified_exec_manager: UnifiedExecProcessManager::new(
|
||||
|
||||
@@ -2128,8 +2128,10 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() ->
|
||||
};
|
||||
let expected_sandbox_policy = sandbox_policy.clone();
|
||||
let mut builder = test_codex().with_config(move |config| {
|
||||
config.permissions.permission_profile = codex_config::Constrained::allow_any(
|
||||
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy),
|
||||
config.permissions.replace_permission_profile_constraint(
|
||||
codex_config::Constrained::allow_any(PermissionProfile::from_legacy_sandbox_policy(
|
||||
&sandbox_policy,
|
||||
)),
|
||||
);
|
||||
config
|
||||
.set_legacy_sandbox_policy(sandbox_policy)
|
||||
@@ -2884,7 +2886,7 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile_constraint().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -2988,7 +2990,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile_constraint().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -3461,7 +3463,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile_constraint().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -3994,7 +3996,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile_constraint().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -4103,7 +4105,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile_constraint().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -4150,7 +4152,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
let services = SessionServices {
|
||||
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
|
||||
&config.permissions.approval_policy,
|
||||
&config.permissions.permission_profile,
|
||||
config.permissions.permission_profile_constraint(),
|
||||
))),
|
||||
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
|
||||
unified_exec_manager: UnifiedExecProcessManager::new(
|
||||
@@ -4335,7 +4337,7 @@ async fn make_session_with_config_and_rx(
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile_constraint().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -4438,7 +4440,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile_constraint().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -5955,7 +5957,7 @@ where
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile: config.permissions.permission_profile.clone(),
|
||||
permission_profile: config.permissions.permission_profile_constraint().clone(),
|
||||
active_permission_profile: config.permissions.active_permission_profile(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
@@ -6002,7 +6004,7 @@ where
|
||||
let services = SessionServices {
|
||||
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
|
||||
&config.permissions.approval_policy,
|
||||
&config.permissions.permission_profile,
|
||||
config.permissions.permission_profile_constraint(),
|
||||
))),
|
||||
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
|
||||
unified_exec_manager: UnifiedExecProcessManager::new(
|
||||
|
||||
@@ -438,8 +438,12 @@ impl Session {
|
||||
per_turn_config.service_tier = session_configuration.service_tier.clone();
|
||||
per_turn_config.personality = session_configuration.personality;
|
||||
per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer;
|
||||
per_turn_config.permissions.permission_profile =
|
||||
session_configuration.permission_profile.clone();
|
||||
per_turn_config
|
||||
.permissions
|
||||
.replace_permission_profile_constraint_with_active_profile(
|
||||
session_configuration.permission_profile.clone(),
|
||||
session_configuration.active_permission_profile.clone(),
|
||||
);
|
||||
let permission_profile = session_configuration.permission_profile();
|
||||
let resolved_web_search_mode =
|
||||
resolve_web_search_mode_for_turn(&per_turn_config.web_search_mode, &permission_profile);
|
||||
@@ -466,8 +470,12 @@ impl Session {
|
||||
Self::build_per_turn_config(session_configuration, session_configuration.cwd.clone());
|
||||
config.model = Some(session_configuration.collaboration_mode.model().to_string());
|
||||
config.permissions.approval_policy = session_configuration.approval_policy.clone();
|
||||
config.permissions.active_permission_profile =
|
||||
session_configuration.active_permission_profile.clone();
|
||||
config
|
||||
.permissions
|
||||
.set_active_permission_profile_for_current_profile(
|
||||
session_configuration.active_permission_profile.clone(),
|
||||
Some(session_configuration.permission_profile.get()),
|
||||
);
|
||||
config
|
||||
}
|
||||
|
||||
|
||||
@@ -3948,7 +3948,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
|
||||
#[allow(deprecated)]
|
||||
let turn_cwd = turn.cwd.clone();
|
||||
let sandbox_policy = pick_allowed_sandbox_policy(
|
||||
&turn.config.permissions.permission_profile,
|
||||
turn.config.permissions.permission_profile_constraint(),
|
||||
turn.config.legacy_sandbox_policy(),
|
||||
turn_cwd.as_path(),
|
||||
);
|
||||
|
||||
@@ -437,7 +437,7 @@ fn config_summary_entries(
|
||||
(
|
||||
"sandbox",
|
||||
summarize_permission_profile(
|
||||
config.permissions.permission_profile.get(),
|
||||
config.permissions.permission_profile_constraint().get(),
|
||||
config.cwd.as_path(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -344,21 +344,6 @@ pub struct ActivePermissionProfile {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub extends: Option<String>,
|
||||
|
||||
/// Bounded user-requested modifications applied on top of the named
|
||||
/// profile, if any.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub modifications: Vec<ActivePermissionProfileModification>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[ts(tag = "type")]
|
||||
pub enum ActivePermissionProfileModification {
|
||||
/// Additional concrete directory that should be writable.
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
AdditionalWritableRoot { path: AbsolutePathBuf },
|
||||
}
|
||||
|
||||
impl ActivePermissionProfile {
|
||||
@@ -366,17 +351,8 @@ impl ActivePermissionProfile {
|
||||
Self {
|
||||
id: id.into(),
|
||||
extends: None,
|
||||
modifications: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_modifications(
|
||||
mut self,
|
||||
modifications: Vec<ActivePermissionProfileModification>,
|
||||
) -> Self {
|
||||
self.modifications = modifications;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PermissionProfile {
|
||||
@@ -444,6 +420,28 @@ impl PermissionProfile {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn materialize_project_roots_with_workspace_roots(
|
||||
self,
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
) -> Self {
|
||||
match self {
|
||||
Self::Managed {
|
||||
file_system,
|
||||
network,
|
||||
} => {
|
||||
let file_system = file_system
|
||||
.to_sandbox_policy()
|
||||
.materialize_project_roots_with_workspace_roots(workspace_roots);
|
||||
Self::Managed {
|
||||
file_system: ManagedFileSystemPermissions::from_sandbox_policy(&file_system),
|
||||
network,
|
||||
}
|
||||
}
|
||||
Self::Disabled => Self::Disabled,
|
||||
Self::External { network } => Self::External { network },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_runtime_permissions(
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
|
||||
@@ -350,6 +350,12 @@ pub enum FileSystemPath {
|
||||
},
|
||||
}
|
||||
|
||||
const PROJECT_ROOTS_GLOB_PATTERN_PREFIX: &str = "codex-project-roots://";
|
||||
|
||||
pub fn project_roots_glob_pattern(subpath: &Path) -> String {
|
||||
format!("{PROJECT_ROOTS_GLOB_PATTERN_PREFIX}{}", subpath.display())
|
||||
}
|
||||
|
||||
impl Default for FileSystemSandboxPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -703,15 +709,100 @@ impl FileSystemSandboxPolicy {
|
||||
pub fn materialize_project_roots_with_cwd(mut self, cwd: &Path) -> Self {
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
||||
for entry in &mut self.entries {
|
||||
let FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::ProjectRoots { .. },
|
||||
} = &entry.path
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
match &entry.path {
|
||||
FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::ProjectRoots { .. },
|
||||
} => {
|
||||
if let Some(path) = resolve_file_system_path(&entry.path, cwd.as_ref()) {
|
||||
entry.path = FileSystemPath::Path { path };
|
||||
}
|
||||
}
|
||||
FileSystemPath::GlobPattern { pattern } => {
|
||||
if let (Some(cwd), Some(subpath)) =
|
||||
(cwd.as_ref(), parse_project_roots_glob_pattern(pattern))
|
||||
{
|
||||
entry.path = FileSystemPath::GlobPattern {
|
||||
pattern: resolve_project_roots_glob_pattern(subpath, cwd),
|
||||
};
|
||||
}
|
||||
}
|
||||
FileSystemPath::Special { value: _ } => {}
|
||||
FileSystemPath::Path { .. } => {}
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
if let Some(path) = resolve_file_system_path(&entry.path, cwd.as_ref()) {
|
||||
entry.path = FileSystemPath::Path { path };
|
||||
/// Replaces symbolic `:workspace_roots` entries with concrete entries for
|
||||
/// each workspace root.
|
||||
pub fn materialize_project_roots_with_workspace_roots(
|
||||
mut self,
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
) -> Self {
|
||||
let mut entries = Vec::with_capacity(self.entries.len());
|
||||
for entry in self.entries {
|
||||
match entry.path {
|
||||
FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::ProjectRoots { subpath },
|
||||
} => {
|
||||
entries.extend(workspace_roots.iter().map(|root| FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: match subpath.as_ref() {
|
||||
Some(subpath) => AbsolutePathBuf::resolve_path_against_base(
|
||||
subpath,
|
||||
root.as_path(),
|
||||
),
|
||||
None => root.clone(),
|
||||
},
|
||||
},
|
||||
access: entry.access,
|
||||
}));
|
||||
}
|
||||
FileSystemPath::GlobPattern { pattern } => {
|
||||
if let Some(subpath) = parse_project_roots_glob_pattern(&pattern) {
|
||||
entries.extend(workspace_roots.iter().map(|root| FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: resolve_project_roots_glob_pattern(subpath, root),
|
||||
},
|
||||
access: entry.access,
|
||||
}));
|
||||
} else {
|
||||
entries.push(FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern { pattern },
|
||||
access: entry.access,
|
||||
});
|
||||
}
|
||||
}
|
||||
FileSystemPath::Path { path } => {
|
||||
entries.push(FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path },
|
||||
access: entry.access,
|
||||
});
|
||||
}
|
||||
FileSystemPath::Special { value } => {
|
||||
entries.push(FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special { value },
|
||||
access: entry.access,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
self.entries = entries;
|
||||
self
|
||||
}
|
||||
|
||||
/// Preserves symbolic `:workspace_roots` entries while also adding concrete
|
||||
/// entries for each provided workspace root.
|
||||
pub fn with_materialized_project_roots_for_workspace_roots(
|
||||
mut self,
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
) -> Self {
|
||||
let materialized = self
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(workspace_roots);
|
||||
for entry in materialized.entries {
|
||||
if !self.entries.contains(&entry) {
|
||||
self.entries.push(entry);
|
||||
}
|
||||
}
|
||||
self
|
||||
@@ -1209,6 +1300,18 @@ fn resolve_entry_path(
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_project_roots_glob_pattern(pattern: &str) -> Option<&Path> {
|
||||
pattern
|
||||
.strip_prefix(PROJECT_ROOTS_GLOB_PATTERN_PREFIX)
|
||||
.map(Path::new)
|
||||
}
|
||||
|
||||
fn resolve_project_roots_glob_pattern(subpath: &Path, root: &AbsolutePathBuf) -> String {
|
||||
AbsolutePathBuf::resolve_path_against_base(subpath, root.as_path())
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn resolve_candidate_path(path: &Path, cwd: &Path) -> Option<AbsolutePathBuf> {
|
||||
if path.is_absolute() {
|
||||
AbsolutePathBuf::from_absolute_path(path).ok()
|
||||
@@ -2750,6 +2853,115 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialize_project_roots_with_workspace_roots_expands_exact_and_glob_entries() {
|
||||
let temp_dir = TempDir::new().expect("tempdir");
|
||||
let first = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("first"))
|
||||
.expect("resolve first root");
|
||||
let second = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("second"))
|
||||
.expect("resolve second root");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::project_roots(Some(".git".into())),
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: project_roots_glob_pattern(Path::new("**/*.env")),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
|
||||
let actual =
|
||||
policy.materialize_project_roots_with_workspace_roots(&[first.clone(), second.clone()]);
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: first.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: second.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: first.join(".git"),
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: second.join(".git"),
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: AbsolutePathBuf::resolve_path_against_base(
|
||||
"**/*.env",
|
||||
first.as_path(),
|
||||
)
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: AbsolutePathBuf::resolve_path_against_base(
|
||||
"**/*.env",
|
||||
second.as_path(),
|
||||
)
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialize_project_roots_with_cwd_expands_symbolic_glob_entries() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: project_roots_glob_pattern(Path::new("**/*.env")),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
}]);
|
||||
|
||||
let actual = policy.materialize_project_roots_with_cwd(cwd.path());
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: AbsolutePathBuf::resolve_path_against_base("**/*.env", cwd.path())
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
}])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_additional_legacy_workspace_writable_roots_protects_metadata() {
|
||||
let temp_dir = TempDir::new().expect("tempdir");
|
||||
|
||||
@@ -67,7 +67,7 @@ pub(super) fn emit_project_config_warnings(app_event_tx: &AppEventSender, config
|
||||
|
||||
pub(super) fn emit_system_bwrap_warning(app_event_tx: &AppEventSender, config: &Config) {
|
||||
let Some(message) = crate::legacy_core::config::system_bwrap_warning(
|
||||
config.permissions.permission_profile.get(),
|
||||
config.permissions.permission_profile_constraint().get(),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -53,10 +53,12 @@ impl ChatWidget {
|
||||
);
|
||||
if let Err(err) = permission_sync {
|
||||
tracing::warn!(%err, "failed to sync permissions from SessionConfigured");
|
||||
self.config.permissions.permission_profile =
|
||||
Constrained::allow_only(session.permission_profile.clone());
|
||||
self.config.permissions.active_permission_profile =
|
||||
session.active_permission_profile.clone();
|
||||
self.config
|
||||
.permissions
|
||||
.replace_permission_profile_constraint_with_active_profile(
|
||||
Constrained::allow_only(session.permission_profile.clone()),
|
||||
session.active_permission_profile.clone(),
|
||||
);
|
||||
}
|
||||
self.config.approvals_reviewer = session.approvals_reviewer;
|
||||
self.status_line_project_root_name_cache = None;
|
||||
|
||||
@@ -902,7 +902,11 @@ fn permissions_display(config: &Config) -> String {
|
||||
}
|
||||
|
||||
let permission_profile = config.permissions.permission_profile();
|
||||
let summary = summarize_permission_profile(&permission_profile, config.cwd.as_path());
|
||||
let summary = summarize_permission_profile(
|
||||
&permission_profile,
|
||||
&config.cwd,
|
||||
config.permissions.workspace_roots(),
|
||||
);
|
||||
if let Some(details) = summary.strip_prefix("read-only")
|
||||
&& !details.contains("(network access enabled)")
|
||||
{
|
||||
|
||||
@@ -15,17 +15,16 @@ use codex_protocol::ThreadId;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::ActivePermissionProfileModification;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_sandbox_summary::summarize_permission_profile;
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::style::Stylize;
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use url::Url;
|
||||
|
||||
@@ -257,6 +256,7 @@ impl StatusHistoryCell {
|
||||
) -> (Self, StatusHistoryHandle) {
|
||||
let approval_policy = AskForApproval::from(config.permissions.approval_policy.value());
|
||||
let permission_profile = config.permissions.permission_profile();
|
||||
let workspace_roots = config.permissions.workspace_roots();
|
||||
let mut config_entries = vec![
|
||||
("workdir", config.cwd.display().to_string()),
|
||||
("model", model_name.to_string()),
|
||||
@@ -267,7 +267,7 @@ impl StatusHistoryCell {
|
||||
),
|
||||
(
|
||||
"sandbox",
|
||||
summarize_permission_profile(&permission_profile, config.cwd.as_path()),
|
||||
summarize_permission_profile(&permission_profile, &config.cwd, workspace_roots),
|
||||
),
|
||||
];
|
||||
if config.model_provider.wire_api == WireApi::Responses {
|
||||
@@ -291,7 +291,8 @@ impl StatusHistoryCell {
|
||||
.map(|(_, v)| v.clone())
|
||||
.unwrap_or_else(|| "<unknown>".to_string());
|
||||
let active_permission_profile = config.permissions.active_permission_profile();
|
||||
let sandbox = status_permission_summary(&permission_profile, config.cwd.as_path());
|
||||
let sandbox = status_permission_summary(&permission_profile, &config.cwd, workspace_roots);
|
||||
let workspace_root_suffix = workspace_root_suffix(workspace_roots, &config.cwd);
|
||||
let approval = status_approval_label(approval_policy, config.approvals_reviewer, &approval);
|
||||
let permissions = status_permissions_label(
|
||||
active_permission_profile.as_ref(),
|
||||
@@ -299,6 +300,7 @@ impl StatusHistoryCell {
|
||||
approval_policy,
|
||||
&sandbox,
|
||||
&approval,
|
||||
workspace_root_suffix.as_deref(),
|
||||
);
|
||||
let model_provider = format_model_provider(config, runtime_model_provider_base_url);
|
||||
let account = compose_account_display(account_display);
|
||||
@@ -542,8 +544,12 @@ impl StatusHistoryCell {
|
||||
}
|
||||
}
|
||||
|
||||
fn status_permission_summary(permission_profile: &PermissionProfile, cwd: &Path) -> String {
|
||||
let summary = summarize_permission_profile(permission_profile, cwd);
|
||||
fn status_permission_summary(
|
||||
permission_profile: &PermissionProfile,
|
||||
cwd: &AbsolutePathBuf,
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
) -> String {
|
||||
let summary = summarize_permission_profile(permission_profile, cwd, workspace_roots);
|
||||
if let Some(details) = summary.strip_prefix("read-only") {
|
||||
if details.contains("(network access enabled)") {
|
||||
return "read-only with network access".to_string();
|
||||
@@ -562,33 +568,31 @@ fn status_permission_summary(permission_profile: &PermissionProfile, cwd: &Path)
|
||||
summary
|
||||
}
|
||||
|
||||
fn workspace_root_suffix(
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
cwd: &AbsolutePathBuf,
|
||||
) -> Option<String> {
|
||||
let extra_roots = workspace_roots
|
||||
.iter()
|
||||
.filter(|root| *root != cwd)
|
||||
.map(|root| root.to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
if extra_roots.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(format!(" [{}]", extra_roots.join(", ")))
|
||||
}
|
||||
}
|
||||
|
||||
fn status_permissions_label(
|
||||
active_permission_profile: Option<&ActivePermissionProfile>,
|
||||
permission_profile: &PermissionProfile,
|
||||
approval_policy: AskForApproval,
|
||||
sandbox: &str,
|
||||
approval: &str,
|
||||
workspace_root_suffix: Option<&str>,
|
||||
) -> String {
|
||||
let active_id = active_permission_profile.map(|active| active.id.as_str());
|
||||
let writable_root_modifications = active_permission_profile
|
||||
.map(|active| {
|
||||
active
|
||||
.modifications
|
||||
.iter()
|
||||
.filter(|modification| {
|
||||
matches!(
|
||||
modification,
|
||||
ActivePermissionProfileModification::AdditionalWritableRoot { .. }
|
||||
)
|
||||
})
|
||||
.count()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let modification_suffix = match writable_root_modifications {
|
||||
0 => String::new(),
|
||||
1 => " + 1 writable root".to_string(),
|
||||
count => format!(" + {count} writable roots"),
|
||||
};
|
||||
match active_id {
|
||||
Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY) => {
|
||||
let label = if sandbox == "read-only with network access" {
|
||||
@@ -596,12 +600,20 @@ fn status_permissions_label(
|
||||
} else {
|
||||
"Read Only"
|
||||
};
|
||||
return format!("{label}{modification_suffix} ({approval})");
|
||||
return format!("{label} ({approval})");
|
||||
}
|
||||
Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE) => match sandbox {
|
||||
"workspace" => return format!("Workspace{modification_suffix} ({approval})"),
|
||||
"workspace" => {
|
||||
return format!(
|
||||
"Workspace{} ({approval})",
|
||||
workspace_root_suffix.unwrap_or("")
|
||||
);
|
||||
}
|
||||
"workspace with network access" => {
|
||||
return format!("Workspace with network access{modification_suffix} ({approval})");
|
||||
return format!(
|
||||
"Workspace with network access{} ({approval})",
|
||||
workspace_root_suffix.unwrap_or("")
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
@@ -614,7 +626,10 @@ fn status_permissions_label(
|
||||
format!("No Sandbox ({approval})")
|
||||
};
|
||||
}
|
||||
Some(id) => return format!("Profile {id}{modification_suffix} ({sandbox}, {approval})"),
|
||||
Some(id) => {
|
||||
let sandbox = decorate_workspace_sandbox_label(sandbox, workspace_root_suffix);
|
||||
return format!("Profile {id} ({sandbox}, {approval})");
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
@@ -622,16 +637,27 @@ fn status_permissions_label(
|
||||
return format!("Read Only ({approval})");
|
||||
}
|
||||
if approval_policy == AskForApproval::OnRequest && sandbox == "workspace" {
|
||||
return format!("Workspace ({approval})");
|
||||
return format!(
|
||||
"Workspace{} ({approval})",
|
||||
workspace_root_suffix.unwrap_or("")
|
||||
);
|
||||
}
|
||||
if approval_policy == AskForApproval::Never
|
||||
&& permission_profile == &PermissionProfile::Disabled
|
||||
{
|
||||
return "Full Access".to_string();
|
||||
}
|
||||
let sandbox = decorate_workspace_sandbox_label(sandbox, workspace_root_suffix);
|
||||
format!("Custom ({sandbox}, {approval})")
|
||||
}
|
||||
|
||||
fn decorate_workspace_sandbox_label(sandbox: &str, workspace_root_suffix: Option<&str>) -> String {
|
||||
match workspace_root_suffix {
|
||||
Some(suffix) if sandbox.starts_with("workspace") => format!("{sandbox}{suffix}"),
|
||||
_ => sandbox.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn status_approval_label(
|
||||
approval_policy: AskForApproval,
|
||||
approvals_reviewer: ApprovalsReviewer,
|
||||
|
||||
@@ -31,12 +31,12 @@ use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::ActivePermissionProfileModification;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use insta::assert_snapshot;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::prelude::*;
|
||||
@@ -97,6 +97,14 @@ async fn test_config(temp_home: &TempDir) -> Config {
|
||||
config
|
||||
}
|
||||
|
||||
fn set_workspace_cwd(config: &mut Config, cwd: AbsolutePathBuf) {
|
||||
config.cwd = cwd.clone();
|
||||
config.workspace_roots = vec![cwd];
|
||||
config
|
||||
.permissions
|
||||
.set_workspace_roots(config.workspace_roots.clone());
|
||||
}
|
||||
|
||||
fn test_status_account_display() -> Option<StatusAccountDisplay> {
|
||||
None
|
||||
}
|
||||
@@ -195,7 +203,7 @@ async fn status_snapshot_includes_reasoning_details() {
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.model_provider_id = "openai".to_string();
|
||||
config.model_reasoning_summary = Some(ReasoningSummary::Detailed);
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(PermissionProfile::workspace_write())
|
||||
@@ -273,7 +281,7 @@ async fn status_permissions_non_default_workspace_write_uses_workspace_label() {
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest.to_core())
|
||||
.expect("set approval policy");
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(app_server_workspace_write_profile(
|
||||
@@ -332,20 +340,15 @@ async fn status_permissions_read_only_profile_shows_additional_writable_roots()
|
||||
&file_system_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
),
|
||||
Some(
|
||||
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_READ_ONLY)
|
||||
.with_modifications(vec![
|
||||
ActivePermissionProfileModification::AdditionalWritableRoot {
|
||||
path: extra_root,
|
||||
},
|
||||
]),
|
||||
),
|
||||
Some(ActivePermissionProfile::new(
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
|
||||
)),
|
||||
)
|
||||
.expect("set permission profile");
|
||||
|
||||
assert_eq!(
|
||||
permissions_text_for(&config).as_deref(),
|
||||
Some("Read Only + 1 writable root (on-request)")
|
||||
Some("Read Only (on-request)")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -419,20 +422,44 @@ async fn status_permissions_named_profile_shows_additional_writable_roots() {
|
||||
/*exclude_tmpdir_env_var*/ false,
|
||||
/*exclude_slash_tmp*/ false,
|
||||
),
|
||||
Some(
|
||||
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE)
|
||||
.with_modifications(vec![
|
||||
ActivePermissionProfileModification::AdditionalWritableRoot {
|
||||
path: extra_root,
|
||||
},
|
||||
]),
|
||||
),
|
||||
Some(ActivePermissionProfile::new(
|
||||
BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
|
||||
)),
|
||||
)
|
||||
.expect("set permission profile");
|
||||
|
||||
assert_eq!(
|
||||
permissions_text_for(&config).as_deref(),
|
||||
Some("Workspace + 1 writable root (on-request)")
|
||||
Some("Workspace (on-request)")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_permissions_workspace_roots_show_additional_directories() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
config
|
||||
.permissions
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest.to_core())
|
||||
.expect("set approval policy");
|
||||
let extra_root = test_path_buf("/workspace/extra").abs();
|
||||
config.workspace_roots = vec![config.cwd.clone(), extra_root.clone()];
|
||||
config
|
||||
.permissions
|
||||
.set_workspace_roots(config.workspace_roots.clone());
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile_with_active_profile(
|
||||
PermissionProfile::workspace_write(),
|
||||
Some(ActivePermissionProfile::new(":workspace")),
|
||||
)
|
||||
.expect("set permission profile");
|
||||
|
||||
assert_eq!(
|
||||
permissions_text_for(&config),
|
||||
Some(format!("Workspace [{}] (on-request)", extra_root.display()))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -489,7 +516,7 @@ async fn status_snapshot_shows_active_user_defined_profile() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile_with_active_profile(
|
||||
@@ -586,7 +613,7 @@ async fn status_snapshot_shows_auto_review_permissions() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
|
||||
config
|
||||
.permissions
|
||||
@@ -689,7 +716,7 @@ async fn status_snapshot_includes_forked_from() {
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.model_provider_id = "openai".to_string();
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -743,7 +770,7 @@ async fn status_snapshot_includes_monthly_limit() {
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.model_provider_id = "openai".to_string();
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -1001,7 +1028,7 @@ async fn status_card_token_usage_excludes_cached_tokens() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -1049,7 +1076,7 @@ async fn status_snapshot_truncates_in_narrow_terminal() {
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.model_provider_id = "openai".to_string();
|
||||
config.model_reasoning_summary = Some(ReasoningSummary::Detailed);
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -1113,7 +1140,7 @@ async fn status_snapshot_shows_missing_limits_message() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -1161,7 +1188,7 @@ async fn status_snapshot_uses_default_reasoning_when_config_empty() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -1212,7 +1239,7 @@ async fn status_snapshot_shows_refreshing_limits_notice() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 500,
|
||||
@@ -1277,7 +1304,7 @@ async fn status_snapshot_includes_credits_and_limits() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -1347,7 +1374,7 @@ async fn status_snapshot_shows_unavailable_limits_message() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -1405,7 +1432,7 @@ async fn status_snapshot_treats_refreshing_empty_limits_as_unavailable() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 500,
|
||||
@@ -1463,7 +1490,7 @@ async fn status_snapshot_shows_stale_limits_message() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -1530,7 +1557,7 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs());
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
|
||||
@@ -11,6 +11,7 @@ workspace = true
|
||||
codex-core = { workspace = true }
|
||||
codex-model-provider-info = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::NetworkAccess;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use std::path::Path;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String {
|
||||
match sandbox_policy {
|
||||
@@ -51,8 +51,39 @@ pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn summarize_permission_profile(permission_profile: &PermissionProfile, cwd: &Path) -> String {
|
||||
match permission_profile.to_legacy_sandbox_policy(cwd) {
|
||||
pub fn summarize_permission_profile(
|
||||
permission_profile: &PermissionProfile,
|
||||
cwd: &AbsolutePathBuf,
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
) -> String {
|
||||
match permission_profile.to_legacy_sandbox_policy(cwd.as_path()) {
|
||||
Ok(SandboxPolicy::WorkspaceWrite {
|
||||
network_access,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
..
|
||||
}) => {
|
||||
let mut summary = "workspace-write".to_string();
|
||||
let mut writable_entries = vec!["workdir".to_string()];
|
||||
if !exclude_slash_tmp {
|
||||
writable_entries.push("/tmp".to_string());
|
||||
}
|
||||
if !exclude_tmpdir_env_var {
|
||||
writable_entries.push("$TMPDIR".to_string());
|
||||
}
|
||||
writable_entries.extend(
|
||||
workspace_roots
|
||||
.iter()
|
||||
.filter(|root| *root != cwd)
|
||||
.map(|root| root.to_string_lossy().to_string()),
|
||||
);
|
||||
|
||||
summary.push_str(&format!(" [{}]", writable_entries.join(", ")));
|
||||
if network_access {
|
||||
summary.push_str(" (network access enabled)");
|
||||
}
|
||||
summary
|
||||
}
|
||||
Ok(policy) => summarize_sandbox_policy(&policy),
|
||||
Err(_) => {
|
||||
if permission_profile.network_sandbox_policy().is_enabled() {
|
||||
@@ -67,6 +98,7 @@ pub fn summarize_permission_profile(permission_profile: &PermissionProfile, cwd:
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -112,4 +144,39 @@ mod tests {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_profile_summary_uses_runtime_workspace_roots_and_hides_internal_writes() {
|
||||
let cwd =
|
||||
AbsolutePathBuf::try_from(if cfg!(windows) { "C:\\repo" } else { "/repo" }).unwrap();
|
||||
let extra_root = AbsolutePathBuf::try_from(if cfg!(windows) {
|
||||
"C:\\repo-extra"
|
||||
} else {
|
||||
"/repo-extra"
|
||||
})
|
||||
.unwrap();
|
||||
let hidden_root = AbsolutePathBuf::try_from(if cfg!(windows) {
|
||||
"C:\\Users\\test\\.codex\\memories"
|
||||
} else {
|
||||
"/Users/test/.codex/memories"
|
||||
})
|
||||
.unwrap();
|
||||
let profile = PermissionProfile::workspace_write_with(
|
||||
std::slice::from_ref(&hidden_root),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
/*exclude_tmpdir_env_var*/ false,
|
||||
/*exclude_slash_tmp*/ false,
|
||||
);
|
||||
|
||||
let summary =
|
||||
summarize_permission_profile(&profile, &cwd, &[cwd.clone(), extra_root.clone()]);
|
||||
|
||||
assert_eq!(
|
||||
summary,
|
||||
format!(
|
||||
"workspace-write [workdir, /tmp, $TMPDIR, {}]",
|
||||
extra_root.display()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user