app-server: use permission ids and runtime workspace roots

This commit is contained in:
Michael Bolin
2026-05-14 18:24:12 -07:00
parent bb493c23fd
commit efd6d658db
55 changed files with 775 additions and 601 deletions

View File

@@ -659,6 +659,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
}],
responsesapi_client_metadata: None,
cwd: None,
runtime_workspace_roots: None,
approval_policy: None,
sandbox_policy: None,
permissions: None,

View File

@@ -103,7 +103,6 @@ use codex_app_server_protocol::MockExperimentalMethodParams;
use codex_app_server_protocol::MockExperimentalMethodResponse;
use codex_app_server_protocol::ModelListParams;
use codex_app_server_protocol::ModelListResponse;
use codex_app_server_protocol::PermissionProfileModificationParams;
use codex_app_server_protocol::PermissionProfileSelectionParams;
use codex_app_server_protocol::PluginDetail;
use codex_app_server_protocol::PluginInstallParams;

View File

@@ -604,6 +604,7 @@ pub(super) async fn handle_pending_thread_resume_request(
permission_profile,
active_permission_profile,
cwd,
workspace_roots,
reasoning_effort,
..
} = pending.config_snapshot;
@@ -620,6 +621,7 @@ pub(super) async fn handle_pending_thread_resume_request(
model_provider: model_provider_id,
service_tier,
cwd,
runtime_workspace_roots: workspace_roots,
instruction_sources,
approval_policy: approval_policy.into(),
approvals_reviewer: approvals_reviewer.into(),

View File

@@ -59,6 +59,25 @@ fn collect_resume_override_mismatches(
));
}
}
if let Some(requested_runtime_workspace_roots) = request.runtime_workspace_roots.as_ref() {
let base_cwd = request
.cwd
.as_deref()
.map(|cwd| {
AbsolutePathBuf::resolve_path_against_base(cwd, config_snapshot.cwd.as_path())
})
.unwrap_or_else(|| config_snapshot.cwd.clone());
let requested_runtime_workspace_roots = requested_runtime_workspace_roots
.iter()
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, base_cwd.as_path()))
.collect::<Vec<_>>();
if requested_runtime_workspace_roots != config_snapshot.workspace_roots {
mismatch_details.push(format!(
"runtime_workspace_roots requested={requested_runtime_workspace_roots:?} active={:?}",
config_snapshot.workspace_roots
));
}
}
if let Some(requested_approval) = request.approval_policy.as_ref() {
let active_approval: AskForApproval = config_snapshot.approval_policy.into();
if requested_approval != &active_approval {
@@ -804,6 +823,7 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd,
runtime_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox,
@@ -837,6 +857,7 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd,
runtime_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox,
@@ -1173,6 +1194,7 @@ impl ThreadRequestProcessor {
model_provider: config_snapshot.model_provider_id,
service_tier: config_snapshot.service_tier,
cwd: config_snapshot.cwd,
runtime_workspace_roots: config_snapshot.workspace_roots,
instruction_sources,
approval_policy: config_snapshot.approval_policy.into(),
approvals_reviewer: config_snapshot.approvals_reviewer.into(),
@@ -1214,6 +1236,7 @@ impl ThreadRequestProcessor {
model_provider: Option<String>,
service_tier: Option<Option<String>>,
cwd: Option<String>,
runtime_workspace_roots: Option<Vec<PathBuf>>,
approval_policy: Option<codex_app_server_protocol::AskForApproval>,
approvals_reviewer: Option<codex_app_server_protocol::ApprovalsReviewer>,
sandbox: Option<SandboxMode>,
@@ -1227,6 +1250,7 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd: cwd.map(PathBuf::from),
workspace_roots: runtime_workspace_roots,
approval_policy: approval_policy
.map(codex_app_server_protocol::AskForApproval::to_core),
approvals_reviewer: approvals_reviewer
@@ -2351,6 +2375,7 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd,
runtime_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox,
@@ -2386,6 +2411,7 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd,
runtime_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox,
@@ -2523,6 +2549,7 @@ impl ThreadRequestProcessor {
model_provider: session_configured.model_provider_id,
service_tier: session_configured.service_tier,
cwd: session_configured.cwd,
runtime_workspace_roots: config_snapshot.workspace_roots,
instruction_sources,
approval_policy: session_configured.approval_policy.into(),
approvals_reviewer: session_configured.approvals_reviewer.into(),
@@ -2987,6 +3014,7 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd,
runtime_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox,
@@ -3052,6 +3080,7 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd,
runtime_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox,
@@ -3181,6 +3210,7 @@ impl ThreadRequestProcessor {
model_provider: session_configured.model_provider_id,
service_tier: session_configured.service_tier,
cwd: session_configured.cwd,
runtime_workspace_roots: config_snapshot.workspace_roots,
instruction_sources,
approval_policy: session_configured.approval_policy.into(),
approvals_reviewer: session_configured.approvals_reviewer.into(),

View File

@@ -636,6 +636,7 @@ mod thread_processor_behavior_tests {
model_provider: None,
service_tier: Some(Some("priority".to_string())),
cwd: None,
runtime_workspace_roots: None,
approval_policy: None,
approvals_reviewer: None,
sandbox: None,
@@ -656,6 +657,8 @@ mod thread_processor_behavior_tests {
permission_profile: codex_protocol::models::PermissionProfile::Disabled,
active_permission_profile: None,
cwd,
workspace_roots: Vec::new(),
profile_workspace_roots: Vec::new(),
ephemeral: false,
reasoning_effort: None,
personality: None,

View File

@@ -179,19 +179,23 @@ pub(super) fn apply_permission_profile_selection_to_config_overrides(
overrides: &mut ConfigOverrides,
permissions: Option<PermissionProfileSelectionParams>,
) {
let Some(PermissionProfileSelectionParams::Profile { id, modifications }) = permissions else {
let Some(selection) = permissions else {
return;
};
overrides.default_permissions = Some(id);
overrides
.additional_writable_roots
.extend(modifications.unwrap_or_default().into_iter().map(
|modification| match modification {
PermissionProfileModificationParams::AdditionalWritableRoot { path } => {
path.to_path_buf()
}
},
));
overrides.default_permissions = Some(selection.id().to_string());
if selection.legacy_additional_writable_roots().is_empty() {
return;
}
let legacy_roots = selection
.legacy_additional_writable_roots()
.iter()
.map(AbsolutePathBuf::to_path_buf);
if let Some(workspace_roots) = overrides.workspace_roots.as_mut() {
workspace_roots.extend(legacy_roots);
} else {
overrides.additional_writable_roots.extend(legacy_roots);
}
}
pub(super) fn thread_response_sandbox_policy(

View File

@@ -66,3 +66,43 @@ fn extract_conversation_summary_prefers_plain_user_messages() -> Result<()> {
assert_eq!(summary, expected);
Ok(())
}
#[test]
fn legacy_permission_profile_modifications_extend_runtime_roots() -> Result<()> {
let root = if cfg!(windows) {
AbsolutePathBuf::try_from("C:\\workspace-extra")?
} else {
AbsolutePathBuf::try_from("/workspace-extra")?
};
let selection = serde_json::from_value::<PermissionProfileSelectionParams>(json!({
"type": "profile",
"id": ":workspace",
"modifications": [
{
"type": "additionalWritableRoot",
"path": root.clone(),
}
],
}))?;
let mut overrides = ConfigOverrides::default();
apply_permission_profile_selection_to_config_overrides(&mut overrides, Some(selection.clone()));
assert_eq!(
overrides.default_permissions,
Some(":workspace".to_string())
);
assert_eq!(
overrides.additional_writable_roots,
vec![root.to_path_buf()]
);
let mut overrides = ConfigOverrides {
workspace_roots: Some(Vec::new()),
..ConfigOverrides::default()
};
apply_permission_profile_selection_to_config_overrides(&mut overrides, Some(selection));
assert_eq!(overrides.additional_writable_roots, Vec::<PathBuf>::new());
assert_eq!(overrides.workspace_roots, Some(vec![root.to_path_buf()]));
Ok(())
}

View File

@@ -16,6 +16,20 @@ pub(crate) struct TurnRequestProcessor {
skills_watcher: Arc<SkillsWatcher>,
}
fn resolve_runtime_workspace_roots(
workspace_roots: Vec<PathBuf>,
base_cwd: &AbsolutePathBuf,
) -> Vec<AbsolutePathBuf> {
let mut resolved_roots = Vec::new();
for path in workspace_roots {
let root = AbsolutePathBuf::resolve_path_against_base(path, base_cwd.as_path());
if !resolved_roots.iter().any(|existing| existing == &root) {
resolved_roots.push(root);
}
}
resolved_roots
}
impl TurnRequestProcessor {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
@@ -355,8 +369,16 @@ impl TurnRequestProcessor {
.map(V2UserInput::into_core)
.collect();
let turn_has_input = !mapped_items.is_empty();
let runtime_workspace_roots_request = params.runtime_workspace_roots.clone();
let snapshot = if params.permissions.is_some() || runtime_workspace_roots_request.is_some()
{
Some(thread.config_snapshot().await)
} else {
None
};
let has_any_overrides = params.cwd.is_some()
|| runtime_workspace_roots_request.is_some()
|| params.approval_policy.is_some()
|| params.approvals_reviewer.is_some()
|| params.sandbox_policy.is_some()
@@ -375,16 +397,45 @@ impl TurnRequestProcessor {
}
let cwd = params.cwd;
let runtime_workspace_roots = if let Some(workspace_roots) =
runtime_workspace_roots_request.clone()
{
let Some(snapshot) = snapshot.as_ref() else {
return Err(internal_error(
"turn/start runtime workspace roots missing thread snapshot",
));
};
let base_cwd = cwd
.as_ref()
.map(|cwd| AbsolutePathBuf::resolve_path_against_base(cwd, snapshot.cwd.as_path()))
.unwrap_or_else(|| snapshot.cwd.clone());
Some(resolve_runtime_workspace_roots(workspace_roots, &base_cwd))
} else {
None
};
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 (permission_profile, active_permission_profile) =
let (permission_profile, active_permission_profile, profile_workspace_roots) =
if let Some(permissions) = params.permissions {
let snapshot = thread.config_snapshot().await;
let Some(snapshot) = snapshot.as_ref() else {
return Err(internal_error(
"turn/start permission selection missing thread snapshot",
));
};
let mut overrides = ConfigOverrides {
cwd: cwd.clone(),
workspace_roots: Some(runtime_workspace_roots_request.clone().unwrap_or_else(
|| {
snapshot
.workspace_roots
.iter()
.map(AbsolutePathBuf::to_path_buf)
.collect()
},
)),
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()
@@ -413,11 +464,12 @@ impl TurnRequestProcessor {
)));
}
(
Some(config.permissions.effective_permission_profile()),
Some(config.permissions.permission_profile().clone()),
config.permissions.active_permission_profile(),
Some(config.permissions.profile_workspace_roots().to_vec()),
)
} else {
(None, None)
(None, None, None)
};
let model = params.model;
let effort = params.effort.map(Some);
@@ -432,11 +484,13 @@ impl TurnRequestProcessor {
thread
.validate_turn_context_overrides(CodexThreadTurnContextOverrides {
cwd: cwd.clone(),
workspace_roots: runtime_workspace_roots.clone(),
approval_policy,
approvals_reviewer,
sandbox_policy: sandbox_policy.clone(),
permission_profile: permission_profile.clone(),
active_permission_profile: active_permission_profile.clone(),
profile_workspace_roots: profile_workspace_roots.clone(),
windows_sandbox_level: None,
model: model.clone(),
effort,
@@ -457,6 +511,8 @@ impl TurnRequestProcessor {
final_output_json_schema: params.output_schema,
responsesapi_client_metadata: params.responsesapi_client_metadata,
cwd,
workspace_roots: runtime_workspace_roots,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox_policy,