app-server: use permission ids and runtime workspace roots

This commit is contained in:
Michael Bolin
2026-05-14 18:06:33 -07:00
parent 033e494dd4
commit d46638770b
56 changed files with 799 additions and 602 deletions
+8
View File
@@ -59,6 +59,8 @@ pub struct ThreadConfigSnapshot {
pub permission_profile: PermissionProfile,
pub active_permission_profile: Option<ActivePermissionProfile>,
pub cwd: AbsolutePathBuf,
pub workspace_roots: Vec<AbsolutePathBuf>,
pub profile_workspace_roots: Vec<AbsolutePathBuf>,
pub ephemeral: bool,
pub reasoning_effort: Option<ReasoningEffort>,
pub personality: Option<Personality>,
@@ -82,6 +84,8 @@ impl ThreadConfigSnapshot {
#[derive(Clone, Default)]
pub struct CodexThreadTurnContextOverrides {
pub cwd: Option<PathBuf>,
pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub approval_policy: Option<AskForApproval>,
pub approvals_reviewer: Option<ApprovalsReviewer>,
pub sandbox_policy: Option<SandboxPolicy>,
@@ -258,6 +262,8 @@ impl CodexThread {
) -> ConstraintResult<()> {
let CodexThreadTurnContextOverrides {
cwd,
workspace_roots,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox_policy,
@@ -283,6 +289,8 @@ impl CodexThread {
let updates = SessionSettingsUpdate {
cwd,
workspace_roots,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox_policy,
+5
View File
@@ -1905,6 +1905,11 @@ async fn workspace_profile_applies_rules_to_runtime_and_profile_workspace_roots(
)
.await?;
assert_eq!(
config.effective_workspace_roots(),
vec![cwd.abs(), runtime_root.abs(), profile_root.abs()]
);
let policy = config.permissions.file_system_sandbox_policy();
for root in [cwd.abs(), runtime_root.abs(), profile_root.abs()] {
assert!(
+17 -1
View File
@@ -394,6 +394,10 @@ impl Permissions {
&self.workspace_roots
}
pub fn profile_workspace_roots(&self) -> &[AbsolutePathBuf] {
self.permission_profile_state.profile_workspace_roots()
}
fn materialized_permission_profile(&self) -> PermissionProfile {
self.permission_profile()
.clone()
@@ -1227,6 +1231,13 @@ impl Config {
Ok(())
}
pub fn effective_workspace_roots(&self) -> Vec<AbsolutePathBuf> {
let mut workspace_roots = self.workspace_roots.clone();
workspace_roots.extend(self.permissions.profile_workspace_roots().iter().cloned());
dedupe_absolute_paths(&mut workspace_roots);
workspace_roots
}
pub fn to_models_manager_config(&self) -> ModelsManagerConfig {
ModelsManagerConfig {
model_context_window: self.model_context_window,
@@ -2579,6 +2590,7 @@ impl Config {
permission_profile,
file_system_sandbox_policy,
mut active_permission_profile,
mut profile_workspace_roots,
) = if let Some(mut permission_profile) = permission_profile {
let (mut file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
@@ -2631,6 +2643,7 @@ impl Config {
permission_profile,
file_system_sandbox_policy,
None,
Vec::new(),
)
} else if profiles_are_active {
let default_permissions = default_permissions.unwrap_or_else(|| {
@@ -2721,6 +2734,7 @@ impl Config {
permission_profile,
file_system_sandbox_policy,
active_permission_profile,
configured_workspace_roots,
)
} else {
let configured_network_proxy_config = NetworkProxyConfig::default();
@@ -2779,6 +2793,7 @@ impl Config {
permission_profile,
file_system_sandbox_policy,
None,
Vec::new(),
)
};
if enable_network_proxy && permission_profile.network_sandbox_policy().is_enabled() {
@@ -3202,6 +3217,7 @@ impl Config {
// The selected profile no longer describes the effective
// permissions after requirements forced a fallback.
active_permission_profile = None;
profile_workspace_roots.clear();
}
apply_requirement_constrained_value(
"web_search_mode",
@@ -3275,7 +3291,7 @@ impl Config {
let permission_profile_state = PermissionProfileState::from_constrained_active_profile(
constrained_permission_profile.value,
active_permission_profile,
Vec::new(),
profile_workspace_roots,
)
.map_err(std::io::Error::from)?;
let otel = otel::resolve_config(cfg.otel.unwrap_or_default(), &mut startup_warnings);
@@ -132,6 +132,14 @@ impl ResolvedPermissionProfile {
}),
}
}
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,
}
}
}
#[derive(Debug, Clone, PartialEq)]
@@ -200,6 +208,12 @@ impl PermissionProfileState {
.active_permission_profile()
}
pub(crate) fn profile_workspace_roots(&self) -> &[AbsolutePathBuf] {
self.resolved_permission_profile
.get()
.profile_workspace_roots()
}
pub(crate) fn can_set_legacy_permission_profile(
&self,
permission_profile: &PermissionProfile,
+6
View File
@@ -146,6 +146,8 @@ pub(super) async fn user_input_or_turn_inner(
approval_policy: Some(approval_policy),
approvals_reviewer,
sandbox_policy: Some(sandbox_policy),
workspace_roots: None,
profile_workspace_roots: None,
permission_profile,
active_permission_profile: None,
windows_sandbox_level: None,
@@ -163,6 +165,8 @@ pub(super) async fn user_input_or_turn_inner(
}
Op::UserInputWithTurnContext {
cwd,
workspace_roots,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox_policy,
@@ -195,6 +199,8 @@ pub(super) async fn user_input_or_turn_inner(
items,
SessionSettingsUpdate {
cwd,
workspace_roots,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox_policy,
+1
View File
@@ -621,6 +621,7 @@ impl Codex {
permission_profile_state: session_permission_profile_state_from_config(&config)?,
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: environment_selections.to_selections(),
+24 -5
View File
@@ -63,9 +63,9 @@ pub(crate) struct SessionConfiguration {
/// When to escalate for approval for execution
pub(super) approval_policy: Constrained<AskForApproval>,
pub(super) approvals_reviewer: ApprovalsReviewer,
/// Permission profile state for the session. Keep the constrained profile
/// and selected profile id in sync by using the methods below instead of
/// mutating the fields independently.
/// Permission profile state for the session. Keep the constrained profile,
/// active profile id, and profile-defined workspace roots in sync by using
/// the methods below instead of mutating the fields independently.
pub(super) permission_profile_state: PermissionProfileState,
pub(super) windows_sandbox_level: WindowsSandboxLevel,
@@ -74,6 +74,9 @@ pub(crate) struct SessionConfiguration {
/// execution sandbox are resolved against this directory **instead** of
/// the process-wide current working directory.
pub(super) cwd: AbsolutePathBuf,
/// Thread-scoped runtime workspace roots for materializing symbolic
/// workspace permissions at session runtime.
pub(super) workspace_roots: Vec<AbsolutePathBuf>,
/// Directory containing all Codex state for this session.
pub(super) codex_home: AbsolutePathBuf,
/// Optional user-facing name for the thread, updated during the session.
@@ -107,13 +110,20 @@ impl SessionConfiguration {
}
pub(super) fn permission_profile(&self) -> PermissionProfile {
self.permission_profile_state.permission_profile().clone()
self.permission_profile_state
.permission_profile()
.clone()
.materialize_project_roots_with_workspace_roots(&self.workspace_roots)
}
pub(super) fn active_permission_profile(&self) -> Option<ActivePermissionProfile> {
self.permission_profile_state.active_permission_profile()
}
pub(super) fn profile_workspace_roots(&self) -> &[AbsolutePathBuf] {
self.permission_profile_state.profile_workspace_roots()
}
pub(super) fn apply_permission_profile_to_permissions(
&self,
permissions: &mut crate::config::Permissions,
@@ -164,6 +174,8 @@ impl SessionConfiguration {
permission_profile: self.permission_profile(),
active_permission_profile: self.active_permission_profile(),
cwd: self.cwd.clone(),
workspace_roots: self.workspace_roots.clone(),
profile_workspace_roots: self.profile_workspace_roots().to_vec(),
ephemeral: self.original_config_do_not_use.ephemeral,
reasoning_effort: self.collaboration_mode.reasoning_effort(),
personality: self.personality,
@@ -243,6 +255,9 @@ impl SessionConfiguration {
let cwd_changed = absolute_cwd.as_path() != self.cwd.as_path();
next_configuration.cwd = absolute_cwd;
if let Some(workspace_roots) = updates.workspace_roots.clone() {
next_configuration.workspace_roots = workspace_roots;
}
if let Some(permission_profile) = updates.permission_profile.clone() {
let active_permission_profile =
@@ -256,6 +271,7 @@ impl SessionConfiguration {
next_configuration.set_permission_profile_projection(
permission_profile,
active_permission_profile,
updates.profile_workspace_roots.clone().unwrap_or_default(),
Some(&current_file_system_sandbox_policy),
)?;
} else if let Some(sandbox_policy) = updates.sandbox_policy.clone() {
@@ -311,6 +327,7 @@ impl SessionConfiguration {
&mut self,
permission_profile: PermissionProfile,
active_permission_profile: Option<ActivePermissionProfile>,
profile_workspace_roots: Vec<AbsolutePathBuf>,
preserve_deny_reads_from: Option<&FileSystemSandboxPolicy>,
) -> ConstraintResult<()> {
let enforcement = permission_profile.enforcement();
@@ -329,7 +346,7 @@ impl SessionConfiguration {
self.permission_profile_state.set_active_permission_profile(
effective_permission_profile,
active_permission_profile,
Vec::new(),
profile_workspace_roots,
)
}
}
@@ -337,6 +354,8 @@ impl SessionConfiguration {
#[derive(Default, Clone)]
pub(crate) struct SessionSettingsUpdate {
pub(crate) cwd: Option<PathBuf>,
pub(crate) workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub(crate) profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub(crate) approval_policy: Option<AskForApproval>,
pub(crate) approvals_reviewer: Option<ApprovalsReviewer>,
pub(crate) sandbox_policy: Option<SandboxPolicy>,
+54
View File
@@ -36,6 +36,7 @@ use codex_protocol::account::PlanType as AccountPlanType;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::FunctionCallOutputBody;
@@ -2917,6 +2918,7 @@ async fn set_rate_limits_retains_previous_credits() {
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: Vec::new(),
@@ -3020,6 +3022,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: Vec::new(),
@@ -3492,6 +3495,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: Vec::new(),
@@ -3675,6 +3679,49 @@ async fn session_configuration_apply_permission_profile_accepts_direct_write_roo
);
}
#[tokio::test]
async fn session_configuration_apply_rebinds_symbolic_profile_to_updated_workspace_roots() {
let mut session_configuration = make_session_configuration_for_tests().await;
let old_root = tempfile::tempdir().expect("create old root");
let new_root = tempfile::tempdir().expect("create new root");
let profile_root = tempfile::tempdir().expect("create profile root");
let old_root = old_root.path().abs();
let new_root = new_root.path().abs();
let profile_root = profile_root.path().abs();
session_configuration.workspace_roots = vec![old_root.clone()];
let file_system_sandbox_policy =
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
access: FileSystemAccessMode::Write,
}]);
let permission_profile = PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
);
let updated = session_configuration
.apply(&SessionSettingsUpdate {
workspace_roots: Some(vec![new_root.clone()]),
permission_profile: Some(permission_profile),
active_permission_profile: Some(ActivePermissionProfile::new("dev")),
profile_workspace_roots: Some(vec![profile_root.clone()]),
..Default::default()
})
.expect("permission profile update should succeed");
let updated_policy = updated.file_system_sandbox_policy();
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd.as_path()));
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd.as_path()));
assert_eq!(
updated.active_permission_profile(),
Some(ActivePermissionProfile::new("dev"))
);
assert_eq!(updated.profile_workspace_roots(), &[profile_root]);
}
#[cfg_attr(windows, ignore)]
#[tokio::test]
async fn new_default_turn_uses_config_aware_skills_for_role_overrides() {
@@ -4032,6 +4079,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: Vec::new(),
@@ -4140,6 +4188,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: default_environments,
@@ -4373,6 +4422,7 @@ async fn make_session_with_config_and_rx(
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: default_environments,
@@ -4475,6 +4525,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: default_environments,
@@ -5124,6 +5175,8 @@ fn op_kind_distinguishes_turn_ops() {
final_output_json_schema: None,
responsesapi_client_metadata: None,
cwd: None,
workspace_roots: None,
profile_workspace_roots: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
@@ -5991,6 +6044,7 @@ where
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: default_environments,
@@ -432,6 +432,10 @@ impl Session {
let config = session_configuration.original_config_do_not_use.clone();
let mut per_turn_config = (*config).clone();
per_turn_config.cwd = cwd;
per_turn_config.workspace_roots = session_configuration.workspace_roots.clone();
per_turn_config
.permissions
.set_workspace_roots(session_configuration.workspace_roots.clone());
per_turn_config.model_reasoning_effort =
session_configuration.collaboration_mode.reasoning_effort();
per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary;
@@ -466,6 +470,10 @@ 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.workspace_roots = session_configuration.workspace_roots.clone();
config
.permissions
.set_workspace_roots(session_configuration.workspace_roots.clone());
config
}