feat: synchronize runtime workspace roots in thread settings

This commit is contained in:
Edward Frazer
2026-05-30 18:54:28 -07:00
parent dd74e192f6
commit 61299d9752
20 changed files with 439 additions and 27 deletions
@@ -194,6 +194,7 @@ pub(crate) fn thread_settings_from_config_snapshot(
) -> ThreadSettings {
ThreadSettings {
cwd: config_snapshot.cwd.clone(),
runtime_workspace_roots: Some(config_snapshot.workspace_roots.clone()),
approval_policy: config_snapshot.approval_policy.into(),
approvals_reviewer: config_snapshot.approvals_reviewer.into(),
sandbox_policy: thread_response_sandbox_policy(
@@ -222,6 +223,7 @@ pub(crate) fn thread_settings_from_core_snapshot(
snapshot.cwd.as_path(),
),
cwd: snapshot.cwd,
runtime_workspace_roots: Some(snapshot.runtime_workspace_roots),
approval_policy: snapshot.approval_policy.into(),
approvals_reviewer: snapshot.approvals_reviewer.into(),
active_permission_profile: thread_response_active_permission_profile(
@@ -3,6 +3,10 @@ use codex_app_server_protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::AdditionalContextEntry as CoreAdditionalContextEntry;
use codex_protocol::protocol::AdditionalContextKind as CoreAdditionalContextKind;
const MAX_RUNTIME_WORKSPACE_ROOTS: usize = 100;
const MAX_RUNTIME_WORKSPACE_ROOT_CHARS: usize = 4096;
const MAX_RUNTIME_WORKSPACE_ROOTS_TOTAL_CHARS: usize = 1 << 20;
#[derive(Clone)]
pub(crate) struct TurnRequestProcessor {
auth_manager: Arc<AuthManager>,
@@ -23,15 +27,38 @@ pub(crate) struct TurnRequestProcessor {
fn resolve_runtime_workspace_roots(
workspace_roots: Vec<PathBuf>,
base_cwd: &AbsolutePathBuf,
) -> Vec<AbsolutePathBuf> {
) -> Result<Vec<AbsolutePathBuf>, JSONRPCErrorError> {
let mut resolved_roots = Vec::new();
for path in workspace_roots {
let root = AbsolutePathBuf::resolve_path_against_base(path, base_cwd.as_path());
if root.as_path().to_string_lossy().chars().count() > MAX_RUNTIME_WORKSPACE_ROOT_CHARS {
return Err(invalid_params(format!(
"Runtime workspace root exceeds the maximum length of {MAX_RUNTIME_WORKSPACE_ROOT_CHARS} characters."
)));
}
if !resolved_roots.iter().any(|existing| existing == &root) {
resolved_roots.push(root);
}
}
resolved_roots
Ok(resolved_roots)
}
fn validate_runtime_workspace_roots(workspace_roots: &[PathBuf]) -> Result<(), JSONRPCErrorError> {
if workspace_roots.len() > MAX_RUNTIME_WORKSPACE_ROOTS {
return Err(invalid_params(format!(
"Runtime workspace roots exceed the maximum count of {MAX_RUNTIME_WORKSPACE_ROOTS}."
)));
}
let total_chars = workspace_roots
.iter()
.map(|path| path.to_string_lossy().chars().count())
.sum::<usize>();
if total_chars > MAX_RUNTIME_WORKSPACE_ROOTS_TOTAL_CHARS {
return Err(invalid_params(format!(
"Runtime workspace roots exceed the maximum total length of {MAX_RUNTIME_WORKSPACE_ROOTS_TOTAL_CHARS} characters."
)));
}
Ok(())
}
fn map_additional_context(
@@ -621,22 +648,23 @@ 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 runtime_workspace_roots =
if let Some(workspace_roots) = runtime_workspace_roots_request.clone() {
validate_runtime_workspace_roots(&workspace_roots)?;
let Some(snapshot) = snapshot.as_ref() else {
return Err(internal_error(format!(
"{method} runtime workspace roots missing thread snapshot"
)));
};
let base_cwd = match cwd.as_ref() {
Some(cwd) => AbsolutePathBuf::relative_to_current_dir(cwd)
.map_err(|err| invalid_request(format!("invalid cwd: {err}")))?,
None => snapshot.cwd.clone(),
};
Some(resolve_runtime_workspace_roots(workspace_roots, &base_cwd)?)
} else {
None
};
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 =
approval_policy.map(codex_app_server_protocol::AskForApproval::to_core);
let approvals_reviewer =
@@ -750,7 +778,7 @@ impl TurnRequestProcessor {
ThreadSettingsBuildParams {
method: "thread/settings/update",
cwd: params.cwd,
runtime_workspace_roots: None,
runtime_workspace_roots: params.runtime_workspace_roots,
approval_policy: params.approval_policy,
approvals_reviewer: params.approvals_reviewer,
sandbox_policy: params.sandbox_policy,
+3
View File
@@ -336,6 +336,9 @@ mod tests {
fn thread_settings(model: &str) -> ThreadSettings {
ThreadSettings {
cwd: AbsolutePathBuf::from_absolute_path("/tmp").expect("absolute path"),
runtime_workspace_roots: Some(vec![
AbsolutePathBuf::from_absolute_path("/tmp").expect("absolute path"),
]),
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::ReadOnly {