Move permission profile snapshots into the protocol (#38651)

## What changed

- Define `PermissionProfileSnapshot` as a protocol model and re-export it from
  `core-api`.
- Store snapshots directly in core permission state while continuing to apply
  constraints to their concrete `PermissionProfile`.
- Preserve active profile identity and profile-declared workspace roots without
  the core-only resolved profile variants.

GitOrigin-RevId: cc5e03908e62054cde5de691faa47e471f2e1af5
This commit is contained in:
sayan-oai
2026-08-14 21:17:29 +00:00
committed by copyberry
parent ff6e7c77a3
commit c530bcd4cd
6 changed files with 124 additions and 263 deletions
+1
View File
@@ -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;
+3 -5
View File
@@ -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(())
}
@@ -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<Self> {
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<String>,
permission_profile: PermissionProfile,
profile_workspace_roots: Vec<AbsolutePathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NamedPermissionProfile {
id: String,
extends: Option<String>,
permission_profile: PermissionProfile,
profile_workspace_roots: Vec<AbsolutePathBuf>,
}
impl ResolvedPermissionProfile {
pub(crate) fn from_active_profile(
permission_profile: PermissionProfile,
active_permission_profile: Option<ActivePermissionProfile>,
profile_workspace_roots: Vec<AbsolutePathBuf>,
) -> 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<ActivePermissionProfile> {
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<AbsolutePathBuf>,
) -> 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<ActivePermissionProfile>,
) -> 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<ActivePermissionProfile> {
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<ResolvedPermissionProfile>,
permission_profile: Constrained<PermissionProfileSnapshot>,
}
impl PermissionProfileState {
pub(crate) fn from_constrained_legacy(
constrained_permission_profile: Constrained<PermissionProfile>,
) -> ConstraintResult<Self> {
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<ActivePermissionProfile>,
profile_workspace_roots: Vec<AbsolutePathBuf>,
) -> ConstraintResult<Self> {
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<PermissionProfile>,
resolved_permission_profile: ResolvedPermissionProfile,
permission_profile: PermissionProfileSnapshot,
) -> ConstraintResult<Self> {
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<ActivePermissionProfile> {
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)
}
}
+1
View File
@@ -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;
+1
View File
@@ -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;
@@ -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<ActivePermissionProfile>,
profile_workspace_roots: Vec<AbsolutePathBuf>,
}
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<AbsolutePathBuf>,
) -> 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<ActivePermissionProfile>,
) -> 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<ActivePermissionProfile> {
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
}
}