app-server: select permission profiles by id

This commit is contained in:
Michael Bolin
2026-05-13 12:58:58 -07:00
parent f65d5b52cb
commit 873daad75f
12 changed files with 1685 additions and 182 deletions
+27 -19
View File
@@ -196,18 +196,24 @@ pub struct InProcessClientSender {
}
impl InProcessClientSender {
pub async fn request(&self, request: ClientRequest) -> IoResult<PendingClientRequestResponse> {
pub fn request(
&self,
request: ClientRequest,
) -> impl std::future::Future<Output = IoResult<PendingClientRequestResponse>> + '_ {
let (response_tx, response_rx) = oneshot::channel();
self.try_send_client_message(InProcessClientMessage::Request {
let send_result = self.try_send_client_message(InProcessClientMessage::Request {
request: Box::new(request),
response_tx,
})?;
response_rx.await.map_err(|err| {
IoError::new(
ErrorKind::BrokenPipe,
format!("in-process request response channel closed: {err}"),
)
})
});
async move {
send_result?;
response_rx.await.map_err(|err| {
IoError::new(
ErrorKind::BrokenPipe,
format!("in-process request response channel closed: {err}"),
)
})
}
}
pub fn notify(&self, notification: ClientNotification) -> IoResult<()> {
@@ -268,8 +274,11 @@ impl InProcessClientHandle {
/// request IDs unique among concurrent requests; reusing an in-flight ID
/// produces an `INVALID_REQUEST` response and can make request routing
/// ambiguous in the caller.
pub async fn request(&self, request: ClientRequest) -> IoResult<PendingClientRequestResponse> {
self.client.request(request).await
pub fn request(
&self,
request: ClientRequest,
) -> impl std::future::Future<Output = IoResult<PendingClientRequestResponse>> + '_ {
self.client.request(request)
}
/// Sends a typed client notification into the in-process runtime.
@@ -448,12 +457,12 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
let was_initialized = session.initialized();
processor
.process_client_request(
IN_PROCESS_CONNECTION_ID,
*request,
Arc::clone(&session),
&outbound_initialized,
)
.await;
IN_PROCESS_CONNECTION_ID,
request,
Arc::clone(&session),
&outbound_initialized,
)
.await;
let opted_out_notification_methods_snapshot =
session.opted_out_notification_methods();
let experimental_api_enabled =
@@ -524,7 +533,6 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
message = client_rx.recv() => {
match message {
Some(InProcessClientMessage::Request { request, response_tx }) => {
let request = *request;
let request_id = request.id().clone();
match pending_request_responses.entry(request_id.clone()) {
Entry::Vacant(entry) => {
@@ -538,7 +546,7 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
}
}
match processor_tx.try_send(ProcessorCommand::Request(Box::new(request))) {
match processor_tx.try_send(ProcessorCommand::Request(request)) {
Ok(()) => {}
Err(mpsc::error::TrySendError::Full(_)) => {
if let Some(response_tx) =
+26 -25
View File
@@ -571,7 +571,7 @@ impl MessageProcessor {
pub(crate) fn process_client_request<'a>(
self: &'a Arc<Self>,
connection_id: ConnectionId,
request: ClientRequest,
request: Box<ClientRequest>,
session: Arc<ConnectionSessionState>,
outbound_initialized: &'a AtomicBool,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
@@ -579,8 +579,11 @@ impl MessageProcessor {
connection_id,
request_id: request.id().clone(),
};
let request_span =
crate::app_server_tracing::typed_request_span(&request, connection_id, &session);
let request_span = crate::app_server_tracing::typed_request_span(
request.as_ref(),
connection_id,
&session,
);
let request_context =
RequestContext::new(request_id.clone(), request_span, /*parent_trace*/ None);
tracing::trace!(
@@ -596,15 +599,14 @@ impl MessageProcessor {
// In-process clients do not have the websocket transport loop that performs
// post-initialize bookkeeping, so they still finalize outbound readiness in
// the shared request handler.
let result = self
.handle_client_request(
request_id.clone(),
request,
Arc::clone(&session),
Some(outbound_initialized),
request_context.clone(),
)
.await;
let result = Box::pin(self.handle_client_request(
request_id.clone(),
*request,
Arc::clone(&session),
Some(outbound_initialized),
request_context.clone(),
))
.await;
if let Err(error) = result {
self.outgoing.send_error(request_id.clone(), error).await;
}
@@ -627,10 +629,10 @@ impl MessageProcessor {
tracing::info!("<- typed notification: {:?}", notification);
}
async fn run_request_with_context<'a>(
async fn run_request_with_context(
outgoing: Arc<OutgoingMessageSender>,
request_context: RequestContext,
request_fut: Pin<Box<dyn Future<Output = ()> + Send + 'a>>,
request_fut: Pin<Box<dyn Future<Output = ()> + Send + '_>>,
) {
outgoing
.register_request_context(request_context.clone())
@@ -769,12 +771,12 @@ impl MessageProcessor {
return Ok(());
}
self.dispatch_initialized_client_request(
Box::pin(self.dispatch_initialized_client_request(
connection_request_id,
codex_request,
session,
request_context,
)
))
.await
}
@@ -812,15 +814,14 @@ impl MessageProcessor {
rpc_gate,
async move {
let processor_for_request = Arc::clone(&processor);
let result = processor_for_request
.handle_initialized_client_request(
connection_request_id,
codex_request,
request_context,
app_server_client_name,
client_version,
)
.await;
let result = Box::pin(processor_for_request.handle_initialized_client_request(
connection_request_id,
codex_request,
request_context,
app_server_client_name,
client_version,
))
.await;
if let Err(error) = result {
processor.outgoing.send_error(error_request_id, error).await;
}
@@ -354,6 +354,8 @@ use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
#[cfg(test)]
use codex_protocol::items::TurnItem;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseItem;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::protocol::AgentStatus;
@@ -430,6 +432,11 @@ use uuid::Uuid;
#[cfg(test)]
use codex_app_server_protocol::ServerRequest;
struct ResolvedPermissionProfileSelection {
permission_profile: PermissionProfile,
active_permission_profile: ActivePermissionProfile,
}
mod account_processor;
mod apps_processor;
mod catalog_processor;
@@ -441,6 +448,7 @@ mod feedback_processor;
mod fs_processor;
mod git_processor;
mod initialize_processor;
mod legacy_sandbox_compat;
mod marketplace_processor;
mod mcp_processor;
mod plugins;
@@ -0,0 +1,398 @@
use super::*;
const WORKSPACE_PERMISSION_PROFILE_ID: &str = ":workspace";
const READ_ONLY_PERMISSION_PROFILE_ID: &str = ":read-only";
const DANGER_NO_SANDBOX_PERMISSION_PROFILE_ID: &str = ":danger-no-sandbox";
pub(super) struct CurrentPermissionProfile<'a> {
pub(super) permission_profile: &'a PermissionProfile,
pub(super) workspace_roots: &'a [AbsolutePathBuf],
}
pub(super) struct LegacySandboxProfileSelection {
pub(super) permissions: String,
pub(super) workspace_roots: Option<Vec<AbsolutePathBuf>>,
expected_sandbox_policy: codex_protocol::protocol::SandboxPolicy,
}
pub(super) enum LegacySandboxResolution {
Noop {
workspace_roots: Option<Vec<AbsolutePathBuf>>,
},
Selection(LegacySandboxProfileSelection),
}
pub(super) fn resolve_legacy_sandbox_profile_selection(
sandbox_policy: &codex_app_server_protocol::SandboxPolicy,
current: Option<CurrentPermissionProfile<'_>>,
cwd: &AbsolutePathBuf,
explicit_workspace_roots: Option<&[AbsolutePathBuf]>,
field_name: &str,
) -> Result<LegacySandboxResolution, JSONRPCErrorError> {
let legacy_workspace_roots =
workspace_roots_from_implicit_legacy_sandbox(cwd, sandbox_policy, explicit_workspace_roots);
if let Some(current_match) = legacy_sandbox_current_match(
sandbox_policy,
current,
cwd,
explicit_workspace_roots,
legacy_workspace_roots.as_deref(),
) {
return Ok(LegacySandboxResolution::Noop {
workspace_roots: current_match.workspace_roots,
});
}
let expected_sandbox_policy = sandbox_policy.to_core();
match sandbox_policy {
codex_app_server_protocol::SandboxPolicy::DangerFullAccess => Ok(
LegacySandboxResolution::Selection(LegacySandboxProfileSelection {
permissions: DANGER_NO_SANDBOX_PERMISSION_PROFILE_ID.to_string(),
workspace_roots: None,
expected_sandbox_policy,
}),
),
codex_app_server_protocol::SandboxPolicy::ReadOnly { network_access: _ } => Ok(
LegacySandboxResolution::Selection(LegacySandboxProfileSelection {
permissions: READ_ONLY_PERMISSION_PROFILE_ID.to_string(),
workspace_roots: None,
expected_sandbox_policy,
}),
),
codex_app_server_protocol::SandboxPolicy::WorkspaceWrite { .. } => Ok(
LegacySandboxResolution::Selection(LegacySandboxProfileSelection {
permissions: WORKSPACE_PERMISSION_PROFILE_ID.to_string(),
workspace_roots: legacy_workspace_roots,
expected_sandbox_policy,
}),
),
codex_app_server_protocol::SandboxPolicy::ExternalSandbox { .. } => {
Err(invalid_request(format!(
"`{field_name}` externalSandbox cannot be mapped to a named permissions profile"
)))
}
}
}
pub(super) fn sandbox_policy_from_legacy_mode(
sandbox_mode: SandboxMode,
) -> codex_app_server_protocol::SandboxPolicy {
match sandbox_mode {
SandboxMode::ReadOnly => codex_protocol::protocol::SandboxPolicy::new_read_only_policy(),
SandboxMode::WorkspaceWrite => {
codex_protocol::protocol::SandboxPolicy::new_workspace_write_policy()
}
SandboxMode::DangerFullAccess => codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
}
.into()
}
pub(super) fn validate_legacy_sandbox_profile_selection(
legacy_selection: &LegacySandboxProfileSelection,
resolved_selection: &ResolvedPermissionProfileSelection,
cwd: &AbsolutePathBuf,
workspace_roots: Option<&[AbsolutePathBuf]>,
field_name: &str,
) -> Result<(), JSONRPCErrorError> {
let workspace_roots = workspace_roots
.or(legacy_selection.workspace_roots.as_deref())
.unwrap_or(&[]);
let materialized_profile = resolved_selection
.permission_profile
.materialize_project_roots_with_workspace_roots(workspace_roots);
let file_system_policy = materialized_profile.file_system_sandbox_policy();
let active_sandbox = codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
&materialized_profile,
&file_system_policy,
materialized_profile.network_sandbox_policy(),
cwd.as_path(),
);
if active_sandbox != legacy_selection.expected_sandbox_policy {
return Err(invalid_request(format!(
"`{field_name}` does not match permissions profile `{}`",
legacy_selection.permissions
)));
}
Ok(())
}
pub(super) fn resolve_cwd_against_fallback(
cwd: Option<&Path>,
fallback_cwd: &AbsolutePathBuf,
) -> AbsolutePathBuf {
match cwd {
Some(cwd) => {
if let Ok(path) = AbsolutePathBuf::try_from(cwd) {
path
} else {
AbsolutePathBuf::resolve_path_against_base(cwd, fallback_cwd.as_path())
}
}
None => fallback_cwd.clone(),
}
}
struct LegacySandboxCurrentMatch {
workspace_roots: Option<Vec<AbsolutePathBuf>>,
}
fn legacy_sandbox_current_match(
sandbox_policy: &codex_app_server_protocol::SandboxPolicy,
current: Option<CurrentPermissionProfile<'_>>,
cwd: &AbsolutePathBuf,
explicit_workspace_roots: Option<&[AbsolutePathBuf]>,
legacy_workspace_roots: Option<&[AbsolutePathBuf]>,
) -> Option<LegacySandboxCurrentMatch> {
let current = current?;
let implicit_legacy_workspace_roots = if explicit_workspace_roots.is_none()
&& matches!(
sandbox_policy,
codex_app_server_protocol::SandboxPolicy::WorkspaceWrite { .. }
) {
legacy_workspace_roots
} else {
None
};
let current_roots_satisfy_implicit_legacy_workspace =
current.workspace_roots.iter().any(|root| root == cwd)
&& sandbox_policy.legacy_writable_roots().is_empty();
let projection_workspace_roots =
if let Some(explicit_workspace_roots) = explicit_workspace_roots {
explicit_workspace_roots
} else if let Some(implicit_legacy_workspace_roots) = implicit_legacy_workspace_roots {
if current_roots_satisfy_implicit_legacy_workspace {
current.workspace_roots
} else {
implicit_legacy_workspace_roots
}
} else {
current.workspace_roots
};
let materialized_profile = current
.permission_profile
.materialize_project_roots_with_workspace_roots(projection_workspace_roots);
let file_system_policy = materialized_profile.file_system_sandbox_policy();
let active_sandbox = codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
&materialized_profile,
&file_system_policy,
materialized_profile.network_sandbox_policy(),
cwd.as_path(),
);
if active_sandbox != sandbox_policy.to_core() {
return None;
}
let workspace_roots = legacy_workspace_roots
.filter(|_| projection_workspace_roots != current.workspace_roots)
.map(<[AbsolutePathBuf]>::to_vec);
Some(LegacySandboxCurrentMatch { workspace_roots })
}
fn workspace_roots_from_implicit_legacy_sandbox(
cwd: &AbsolutePathBuf,
sandbox_policy: &codex_app_server_protocol::SandboxPolicy,
explicit_workspace_roots: Option<&[AbsolutePathBuf]>,
) -> Option<Vec<AbsolutePathBuf>> {
if explicit_workspace_roots.is_some()
|| !matches!(
sandbox_policy,
codex_app_server_protocol::SandboxPolicy::WorkspaceWrite { .. }
)
{
None
} else {
Some(workspace_roots_from_legacy_sandbox(cwd, sandbox_policy))
}
}
fn workspace_roots_from_legacy_sandbox(
cwd: &AbsolutePathBuf,
sandbox_policy: &codex_app_server_protocol::SandboxPolicy,
) -> Vec<AbsolutePathBuf> {
let mut roots = Vec::with_capacity(1 + sandbox_policy.legacy_writable_roots().len());
push_unique_root(&mut roots, cwd.clone());
for root in sandbox_policy.legacy_writable_roots() {
push_unique_root(&mut roots, root.clone());
}
roots
}
fn push_unique_root(roots: &mut Vec<AbsolutePathBuf>, root: AbsolutePathBuf) {
if !roots.iter().any(|existing| existing == &root) {
roots.push(root);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn abs_test_path(name: &str) -> AbsolutePathBuf {
AbsolutePathBuf::from_absolute_path(std::env::temp_dir().join(name))
.expect("temp dir path should be absolute")
}
fn workspace_write_policy() -> codex_app_server_protocol::SandboxPolicy {
codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
legacy_writable_roots: Vec::new(),
}
}
#[test]
fn legacy_workspace_sandbox_updates_roots_when_current_profile_matches_new_cwd() {
let old_root = abs_test_path("codex-old-workspace-root");
let cwd = abs_test_path("codex-new-workspace-root");
let policy = workspace_write_policy();
let resolution = resolve_legacy_sandbox_profile_selection(
&policy,
Some(CurrentPermissionProfile {
permission_profile: &PermissionProfile::workspace_write(),
workspace_roots: std::slice::from_ref(&old_root),
}),
&cwd,
/*explicit_workspace_roots*/ None,
"sandboxPolicy",
)
.expect("legacy sandbox should resolve");
match resolution {
LegacySandboxResolution::Noop {
workspace_roots: Some(workspace_roots),
} => assert_eq!(workspace_roots, vec![cwd]),
_ => panic!("expected workspace-roots-only resolution, got unexpected selection"),
}
}
#[test]
fn legacy_workspace_sandbox_is_noop_when_current_workspace_roots_match() {
let cwd = abs_test_path("codex-current-workspace-root");
let policy = workspace_write_policy();
let resolution = resolve_legacy_sandbox_profile_selection(
&policy,
Some(CurrentPermissionProfile {
permission_profile: &PermissionProfile::workspace_write(),
workspace_roots: std::slice::from_ref(&cwd),
}),
&cwd,
/*explicit_workspace_roots*/ None,
"sandboxPolicy",
)
.expect("legacy sandbox should resolve");
match resolution {
LegacySandboxResolution::Noop {
workspace_roots: None,
} => {}
_ => panic!("expected no-op resolution, got unexpected workspace roots or selection"),
}
}
#[test]
fn legacy_workspace_sandbox_preserves_current_extra_roots_when_cwd_is_still_a_root() {
let cwd = abs_test_path("codex-current-workspace-root-with-extra");
let extra_root = abs_test_path("codex-extra-workspace-root");
let workspace_roots = vec![cwd.clone(), extra_root];
let policy = workspace_write_policy();
let resolution = resolve_legacy_sandbox_profile_selection(
&policy,
Some(CurrentPermissionProfile {
permission_profile: &PermissionProfile::workspace_write(),
workspace_roots: &workspace_roots,
}),
&cwd,
/*explicit_workspace_roots*/ None,
"sandboxPolicy",
)
.expect("legacy sandbox should resolve");
match resolution {
LegacySandboxResolution::Noop {
workspace_roots: None,
} => {}
_ => panic!("expected no-op resolution, got unexpected workspace roots or selection"),
}
}
#[test]
fn legacy_workspace_sandbox_honors_non_empty_legacy_writable_roots() {
let cwd = abs_test_path("codex-current-workspace-root-with-legacy-extra");
let current_extra_root = abs_test_path("codex-current-extra-workspace-root");
let requested_extra_root = abs_test_path("codex-requested-extra-workspace-root");
let workspace_roots = vec![cwd.clone(), current_extra_root];
let policy = codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
legacy_writable_roots: vec![requested_extra_root.clone()],
};
let resolution = resolve_legacy_sandbox_profile_selection(
&policy,
Some(CurrentPermissionProfile {
permission_profile: &PermissionProfile::workspace_write(),
workspace_roots: &workspace_roots,
}),
&cwd,
/*explicit_workspace_roots*/ None,
"sandboxPolicy",
)
.expect("legacy sandbox should resolve");
match resolution {
LegacySandboxResolution::Noop {
workspace_roots: Some(workspace_roots),
} => assert_eq!(workspace_roots, vec![cwd, requested_extra_root]),
_ => panic!("expected workspace-roots-only resolution, got unexpected selection"),
}
}
#[test]
fn legacy_workspace_selection_validation_requires_exact_projection() {
let cwd = abs_test_path("codex-projection-mismatch-root");
let policy = workspace_write_policy();
let resolution = resolve_legacy_sandbox_profile_selection(
&policy,
/*current*/ None,
&cwd,
/*explicit_workspace_roots*/ None,
"sandboxPolicy",
)
.expect("legacy sandbox should resolve");
let LegacySandboxResolution::Selection(selection) = resolution else {
panic!("expected named profile selection");
};
let resolved_selection = ResolvedPermissionProfileSelection {
permission_profile: PermissionProfile::workspace_write_with(
&[],
codex_protocol::permissions::NetworkSandboxPolicy::Restricted,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ false,
),
active_permission_profile: codex_protocol::models::ActivePermissionProfile::new(
":workspace",
),
};
let err = validate_legacy_sandbox_profile_selection(
&selection,
&resolved_selection,
&cwd,
selection.workspace_roots.as_deref(),
"sandboxPolicy",
)
.expect_err("mismatched workspace profile should be rejected");
assert_eq!(
err.message,
"`sandboxPolicy` does not match permissions profile `:workspace`"
);
}
}
@@ -1,3 +1,4 @@
use super::legacy_sandbox_compat::*;
use super::*;
use crate::error_code::method_not_found;
use codex_protocol::models::ActivePermissionProfile;
@@ -42,6 +43,23 @@ fn absolute_path_from_history_path(
}
}
fn effective_cwd_for_legacy_sandbox(
request_cwd: Option<&str>,
history_cwd: Option<&Path>,
persisted_permission_state: Option<&PersistedThreadPermissionState>,
) -> Option<AbsolutePathBuf> {
let history_cwd =
history_cwd.and_then(|cwd| absolute_path_from_history_path(cwd, /*base*/ None));
request_cwd
.and_then(|cwd| absolute_path_from_history_path(Path::new(cwd), history_cwd.as_ref()))
.or_else(|| history_cwd.clone())
.or_else(|| {
persisted_permission_state
.and_then(|state| state.workspace_roots.first())
.cloned()
})
}
fn persisted_thread_permission_state(
history: &InitialHistory,
fallback_cwd: Option<&Path>,
@@ -58,28 +76,14 @@ fn persisted_thread_permission_state(
RolloutItem::SessionMeta(meta_line) => {
cwd = absolute_path_from_history_path(meta_line.meta.cwd.as_path(), cwd.as_ref())
.or(cwd);
workspace_roots = Some(if meta_line.meta.workspace_roots.is_empty() {
cwd.iter().cloned().collect()
} else {
meta_line.meta.workspace_roots
});
workspace_roots = Some(meta_line.meta.workspace_roots);
}
RolloutItem::TurnContext(context) => {
cwd = absolute_path_from_history_path(context.cwd.as_path(), cwd.as_ref()).or(cwd);
let context_permission_profile = context.permission_profile();
workspace_roots = Some(context.workspace_roots);
let context_cwd = cwd
.as_ref()
.map(AbsolutePathBuf::as_path)
.unwrap_or(context.cwd.as_path());
permission_profile = Some(context.permission_profile.unwrap_or_else(|| {
PermissionProfile::from_legacy_sandbox_policy_for_cwd(
&context.sandbox_policy,
context_cwd,
)
}));
if context.active_permission_profile.is_some() {
active_permission_profile = context.active_permission_profile;
}
permission_profile = Some(context_permission_profile);
active_permission_profile = context.active_permission_profile;
}
RolloutItem::EventMsg(EventMsg::SessionConfigured(event)) => {
cwd = Some(event.cwd.clone());
@@ -107,7 +111,6 @@ fn persisted_thread_permission_state(
workspace_roots: workspace_roots.unwrap_or_else(|| cwd.into_iter().collect()),
})
}
fn collect_resume_override_mismatches(
request: &ThreadResumeParams,
config_snapshot: &ThreadConfigSnapshot,
@@ -189,12 +192,6 @@ fn collect_resume_override_mismatches(
));
}
}
if request.permissions.is_some() {
mismatch_details.push(format!(
"permissions override was provided and ignored while running; active={:?}",
config_snapshot.active_permission_profile
));
}
if let Some(requested_personality) = request.personality.as_ref()
&& config_snapshot.personality.as_ref() != Some(requested_personality)
{
@@ -1068,7 +1065,7 @@ impl ThreadRequestProcessor {
let requested_permissions_trust_project =
requested_permissions_trust_project(&typesafe_overrides, config.cwd.as_path());
let effective_permissions_trust_project = permission_profile_trusts_project(
&config.permissions.permission_profile(),
config.permissions.permission_profile_ref(),
config.cwd.as_path(),
);
@@ -1343,6 +1340,47 @@ impl ThreadRequestProcessor {
overrides
}
async fn validate_active_permission_profile_selection(
&self,
permissions: String,
request_overrides: Option<HashMap<String, serde_json::Value>>,
cwd: Option<PathBuf>,
fallback_cwd: Option<PathBuf>,
) -> Result<ResolvedPermissionProfileSelection, JSONRPCErrorError> {
let mut overrides = ConfigOverrides {
cwd,
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
..Default::default()
};
apply_permission_profile_selection_to_config_overrides(&mut overrides, Some(permissions));
let config = self
.config_manager
.load_for_cwd(request_overrides, overrides, fallback_cwd)
.await
.map_err(|err| config_load_error(&err))?;
if let Some(warning) = config.startup_warnings.iter().find(|warning| {
warning.contains("Configured value for `permission_profile` is disallowed")
}) {
return Err(invalid_request(format!(
"invalid permission profile selection: {warning}"
)));
}
let active_permission_profile =
config
.permissions
.active_permission_profile()
.ok_or_else(|| {
invalid_request(
"permission profile selection did not resolve to a named profile",
)
})?;
Ok(ResolvedPermissionProfileSelection {
permission_profile: config.permissions.permission_profile(),
active_permission_profile,
})
}
fn parse_environment_selections(
&self,
environments: Option<Vec<TurnEnvironmentParams>>,
@@ -2464,6 +2502,7 @@ impl ThreadRequestProcessor {
persist_extended_history: _persist_extended_history,
} = params;
let include_turns = !exclude_turns;
let mut workspace_roots = workspace_roots;
let (thread_history, resume_source_thread) = match if let Some(history) = history {
self.resume_thread_from_history(history.as_slice())
@@ -2489,8 +2528,110 @@ impl ThreadRequestProcessor {
.as_ref()
.map(|thread| &thread.sandbox_policy),
);
let permissions_were_explicit = permissions.is_some();
let sandbox_was_explicit = sandbox.is_some();
let permission_profile_selection = if let Some(permissions) = permissions {
match self
.validate_active_permission_profile_selection(
permissions,
request_overrides.clone(),
cwd.clone().map(PathBuf::from),
history_cwd.clone(),
)
.await
{
Ok(selection) => Some(selection),
Err(error) => {
self.outgoing.send_error(request_id, error).await;
return Ok(());
}
}
} else if let Some(sandbox_mode) = sandbox {
let sandbox_policy = sandbox_policy_from_legacy_mode(sandbox_mode);
let Some(effective_cwd) = effective_cwd_for_legacy_sandbox(
cwd.as_deref(),
history_cwd.as_deref(),
persisted_permission_state.as_ref(),
) else {
self.outgoing
.send_error(
request_id,
invalid_request("`sandbox` requires a cwd to resolve legacy permissions"),
)
.await;
return Ok(());
};
match resolve_legacy_sandbox_profile_selection(
&sandbox_policy,
persisted_permission_state
.as_ref()
.map(|state| CurrentPermissionProfile {
permission_profile: &state.permission_profile,
workspace_roots: &state.workspace_roots,
}),
&effective_cwd,
workspace_roots.as_deref(),
"sandbox",
) {
Ok(LegacySandboxResolution::Noop {
workspace_roots: legacy_workspace_roots,
}) => {
if workspace_roots.is_none() {
workspace_roots = legacy_workspace_roots;
}
None
}
Ok(LegacySandboxResolution::Selection(legacy_selection)) => {
if workspace_roots.is_none() {
workspace_roots = legacy_selection.workspace_roots.clone();
}
match self
.validate_active_permission_profile_selection(
legacy_selection.permissions.clone(),
request_overrides.clone(),
cwd.clone().map(PathBuf::from),
history_cwd.clone(),
)
.await
.and_then(|selection| {
validate_legacy_sandbox_profile_selection(
&legacy_selection,
&selection,
&effective_cwd,
workspace_roots.as_deref(),
"sandbox",
)
.map(|()| selection)
}) {
Ok(selection) => Some(selection),
Err(error) => {
self.outgoing.send_error(request_id, error).await;
return Ok(());
}
}
}
Err(error) => {
self.outgoing.send_error(request_id, error).await;
return Ok(());
}
}
} else {
None
};
let active_permission_profile = permission_profile_selection
.as_ref()
.map(|selection| selection.active_permission_profile.clone())
.or_else(|| {
persisted_permission_state
.as_ref()
.and_then(|state| state.active_permission_profile.clone())
});
let expected_active_permission_profile_source = permission_profile_selection
.as_ref()
.map(|selection| selection.permission_profile.clone())
.or_else(|| {
persisted_permission_state
.as_ref()
.map(|state| state.permission_profile.clone())
});
let workspace_roots_were_explicit = workspace_roots.is_some();
let mut typesafe_overrides = self.build_thread_config_overrides(
model,
@@ -2500,26 +2641,20 @@ impl ThreadRequestProcessor {
workspace_roots,
approval_policy,
approvals_reviewer,
sandbox,
permissions,
/*sandbox*/ None,
/*permissions*/ None,
base_instructions,
developer_instructions,
personality,
);
if !permissions_were_explicit
&& !sandbox_was_explicit
&& let Some(persisted_permission_state) = persisted_permission_state.as_ref()
{
typesafe_overrides.permission_profile =
Some(persisted_permission_state.permission_profile.clone());
}
typesafe_overrides.permission_profile = expected_active_permission_profile_source.clone();
if !workspace_roots_were_explicit {
if let Some(persisted_permission_state) = persisted_permission_state.as_ref() {
typesafe_overrides.workspace_roots = Some(
persisted_permission_state
.workspace_roots
.iter()
.map(AbsolutePathBuf::to_path_buf)
.map(codex_utils_absolute_path::AbsolutePathBuf::to_path_buf)
.collect(),
);
} else if let Some(root) = history_cwd
@@ -2549,17 +2684,12 @@ impl ThreadRequestProcessor {
return Ok(());
}
};
if !permissions_were_explicit
&& !sandbox_was_explicit
&& let Some(persisted_permission_state) = persisted_permission_state
{
config
.permissions
.set_active_permission_profile_for_current_profile(
persisted_permission_state.active_permission_profile,
Some(&persisted_permission_state.permission_profile),
);
}
config
.permissions
.set_active_permission_profile_for_current_profile(
active_permission_profile,
expected_active_permission_profile_source.as_ref(),
);
let instruction_sources = Self::instruction_sources_from_config(&config).await;
let response_history = thread_history.clone();
@@ -2833,6 +2963,96 @@ impl ThreadRequestProcessor {
)
.await?;
let config_snapshot = existing_thread.config_snapshot().await;
let mut workspace_roots = params.workspace_roots.clone();
let permission_profile_selection = if let Some(permissions) = params.permissions.clone()
{
Some(
self.validate_active_permission_profile_selection(
permissions,
/*request_overrides*/ None,
/*cwd*/ None,
Some(config_snapshot.cwd.to_path_buf()),
)
.await?,
)
} else if let Some(sandbox_mode) = params.sandbox {
let sandbox_policy = sandbox_policy_from_legacy_mode(sandbox_mode);
match resolve_legacy_sandbox_profile_selection(
&sandbox_policy,
Some(CurrentPermissionProfile {
permission_profile: &config_snapshot.permission_profile,
workspace_roots: &config_snapshot.workspace_roots,
}),
&config_snapshot.cwd,
workspace_roots.as_deref(),
"sandbox",
)? {
LegacySandboxResolution::Noop {
workspace_roots: legacy_workspace_roots,
} => {
if workspace_roots.is_none() {
workspace_roots = legacy_workspace_roots;
}
None
}
LegacySandboxResolution::Selection(legacy_selection) => {
if workspace_roots.is_none() {
workspace_roots = legacy_selection.workspace_roots.clone();
}
let selection = self
.validate_active_permission_profile_selection(
legacy_selection.permissions.clone(),
/*request_overrides*/ None,
/*cwd*/ None,
Some(config_snapshot.cwd.to_path_buf()),
)
.await?;
validate_legacy_sandbox_profile_selection(
&legacy_selection,
&selection,
&config_snapshot.cwd,
workspace_roots.as_deref(),
"sandbox",
)?;
Some(selection)
}
}
} else {
None
};
if workspace_roots.is_some() || permission_profile_selection.is_some() {
existing_thread
.update_turn_context_overrides(CodexThreadTurnContextOverrides {
cwd: None,
workspace_roots: workspace_roots.map(|roots| {
roots
.into_iter()
.map(AbsolutePathBuf::into_path_buf)
.collect()
}),
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: permission_profile_selection
.as_ref()
.map(|selection| selection.permission_profile.clone()),
active_permission_profile: permission_profile_selection
.map(|selection| selection.active_permission_profile),
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await
.map_err(|err| {
invalid_request(format!("invalid thread resume override: {err}"))
})?;
}
let config_snapshot = existing_thread.config_snapshot().await;
let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot);
if !mismatch_details.is_empty() {
@@ -3209,8 +3429,78 @@ impl ThreadRequestProcessor {
.ok_or_else(|| {
invalid_request("thread history is missing persisted permission configuration")
})?;
let permissions_were_explicit = permissions.is_some();
let sandbox_was_explicit = sandbox.is_some();
let mut workspace_roots = workspace_roots;
let permission_profile_selection = if let Some(permissions) = permissions {
Some(
self.validate_active_permission_profile_selection(
permissions,
request_overrides.clone(),
cwd.clone().map(PathBuf::from),
history_cwd.clone(),
)
.await?,
)
} else if let Some(sandbox_mode) = sandbox {
let sandbox_policy = sandbox_policy_from_legacy_mode(sandbox_mode);
let effective_cwd = effective_cwd_for_legacy_sandbox(
cwd.as_deref(),
history_cwd.as_deref(),
Some(&persisted_permission_state),
)
.ok_or_else(|| {
invalid_request("`sandbox` requires a cwd to resolve legacy permissions")
})?;
match resolve_legacy_sandbox_profile_selection(
&sandbox_policy,
Some(CurrentPermissionProfile {
permission_profile: &persisted_permission_state.permission_profile,
workspace_roots: &persisted_permission_state.workspace_roots,
}),
&effective_cwd,
workspace_roots.as_deref(),
"sandbox",
)? {
LegacySandboxResolution::Noop {
workspace_roots: legacy_workspace_roots,
} => {
if workspace_roots.is_none() {
workspace_roots = legacy_workspace_roots;
}
None
}
LegacySandboxResolution::Selection(legacy_selection) => {
if workspace_roots.is_none() {
workspace_roots = legacy_selection.workspace_roots.clone();
}
let selection = self
.validate_active_permission_profile_selection(
legacy_selection.permissions.clone(),
request_overrides.clone(),
cwd.clone().map(PathBuf::from),
history_cwd.clone(),
)
.await?;
validate_legacy_sandbox_profile_selection(
&legacy_selection,
&selection,
&effective_cwd,
workspace_roots.as_deref(),
"sandbox",
)?;
Some(selection)
}
}
} else {
None
};
let active_permission_profile = permission_profile_selection
.as_ref()
.map(|selection| selection.active_permission_profile.clone())
.or_else(|| persisted_permission_state.active_permission_profile.clone());
let expected_active_permission_profile_source = permission_profile_selection
.as_ref()
.map(|selection| selection.permission_profile.clone())
.or_else(|| Some(persisted_permission_state.permission_profile.clone()));
let workspace_roots_were_explicit = workspace_roots.is_some();
let mut typesafe_overrides = self.build_thread_config_overrides(
model,
@@ -3220,22 +3510,19 @@ impl ThreadRequestProcessor {
workspace_roots,
approval_policy,
approvals_reviewer,
sandbox,
permissions,
/*sandbox*/ None,
/*permissions*/ None,
base_instructions,
developer_instructions,
/*personality*/ None,
);
if !permissions_were_explicit && !sandbox_was_explicit {
typesafe_overrides.permission_profile =
Some(persisted_permission_state.permission_profile.clone());
}
typesafe_overrides.permission_profile = expected_active_permission_profile_source.clone();
if !workspace_roots_were_explicit {
typesafe_overrides.workspace_roots = Some(
persisted_permission_state
.workspace_roots
.iter()
.map(AbsolutePathBuf::to_path_buf)
.map(codex_utils_absolute_path::AbsolutePathBuf::to_path_buf)
.collect(),
);
}
@@ -3246,14 +3533,12 @@ impl ThreadRequestProcessor {
.load_for_cwd(request_overrides, typesafe_overrides, history_cwd)
.await
.map_err(|err| config_load_error(&err))?;
if !permissions_were_explicit && !sandbox_was_explicit {
config
.permissions
.set_active_permission_profile_for_current_profile(
persisted_permission_state.active_permission_profile,
Some(&persisted_permission_state.permission_profile),
);
}
config
.permissions
.set_active_permission_profile_for_current_profile(
active_permission_profile,
expected_active_permission_profile_source.as_ref(),
);
let fallback_model_provider = config.model_provider_id.clone();
let instruction_sources = Self::instruction_sources_from_config(&config).await;
@@ -495,6 +495,245 @@ mod thread_processor_behavior_tests {
));
}
#[test]
fn persisted_thread_permission_state_uses_latest_turn_active_profile() {
let cwd = test_path_buf("/tmp/project").abs();
let workspace_root = test_path_buf("/tmp/workspace").abs();
let active_permission_profile =
codex_protocol::models::ActivePermissionProfile::new(":workspace");
let permission_profile = codex_protocol::models::PermissionProfile::workspace_write();
let history = codex_protocol::protocol::InitialHistory::Forked(vec![
codex_protocol::protocol::RolloutItem::TurnContext(
codex_protocol::protocol::TurnContextItem {
turn_id: Some("turn-1".to_string()),
trace_id: None,
cwd: cwd.to_path_buf(),
workspace_roots: vec![workspace_root.clone()],
current_date: None,
timezone: None,
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::new_workspace_write_policy(),
permission_profile: Some(permission_profile.clone()),
active_permission_profile: Some(active_permission_profile.clone()),
network: None,
file_system_sandbox_policy: None,
model: "gpt-5".to_string(),
personality: None,
collaboration_mode: None,
realtime_active: None,
effort: None,
summary: codex_protocol::config_types::ReasoningSummary::Auto,
user_instructions: None,
developer_instructions: None,
final_output_json_schema: None,
truncation_policy: None,
},
),
]);
let persisted = persisted_thread_permission_state(
&history,
Some(cwd.as_path()),
/*fallback_sandbox_policy*/ None,
)
.expect("permission state should be reconstructed");
assert_eq!(persisted.permission_profile, permission_profile);
assert_eq!(
persisted.active_permission_profile,
Some(active_permission_profile)
);
assert_eq!(persisted.workspace_roots, vec![workspace_root]);
}
#[test]
fn persisted_thread_permission_state_uses_legacy_turn_context_file_system_policy() {
let cwd = test_path_buf("/tmp/project").abs();
let workspace_root = test_path_buf("/tmp/workspace").abs();
let sandbox_policy = SandboxPolicy::new_workspace_write_policy();
let file_system_sandbox_policy =
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: "/tmp/project/**/*.env".to_string(),
},
access: FileSystemAccessMode::None,
}]);
let expected_permission_profile =
codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement(
codex_protocol::models::SandboxEnforcement::from_legacy_sandbox_policy(
&sandbox_policy,
),
&file_system_sandbox_policy,
NetworkSandboxPolicy::from(&sandbox_policy),
);
let history = codex_protocol::protocol::InitialHistory::Forked(vec![
codex_protocol::protocol::RolloutItem::TurnContext(
codex_protocol::protocol::TurnContextItem {
turn_id: Some("turn-1".to_string()),
trace_id: None,
cwd: cwd.to_path_buf(),
workspace_roots: vec![workspace_root.clone()],
current_date: None,
timezone: None,
approval_policy: AskForApproval::Never,
sandbox_policy,
permission_profile: None,
active_permission_profile: None,
network: None,
file_system_sandbox_policy: Some(file_system_sandbox_policy),
model: "gpt-5".to_string(),
personality: None,
collaboration_mode: None,
realtime_active: None,
effort: None,
summary: codex_protocol::config_types::ReasoningSummary::Auto,
user_instructions: None,
developer_instructions: None,
final_output_json_schema: None,
truncation_policy: None,
},
),
]);
let persisted = persisted_thread_permission_state(
&history,
Some(cwd.as_path()),
/*fallback_sandbox_policy*/ None,
)
.expect("permission state should be reconstructed");
assert_eq!(persisted.permission_profile, expected_permission_profile);
assert_eq!(persisted.workspace_roots, vec![workspace_root]);
}
#[test]
fn persisted_thread_permission_state_clears_stale_active_profile() {
let cwd = test_path_buf("/tmp/project").abs();
let workspace_root = test_path_buf("/tmp/workspace").abs();
let active_permission_profile =
codex_protocol::models::ActivePermissionProfile::new(":workspace");
let workspace_profile = codex_protocol::models::PermissionProfile::workspace_write();
let read_only_profile = codex_protocol::models::PermissionProfile::read_only();
let first_context = codex_protocol::protocol::TurnContextItem {
turn_id: Some("turn-1".to_string()),
trace_id: None,
cwd: cwd.to_path_buf(),
workspace_roots: vec![workspace_root.clone()],
current_date: None,
timezone: None,
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::new_workspace_write_policy(),
permission_profile: Some(workspace_profile),
active_permission_profile: Some(active_permission_profile),
network: None,
file_system_sandbox_policy: None,
model: "gpt-5".to_string(),
personality: None,
collaboration_mode: None,
realtime_active: None,
effort: None,
summary: codex_protocol::config_types::ReasoningSummary::Auto,
user_instructions: None,
developer_instructions: None,
final_output_json_schema: None,
truncation_policy: None,
};
let mut second_context = first_context.clone();
second_context.turn_id = Some("turn-2".to_string());
second_context.permission_profile = Some(read_only_profile.clone());
second_context.active_permission_profile = None;
let history = codex_protocol::protocol::InitialHistory::Forked(vec![
codex_protocol::protocol::RolloutItem::TurnContext(first_context),
codex_protocol::protocol::RolloutItem::TurnContext(second_context),
]);
let persisted = persisted_thread_permission_state(
&history,
Some(cwd.as_path()),
/*fallback_sandbox_policy*/ None,
)
.expect("permission state should be reconstructed");
assert_eq!(persisted.permission_profile, read_only_profile);
assert_eq!(persisted.active_permission_profile, None);
assert_eq!(persisted.workspace_roots, vec![workspace_root]);
}
#[test]
fn persisted_thread_permission_state_preserves_empty_workspace_roots_from_event_roundtrip() {
let cwd = test_path_buf("/tmp/project").abs();
let thread_id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8").unwrap();
let permission_profile = codex_protocol::models::PermissionProfile::workspace_write();
let event = codex_protocol::protocol::EventMsg::SessionConfigured(
codex_protocol::protocol::SessionConfiguredEvent {
session_id: thread_id.into(),
thread_id,
forked_from_id: None,
thread_source: None,
thread_name: None,
model: "gpt-5".to_string(),
model_provider_id: "mock_provider".to_string(),
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
permission_profile: permission_profile.clone(),
active_permission_profile: None,
cwd: cwd.clone(),
workspace_roots: Vec::new(),
reasoning_effort: None,
initial_messages: None,
network_proxy: None,
rollout_path: None,
},
);
let event = serde_json::from_value(serde_json::to_value(event).unwrap()).unwrap();
let history = codex_protocol::protocol::InitialHistory::Forked(vec![
codex_protocol::protocol::RolloutItem::EventMsg(event),
]);
let persisted = persisted_thread_permission_state(
&history,
Some(cwd.as_path()),
/*fallback_sandbox_policy*/ None,
)
.expect("permission state should be reconstructed");
assert_eq!(persisted.permission_profile, permission_profile);
assert_eq!(persisted.workspace_roots, Vec::<AbsolutePathBuf>::new());
}
#[test]
fn persisted_thread_permission_state_preserves_empty_workspace_roots_from_session_meta() {
let cwd = test_path_buf("/tmp/project").abs();
let thread_id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8").unwrap();
let history = codex_protocol::protocol::InitialHistory::Forked(vec![
codex_protocol::protocol::RolloutItem::SessionMeta(
codex_protocol::protocol::SessionMetaLine {
meta: codex_protocol::protocol::SessionMeta {
id: thread_id,
cwd: cwd.clone().into_path_buf(),
workspace_roots: Vec::new(),
..Default::default()
},
git: None,
},
),
]);
let persisted = persisted_thread_permission_state(
&history,
Some(cwd.as_path()),
Some(&SandboxPolicy::new_workspace_write_policy()),
)
.expect("permission state should be reconstructed");
assert_eq!(persisted.workspace_roots, Vec::<AbsolutePathBuf>::new());
}
#[test]
fn config_load_error_marks_cloud_requirements_failures_for_relogin() {
let err = std::io::Error::other(CloudRequirementsLoadError::new(
@@ -1,3 +1,4 @@
use super::legacy_sandbox_compat::*;
use super::*;
#[derive(Clone)]
@@ -376,48 +377,72 @@ impl TurnRequestProcessor {
}
let cwd = params.cwd;
let workspace_roots = params.workspace_roots;
let mut workspace_roots = params.workspace_roots;
let approval_policy = params.approval_policy.map(AskForApproval::to_core);
let approvals_reviewer = params
.approvals_reviewer
.map(codex_app_server_protocol::ApprovalsReviewer::to_core);
let sandbox_policy = params.sandbox_policy.map(|p| p.to_core());
let legacy_sandbox_policy = params.sandbox_policy;
let sandbox_policy = None;
let (permission_profile, active_permission_profile) =
if let Some(permissions) = params.permissions {
let snapshot = thread.config_snapshot().await;
let mut overrides = ConfigOverrides {
cwd: cwd.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
..Default::default()
};
apply_permission_profile_selection_to_config_overrides(
&mut overrides,
Some(permissions),
);
let config = self
.config_manager
.load_for_cwd(
/*request_overrides*/ None,
overrides,
let selection = self
.validate_active_permission_profile_selection(
permissions,
cwd.clone(),
Some(snapshot.cwd.to_path_buf()),
)
.await
.map_err(|err| config_load_error(&err))?;
// Startup config is allowed to fall back when requirements
// disallow a configured profile. An explicit turn request
// is different: reject it before accepting user input.
if let Some(warning) = config.startup_warnings.iter().find(|warning| {
warning.contains("Configured value for `permission_profile` is disallowed")
}) {
return Err(invalid_request(format!(
"invalid turn context override: {warning}"
)));
}
.await?;
(
Some(config.permissions.permission_profile()),
config.permissions.active_permission_profile(),
Some(selection.permission_profile),
Some(selection.active_permission_profile),
)
} else if let Some(legacy_sandbox_policy) = legacy_sandbox_policy.as_ref() {
let snapshot = thread.config_snapshot().await;
let effective_cwd = resolve_cwd_against_fallback(cwd.as_deref(), &snapshot.cwd);
match resolve_legacy_sandbox_profile_selection(
legacy_sandbox_policy,
Some(CurrentPermissionProfile {
permission_profile: &snapshot.permission_profile,
workspace_roots: &snapshot.workspace_roots,
}),
&effective_cwd,
workspace_roots.as_deref(),
"sandboxPolicy",
)? {
LegacySandboxResolution::Noop {
workspace_roots: legacy_workspace_roots,
} => {
if workspace_roots.is_none() {
workspace_roots = legacy_workspace_roots;
}
(None, None)
}
LegacySandboxResolution::Selection(legacy_selection) => {
if workspace_roots.is_none() {
workspace_roots = legacy_selection.workspace_roots.clone();
}
let selection = self
.validate_active_permission_profile_selection(
legacy_selection.permissions.clone(),
cwd.clone(),
Some(snapshot.cwd.to_path_buf()),
)
.await?;
validate_legacy_sandbox_profile_selection(
&legacy_selection,
&selection,
&effective_cwd,
workspace_roots.as_deref(),
"sandboxPolicy",
)?;
(
Some(selection.permission_profile),
Some(selection.active_permission_profile),
)
}
}
} else {
(None, None)
};
@@ -522,6 +547,49 @@ impl TurnRequestProcessor {
Ok(TurnStartResponse { turn })
}
async fn validate_active_permission_profile_selection(
&self,
permissions: String,
cwd: Option<PathBuf>,
fallback_cwd: Option<PathBuf>,
) -> Result<ResolvedPermissionProfileSelection, JSONRPCErrorError> {
let mut overrides = ConfigOverrides {
cwd,
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
..Default::default()
};
apply_permission_profile_selection_to_config_overrides(&mut overrides, Some(permissions));
let config = self
.config_manager
.load_for_cwd(/*request_overrides*/ None, overrides, fallback_cwd)
.await
.map_err(|err| config_load_error(&err))?;
// Startup config is allowed to fall back when requirements disallow a
// configured profile. An explicit turn request is different: reject it
// before accepting user input.
if let Some(warning) = config.startup_warnings.iter().find(|warning| {
warning.contains("Configured value for `permission_profile` is disallowed")
}) {
return Err(invalid_request(format!(
"invalid turn context override: {warning}"
)));
}
let active_permission_profile =
config
.permissions
.active_permission_profile()
.ok_or_else(|| {
invalid_request(
"permission profile selection did not resolve to a named profile",
)
})?;
Ok(ResolvedPermissionProfileSelection {
permission_profile: config.permissions.permission_profile(),
active_permission_profile,
})
}
async fn thread_inject_items_response_inner(
&self,
params: ThreadInjectItemsParams,