diff --git a/codex-rs/core-api/src/lib.rs b/codex-rs/core-api/src/lib.rs index effe8c697f..a97099f0b6 100644 --- a/codex-rs/core-api/src/lib.rs +++ b/codex-rs/core-api/src/lib.rs @@ -110,6 +110,7 @@ pub use codex_protocol::dynamic_tools::DynamicToolNamespaceTool; pub use codex_protocol::dynamic_tools::DynamicToolSpec; pub use codex_protocol::error::Result as CodexResult; pub use codex_protocol::models::PermissionProfile; +pub use codex_protocol::models::PermissionProfileSnapshot; pub use codex_protocol::openai_models::ModelPreset; pub use codex_protocol::protocol::AskForApproval; pub use codex_protocol::protocol::EnvironmentConfigState; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 07f1bdfbeb..2361fea0cd 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -106,6 +106,7 @@ use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::ActivePermissionProfile; use codex_protocol::models::BaseInstructionsProvenance; use codex_protocol::models::PermissionProfile; +pub use codex_protocol::models::PermissionProfileSnapshot; use codex_protocol::models::SandboxEnforcement; use codex_protocol::openai_models::ModelMessages; use codex_protocol::openai_models::ModelsResponse; @@ -179,7 +180,6 @@ use permission_profile_catalog::permission_profile_catalog_from_permissions; use permission_profile_catalog::permission_profile_is_allowed; use permission_profile_catalog::validate_permission_profile_for_deny_read; pub(crate) use permissions::is_builtin_permission_profile_name; -pub use resolved_permission_profile::PermissionProfileSnapshot; pub(crate) use resolved_permission_profile::PermissionProfileState; const DEFAULT_IGNORE_LARGE_UNTRACKED_DIRS: i64 = 200; @@ -361,10 +361,8 @@ impl Permissions { snapshot: PermissionProfileSnapshot, ) -> ConstraintResult<()> { let permission_profile = Constrained::allow_only(snapshot.permission_profile().clone()); - self.permission_profile_state = PermissionProfileState::from_constrained_resolved( - permission_profile, - snapshot.into_resolved_permission_profile(), - )?; + self.permission_profile_state = + PermissionProfileState::from_constrained_snapshot(permission_profile, snapshot)?; Ok(()) } diff --git a/codex-rs/core/src/config/resolved_permission_profile.rs b/codex-rs/core/src/config/resolved_permission_profile.rs index 22f0357e56..ba61c765b2 100644 --- a/codex-rs/core/src/config/resolved_permission_profile.rs +++ b/codex-rs/core/src/config/resolved_permission_profile.rs @@ -1,241 +1,22 @@ use codex_config::Constrained; use codex_config::ConstraintResult; use codex_protocol::models::ActivePermissionProfile; -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::models::PermissionProfileSnapshot; use codex_utils_absolute_path::AbsolutePathBuf; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum BuiltInPermissionProfileId { - ReadOnly, - Workspace, - DangerFullAccess, -} - -impl BuiltInPermissionProfileId { - fn from_str(id: &str) -> Option { - match id { - BUILT_IN_PERMISSION_PROFILE_READ_ONLY => Some(Self::ReadOnly), - BUILT_IN_PERMISSION_PROFILE_WORKSPACE => Some(Self::Workspace), - BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS => Some(Self::DangerFullAccess), - _ => None, - } - } - - fn as_str(self) -> &'static str { - match self { - Self::ReadOnly => BUILT_IN_PERMISSION_PROFILE_READ_ONLY, - Self::Workspace => BUILT_IN_PERMISSION_PROFILE_WORKSPACE, - Self::DangerFullAccess => BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum ResolvedPermissionProfile { - Legacy(LegacyPermissionProfile), - BuiltIn(BuiltInPermissionProfile), - Named(NamedPermissionProfile), -} - -/// Trusted snapshot of a resolved permission profile. -/// -/// This is a bridge for already-resolved session/config state. It keeps the -/// concrete `PermissionProfile`, optional active profile id, and -/// profile-defined workspace roots together so `Permissions` can validate and -/// install them atomically. It is not a resolver: callers that are handling -/// user-selected profile ids should resolve those ids through config instead -/// of constructing this type directly. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PermissionProfileSnapshot { - resolved_permission_profile: ResolvedPermissionProfile, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct LegacyPermissionProfile { - permission_profile: PermissionProfile, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct BuiltInPermissionProfile { - id: BuiltInPermissionProfileId, - extends: Option, - permission_profile: PermissionProfile, - profile_workspace_roots: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct NamedPermissionProfile { - id: String, - extends: Option, - permission_profile: PermissionProfile, - profile_workspace_roots: Vec, -} - -impl ResolvedPermissionProfile { - pub(crate) fn from_active_profile( - permission_profile: PermissionProfile, - active_permission_profile: Option, - profile_workspace_roots: Vec, - ) -> Self { - let Some(active_permission_profile) = active_permission_profile else { - return Self::legacy(permission_profile); - }; - - let ActivePermissionProfile { id, extends } = active_permission_profile; - if let Some(built_in_id) = BuiltInPermissionProfileId::from_str(&id) { - Self::BuiltIn(BuiltInPermissionProfile { - id: built_in_id, - extends, - permission_profile, - profile_workspace_roots, - }) - } else { - Self::Named(NamedPermissionProfile { - id, - extends, - permission_profile, - profile_workspace_roots, - }) - } - } - - pub(crate) fn legacy(permission_profile: PermissionProfile) -> Self { - Self::Legacy(LegacyPermissionProfile { permission_profile }) - } - - pub(crate) fn permission_profile(&self) -> &PermissionProfile { - match self { - Self::Legacy(profile) => &profile.permission_profile, - Self::BuiltIn(profile) => &profile.permission_profile, - Self::Named(profile) => &profile.permission_profile, - } - } - - pub(crate) fn active_permission_profile(&self) -> Option { - match self { - Self::Legacy(_) => None, - Self::BuiltIn(profile) => Some(ActivePermissionProfile { - id: profile.id.as_str().to_string(), - extends: profile.extends.clone(), - }), - Self::Named(profile) => Some(ActivePermissionProfile { - id: profile.id.clone(), - extends: profile.extends.clone(), - }), - } - } - - pub(crate) fn profile_workspace_roots(&self) -> &[AbsolutePathBuf] { - match self { - Self::Legacy(_) => &[], - Self::BuiltIn(profile) => &profile.profile_workspace_roots, - Self::Named(profile) => &profile.profile_workspace_roots, - } - } -} - -impl PermissionProfileSnapshot { - /// Create a snapshot with no active profile id. - /// - /// Prefer this only for legacy data or local overrides that genuinely do - /// not have a named/built-in profile identity. Using this for a built-in or - /// named profile will intentionally clear the active profile metadata. - pub fn legacy(permission_profile: PermissionProfile) -> Self { - Self { - resolved_permission_profile: ResolvedPermissionProfile::legacy(permission_profile), - } - } - - /// Create a snapshot for a known active profile id. - /// - /// Use this only after a trusted caller has already resolved the active id - /// to the supplied concrete `PermissionProfile`. This constructor does not - /// verify that the id and profile match; `Permissions` will still enforce - /// configured permission constraints when the snapshot is installed. - pub fn active( - permission_profile: PermissionProfile, - active_permission_profile: ActivePermissionProfile, - ) -> Self { - Self::active_with_profile_workspace_roots( - permission_profile, - active_permission_profile, - Vec::new(), - ) - } - - /// Create a snapshot for a known active profile id with profile roots. - /// - /// As with `active`, the caller is responsible for passing the concrete - /// profile and active id that were resolved together. Use this variant when - /// the selected profile declared workspace roots that should remain - /// distinct from turn-scoped runtime workspace roots. - pub fn active_with_profile_workspace_roots( - permission_profile: PermissionProfile, - active_permission_profile: ActivePermissionProfile, - profile_workspace_roots: Vec, - ) -> Self { - Self { - resolved_permission_profile: ResolvedPermissionProfile::from_active_profile( - permission_profile, - Some(active_permission_profile), - profile_workspace_roots, - ), - } - } - - /// Reconstruct a trusted snapshot from session state. - /// - /// This is intended for session responses emitted by core, where the - /// concrete profile and active profile id were captured together. Avoid - /// using this as a shortcut for arbitrary user input because mismatched - /// arguments can still misrepresent the active profile identity. - pub fn from_session_snapshot( - permission_profile: PermissionProfile, - active_permission_profile: Option, - ) -> Self { - match active_permission_profile { - Some(active_permission_profile) => { - Self::active(permission_profile, active_permission_profile) - } - None => Self::legacy(permission_profile), - } - } - - /// Borrow the concrete permission profile captured in this snapshot. - pub fn permission_profile(&self) -> &PermissionProfile { - self.resolved_permission_profile.permission_profile() - } - - /// Return the active profile id captured in this snapshot, if any. - pub fn active_permission_profile(&self) -> Option { - self.resolved_permission_profile.active_permission_profile() - } - - /// Borrow profile-declared workspace roots captured in this snapshot. - pub fn profile_workspace_roots(&self) -> &[AbsolutePathBuf] { - self.resolved_permission_profile.profile_workspace_roots() - } - - pub(crate) fn into_resolved_permission_profile(self) -> ResolvedPermissionProfile { - self.resolved_permission_profile - } -} - #[derive(Debug, Clone, PartialEq)] pub(crate) struct PermissionProfileState { - resolved_permission_profile: Constrained, + permission_profile: Constrained, } impl PermissionProfileState { pub(crate) fn from_constrained_legacy( constrained_permission_profile: Constrained, ) -> ConstraintResult { - let resolved = - ResolvedPermissionProfile::legacy(constrained_permission_profile.get().clone()); - Self::from_constrained_resolved(constrained_permission_profile, resolved) + let snapshot = + PermissionProfileSnapshot::legacy(constrained_permission_profile.get().clone()); + Self::from_constrained_snapshot(constrained_permission_profile, snapshot) } pub(crate) fn from_constrained_active_profile( @@ -243,73 +24,66 @@ impl PermissionProfileState { active_permission_profile: Option, profile_workspace_roots: Vec, ) -> ConstraintResult { - let resolved = ResolvedPermissionProfile::from_active_profile( - constrained_permission_profile.get().clone(), - active_permission_profile, - profile_workspace_roots, - ); - Self::from_constrained_resolved(constrained_permission_profile, resolved) + let permission_profile = constrained_permission_profile.get().clone(); + let snapshot = match active_permission_profile { + Some(active_permission_profile) => { + PermissionProfileSnapshot::active_with_profile_workspace_roots( + permission_profile, + active_permission_profile, + profile_workspace_roots, + ) + } + None => PermissionProfileSnapshot::legacy(permission_profile), + }; + Self::from_constrained_snapshot(constrained_permission_profile, snapshot) } - pub(crate) fn from_constrained_resolved( + pub(crate) fn from_constrained_snapshot( constrained_permission_profile: Constrained, - resolved_permission_profile: ResolvedPermissionProfile, + permission_profile: PermissionProfileSnapshot, ) -> ConstraintResult { - let permission_profile_constraint = constrained_permission_profile; - let resolved_permission_profile = Constrained::new( - resolved_permission_profile, - move |candidate: &ResolvedPermissionProfile| { - permission_profile_constraint.can_set(candidate.permission_profile()) - }, - )?; - Ok(Self { - resolved_permission_profile, - }) + let permission_profile = Constrained::new(permission_profile, move |candidate| { + constrained_permission_profile.can_set(candidate.permission_profile()) + })?; + Ok(Self { permission_profile }) } pub(crate) fn permission_profile(&self) -> &PermissionProfile { - self.resolved_permission_profile.get().permission_profile() + self.permission_profile.get().permission_profile() } pub(crate) fn snapshot(&self) -> PermissionProfileSnapshot { - PermissionProfileSnapshot { - resolved_permission_profile: self.resolved_permission_profile.get().clone(), - } + self.permission_profile.get().clone() } pub(crate) fn active_permission_profile(&self) -> Option { - self.resolved_permission_profile - .get() - .active_permission_profile() + self.permission_profile.get().active_permission_profile() } pub(crate) fn profile_workspace_roots(&self) -> &[AbsolutePathBuf] { - self.resolved_permission_profile - .get() - .profile_workspace_roots() + self.permission_profile.get().profile_workspace_roots() } pub(crate) fn can_set_legacy_permission_profile( &self, permission_profile: &PermissionProfile, ) -> ConstraintResult<()> { - let candidate = ResolvedPermissionProfile::legacy(permission_profile.clone()); - self.resolved_permission_profile.can_set(&candidate) + let candidate = PermissionProfileSnapshot::legacy(permission_profile.clone()); + self.permission_profile.can_set(&candidate) } pub(crate) fn set_legacy_permission_profile( &mut self, permission_profile: PermissionProfile, ) -> ConstraintResult<()> { - self.resolved_permission_profile - .set(ResolvedPermissionProfile::legacy(permission_profile)) + self.permission_profile + .set(PermissionProfileSnapshot::legacy(permission_profile)) } pub(crate) fn set_permission_profile_snapshot( &mut self, snapshot: PermissionProfileSnapshot, ) -> ConstraintResult<()> { - self.resolved_permission_profile - .set(snapshot.into_resolved_permission_profile()) + self.permission_profile.set(snapshot) } } diff --git a/codex-rs/protocol/src/lib.rs b/codex-rs/protocol/src/lib.rs index 7bb02d316b..5608f5d8ea 100644 --- a/codex-rs/protocol/src/lib.rs +++ b/codex-rs/protocol/src/lib.rs @@ -30,6 +30,7 @@ pub mod network_policy; pub mod num_format; pub mod openai_models; pub mod parse_command; +mod permission_profile_snapshot; pub mod permissions; pub mod plan_tool; pub mod protocol; diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 5d10a56f3a..dad99f2dc4 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -33,6 +33,7 @@ mod executed_tool_calls; pub use crate::local_media::MAX_PROMPT_AUDIO_INPUT_BYTES; pub use crate::local_media::snapshot_local_user_input; +pub use crate::permission_profile_snapshot::PermissionProfileSnapshot; pub use executed_tool_calls::ExecutedToolCall; pub use executed_tool_calls::ExecutedToolCallArguments; pub use executed_tool_calls::ExecutedToolCallTruncation; diff --git a/codex-rs/protocol/src/permission_profile_snapshot.rs b/codex-rs/protocol/src/permission_profile_snapshot.rs new file mode 100644 index 0000000000..0f97cc202d --- /dev/null +++ b/codex-rs/protocol/src/permission_profile_snapshot.rs @@ -0,0 +1,86 @@ +use codex_utils_absolute_path::AbsolutePathBuf; + +use crate::models::ActivePermissionProfile; +use crate::models::PermissionProfile; + +/// Trusted snapshot of a resolved permission profile. +/// +/// Keeps the concrete permissions, optional active profile identity, and +/// profile-defined workspace roots together for atomic installation. Callers +/// handling user-selected profile ids must resolve those ids before +/// constructing a snapshot. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PermissionProfileSnapshot { + permission_profile: PermissionProfile, + active_permission_profile: Option, + profile_workspace_roots: Vec, +} + +impl PermissionProfileSnapshot { + /// Create a snapshot with no active profile identity. + /// + /// Use this only for legacy data or local overrides that genuinely do not + /// have a named or built-in profile identity. + pub fn legacy(permission_profile: PermissionProfile) -> Self { + Self { + permission_profile, + active_permission_profile: None, + profile_workspace_roots: Vec::new(), + } + } + + /// Create a snapshot for an already-resolved active profile. + pub fn active( + permission_profile: PermissionProfile, + active_permission_profile: ActivePermissionProfile, + ) -> Self { + Self::active_with_profile_workspace_roots( + permission_profile, + active_permission_profile, + Vec::new(), + ) + } + + /// Create a snapshot for an active profile and its declared roots. + /// + /// Profile roots remain distinct from turn-scoped runtime workspace roots. + pub fn active_with_profile_workspace_roots( + permission_profile: PermissionProfile, + active_permission_profile: ActivePermissionProfile, + profile_workspace_roots: Vec, + ) -> Self { + Self { + permission_profile, + active_permission_profile: Some(active_permission_profile), + profile_workspace_roots, + } + } + + /// Reconstruct a trusted snapshot from already-resolved session state. + pub fn from_session_snapshot( + permission_profile: PermissionProfile, + active_permission_profile: Option, + ) -> Self { + match active_permission_profile { + Some(active_permission_profile) => { + Self::active(permission_profile, active_permission_profile) + } + None => Self::legacy(permission_profile), + } + } + + /// Borrow the concrete permissions captured in this snapshot. + pub fn permission_profile(&self) -> &PermissionProfile { + &self.permission_profile + } + + /// Return the active profile identity captured in this snapshot, if any. + pub fn active_permission_profile(&self) -> Option { + self.active_permission_profile.clone() + } + + /// Borrow profile-declared workspace roots captured in this snapshot. + pub fn profile_workspace_roots(&self) -> &[AbsolutePathBuf] { + &self.profile_workspace_roots + } +}