Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codex-rs/app-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ pub async fn run_main_with_transport_options(
});
}
if let Some(warning) =
codex_core::config::system_bwrap_warning(config.permissions.permission_profile().get())
codex_core::config::system_bwrap_warning(config.permissions.permission_profile())
{
config_warnings.push(ConfigWarningNotification {
summary: warning,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ 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(),
/*policy_decider*/ None,
/*blocked_request_observer*/ None,
managed_network_requirements_enabled,
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/cli/src/debug_sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
/*policy_decider*/ None,
/*blocked_request_observer*/ None,
managed_network_requirements_enabled,
Expand Down
9 changes: 8 additions & 1 deletion codex-rs/codex-mcp/src/connection_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,21 @@ impl McpConnectionManager {
pub fn new_uninitialized(
approval_policy: &Constrained<AskForApproval>,
permission_profile: &Constrained<PermissionProfile>,
) -> Self {
Self::new_uninitialized_with_permission_profile(approval_policy, permission_profile.get())
}

pub fn new_uninitialized_with_permission_profile(
approval_policy: &Constrained<AskForApproval>,
permission_profile: &PermissionProfile,
) -> Self {
Self {
clients: HashMap::new(),
server_metadata: HashMap::new(),
host_owned_codex_apps_enabled: false,
elicitation_requests: ElicitationRequestManager::new(
approval_policy.value(),
permission_profile.get().clone(),
permission_profile.clone(),
/*reviewer*/ None,
),
startup_cancellation_token: CancellationToken::new(),
Expand Down
38 changes: 24 additions & 14 deletions codex-rs/core/src/config/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ use std::path::Path;
use std::time::Duration;
use tempfile::TempDir;

fn active_permission_profile_state(
permission_profile: PermissionProfile,
profile_id: impl Into<String>,
) -> PermissionProfileState {
PermissionProfileState::from_constrained_active_profile(
Constrained::allow_any(permission_profile),
Some(ActivePermissionProfile::new(profile_id)),
Vec::new(),
)
.expect("active permission profile state should be valid")
}

fn stdio_mcp(command: &str) -> McpServerConfig {
McpServerConfig {
transport: McpServerTransportConfig::Stdio {
Expand Down Expand Up @@ -7541,12 +7553,10 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
model_provider: fixture.openai_provider.clone(),
permissions: Permissions {
approval_policy: Constrained::allow_any(AskForApproval::Never),
constrained_permissions_profile: Constrained::allow_any(
PermissionProfile::read_only()
),
active_permission_profile: Some(ActivePermissionProfile::new(
permission_profile_state: active_permission_profile_state(
PermissionProfile::read_only(),
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
)),
),
workspace_roots: vec![fixture.cwd()],
network: None,
allow_login_shell: true,
Expand Down Expand Up @@ -7993,10 +8003,10 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
model_provider: fixture.openai_custom_provider.clone(),
permissions: Permissions {
approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted),
constrained_permissions_profile: Constrained::allow_any(PermissionProfile::read_only()),
active_permission_profile: Some(ActivePermissionProfile::new(
permission_profile_state: active_permission_profile_state(
PermissionProfile::read_only(),
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
)),
),
workspace_roots: vec![fixture.cwd()],
network: None,
allow_login_shell: true,
Expand Down Expand Up @@ -8157,10 +8167,10 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
model_provider: fixture.openai_provider.clone(),
permissions: Permissions {
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
constrained_permissions_profile: Constrained::allow_any(PermissionProfile::read_only()),
active_permission_profile: Some(ActivePermissionProfile::new(
permission_profile_state: active_permission_profile_state(
PermissionProfile::read_only(),
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
)),
),
workspace_roots: vec![fixture.cwd()],
network: None,
allow_login_shell: true,
Expand Down Expand Up @@ -8306,10 +8316,10 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
model_provider: fixture.openai_provider.clone(),
permissions: Permissions {
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
constrained_permissions_profile: Constrained::allow_any(PermissionProfile::read_only()),
active_permission_profile: Some(ActivePermissionProfile::new(
permission_profile_state: active_permission_profile_state(
PermissionProfile::read_only(),
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
)),
),
workspace_roots: vec![fixture.cwd()],
network: None,
allow_login_shell: true,
Expand Down
155 changes: 104 additions & 51 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ mod managed_features;
mod network_proxy_spec;
mod otel;
mod permissions;
mod resolved_permission_profile;
#[cfg(test)]
mod schema;
pub use codex_config::ConfigLoadOptions;
Expand All @@ -148,6 +149,7 @@ pub use managed_features::ManagedFeatures;
pub use network_proxy_spec::NetworkProxySpec;
pub use network_proxy_spec::StartedNetworkProxy;
pub(crate) use permissions::resolve_permission_profile;
pub(crate) use resolved_permission_profile::PermissionProfileState;

const DEFAULT_IGNORE_LARGE_UNTRACKED_DIRS: i64 = 200;
const DEFAULT_IGNORE_LARGE_UNTRACKED_FILES: i64 = 10 * 1024 * 1024;
Expand Down Expand Up @@ -247,15 +249,11 @@ pub(crate) async fn test_config() -> Config {
pub struct Permissions {
/// Approval policy for executing commands.
pub approval_policy: Constrained<AskForApproval>,
/// Canonical constrained permissions profile before runtime workspace-root
/// materialization has been applied.
constrained_permissions_profile: Constrained<PermissionProfile>,
/// Named or implicit built-in profile selected by config, rather than an
/// ad-hoc override.
active_permission_profile: Option<ActivePermissionProfile>,
/// Constrained permission profile plus its selected profile identity, if
/// the profile came from a built-in or named config profile.
permission_profile_state: PermissionProfileState,
/// Thread-scoped runtime workspace roots. Symbolic `:workspace_roots`
/// entries in `constrained_permissions_profile` are materialized against
/// these roots.
/// entries in the permission profile are materialized against these roots.
workspace_roots: Vec<AbsolutePathBuf>,
/// Effective network configuration applied to all spawned processes.
pub network: Option<NetworkProxySpec>,
Expand Down Expand Up @@ -286,8 +284,10 @@ impl Permissions {
) -> Self {
Self {
approval_policy,
constrained_permissions_profile: permission_profile,
active_permission_profile: None,
permission_profile_state: PermissionProfileState::from_constrained_legacy(
permission_profile,
)
.expect("initial constrained permission profile should be valid"),
workspace_roots: Vec::new(),
network: None,
allow_login_shell: true,
Expand All @@ -297,21 +297,88 @@ impl Permissions {
}
}

/// Borrow the constrained canonical profile. This preserves the raw
/// symbolic `:workspace_roots` form for session/thread state.
pub fn permission_profile(&self) -> &Constrained<PermissionProfile> {
&self.constrained_permissions_profile
pub(crate) fn permission_profile_state(&self) -> &PermissionProfileState {
&self.permission_profile_state
}

pub(crate) fn set_permission_profile_state(
&mut self,
permission_profile_state: PermissionProfileState,
) {
self.permission_profile_state = permission_profile_state;
}

/// Apply a permission profile snapshot emitted by core session state.
///
/// This is a trusted-state bridge for consumers of `SessionConfigured`.
/// Config loading and app-server selection should resolve named profiles
/// through config instead of constructing this pair directly.
pub fn set_permission_profile_from_session_snapshot(
&mut self,
permission_profile: PermissionProfile,
active_permission_profile: Option<ActivePermissionProfile>,
) -> ConstraintResult<()> {
self.set_permission_profile_from_session_snapshot_with_profile_workspace_roots(
permission_profile,
active_permission_profile,
Vec::new(),
)
}

pub fn set_permission_profile_from_session_snapshot_with_profile_workspace_roots(
&mut self,
permission_profile: PermissionProfile,
active_permission_profile: Option<ActivePermissionProfile>,
profile_workspace_roots: Vec<AbsolutePathBuf>,
) -> ConstraintResult<()> {
self.permission_profile_state.set_active_permission_profile(
permission_profile,
active_permission_profile,
profile_workspace_roots,
)
}

/// Set the full constrained profile value and preserve the active profile
/// sidecar when the caller has already validated both together.
pub fn set_constrained_permission_profile_with_active_profile(
/// Replace the current permission constraints with a trusted session
/// snapshot. This is only for clients that must mirror core session state
/// after their local config constraints reject the snapshot.
pub fn replace_permission_profile_from_session_snapshot(
&mut self,
permission_profile: Constrained<PermissionProfile>,
active_permission_profile: Option<ActivePermissionProfile>,
) {
self.constrained_permissions_profile = permission_profile;
self.active_permission_profile = active_permission_profile;
) -> ConstraintResult<()> {
self.replace_permission_profile_from_session_snapshot_with_profile_workspace_roots(
permission_profile,
active_permission_profile,
Vec::new(),
)
}

pub fn replace_permission_profile_from_session_snapshot_with_profile_workspace_roots(
&mut self,
permission_profile: Constrained<PermissionProfile>,
active_permission_profile: Option<ActivePermissionProfile>,
profile_workspace_roots: Vec<AbsolutePathBuf>,
) -> ConstraintResult<()> {
self.permission_profile_state = PermissionProfileState::from_constrained_active_profile(
permission_profile,
active_permission_profile,
profile_workspace_roots,
)?;
Ok(())
}

/// Borrow the canonical profile before runtime workspace-root
/// materialization has been applied.
pub fn permission_profile(&self) -> &PermissionProfile {
self.permission_profile_state.permission_profile()
}

pub fn can_set_permission_profile(
&self,
permission_profile: &PermissionProfile,
) -> ConstraintResult<()> {
self.permission_profile_state
.can_set_legacy_permission_profile(permission_profile)
}

pub fn set_workspace_roots(&mut self, workspace_roots: Vec<AbsolutePathBuf>) {
Expand All @@ -329,8 +396,7 @@ impl Permissions {
}

fn materialized_permission_profile(&self) -> PermissionProfile {
self.constrained_permissions_profile
.get()
self.permission_profile()
.clone()
.materialize_project_roots_with_workspace_roots(&self.workspace_roots)
}
Expand All @@ -343,7 +409,7 @@ impl Permissions {

/// Named profile selected by config, if the current profile has one.
pub fn active_permission_profile(&self) -> Option<ActivePermissionProfile> {
self.active_permission_profile.clone()
self.permission_profile_state.active_permission_profile()
}

/// Effective filesystem sandbox policy derived from the canonical profile.
Expand All @@ -354,9 +420,7 @@ impl Permissions {

/// Effective network sandbox policy derived from the canonical profile.
pub fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
self.constrained_permissions_profile
.get()
.network_sandbox_policy()
self.permission_profile().network_sandbox_policy()
}

/// Legacy compatibility projection derived from the canonical profile.
Expand Down Expand Up @@ -386,8 +450,8 @@ impl Permissions {
&file_system_sandbox_policy,
network_sandbox_policy,
);
self.constrained_permissions_profile
.can_set(&permission_profile)
self.permission_profile_state
.can_set_legacy_permission_profile(&permission_profile)
}

/// Set permissions from a legacy sandbox policy and keep every permission
Expand Down Expand Up @@ -427,9 +491,8 @@ impl Permissions {
],
};

self.constrained_permissions_profile
.set(permission_profile)?;
self.active_permission_profile = None;
self.permission_profile_state
.set_legacy_permission_profile(permission_profile)?;
Ok(())
}

Expand All @@ -438,23 +501,8 @@ impl Permissions {
&mut self,
permission_profile: PermissionProfile,
) -> ConstraintResult<()> {
self.set_permission_profile_with_active_profile(
permission_profile,
/*active_permission_profile*/ None,
)
}

/// Set permissions from the canonical profile and record the named source
/// profile, if one is known.
pub fn set_permission_profile_with_active_profile(
&mut self,
permission_profile: PermissionProfile,
active_permission_profile: Option<ActivePermissionProfile>,
) -> ConstraintResult<()> {
self.constrained_permissions_profile
.set(permission_profile)?;
self.active_permission_profile = active_permission_profile;
Ok(())
self.permission_profile_state
.set_legacy_permission_profile(permission_profile)
}
}

Expand Down Expand Up @@ -3225,6 +3273,12 @@ impl Config {
.value
.set(effective_permission_profile)
.map_err(std::io::Error::from)?;
let permission_profile_state = PermissionProfileState::from_constrained_active_profile(
constrained_permission_profile.value,
active_permission_profile,
Vec::new(),
)
.map_err(std::io::Error::from)?;
let otel = otel::resolve_config(cfg.otel.unwrap_or_default(), &mut startup_warnings);
let config = Self {
model,
Expand All @@ -3240,8 +3294,7 @@ impl Config {
startup_warnings,
permissions: Permissions {
approval_policy: constrained_approval_policy.value,
constrained_permissions_profile: constrained_permission_profile.value,
active_permission_profile,
permission_profile_state,
workspace_roots,
network,
allow_login_shell,
Expand Down Expand Up @@ -3526,7 +3579,7 @@ impl Config {

pub fn managed_network_requirements_enabled(&self) -> bool {
!matches!(
self.permissions.permission_profile().get(),
self.permissions.permission_profile(),
PermissionProfile::Disabled
) && self
.config_layer_stack
Expand Down
Loading
Loading