Merge 63d0cc87d3 into sapling-pr-archive-bolinfest

This commit is contained in:
Michael Bolin
2026-05-08 14:27:56 -07:00
committed by GitHub
4 changed files with 183 additions and 11 deletions
@@ -191,12 +191,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)
{
@@ -2883,6 +2877,49 @@ impl ThreadRequestProcessor {
)
.await?;
let active_permission_profile = if let Some(permissions) = params.permissions.clone() {
let config_snapshot = existing_thread.config_snapshot().await;
Some(
self.validate_active_permission_profile_selection(
permissions,
/*request_overrides*/ None,
/*cwd*/ None,
Some(config_snapshot.cwd.to_path_buf()),
)
.await?,
)
} else {
None
};
if params.workspace_roots.is_some() || active_permission_profile.is_some() {
existing_thread
.update_turn_context_overrides(CodexThreadTurnContextOverrides {
cwd: None,
workspace_roots: params.workspace_roots.clone().map(|roots| {
roots
.into_iter()
.map(AbsolutePathBuf::into_path_buf)
.collect()
}),
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
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() {
@@ -24,6 +24,7 @@ use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::PatchApplyStatus;
use codex_app_server_protocol::PatchChangeKind;
use codex_app_server_protocol::PermissionProfileSelectionParams;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest;
@@ -1937,6 +1938,124 @@ async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> R
Ok(())
}
#[tokio::test]
async fn thread_resume_running_applies_workspace_roots_and_active_profile_name() -> Result<()> {
let server = responses::start_mock_server().await;
let first_response = responses::sse_response(responses::sse(vec![
responses::ev_response_created("resp-1"),
responses::ev_assistant_message("msg-1", "Done"),
responses::ev_completed("resp-1"),
]));
let second_response = responses::sse_response(responses::sse(vec![
responses::ev_response_created("resp-2"),
responses::ev_assistant_message("msg-2", "Done"),
responses::ev_completed("resp-2"),
]))
.set_delay(std::time::Duration::from_millis(500));
let _response_mock =
responses::mount_response_sequence(&server, vec![first_response, second_response]).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let workspace_root = codex_home.path().join("replacement-root");
std::fs::create_dir_all(&workspace_root)?;
let workspace_root = AbsolutePathBuf::from_absolute_path(workspace_root.canonicalize()?)?;
let mut primary = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??;
let start_id = primary
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.4".to_string()),
..Default::default()
})
.await?;
let start_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
primary.read_stream_until_response_message(RequestId::Integer(start_id)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
let seed_turn_id = primary
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![UserInput::Text {
text: "seed history".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
primary.read_stream_until_response_message(RequestId::Integer(seed_turn_id)),
)
.await??;
timeout(
DEFAULT_READ_TIMEOUT,
primary.read_stream_until_notification_message("turn/completed"),
)
.await??;
primary.clear_message_buffer();
let running_turn_id = primary
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![UserInput::Text {
text: "keep running".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
primary.read_stream_until_response_message(RequestId::Integer(running_turn_id)),
)
.await??;
timeout(
DEFAULT_READ_TIMEOUT,
primary.read_stream_until_notification_message("turn/started"),
)
.await??;
let resume_id = primary
.send_thread_resume_request(ThreadResumeParams {
thread_id: thread.id.clone(),
workspace_roots: Some(vec![workspace_root.clone()]),
permissions: Some(PermissionProfileSelectionParams::Profile {
id: ":workspace".to_string(),
}),
..Default::default()
})
.await?;
let resume_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
primary.read_stream_until_response_message(RequestId::Integer(resume_id)),
)
.await??;
let ThreadResumeResponse {
workspace_roots,
active_permission_profile,
..
} = to_response::<ThreadResumeResponse>(resume_resp)?;
assert_eq!(workspace_roots, vec![workspace_root]);
assert_eq!(
active_permission_profile
.as_ref()
.map(|profile| profile.id.as_str()),
Some(":workspace")
);
timeout(
DEFAULT_READ_TIMEOUT,
primary.read_stream_until_notification_message("turn/completed"),
)
.await??;
Ok(())
}
#[tokio::test]
async fn thread_resume_can_skip_turns_when_thread_is_running() -> Result<()> {
let server = responses::start_mock_server().await;
+2 -2
View File
@@ -757,7 +757,7 @@ impl ConfigToml {
SandboxMode::ReadOnly => PermissionProfile::read_only(),
SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() {
Some(SandboxWorkspaceWrite {
writable_roots,
writable_roots: _,
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
@@ -768,7 +768,7 @@ impl ConfigToml {
NetworkSandboxPolicy::Restricted
};
PermissionProfile::workspace_write_with(
writable_roots,
&[],
network_policy,
*exclude_tmpdir_env_var,
*exclude_slash_tmp,
+19 -3
View File
@@ -244,6 +244,23 @@ impl CodexThread {
&self,
overrides: CodexThreadTurnContextOverrides,
) -> ConstraintResult<()> {
let updates = self.turn_context_updates_from_overrides(overrides).await;
self.codex.session.validate_settings(&updates).await
}
/// Apply persistent thread context overrides immediately.
pub async fn update_turn_context_overrides(
&self,
overrides: CodexThreadTurnContextOverrides,
) -> ConstraintResult<()> {
let updates = self.turn_context_updates_from_overrides(overrides).await;
self.codex.session.update_settings(updates).await
}
async fn turn_context_updates_from_overrides(
&self,
overrides: CodexThreadTurnContextOverrides,
) -> SessionSettingsUpdate {
let CodexThreadTurnContextOverrides {
cwd,
workspace_roots,
@@ -270,7 +287,7 @@ impl CodexThread {
.with_updates(model, effort, /*developer_instructions*/ None)
};
let updates = SessionSettingsUpdate {
SessionSettingsUpdate {
cwd,
workspace_roots,
approval_policy,
@@ -284,8 +301,7 @@ impl CodexThread {
service_tier,
personality,
..Default::default()
};
self.codex.session.validate_settings(&updates).await
}
}
/// Use sparingly: this is intended to be removed soon.