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
@@ -614,6 +614,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<(
model_provider: None,
service_tier: None,
cwd: None,
runtime_workspace_roots: None,
approval_policy: None,
approvals_reviewer: None,
sandbox: None,
@@ -184,6 +184,79 @@ async fn thread_resume_rejects_unmaterialized_thread() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn turn_start_updates_runtime_workspace_roots_for_loaded_thread() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let extra_root_tmp = TempDir::new()?;
let extra_root = extra_root_tmp.path().join("extra-root");
std::fs::create_dir_all(&extra_root)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.4".to_string()),
..Default::default()
})
.await?;
let start_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
let turn_id = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
}],
runtime_workspace_roots: Some(vec![extra_root.clone(), extra_root.join(".")]),
..Default::default()
})
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(turn_id)),
)
.await??;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
let resume_id = mcp
.send_thread_resume_request(ThreadResumeParams {
thread_id: thread.id,
exclude_turns: true,
..Default::default()
})
.await?;
let resume_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(resume_id)),
)
.await??;
let ThreadResumeResponse {
runtime_workspace_roots,
..
} = to_response::<ThreadResumeResponse>(resume_resp)?;
assert_eq!(
runtime_workspace_roots,
vec![AbsolutePathBuf::from_absolute_path(extra_root)?]
);
Ok(())
}
#[tokio::test]
async fn thread_goal_get_rejects_unmaterialized_thread() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
@@ -236,6 +236,48 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn thread_start_resolves_runtime_workspace_roots_against_cwd() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?;
let cwd_tmp = TempDir::new()?;
let cwd = cwd_tmp.path().to_path_buf();
let relative_root = PathBuf::from("extra-root");
std::fs::create_dir_all(cwd.join(&relative_root))?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let req_id = mcp
.send_thread_start_request(ThreadStartParams {
cwd: Some(cwd.to_string_lossy().to_string()),
runtime_workspace_roots: Some(vec![relative_root.clone()]),
..Default::default()
})
.await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
)
.await??;
let ThreadStartResponse {
cwd: response_cwd,
runtime_workspace_roots,
..
} = to_response::<ThreadStartResponse>(resp)?;
assert_eq!(response_cwd, cwd.abs());
assert_eq!(
runtime_workspace_roots,
vec![cwd_tmp.path().join(relative_root).abs()]
);
Ok(())
}
#[tokio::test]
async fn thread_start_rejects_unknown_environment_as_invalid_request() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
@@ -34,7 +34,6 @@ use codex_app_server_protocol::JSONRPCNotification;
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::ServerRequest;
use codex_app_server_protocol::ServerRequestResolvedNotification;
@@ -780,10 +779,11 @@ async fn turn_start_rejects_invalid_permission_selection_before_starting_turn()
text: "Hello".to_string(),
text_elements: Vec::new(),
}],
permissions: Some(PermissionProfileSelectionParams::Profile {
id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(),
modifications: None,
}),
permissions: Some(
BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS
.to_string()
.into(),
),
..Default::default()
})
.await?;
@@ -1891,6 +1891,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
}],
responsesapi_client_metadata: None,
cwd: Some(first_cwd.clone()),
runtime_workspace_roots: None,
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
@@ -1932,6 +1933,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
}],
responsesapi_client_metadata: None,
cwd: Some(second_cwd.clone()),
runtime_workspace_roots: None,
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess),