mirror of
https://github.com/openai/codex.git
synced 2026-05-21 19:45:26 +00:00
Compare commits
2 Commits
jif/tool-1
...
fcoury/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4945c7bb0 | ||
|
|
a9bbaebff9 |
@@ -63,7 +63,7 @@ pub struct TurnStartParams {
|
||||
pub environments: Option<Vec<TurnEnvironmentParams>>,
|
||||
/// Override the working directory for this turn and subsequent turns.
|
||||
#[ts(optional = nullable)]
|
||||
pub cwd: Option<PathBuf>,
|
||||
pub cwd: Option<String>,
|
||||
/// Override the approval policy for this turn and subsequent turns.
|
||||
#[experimental(nested)]
|
||||
#[ts(optional = nullable)]
|
||||
|
||||
@@ -1262,7 +1262,7 @@ fn live_elicitation_timeout_pause(
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
sandbox_policy: Some(SandboxPolicy::DangerFullAccess),
|
||||
effort: Some(ReasoningEffort::High),
|
||||
cwd: Some(workspace),
|
||||
cwd: Some(workspace.to_string_lossy().into_owned()),
|
||||
..Default::default()
|
||||
})?;
|
||||
println!("< turn/start response: {turn_response:?}");
|
||||
|
||||
@@ -374,7 +374,7 @@ impl TurnRequestProcessor {
|
||||
));
|
||||
}
|
||||
|
||||
let cwd = params.cwd;
|
||||
let cwd = params.cwd.map(PathBuf::from);
|
||||
let approval_policy = params.approval_policy.map(AskForApproval::to_core);
|
||||
let approvals_reviewer = params
|
||||
.approvals_reviewer
|
||||
|
||||
@@ -774,7 +774,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
input: items.into_iter().map(Into::into).collect(),
|
||||
responsesapi_client_metadata: None,
|
||||
environments: None,
|
||||
cwd: Some(default_cwd),
|
||||
cwd: Some(default_cwd.to_string_lossy().into_owned()),
|
||||
approval_policy: Some(default_approval_policy.into()),
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: None,
|
||||
|
||||
@@ -13,9 +13,12 @@ use codex_app_server_protocol::MarketplaceRemoveResponse;
|
||||
use codex_app_server_protocol::MarketplaceUpgradeParams;
|
||||
use codex_app_server_protocol::MarketplaceUpgradeResponse;
|
||||
|
||||
use codex_app_server_client::TypedRequestError;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
impl App {
|
||||
pub(super) fn fetch_mcp_inventory(
|
||||
@@ -81,8 +84,9 @@ impl App {
|
||||
let request_handle = app_server.request_handle();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
let cwd = self.config.cwd.to_path_buf();
|
||||
let is_remote = app_server.is_remote();
|
||||
tokio::spawn(async move {
|
||||
let result = fetch_skills_list(request_handle, cwd)
|
||||
let result = fetch_skills_list(request_handle, cwd, is_remote)
|
||||
.await
|
||||
.map_err(|err| format!("{err:#}"));
|
||||
app_event_tx.send(AppEvent::SkillsListLoaded { result });
|
||||
@@ -94,8 +98,9 @@ impl App {
|
||||
let request_handle = app_server.request_handle();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
let cwd = self.config.cwd.to_path_buf();
|
||||
let is_remote = app_server.is_remote();
|
||||
tokio::spawn(async move {
|
||||
let result = fetch_hooks_list(request_handle, cwd.clone()).await;
|
||||
let result = fetch_hooks_list(request_handle, cwd.clone(), is_remote).await;
|
||||
let response = match result {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
@@ -133,8 +138,9 @@ impl App {
|
||||
pub(super) fn fetch_plugins_list(&mut self, app_server: &AppServerSession, cwd: PathBuf) {
|
||||
let request_handle = app_server.request_handle();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
let is_remote = app_server.is_remote();
|
||||
tokio::spawn(async move {
|
||||
let result = fetch_plugins_list(request_handle, cwd.clone())
|
||||
let result = fetch_plugins_list(request_handle, cwd.clone(), is_remote)
|
||||
.await
|
||||
.map_err(|err| err.to_string());
|
||||
app_event_tx.send(AppEvent::PluginsLoaded { cwd, result });
|
||||
@@ -144,8 +150,9 @@ impl App {
|
||||
pub(super) fn fetch_hooks_list(&mut self, app_server: &AppServerSession, cwd: PathBuf) {
|
||||
let request_handle = app_server.request_handle();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
let is_remote = app_server.is_remote();
|
||||
tokio::spawn(async move {
|
||||
let result = fetch_hooks_list(request_handle, cwd.clone())
|
||||
let result = fetch_hooks_list(request_handle, cwd.clone(), is_remote)
|
||||
.await
|
||||
.map_err(|err| err.to_string());
|
||||
app_event_tx.send(AppEvent::HooksLoaded { cwd, result });
|
||||
@@ -635,41 +642,91 @@ pub(super) async fn send_add_credits_nudge_email(
|
||||
Ok(response.status)
|
||||
}
|
||||
|
||||
async fn request_typed_with_local_path_base<T>(
|
||||
request_handle: &AppServerRequestHandle,
|
||||
is_remote: bool,
|
||||
method: &'static str,
|
||||
request: ClientRequest,
|
||||
local_path_base: &std::path::Path,
|
||||
) -> std::result::Result<T, TypedRequestError>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
if !is_remote {
|
||||
return request_handle.request_typed(request).await;
|
||||
}
|
||||
|
||||
let response =
|
||||
request_handle
|
||||
.request(request)
|
||||
.await
|
||||
.map_err(|source| TypedRequestError::Transport {
|
||||
method: method.to_string(),
|
||||
source,
|
||||
})?;
|
||||
let result = response.map_err(|source| TypedRequestError::Server {
|
||||
method: method.to_string(),
|
||||
source,
|
||||
})?;
|
||||
let path_base_guard = AbsolutePathBufGuard::new(local_path_base);
|
||||
let response =
|
||||
serde_json::from_value(result).map_err(|source| TypedRequestError::Deserialize {
|
||||
method: method.to_string(),
|
||||
source,
|
||||
})?;
|
||||
drop(path_base_guard);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub(super) async fn fetch_skills_list(
|
||||
request_handle: AppServerRequestHandle,
|
||||
cwd: PathBuf,
|
||||
is_remote: bool,
|
||||
) -> Result<SkillsListResponse> {
|
||||
let local_path_base = cwd.clone();
|
||||
let request_id = RequestId::String(format!("startup-skills-list-{}", Uuid::new_v4()));
|
||||
// Use the cloneable request handle so startup can issue this RPC from a background task without
|
||||
// extending a borrow of `AppServerSession` across the first frame render.
|
||||
request_handle
|
||||
.request_typed(ClientRequest::SkillsList {
|
||||
request_typed_with_local_path_base(
|
||||
&request_handle,
|
||||
is_remote,
|
||||
"skills/list",
|
||||
ClientRequest::SkillsList {
|
||||
request_id,
|
||||
params: SkillsListParams {
|
||||
cwds: vec![cwd],
|
||||
force_reload: true,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("skills/list failed in TUI")
|
||||
},
|
||||
local_path_base.as_path(),
|
||||
)
|
||||
.await
|
||||
.wrap_err("skills/list failed in TUI")
|
||||
}
|
||||
|
||||
pub(super) async fn fetch_plugins_list(
|
||||
request_handle: AppServerRequestHandle,
|
||||
cwd: PathBuf,
|
||||
is_remote: bool,
|
||||
) -> Result<PluginListResponse> {
|
||||
let local_path_base = cwd.clone();
|
||||
let cwd = AbsolutePathBuf::try_from(cwd).wrap_err("plugin list cwd must be absolute")?;
|
||||
let request_id = RequestId::String(format!("plugin-list-{}", Uuid::new_v4()));
|
||||
let mut response = request_handle
|
||||
.request_typed(ClientRequest::PluginList {
|
||||
let mut response = request_typed_with_local_path_base(
|
||||
&request_handle,
|
||||
is_remote,
|
||||
"plugin/list",
|
||||
ClientRequest::PluginList {
|
||||
request_id,
|
||||
params: PluginListParams {
|
||||
cwds: Some(vec![cwd]),
|
||||
marketplace_kinds: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("plugin/list failed in TUI")?;
|
||||
},
|
||||
local_path_base.as_path(),
|
||||
)
|
||||
.await
|
||||
.wrap_err("plugin/list failed in TUI")?;
|
||||
hide_cli_only_plugin_marketplaces(&mut response);
|
||||
Ok(response)
|
||||
}
|
||||
@@ -677,15 +734,22 @@ pub(super) async fn fetch_plugins_list(
|
||||
pub(super) async fn fetch_hooks_list(
|
||||
request_handle: AppServerRequestHandle,
|
||||
cwd: PathBuf,
|
||||
is_remote: bool,
|
||||
) -> Result<HooksListResponse> {
|
||||
let local_path_base = cwd.clone();
|
||||
let request_id = RequestId::String(format!("hooks-list-{}", Uuid::new_v4()));
|
||||
request_handle
|
||||
.request_typed(ClientRequest::HooksList {
|
||||
request_typed_with_local_path_base(
|
||||
&request_handle,
|
||||
is_remote,
|
||||
"hooks/list",
|
||||
ClientRequest::HooksList {
|
||||
request_id,
|
||||
params: HooksListParams { cwds: vec![cwd] },
|
||||
})
|
||||
.await
|
||||
.wrap_err("hooks/list failed in TUI")
|
||||
},
|
||||
local_path_base.as_path(),
|
||||
)
|
||||
.await
|
||||
.wrap_err("hooks/list failed in TUI")
|
||||
}
|
||||
|
||||
const CLI_HIDDEN_PLUGIN_MARKETPLACES: &[&str] = &["openai-bundled"];
|
||||
|
||||
@@ -616,12 +616,19 @@ impl App {
|
||||
Ok(true)
|
||||
}
|
||||
AppCommand::ListSkills { cwds, force_reload } => {
|
||||
let local_path_base = cwds
|
||||
.first()
|
||||
.map(std::path::PathBuf::as_path)
|
||||
.unwrap_or_else(|| self.config.cwd.as_path());
|
||||
self.handle_skills_list_result(
|
||||
app_server
|
||||
.skills_list(codex_app_server_protocol::SkillsListParams {
|
||||
cwds: cwds.clone(),
|
||||
force_reload: *force_reload,
|
||||
})
|
||||
.skills_list(
|
||||
codex_app_server_protocol::SkillsListParams {
|
||||
cwds: cwds.clone(),
|
||||
force_reload: *force_reload,
|
||||
},
|
||||
local_path_base,
|
||||
)
|
||||
.await,
|
||||
"failed to refresh skills",
|
||||
);
|
||||
|
||||
@@ -114,16 +114,55 @@ use codex_protocol::openai_models::ModelServiceTier;
|
||||
use codex_protocol::openai_models::ModelUpgrade;
|
||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::eyre::Result;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn bootstrap_request_error(context: &'static str, err: TypedRequestError) -> color_eyre::Report {
|
||||
color_eyre::eyre::eyre!("{context}: {err}")
|
||||
}
|
||||
|
||||
async fn request_typed_with_local_path_base<T>(
|
||||
client: &AppServerClient,
|
||||
is_remote: bool,
|
||||
method: &'static str,
|
||||
request: ClientRequest,
|
||||
local_path_base: &Path,
|
||||
) -> std::result::Result<T, TypedRequestError>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
if !is_remote {
|
||||
return client.request_typed(request).await;
|
||||
}
|
||||
|
||||
let response =
|
||||
client
|
||||
.request(request)
|
||||
.await
|
||||
.map_err(|source| TypedRequestError::Transport {
|
||||
method: method.to_string(),
|
||||
source,
|
||||
})?;
|
||||
let result = response.map_err(|source| TypedRequestError::Server {
|
||||
method: method.to_string(),
|
||||
source,
|
||||
})?;
|
||||
let path_base_guard = AbsolutePathBufGuard::new(local_path_base);
|
||||
let response =
|
||||
serde_json::from_value(result).map_err(|source| TypedRequestError::Deserialize {
|
||||
method: method.to_string(),
|
||||
source,
|
||||
})?;
|
||||
drop(path_base_guard);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Data collected during the TUI bootstrap phase that the main event loop
|
||||
/// needs to configure the UI, telemetry, and initial rate-limit prefetch.
|
||||
///
|
||||
@@ -148,7 +187,32 @@ pub(crate) struct AppServerBootstrap {
|
||||
pub(crate) struct AppServerSession {
|
||||
client: AppServerClient,
|
||||
next_request_id: i64,
|
||||
remote_cwd_override: Option<PathBuf>,
|
||||
remote_cwds: RemoteCwds,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RemoteCwds {
|
||||
explicit_cwd: Option<String>,
|
||||
by_thread: HashMap<ThreadId, String>,
|
||||
}
|
||||
|
||||
impl RemoteCwds {
|
||||
fn with_explicit_cwd(mut self, explicit_cwd: Option<PathBuf>) -> Self {
|
||||
self.explicit_cwd = explicit_cwd.map(|cwd| cwd.to_string_lossy().into_owned());
|
||||
self
|
||||
}
|
||||
|
||||
fn explicit_cwd(&self) -> Option<&str> {
|
||||
self.explicit_cwd.as_deref()
|
||||
}
|
||||
|
||||
fn cwd_for_thread(&self, thread_id: &ThreadId) -> Option<&str> {
|
||||
self.by_thread.get(thread_id).map(String::as_str)
|
||||
}
|
||||
|
||||
fn record_thread_cwd(&mut self, thread_id: ThreadId, cwd: String) {
|
||||
self.by_thread.insert(thread_id, cwd);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -176,17 +240,17 @@ impl AppServerSession {
|
||||
Self {
|
||||
client,
|
||||
next_request_id: 1,
|
||||
remote_cwd_override: None,
|
||||
remote_cwds: RemoteCwds::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_remote_cwd_override(mut self, remote_cwd_override: Option<PathBuf>) -> Self {
|
||||
self.remote_cwd_override = remote_cwd_override;
|
||||
self.remote_cwds = self.remote_cwds.with_explicit_cwd(remote_cwd_override);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn remote_cwd_override(&self) -> Option<&std::path::Path> {
|
||||
self.remote_cwd_override.as_deref()
|
||||
self.remote_cwds.explicit_cwd().map(std::path::Path::new)
|
||||
}
|
||||
|
||||
pub(crate) fn is_remote(&self) -> bool {
|
||||
@@ -336,22 +400,26 @@ impl AppServerSession {
|
||||
session_start_source: Option<ThreadStartSource>,
|
||||
) -> Result<AppServerStartedThread> {
|
||||
let request_id = self.next_request_id();
|
||||
let thread_params_mode = self.thread_params_mode();
|
||||
let response: ThreadStartResponse = self
|
||||
.client
|
||||
.request_typed(ClientRequest::ThreadStart {
|
||||
request_id,
|
||||
params: thread_start_params_from_config(
|
||||
config,
|
||||
self.thread_params_mode(),
|
||||
self.remote_cwd_override.as_deref(),
|
||||
session_start_source,
|
||||
),
|
||||
})
|
||||
.request_thread_lifecycle_response(
|
||||
"thread/start",
|
||||
ClientRequest::ThreadStart {
|
||||
request_id,
|
||||
params: thread_start_params_from_config(
|
||||
config,
|
||||
thread_params_mode,
|
||||
self.remote_cwds.explicit_cwd(),
|
||||
session_start_source,
|
||||
),
|
||||
},
|
||||
config,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
bootstrap_request_error("thread/start failed during TUI bootstrap", err)
|
||||
})?;
|
||||
started_thread_from_start_response(response, config, self.thread_params_mode()).await
|
||||
started_thread_from_start_response(response, config, thread_params_mode).await
|
||||
}
|
||||
|
||||
pub(crate) async fn resume_thread(
|
||||
@@ -360,17 +428,21 @@ impl AppServerSession {
|
||||
thread_id: ThreadId,
|
||||
) -> Result<AppServerStartedThread> {
|
||||
let request_id = self.next_request_id();
|
||||
let thread_params_mode = self.thread_params_mode();
|
||||
let response: ThreadResumeResponse = self
|
||||
.client
|
||||
.request_typed(ClientRequest::ThreadResume {
|
||||
request_id,
|
||||
params: thread_resume_params_from_config(
|
||||
config.clone(),
|
||||
thread_id,
|
||||
self.thread_params_mode(),
|
||||
self.remote_cwd_override.as_deref(),
|
||||
),
|
||||
})
|
||||
.request_thread_lifecycle_response(
|
||||
"thread/resume",
|
||||
ClientRequest::ThreadResume {
|
||||
request_id,
|
||||
params: thread_resume_params_from_config(
|
||||
config.clone(),
|
||||
thread_id,
|
||||
thread_params_mode,
|
||||
self.remote_cwds.cwd_for_thread(&thread_id),
|
||||
),
|
||||
},
|
||||
&config,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
bootstrap_request_error("thread/resume failed during TUI bootstrap", err)
|
||||
@@ -379,8 +451,7 @@ impl AppServerSession {
|
||||
.fork_parent_title_from_app_server(response.thread.forked_from_id.as_deref())
|
||||
.await;
|
||||
let mut started =
|
||||
started_thread_from_resume_response(response, &config, self.thread_params_mode())
|
||||
.await?;
|
||||
started_thread_from_resume_response(response, &config, thread_params_mode).await?;
|
||||
started.session.fork_parent_title = fork_parent_title;
|
||||
Ok(started)
|
||||
}
|
||||
@@ -391,17 +462,21 @@ impl AppServerSession {
|
||||
thread_id: ThreadId,
|
||||
) -> Result<AppServerStartedThread> {
|
||||
let request_id = self.next_request_id();
|
||||
let thread_params_mode = self.thread_params_mode();
|
||||
let response: ThreadForkResponse = self
|
||||
.client
|
||||
.request_typed(ClientRequest::ThreadFork {
|
||||
request_id,
|
||||
params: thread_fork_params_from_config(
|
||||
config.clone(),
|
||||
thread_id,
|
||||
self.thread_params_mode(),
|
||||
self.remote_cwd_override.as_deref(),
|
||||
),
|
||||
})
|
||||
.request_thread_lifecycle_response(
|
||||
"thread/fork",
|
||||
ClientRequest::ThreadFork {
|
||||
request_id,
|
||||
params: thread_fork_params_from_config(
|
||||
config.clone(),
|
||||
thread_id,
|
||||
thread_params_mode,
|
||||
self.remote_cwds.cwd_for_thread(&thread_id),
|
||||
),
|
||||
},
|
||||
&config,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
bootstrap_request_error("thread/fork failed during TUI bootstrap", err)
|
||||
@@ -410,7 +485,7 @@ impl AppServerSession {
|
||||
.fork_parent_title_from_app_server(response.thread.forked_from_id.as_deref())
|
||||
.await;
|
||||
let mut started =
|
||||
started_thread_from_fork_response(response, &config, self.thread_params_mode()).await?;
|
||||
started_thread_from_fork_response(response, &config, thread_params_mode).await?;
|
||||
started.session.fork_parent_title = fork_parent_title;
|
||||
Ok(started)
|
||||
}
|
||||
@@ -422,6 +497,58 @@ impl AppServerSession {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a thread lifecycle request as raw JSON so the TUI can capture the
|
||||
/// raw cwd before typed deserialization resolves paths against a local base.
|
||||
async fn request_thread_lifecycle_response<T>(
|
||||
&mut self,
|
||||
method: &'static str,
|
||||
request: ClientRequest,
|
||||
config: &Config,
|
||||
) -> std::result::Result<T, TypedRequestError>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let response =
|
||||
self.client
|
||||
.request(request)
|
||||
.await
|
||||
.map_err(|source| TypedRequestError::Transport {
|
||||
method: method.to_string(),
|
||||
source,
|
||||
})?;
|
||||
let result = response.map_err(|source| TypedRequestError::Server {
|
||||
method: method.to_string(),
|
||||
source,
|
||||
})?;
|
||||
let raw_cwd = self
|
||||
.is_remote()
|
||||
.then(|| thread_lifecycle_response_cwd(&result))
|
||||
.flatten();
|
||||
let raw_thread_id = raw_cwd
|
||||
.as_ref()
|
||||
.and_then(|_| thread_lifecycle_response_thread_id(&result));
|
||||
let path_base_guard = AbsolutePathBufGuard::new(config.cwd.as_path());
|
||||
let response =
|
||||
serde_json::from_value(result).map_err(|source| TypedRequestError::Deserialize {
|
||||
method: method.to_string(),
|
||||
source,
|
||||
})?;
|
||||
drop(path_base_guard);
|
||||
if let (Some(raw_thread_id), Some(raw_cwd)) = (raw_thread_id, raw_cwd) {
|
||||
match ThreadId::from_string(&raw_thread_id) {
|
||||
Ok(thread_id) => self.remote_cwds.record_thread_cwd(thread_id, raw_cwd),
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
thread_id = %raw_thread_id,
|
||||
%err,
|
||||
"Failed to record remote cwd for app-server thread"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn fork_parent_title_from_app_server(
|
||||
&mut self,
|
||||
forked_from_id: Option<&str>,
|
||||
@@ -536,36 +663,35 @@ impl AppServerSession {
|
||||
output_schema: Option<serde_json::Value>,
|
||||
) -> Result<TurnStartResponse> {
|
||||
let request_id = self.next_request_id();
|
||||
let (sandbox_policy, permissions) = turn_permissions_overrides(
|
||||
let local_path_base = cwd.clone();
|
||||
let remote_cwd = self.remote_cwds.cwd_for_thread(&thread_id);
|
||||
let params = turn_start_params(
|
||||
thread_id,
|
||||
items,
|
||||
cwd,
|
||||
&permission_profile,
|
||||
active_permission_profile,
|
||||
cwd.as_path(),
|
||||
self.thread_params_mode(),
|
||||
remote_cwd,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
model,
|
||||
effort,
|
||||
summary,
|
||||
service_tier,
|
||||
collaboration_mode,
|
||||
personality,
|
||||
output_schema,
|
||||
);
|
||||
self.client
|
||||
.request_typed(ClientRequest::TurnStart {
|
||||
request_id,
|
||||
params: TurnStartParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
input: items,
|
||||
responsesapi_client_metadata: None,
|
||||
environments: None,
|
||||
cwd: Some(cwd),
|
||||
approval_policy: Some(approval_policy),
|
||||
approvals_reviewer: Some(approvals_reviewer.into()),
|
||||
sandbox_policy,
|
||||
permissions,
|
||||
model: Some(model),
|
||||
service_tier,
|
||||
effort,
|
||||
summary,
|
||||
personality,
|
||||
output_schema,
|
||||
collaboration_mode,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("turn/start failed in TUI")
|
||||
request_typed_with_local_path_base(
|
||||
&self.client,
|
||||
self.is_remote(),
|
||||
"turn/start",
|
||||
ClientRequest::TurnStart { request_id, params },
|
||||
local_path_base.as_path(),
|
||||
)
|
||||
.await
|
||||
.wrap_err("turn/start failed in TUI")
|
||||
}
|
||||
|
||||
pub(crate) async fn turn_interrupt(
|
||||
@@ -861,12 +987,18 @@ impl AppServerSession {
|
||||
pub(crate) async fn skills_list(
|
||||
&mut self,
|
||||
params: SkillsListParams,
|
||||
local_path_base: &Path,
|
||||
) -> Result<SkillsListResponse> {
|
||||
let request_id = self.next_request_id();
|
||||
self.client
|
||||
.request_typed(ClientRequest::SkillsList { request_id, params })
|
||||
.await
|
||||
.wrap_err("skills/list failed in TUI")
|
||||
request_typed_with_local_path_base(
|
||||
&self.client,
|
||||
self.is_remote(),
|
||||
"skills/list",
|
||||
ClientRequest::SkillsList { request_id, params },
|
||||
local_path_base,
|
||||
)
|
||||
.await
|
||||
.wrap_err("skills/list failed in TUI")
|
||||
}
|
||||
|
||||
pub(crate) async fn reload_user_config(&mut self) -> Result<()> {
|
||||
@@ -1172,7 +1304,7 @@ fn permissions_selection_from_config(
|
||||
fn thread_start_params_from_config(
|
||||
config: &Config,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
remote_cwd_override: Option<&std::path::Path>,
|
||||
remote_cwd_override: Option<&str>,
|
||||
session_start_source: Option<ThreadStartSource>,
|
||||
) -> ThreadStartParams {
|
||||
let permissions = permissions_selection_from_config(config, thread_params_mode);
|
||||
@@ -1206,7 +1338,7 @@ fn thread_resume_params_from_config(
|
||||
config: Config,
|
||||
thread_id: ThreadId,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
remote_cwd_override: Option<&std::path::Path>,
|
||||
remote_cwd_override: Option<&str>,
|
||||
) -> ThreadResumeParams {
|
||||
let permissions = permissions_selection_from_config(&config, thread_params_mode);
|
||||
let sandbox = permissions
|
||||
@@ -1237,7 +1369,7 @@ fn thread_fork_params_from_config(
|
||||
config: Config,
|
||||
thread_id: ThreadId,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
remote_cwd_override: Option<&std::path::Path>,
|
||||
remote_cwd_override: Option<&str>,
|
||||
) -> ThreadForkParams {
|
||||
let permissions = permissions_selection_from_config(&config, thread_params_mode);
|
||||
let sandbox = permissions
|
||||
@@ -1271,13 +1403,89 @@ fn thread_fork_params_from_config(
|
||||
fn thread_cwd_from_config(
|
||||
config: &Config,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
remote_cwd_override: Option<&std::path::Path>,
|
||||
remote_cwd_override: Option<&str>,
|
||||
) -> Option<String> {
|
||||
match thread_params_mode {
|
||||
ThreadParamsMode::Embedded => Some(config.cwd.to_string_lossy().to_string()),
|
||||
ThreadParamsMode::Remote => {
|
||||
remote_cwd_override.map(|cwd| cwd.to_string_lossy().to_string())
|
||||
}
|
||||
ThreadParamsMode::Remote => remote_cwd_override.map(ToString::to_string),
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_lifecycle_response_cwd(result: &serde_json::Value) -> Option<String> {
|
||||
result
|
||||
.get("cwd")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.or_else(|| {
|
||||
result
|
||||
.get("thread")
|
||||
.and_then(|thread| thread.get("cwd"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
})
|
||||
.filter(|cwd| !cwd.is_empty())
|
||||
.map(ToString::to_string)
|
||||
}
|
||||
|
||||
fn thread_lifecycle_response_thread_id(result: &serde_json::Value) -> Option<String> {
|
||||
result
|
||||
.get("thread")
|
||||
.and_then(|thread| thread.get("id"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.filter(|thread_id| !thread_id.is_empty())
|
||||
.map(ToString::to_string)
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "turn/start mirrors the app-server request shape"
|
||||
)]
|
||||
fn turn_start_params(
|
||||
thread_id: ThreadId,
|
||||
items: Vec<UserInput>,
|
||||
cwd: PathBuf,
|
||||
permission_profile: &PermissionProfile,
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
remote_cwd: Option<&str>,
|
||||
approval_policy: AskForApproval,
|
||||
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
|
||||
model: String,
|
||||
effort: Option<codex_protocol::openai_models::ReasoningEffort>,
|
||||
summary: Option<codex_protocol::config_types::ReasoningSummary>,
|
||||
service_tier: Option<Option<String>>,
|
||||
collaboration_mode: Option<codex_protocol::config_types::CollaborationMode>,
|
||||
personality: Option<codex_protocol::config_types::Personality>,
|
||||
output_schema: Option<serde_json::Value>,
|
||||
) -> TurnStartParams {
|
||||
let remote_cwd_path = remote_cwd.map(Path::new);
|
||||
let permission_cwd = remote_cwd_path.unwrap_or(cwd.as_path());
|
||||
let (sandbox_policy, permissions) = turn_permissions_overrides(
|
||||
permission_profile,
|
||||
active_permission_profile,
|
||||
permission_cwd,
|
||||
thread_params_mode,
|
||||
);
|
||||
let cwd = match thread_params_mode {
|
||||
ThreadParamsMode::Embedded => Some(cwd.to_string_lossy().into_owned()),
|
||||
ThreadParamsMode::Remote => remote_cwd.map(ToString::to_string),
|
||||
};
|
||||
|
||||
TurnStartParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
input: items,
|
||||
responsesapi_client_metadata: None,
|
||||
environments: None,
|
||||
cwd,
|
||||
approval_policy: Some(approval_policy),
|
||||
approvals_reviewer: Some(approvals_reviewer.into()),
|
||||
sandbox_policy,
|
||||
permissions,
|
||||
model: Some(model),
|
||||
service_tier,
|
||||
effort,
|
||||
summary,
|
||||
personality,
|
||||
output_schema,
|
||||
collaboration_mode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1524,6 +1732,7 @@ mod tests {
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
use codex_utils_absolute_path::test_support::test_path_buf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
|
||||
async fn build_config(temp_dir: &TempDir) -> Config {
|
||||
@@ -1534,6 +1743,182 @@ mod tests {
|
||||
.expect("config should build")
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TestThreadLifecycleResponse {
|
||||
cwd: AbsolutePathBuf,
|
||||
instruction_sources: Vec<AbsolutePathBuf>,
|
||||
thread: TestThreadLifecycleThread,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TestThreadLifecycleThread {
|
||||
cwd: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_lifecycle_response_preserves_raw_cwd_when_paths_need_local_base() {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
let config = build_config(&temp_dir).await;
|
||||
let thread_id = ThreadId::new();
|
||||
let (server_cwd, instruction_source) = if cfg!(windows) {
|
||||
("/home/remote/project", "/home/remote/project/AGENTS.md")
|
||||
} else {
|
||||
(
|
||||
r"C:\Users\remote\project",
|
||||
r"C:\Users\remote\project\AGENTS.md",
|
||||
)
|
||||
};
|
||||
let response = json!({
|
||||
"cwd": server_cwd,
|
||||
"instructionSources": [instruction_source],
|
||||
"thread": {
|
||||
"id": thread_id.to_string(),
|
||||
"cwd": server_cwd
|
||||
}
|
||||
});
|
||||
assert!(
|
||||
serde_json::from_value::<TestThreadLifecycleResponse>(response.clone()).is_err(),
|
||||
"remote cwd should not decode without a local base path"
|
||||
);
|
||||
|
||||
let raw_cwd = thread_lifecycle_response_cwd(&response);
|
||||
let raw_thread_id = thread_lifecycle_response_thread_id(&response);
|
||||
let path_base_guard = AbsolutePathBufGuard::new(config.cwd.as_path());
|
||||
let decoded = serde_json::from_value::<TestThreadLifecycleResponse>(response)
|
||||
.expect("remote thread response should decode through local path base");
|
||||
drop(path_base_guard);
|
||||
|
||||
assert_eq!(raw_cwd.as_deref(), Some(server_cwd));
|
||||
assert_eq!(raw_thread_id, Some(thread_id.to_string()));
|
||||
assert!(decoded.cwd.as_path().is_absolute());
|
||||
assert!(decoded.thread.cwd.as_path().is_absolute());
|
||||
assert_eq!(decoded.instruction_sources.len(), 1);
|
||||
assert!(decoded.instruction_sources[0].as_path().is_absolute());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_cwds_are_thread_scoped() {
|
||||
let thread_a = ThreadId::new();
|
||||
let thread_b = ThreadId::new();
|
||||
let mut remote_cwds =
|
||||
RemoteCwds::default().with_explicit_cwd(Some(PathBuf::from("/repo/default")));
|
||||
|
||||
remote_cwds.record_thread_cwd(thread_a, "/repo/a".to_string());
|
||||
remote_cwds.record_thread_cwd(thread_b, "/repo/b".to_string());
|
||||
|
||||
assert_eq!(remote_cwds.explicit_cwd(), Some("/repo/default"));
|
||||
assert_eq!(remote_cwds.cwd_for_thread(&thread_a), Some("/repo/a"));
|
||||
assert_eq!(remote_cwds.cwd_for_thread(&thread_b), Some("/repo/b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_turn_start_uses_thread_scoped_cwd() {
|
||||
let thread_a = ThreadId::new();
|
||||
let thread_b = ThreadId::new();
|
||||
let mut remote_cwds = RemoteCwds::default();
|
||||
remote_cwds.record_thread_cwd(thread_a, "/repo/a".to_string());
|
||||
remote_cwds.record_thread_cwd(thread_b, "/repo/b".to_string());
|
||||
|
||||
let params = turn_start_params(
|
||||
thread_a,
|
||||
Vec::new(),
|
||||
test_path_buf("/local/project").abs().to_path_buf(),
|
||||
&PermissionProfile::read_only(),
|
||||
/*active_permission_profile*/ None,
|
||||
ThreadParamsMode::Remote,
|
||||
remote_cwds.cwd_for_thread(&thread_a),
|
||||
AskForApproval::Never,
|
||||
codex_protocol::config_types::ApprovalsReviewer::User,
|
||||
"gpt-5.4".to_string(),
|
||||
/*effort*/ None,
|
||||
/*summary*/ None,
|
||||
/*service_tier*/ None,
|
||||
/*collaboration_mode*/ None,
|
||||
/*personality*/ None,
|
||||
/*output_schema*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(params.cwd.as_deref(), Some("/repo/a"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_turn_start_omits_cwd_when_thread_cwd_is_unknown() {
|
||||
let params = turn_start_params(
|
||||
ThreadId::new(),
|
||||
Vec::new(),
|
||||
test_path_buf("/local/project").abs().to_path_buf(),
|
||||
&PermissionProfile::read_only(),
|
||||
/*active_permission_profile*/ None,
|
||||
ThreadParamsMode::Remote,
|
||||
/*remote_cwd*/ None,
|
||||
AskForApproval::Never,
|
||||
codex_protocol::config_types::ApprovalsReviewer::User,
|
||||
"gpt-5.4".to_string(),
|
||||
/*effort*/ None,
|
||||
/*summary*/ None,
|
||||
/*service_tier*/ None,
|
||||
/*collaboration_mode*/ None,
|
||||
/*personality*/ None,
|
||||
/*output_schema*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(params.cwd, None);
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TestSkillsListResponse {
|
||||
data: Vec<TestSkillsListEntry>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TestSkillsListEntry {
|
||||
cwd: AbsolutePathBuf,
|
||||
skills: Vec<TestSkillMetadata>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TestSkillMetadata {
|
||||
path: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skills_list_response_decodes_with_local_path_base() {
|
||||
let (server_cwd, skill_path) = if cfg!(windows) {
|
||||
(
|
||||
"/home/remote/project",
|
||||
"/home/remote/project/.codex/skills/demo/SKILL.md",
|
||||
)
|
||||
} else {
|
||||
(
|
||||
r"C:\Users\remote\project",
|
||||
r"C:\Users\remote\project\.codex\skills\demo\SKILL.md",
|
||||
)
|
||||
};
|
||||
let response = json!({
|
||||
"data": [{
|
||||
"cwd": server_cwd,
|
||||
"skills": [{
|
||||
"path": skill_path
|
||||
}]
|
||||
}]
|
||||
});
|
||||
assert!(
|
||||
serde_json::from_value::<TestSkillsListResponse>(response.clone()).is_err(),
|
||||
"remote skills/list paths should not decode without a local base path"
|
||||
);
|
||||
|
||||
let local_base = test_path_buf("/local/project").abs();
|
||||
let path_base_guard = AbsolutePathBufGuard::new(local_base.as_path());
|
||||
let decoded = serde_json::from_value::<TestSkillsListResponse>(response)
|
||||
.expect("skills/list should decode through a local path base");
|
||||
drop(path_base_guard);
|
||||
|
||||
assert!(decoded.data[0].cwd.as_path().is_absolute());
|
||||
assert!(decoded.data[0].skills[0].path.as_path().is_absolute());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_params_include_cwd_for_embedded_sessions() {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
@@ -1751,7 +2136,7 @@ mod tests {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
let config = build_config(&temp_dir).await;
|
||||
let thread_id = ThreadId::new();
|
||||
let remote_cwd = PathBuf::from("repo/on/server");
|
||||
let remote_cwd = "repo/on/server";
|
||||
let expected_sandbox = sandbox_mode_from_permission_profile(
|
||||
&config.permissions.permission_profile(),
|
||||
config.cwd.as_path(),
|
||||
@@ -1760,20 +2145,20 @@ mod tests {
|
||||
let start = thread_start_params_from_config(
|
||||
&config,
|
||||
ThreadParamsMode::Remote,
|
||||
Some(remote_cwd.as_path()),
|
||||
Some(remote_cwd),
|
||||
/*session_start_source*/ None,
|
||||
);
|
||||
let resume = thread_resume_params_from_config(
|
||||
config.clone(),
|
||||
thread_id,
|
||||
ThreadParamsMode::Remote,
|
||||
Some(remote_cwd.as_path()),
|
||||
Some(remote_cwd),
|
||||
);
|
||||
let fork = thread_fork_params_from_config(
|
||||
config,
|
||||
thread_id,
|
||||
ThreadParamsMode::Remote,
|
||||
Some(remote_cwd.as_path()),
|
||||
Some(remote_cwd),
|
||||
);
|
||||
|
||||
assert_eq!(start.cwd.as_deref(), Some("repo/on/server"));
|
||||
|
||||
Reference in New Issue
Block a user