Make runtime workspace roots absolute in app-server API (#26552)

Stacked on #26532.

## Why

#26532 moves cwd normalization to the app-server/core boundary.
`runtimeWorkspaceRoots` still accepted raw paths in v2 requests and in
`ConfigOverrides`, which left core responsible for interpreting those
roots later. This makes runtime workspace roots follow the same
absolute-path boundary as cwd.

## What

- Change v2 `runtimeWorkspaceRoots` request fields for `thread/start`,
`thread/resume`, `thread/fork`, and `turn/start` to `AbsolutePathBuf`.
- Deduplicate already-absolute runtime roots in app-server handlers and
pass them through `ConfigOverrides.workspace_roots` as
`AbsolutePathBuf`.
- Update TUI and exec client request builders to pass absolute runtime
roots directly.
- Update app-server docs, schema fixtures, and focused tests for
absolute runtime roots.

## Testing

- `just test -p codex-app-server-protocol`
- `just test -p codex-app-server runtime_workspace_roots`
- `just test -p codex-core
session_permission_profile_rebinds_runtime_workspace_roots`
- `just test -p codex-tui app_server_session`
- `just test -p codex-exec`
This commit is contained in:
pakrym-oai authored and GitHub committed 2026-06-05 11:36:53 -07:00
1 parent 8d72fb6de9
commit 76c0a5379c
14 files changed
+73 -148

No files matched your search

@@ -56,17 +56,7 @@ 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<_>>();
let requested_runtime_workspace_roots = requested_runtime_workspace_roots.to_vec();
if requested_runtime_workspace_roots != config_snapshot.workspace_roots {
mismatch_details.push(format!(
"runtime_workspace_roots requested={requested_runtime_workspace_roots:?} active={:?}",
@@ -846,6 +836,7 @@ impl ThreadRequestProcessor {
));
}
let environment_selections = self.parse_environment_selections(environments)?;
let runtime_workspace_roots = runtime_workspace_roots.map(resolve_runtime_workspace_roots);
let mut typesafe_overrides = self.build_thread_config_overrides(
model,
model_provider,
@@ -1215,7 +1206,7 @@ impl ThreadRequestProcessor {
model_provider: Option<String>,
service_tier: Option<Option<String>>,
cwd: Option<String>,
runtime_workspace_roots: Option<Vec<PathBuf>>,
runtime_workspace_roots: Option<Vec<AbsolutePathBuf>>,
approval_policy: Option<codex_app_server_protocol::AskForApproval>,
approvals_reviewer: Option<codex_app_server_protocol::ApprovalsReviewer>,
sandbox: Option<SandboxMode>,
@@ -2483,6 +2474,7 @@ impl ThreadRequestProcessor {
};
let history_cwd = thread_history.session_cwd();
let runtime_workspace_roots = runtime_workspace_roots.map(resolve_runtime_workspace_roots);
let mut typesafe_overrides = self.build_thread_config_overrides(
model,
model_provider,
@@ -3201,6 +3193,7 @@ impl ThreadRequestProcessor {
} else {
Some(cli_overrides)
};
let runtime_workspace_roots = runtime_workspace_roots.map(resolve_runtime_workspace_roots);
let mut typesafe_overrides = self.build_thread_config_overrides(
model,
model_provider,
@@ -18,28 +18,6 @@ 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
}
fn resolve_request_cwd(cwd: Option<PathBuf>) -> Result<Option<AbsolutePathBuf>, JSONRPCErrorError> {
cwd.map(|cwd| {
AbsolutePathBuf::relative_to_current_dir(path_utils::normalize_for_native_workdir(cwd))
.map_err(|err| invalid_request(format!("invalid cwd: {err}")))
})
.transpose()
}
fn map_additional_context(
additional_context: Option<HashMap<String, AdditionalContextEntry>>,
) -> BTreeMap<String, CoreAdditionalContextEntry> {
@@ -66,7 +44,7 @@ fn map_additional_context(
struct ThreadSettingsBuildParams {
method: &'static str,
cwd: Option<AbsolutePathBuf>,
runtime_workspace_roots: Option<Vec<PathBuf>>,
runtime_workspace_roots: Option<Vec<AbsolutePathBuf>>,
approval_policy: Option<codex_app_server_protocol::AskForApproval>,
approvals_reviewer: Option<codex_app_server_protocol::ApprovalsReviewer>,
sandbox_policy: Option<codex_app_server_protocol::SandboxPolicy>,
@@ -533,7 +511,7 @@ impl TurnRequestProcessor {
// `thread/settings/update` only acknowledges that the update was queued.
// Clients that send dependent partial updates should wait for
// `thread/settings/updated` or combine the fields in one request.
let snapshot = if permissions.is_some() || runtime_workspace_roots_request.is_some() {
let snapshot = if permissions.is_some() {
Some(thread.config_snapshot().await)
} else {
None
@@ -552,22 +530,8 @@ impl TurnRequestProcessor {
|| collaboration_mode.is_some()
|| personality.is_some();
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(format!(
"{method} 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 runtime_workspace_roots =
runtime_workspace_roots_request.map(resolve_runtime_workspace_roots);
let approval_policy =
approval_policy.map(codex_app_server_protocol::AskForApproval::to_core);
let approvals_reviewer =
@@ -582,15 +546,11 @@ impl TurnRequestProcessor {
};
let overrides = ConfigOverrides {
cwd: cwd.as_ref().map(AbsolutePathBuf::to_path_buf),
workspace_roots: Some(runtime_workspace_roots_request.clone().unwrap_or_else(
|| {
snapshot
.workspace_roots
.iter()
.map(AbsolutePathBuf::to_path_buf)
.collect()
},
)),
workspace_roots: Some(
runtime_workspace_roots
.clone()
.unwrap_or_else(|| snapshot.workspace_roots.clone()),
),
default_permissions: Some(permissions),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),