mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Pair thread environment settings (#26687)
## Why Thread cwd and environment selections are a single logical setting in core: updating one without the other can silently desynchronize the next-turn execution context. This change makes that relationship explicit in the internal thread settings flow while preserving the existing app-server public API shape. ## What changed - Moved the cwd/environment pair through internal `ThreadSettingsOverrides.environment_settings` instead of a top-level internal `cwd` field. - Kept `thread/settings/update` public params unchanged, with app-server translating top-level `cwd` into the paired internal settings shape. - Moved `Op::UserInput` environment overrides into thread settings so user turns and settings updates use the same core path. - Updated core, app-server, MCP, memories, sample, and test callsites to construct the paired settings shape. ## Verification - `git diff --check` - Local test run starting after PR creation.
This commit is contained in:
@@ -458,7 +458,6 @@ async fn send_input_submits_user_message() {
|
||||
let expected = (
|
||||
thread_id,
|
||||
Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello from tests".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -797,7 +796,6 @@ async fn spawn_agent_creates_thread_and_sends_prompt() {
|
||||
let expected = (
|
||||
thread_id,
|
||||
Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "spawned".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -1017,7 +1015,6 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||
let expected = (
|
||||
child_thread_id,
|
||||
Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "child task".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
|
||||
@@ -196,7 +196,6 @@ pub(crate) async fn run_codex_thread_one_shot(
|
||||
|
||||
// Send the initial input to kick off the one-shot turn.
|
||||
io.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: input,
|
||||
final_output_json_schema,
|
||||
responsesapi_client_metadata: None,
|
||||
|
||||
@@ -32,6 +32,7 @@ use codex_protocol::protocol::ThreadMemoryMode;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
use codex_protocol::protocol::TokenUsageInfo;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_thread_store::StoredThread;
|
||||
@@ -59,7 +60,7 @@ pub struct ThreadConfigSnapshot {
|
||||
pub approvals_reviewer: ApprovalsReviewer,
|
||||
pub permission_profile: PermissionProfile,
|
||||
pub active_permission_profile: Option<ActivePermissionProfile>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub environments: TurnEnvironmentSelections,
|
||||
pub workspace_roots: Vec<AbsolutePathBuf>,
|
||||
pub profile_workspace_roots: Vec<AbsolutePathBuf>,
|
||||
pub ephemeral: bool,
|
||||
@@ -114,10 +115,18 @@ impl TryStartTurnIfIdleError {
|
||||
}
|
||||
|
||||
impl ThreadConfigSnapshot {
|
||||
pub fn cwd(&self) -> &AbsolutePathBuf {
|
||||
&self.environments.legacy_fallback_cwd
|
||||
}
|
||||
|
||||
pub fn environment_selections(&self) -> &[TurnEnvironmentSelection] {
|
||||
&self.environments.environments
|
||||
}
|
||||
|
||||
pub fn sandbox_policy(&self) -> SandboxPolicy {
|
||||
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
|
||||
&self.permission_profile,
|
||||
self.cwd.as_path(),
|
||||
self.cwd().as_path(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -125,7 +134,7 @@ impl ThreadConfigSnapshot {
|
||||
/// Thread settings overrides that app-server validates before starting a turn.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CodexThreadSettingsOverrides {
|
||||
pub cwd: Option<AbsolutePathBuf>,
|
||||
pub environments: Option<TurnEnvironmentSelections>,
|
||||
pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
pub profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
pub approval_policy: Option<AskForApproval>,
|
||||
@@ -330,7 +339,7 @@ impl CodexThread {
|
||||
overrides: CodexThreadSettingsOverrides,
|
||||
) -> SessionSettingsUpdate {
|
||||
let CodexThreadSettingsOverrides {
|
||||
cwd,
|
||||
environments,
|
||||
workspace_roots,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
@@ -357,7 +366,7 @@ impl CodexThread {
|
||||
};
|
||||
|
||||
SessionSettingsUpdate {
|
||||
cwd,
|
||||
environments,
|
||||
workspace_roots,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
|
||||
@@ -721,19 +721,27 @@ async fn run_review_on_session(
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let guardian_permission_profile = PermissionProfile::read_only();
|
||||
let parent_turn_environments = params.parent_turn.environments.to_selections();
|
||||
let parent_turn_legacy_fallback_cwd = params
|
||||
.parent_turn
|
||||
.environments
|
||||
.primary()
|
||||
.map(|environment| environment.cwd.clone())
|
||||
.unwrap_or_else(|| params.parent_turn.config.cwd.clone());
|
||||
|
||||
let submit_result = run_before_review_deadline(
|
||||
deadline,
|
||||
params.external_cancel.as_ref(),
|
||||
Box::pin(review_session.codex.submit(Op::UserInput {
|
||||
items: prompt_items.items,
|
||||
environments: None,
|
||||
final_output_json_schema: Some(params.schema.clone()),
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
#[allow(deprecated)]
|
||||
cwd: Some(params.parent_turn.cwd.clone()),
|
||||
environments: Some(codex_protocol::protocol::TurnEnvironmentSelections::new(
|
||||
parent_turn_legacy_fallback_cwd,
|
||||
parent_turn_environments,
|
||||
)),
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
sandbox_policy: None,
|
||||
permission_profile: Some(guardian_permission_profile),
|
||||
|
||||
@@ -122,7 +122,7 @@ async fn thread_settings_update(
|
||||
thread_settings: ThreadSettingsOverrides,
|
||||
) -> SessionSettingsUpdate {
|
||||
let ThreadSettingsOverrides {
|
||||
cwd,
|
||||
environments,
|
||||
workspace_roots,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
@@ -151,7 +151,7 @@ async fn thread_settings_update(
|
||||
}
|
||||
};
|
||||
SessionSettingsUpdate {
|
||||
cwd,
|
||||
environments,
|
||||
workspace_roots,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
@@ -173,6 +173,7 @@ async fn thread_settings_applied_event(sess: &Session) -> EventMsg {
|
||||
let state = sess.state.lock().await;
|
||||
state.session_configuration.thread_config_snapshot()
|
||||
};
|
||||
let cwd = snapshot.cwd().clone();
|
||||
EventMsg::ThreadSettingsApplied(ThreadSettingsAppliedEvent {
|
||||
thread_settings: ThreadSettingsSnapshot {
|
||||
model: snapshot.model,
|
||||
@@ -182,7 +183,7 @@ async fn thread_settings_applied_event(sess: &Session) -> EventMsg {
|
||||
approvals_reviewer: snapshot.approvals_reviewer,
|
||||
permission_profile: snapshot.permission_profile,
|
||||
active_permission_profile: snapshot.active_permission_profile,
|
||||
cwd: snapshot.cwd,
|
||||
cwd,
|
||||
reasoning_effort: snapshot.reasoning_effort,
|
||||
reasoning_summary: snapshot.reasoning_summary,
|
||||
personality: snapshot.personality,
|
||||
@@ -200,7 +201,6 @@ pub(super) async fn user_input_or_turn_inner(
|
||||
) {
|
||||
let Op::UserInput {
|
||||
items,
|
||||
environments,
|
||||
final_output_json_schema,
|
||||
responsesapi_client_metadata,
|
||||
additional_context,
|
||||
@@ -216,7 +216,6 @@ pub(super) async fn user_input_or_turn_inner(
|
||||
SessionSettingsUpdate::default()
|
||||
};
|
||||
updates.final_output_json_schema = Some(final_output_json_schema);
|
||||
updates.environments = environments;
|
||||
|
||||
let Ok(current_context) = sess.new_turn_with_sub_id(sub_id.clone(), updates).await else {
|
||||
// new_turn_with_sub_id already emits the error event.
|
||||
|
||||
@@ -116,6 +116,7 @@ use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
use codex_protocol::protocol::TurnContextNetworkItem;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_protocol::request_permissions::PermissionGrantScope;
|
||||
use codex_protocol::request_permissions::RequestPermissionProfile;
|
||||
@@ -602,11 +603,13 @@ impl Codex {
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: session_permission_profile_state_from_config(&config)?,
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(
|
||||
config.cwd.clone(),
|
||||
environment_selections.to_selections(),
|
||||
),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: environment_selections.to_selections(),
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name,
|
||||
app_server_client_name: None,
|
||||
@@ -814,7 +817,10 @@ impl Codex {
|
||||
|
||||
pub(crate) async fn thread_environment_selections(&self) -> Vec<TurnEnvironmentSelection> {
|
||||
let state = self.session.state.lock().await;
|
||||
state.session_configuration.environments.clone()
|
||||
state
|
||||
.session_configuration
|
||||
.environment_selections()
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
pub(crate) fn state_db(&self) -> Option<state_db::StateDbHandle> {
|
||||
@@ -1114,7 +1120,6 @@ impl Session {
|
||||
self,
|
||||
self.next_internal_sub_id(),
|
||||
Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text,
|
||||
text_elements: Vec::new(),
|
||||
@@ -1414,12 +1419,12 @@ impl Session {
|
||||
.then(|| Self::build_effective_session_config(&state.session_configuration));
|
||||
let new_config =
|
||||
notify_config_contributors.then(|| Self::build_effective_session_config(&updated));
|
||||
let previous_cwd = state.session_configuration.cwd.clone();
|
||||
let previous_cwd = state.session_configuration.cwd().clone();
|
||||
let previous_permission_profile = state.session_configuration.permission_profile();
|
||||
let updated_permission_profile = updated.permission_profile();
|
||||
let permission_profile_changed =
|
||||
previous_permission_profile != updated_permission_profile;
|
||||
let next_cwd = updated.cwd.clone();
|
||||
let next_cwd = updated.cwd().clone();
|
||||
let codex_home = updated.codex_home.clone();
|
||||
let session_source = updated.session_source.clone();
|
||||
state.session_configuration = updated;
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
use std::sync::OnceLock;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
@@ -75,11 +76,9 @@ pub(crate) struct SessionConfiguration {
|
||||
pub(super) permission_profile_state: PermissionProfileState,
|
||||
pub(super) windows_sandbox_level: WindowsSandboxLevel,
|
||||
|
||||
/// Absolute working directory that should be treated as the *root* of the
|
||||
/// session. All relative paths supplied by the model as well as the
|
||||
/// execution sandbox are resolved against this directory **instead** of
|
||||
/// the process-wide current working directory.
|
||||
pub(super) cwd: AbsolutePathBuf,
|
||||
/// Sticky thread-level environment selections plus the legacy cwd used
|
||||
/// when a turn does not select an environment.
|
||||
pub(super) environments: TurnEnvironmentSelections,
|
||||
/// Thread-scoped runtime workspace roots for materializing symbolic
|
||||
/// workspace permissions at session runtime.
|
||||
pub(super) workspace_roots: Vec<AbsolutePathBuf>,
|
||||
@@ -87,8 +86,6 @@ pub(crate) struct SessionConfiguration {
|
||||
pub(super) codex_home: AbsolutePathBuf,
|
||||
/// Optional user-facing name for the thread, updated during the session.
|
||||
pub(super) thread_name: Option<String>,
|
||||
/// Sticky environments for turns that do not provide a turn-local override.
|
||||
pub(super) environments: Vec<TurnEnvironmentSelection>,
|
||||
|
||||
// TODO(pakrym): Remove config from here
|
||||
pub(super) original_config_do_not_use: Arc<Config>,
|
||||
@@ -110,6 +107,14 @@ pub(crate) struct SessionConfiguration {
|
||||
}
|
||||
|
||||
impl SessionConfiguration {
|
||||
pub(super) fn cwd(&self) -> &AbsolutePathBuf {
|
||||
&self.environments.legacy_fallback_cwd
|
||||
}
|
||||
|
||||
pub(super) fn environment_selections(&self) -> &[TurnEnvironmentSelection] {
|
||||
&self.environments.environments
|
||||
}
|
||||
|
||||
pub(crate) fn codex_home(&self) -> &AbsolutePathBuf {
|
||||
&self.codex_home
|
||||
}
|
||||
@@ -153,7 +158,7 @@ impl SessionConfiguration {
|
||||
let permission_profile = self.permission_profile();
|
||||
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
&self.cwd,
|
||||
self.cwd(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -176,7 +181,7 @@ impl SessionConfiguration {
|
||||
approvals_reviewer: self.approvals_reviewer,
|
||||
permission_profile: self.permission_profile(),
|
||||
active_permission_profile: self.active_permission_profile(),
|
||||
cwd: self.cwd.clone(),
|
||||
environments: self.environments.clone(),
|
||||
workspace_roots: self.workspace_roots.clone(),
|
||||
profile_workspace_roots: self.profile_workspace_roots().to_vec(),
|
||||
ephemeral: self.original_config_do_not_use.ephemeral,
|
||||
@@ -199,11 +204,11 @@ impl SessionConfiguration {
|
||||
let legacy_file_system_projection =
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
|
||||
¤t_sandbox_policy,
|
||||
&self.cwd,
|
||||
self.cwd(),
|
||||
¤t_file_system_sandbox_policy,
|
||||
);
|
||||
let file_system_policy_matches_legacy = current_file_system_sandbox_policy
|
||||
.is_semantically_equivalent_to(&legacy_file_system_projection, &self.cwd);
|
||||
.is_semantically_equivalent_to(&legacy_file_system_projection, self.cwd());
|
||||
let file_system_policy_has_rebindable_project_root_write =
|
||||
current_file_system_sandbox_policy
|
||||
.entries
|
||||
@@ -249,18 +254,21 @@ impl SessionConfiguration {
|
||||
next_configuration.windows_sandbox_level = windows_sandbox_level;
|
||||
}
|
||||
|
||||
let absolute_cwd = updates.cwd.clone().unwrap_or_else(|| self.cwd.clone());
|
||||
|
||||
let cwd_changed = absolute_cwd.as_path() != self.cwd.as_path();
|
||||
next_configuration.cwd = absolute_cwd;
|
||||
let current_cwd = self.cwd().clone();
|
||||
let next_environments = updates
|
||||
.environments
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.environments.clone());
|
||||
let cwd_changed = next_environments.legacy_fallback_cwd.as_path() != current_cwd.as_path();
|
||||
next_configuration.environments = next_environments;
|
||||
if let Some(workspace_roots) = updates.workspace_roots.clone() {
|
||||
next_configuration.workspace_roots = workspace_roots;
|
||||
} else if cwd_changed && self.workspace_roots.contains(&self.cwd) {
|
||||
} else if cwd_changed && self.workspace_roots.contains(¤t_cwd) {
|
||||
let mut retargeted_workspace_roots =
|
||||
Vec::with_capacity(next_configuration.workspace_roots.len());
|
||||
for root in &self.workspace_roots {
|
||||
let root = if root == &self.cwd {
|
||||
next_configuration.cwd.clone()
|
||||
let root = if root == ¤t_cwd {
|
||||
next_configuration.cwd().clone()
|
||||
} else {
|
||||
root.clone()
|
||||
};
|
||||
@@ -317,7 +325,7 @@ impl SessionConfiguration {
|
||||
let file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
|
||||
&sandbox_policy,
|
||||
&next_configuration.cwd,
|
||||
next_configuration.cwd(),
|
||||
¤t_file_system_sandbox_policy,
|
||||
);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
|
||||
@@ -340,7 +348,7 @@ impl SessionConfiguration {
|
||||
let file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
|
||||
¤t_sandbox_policy,
|
||||
&next_configuration.cwd,
|
||||
next_configuration.cwd(),
|
||||
¤t_file_system_sandbox_policy,
|
||||
);
|
||||
next_configuration
|
||||
@@ -401,7 +409,7 @@ impl SessionConfiguration {
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct SessionSettingsUpdate {
|
||||
pub(crate) cwd: Option<AbsolutePathBuf>,
|
||||
pub(crate) environments: Option<TurnEnvironmentSelections>,
|
||||
pub(crate) workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
pub(crate) profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
pub(crate) approval_policy: Option<AskForApproval>,
|
||||
@@ -414,10 +422,6 @@ pub(crate) struct SessionSettingsUpdate {
|
||||
pub(crate) reasoning_summary: Option<ReasoningSummaryConfig>,
|
||||
pub(crate) service_tier: Option<Option<String>>,
|
||||
pub(crate) final_output_json_schema: Option<Option<Value>>,
|
||||
/// Turn-local environment override. `None` inherits the sticky thread
|
||||
/// environments stored on `SessionConfiguration`; `Some([])` explicitly
|
||||
/// disables environments for this turn.
|
||||
pub(crate) environments: Option<Vec<TurnEnvironmentSelection>>,
|
||||
pub(crate) personality: Option<Personality>,
|
||||
pub(crate) app_server_client_name: Option<String>,
|
||||
pub(crate) app_server_client_version: Option<String>,
|
||||
@@ -634,7 +638,7 @@ impl Session {
|
||||
Arc::clone(&environment_manager),
|
||||
Arc::clone(&plugins_manager),
|
||||
Arc::clone(&skills_manager),
|
||||
session_configuration.environments.clone(),
|
||||
session_configuration.environment_selections().to_vec(),
|
||||
)
|
||||
.instrument(info_span!(
|
||||
"session_init.plugin_skill_warmup",
|
||||
@@ -686,7 +690,7 @@ impl Session {
|
||||
nickname: session_configuration.session_source.get_nickname(),
|
||||
agent_role: session_configuration.session_source.get_agent_role(),
|
||||
session_source: session_configuration.session_source.clone(),
|
||||
cwd: session_configuration.cwd.to_path_buf(),
|
||||
cwd: session_configuration.cwd().to_path_buf(),
|
||||
rollout_path: rollout_path.clone(),
|
||||
model: session_configuration.collaboration_mode.model().to_string(),
|
||||
provider_name: config.model_provider_id.clone(),
|
||||
@@ -790,7 +794,7 @@ impl Session {
|
||||
/*inc*/ 1,
|
||||
&[(
|
||||
"is_git",
|
||||
if get_git_repo_root(&session_configuration.cwd).is_some() {
|
||||
if get_git_repo_root(session_configuration.cwd()).is_some() {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
@@ -809,7 +813,7 @@ impl Session {
|
||||
config.permissions.approval_policy.value(),
|
||||
config
|
||||
.permissions
|
||||
.legacy_sandbox_policy(session_configuration.cwd.as_path()),
|
||||
.legacy_sandbox_policy(session_configuration.cwd().as_path()),
|
||||
mcp_servers.keys().map(String::as_str).collect(),
|
||||
);
|
||||
|
||||
@@ -844,7 +848,7 @@ impl Session {
|
||||
ShellSnapshot::start_snapshotting(
|
||||
config.codex_home.clone(),
|
||||
thread_id,
|
||||
session_configuration.cwd.clone(),
|
||||
session_configuration.cwd().clone(),
|
||||
&mut default_shell,
|
||||
session_telemetry.clone(),
|
||||
state_db_ctx.clone(),
|
||||
@@ -1089,7 +1093,7 @@ impl Session {
|
||||
approvals_reviewer: session_configuration.approvals_reviewer,
|
||||
permission_profile: session_configuration.permission_profile(),
|
||||
active_permission_profile: session_configuration.active_permission_profile(),
|
||||
cwd: session_configuration.cwd.clone(),
|
||||
cwd: session_configuration.cwd().clone(),
|
||||
reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(),
|
||||
initial_messages,
|
||||
network_proxy: session_network_proxy.filter(|_| {
|
||||
@@ -1135,7 +1139,7 @@ impl Session {
|
||||
}
|
||||
let turn_environment = crate::environment_selection::resolve_environment_selections(
|
||||
sess.services.environment_manager.as_ref(),
|
||||
&session_configuration.environments,
|
||||
session_configuration.environment_selections(),
|
||||
)
|
||||
.map_err(|err| {
|
||||
CodexErr::InvalidRequest(err.to_string().replace(
|
||||
@@ -1152,7 +1156,7 @@ impl Session {
|
||||
),
|
||||
None => McpRuntimeContext::new(
|
||||
Arc::clone(&sess.services.environment_manager),
|
||||
session_configuration.cwd.to_path_buf(),
|
||||
session_configuration.cwd().to_path_buf(),
|
||||
),
|
||||
};
|
||||
let (mcp_connection_manager, cancel_token) = McpConnectionManager::new(
|
||||
|
||||
+135
-127
@@ -22,6 +22,7 @@ use codex_config::RequirementSource;
|
||||
use codex_config::Sourced;
|
||||
use codex_config::loader::project_trust_key;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use core_test_support::test_codex::local_selections;
|
||||
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -53,6 +54,7 @@ use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::NonSteerableTurnKind;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
use codex_protocol::request_permissions::PermissionGrantScope;
|
||||
use codex_protocol::request_permissions::RequestPermissionProfile;
|
||||
use tracing::Span;
|
||||
@@ -142,6 +144,7 @@ use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::test_codex::local;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::test_path_buf;
|
||||
use core_test_support::tracing::install_test_tracing;
|
||||
@@ -2392,7 +2395,8 @@ async fn session_permission_profile_rebinds_runtime_workspace_roots() -> anyhow:
|
||||
);
|
||||
|
||||
let mut session_configuration = make_session_configuration_for_tests().await;
|
||||
session_configuration.cwd = config.cwd.clone();
|
||||
session_configuration.environments =
|
||||
TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new());
|
||||
session_configuration.workspace_roots = config.workspace_roots.clone();
|
||||
session_configuration.permission_profile_state = session_permission_profile_state;
|
||||
|
||||
@@ -2404,8 +2408,8 @@ async fn session_permission_profile_rebinds_runtime_workspace_roots() -> anyhow:
|
||||
..Default::default()
|
||||
})?;
|
||||
let updated_policy = updated.file_system_sandbox_policy();
|
||||
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd().as_path()));
|
||||
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd().as_path()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2437,7 +2441,6 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
|
||||
initial
|
||||
.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "fork seed".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -2483,7 +2486,6 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
|
||||
forked
|
||||
.thread
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "after fork".into(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -3157,11 +3159,10 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: Vec::new(),
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -3265,11 +3266,10 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: Vec::new(),
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -3797,11 +3797,10 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: Vec::new(),
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -3920,7 +3919,8 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd
|
||||
let project_root = project_root.abs();
|
||||
let docs_dir = docs_dir.abs();
|
||||
|
||||
session_configuration.cwd = original_cwd.abs();
|
||||
session_configuration.environments =
|
||||
TurnEnvironmentSelections::new(original_cwd.abs(), Vec::new());
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
@@ -3954,7 +3954,7 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd
|
||||
|
||||
let updated = session_configuration
|
||||
.apply(&SessionSettingsUpdate {
|
||||
cwd: Some(project_root),
|
||||
environments: Some(TurnEnvironmentSelections::new(project_root, Vec::new())),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("cwd-only update should succeed");
|
||||
@@ -3969,7 +3969,8 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd
|
||||
async fn session_configuration_apply_permission_profile_preserves_existing_deny_read_entries() {
|
||||
let mut session_configuration = make_session_configuration_for_tests().await;
|
||||
let cwd = tempfile::tempdir().expect("create temp dir");
|
||||
session_configuration.cwd = cwd.path().abs();
|
||||
session_configuration.environments =
|
||||
TurnEnvironmentSelections::new(cwd.path().abs(), Vec::new());
|
||||
|
||||
let workspace_policy = SandboxPolicy::new_workspace_write_policy();
|
||||
let deny_entry = FileSystemSandboxEntry {
|
||||
@@ -3981,7 +3982,7 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_
|
||||
let mut existing_file_system_policy =
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
|
||||
&workspace_policy,
|
||||
session_configuration.cwd.as_path(),
|
||||
session_configuration.cwd().as_path(),
|
||||
);
|
||||
existing_file_system_policy.glob_scan_max_depth = Some(2);
|
||||
existing_file_system_policy.entries.push(deny_entry.clone());
|
||||
@@ -3997,7 +3998,7 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_
|
||||
|
||||
let requested_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
|
||||
&workspace_policy,
|
||||
session_configuration.cwd.as_path(),
|
||||
session_configuration.cwd().as_path(),
|
||||
);
|
||||
let permission_profile = codex_protocol::models::PermissionProfile::from_runtime_permissions(
|
||||
&requested_file_system_policy,
|
||||
@@ -4024,7 +4025,8 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_
|
||||
async fn session_configuration_apply_permission_profile_accepts_direct_write_roots() {
|
||||
let mut session_configuration = make_session_configuration_for_tests().await;
|
||||
let cwd = tempfile::tempdir().expect("create cwd");
|
||||
session_configuration.cwd = cwd.path().abs();
|
||||
session_configuration.environments =
|
||||
TurnEnvironmentSelections::new(cwd.path().abs(), Vec::new());
|
||||
let external_write_dir = tempfile::tempdir().expect("create external write root");
|
||||
let external_write_path = AbsolutePathBuf::from_absolute_path(
|
||||
codex_utils_absolute_path::canonicalize_preserving_symlinks(external_write_dir.path())
|
||||
@@ -4100,8 +4102,8 @@ async fn session_configuration_apply_rebinds_symbolic_profile_to_updated_workspa
|
||||
.expect("permission profile update should succeed");
|
||||
|
||||
let updated_policy = updated.file_system_sandbox_policy();
|
||||
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd().as_path()));
|
||||
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd().as_path()));
|
||||
assert_eq!(
|
||||
updated.active_permission_profile(),
|
||||
Some(ActivePermissionProfile::new("dev"))
|
||||
@@ -4118,7 +4120,8 @@ async fn session_configuration_apply_retargets_implicit_workspace_root_on_cwd_up
|
||||
let old_root = old_root.path().abs();
|
||||
let new_root = new_root.path().abs();
|
||||
let extra_root = extra_root.path().abs();
|
||||
session_configuration.cwd = old_root.clone();
|
||||
session_configuration.environments =
|
||||
TurnEnvironmentSelections::new(old_root.clone(), Vec::new());
|
||||
session_configuration.workspace_roots = vec![old_root.clone(), extra_root.clone()];
|
||||
|
||||
let file_system_sandbox_policy =
|
||||
@@ -4138,7 +4141,7 @@ async fn session_configuration_apply_retargets_implicit_workspace_root_on_cwd_up
|
||||
|
||||
let updated = session_configuration
|
||||
.apply(&SessionSettingsUpdate {
|
||||
cwd: Some(new_root.clone()),
|
||||
environments: Some(TurnEnvironmentSelections::new(new_root.clone(), Vec::new())),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("cwd-only update should succeed");
|
||||
@@ -4148,9 +4151,9 @@ async fn session_configuration_apply_retargets_implicit_workspace_root_on_cwd_up
|
||||
vec![new_root.clone(), extra_root.clone()]
|
||||
);
|
||||
let updated_policy = updated.file_system_sandbox_policy();
|
||||
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(updated_policy.can_write_path_with_cwd(extra_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd.as_path()));
|
||||
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd().as_path()));
|
||||
assert!(updated_policy.can_write_path_with_cwd(extra_root.as_path(), updated.cwd().as_path()));
|
||||
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd().as_path()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -4353,8 +4356,9 @@ async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_upda
|
||||
let workspace = tempfile::tempdir().expect("create temp dir");
|
||||
let original_cwd = workspace.path().join("repo-a").abs();
|
||||
let project_root = workspace.path().join("repo-b").abs();
|
||||
session_configuration.cwd = original_cwd.clone();
|
||||
session_configuration.workspace_roots = vec![session_configuration.cwd.clone()];
|
||||
session_configuration.environments =
|
||||
TurnEnvironmentSelections::new(original_cwd.clone(), Vec::new());
|
||||
session_configuration.workspace_roots = vec![session_configuration.cwd().clone()];
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
@@ -4363,7 +4367,7 @@ async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_upda
|
||||
};
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
|
||||
&sandbox_policy,
|
||||
&session_configuration.cwd,
|
||||
session_configuration.cwd(),
|
||||
);
|
||||
session_configuration
|
||||
.set_permission_profile_for_tests(
|
||||
@@ -4377,7 +4381,10 @@ async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_upda
|
||||
|
||||
let updated = session_configuration
|
||||
.apply(&SessionSettingsUpdate {
|
||||
cwd: Some(project_root.clone()),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
project_root.clone(),
|
||||
Vec::new(),
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("cwd-only update should succeed");
|
||||
@@ -4386,13 +4393,13 @@ async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_upda
|
||||
assert!(
|
||||
updated
|
||||
.file_system_sandbox_policy()
|
||||
.can_write_path_with_cwd(project_root.as_path(), updated.cwd.as_path()),
|
||||
.can_write_path_with_cwd(project_root.as_path(), updated.cwd().as_path()),
|
||||
"cwd-only update should keep the new cwd writable"
|
||||
);
|
||||
assert!(
|
||||
!updated
|
||||
.file_system_sandbox_policy()
|
||||
.can_write_path_with_cwd(original_cwd.as_path(), updated.cwd.as_path()),
|
||||
.can_write_path_with_cwd(original_cwd.as_path(), updated.cwd().as_path()),
|
||||
"cwd-only update should not keep the old implicit cwd writable"
|
||||
);
|
||||
}
|
||||
@@ -4408,7 +4415,8 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up
|
||||
let original_cwd = original_cwd.abs();
|
||||
let next_cwd = next_cwd.abs();
|
||||
|
||||
session_configuration.cwd = original_cwd.clone();
|
||||
session_configuration.environments =
|
||||
TurnEnvironmentSelections::new(original_cwd.clone(), Vec::new());
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
@@ -4435,7 +4443,7 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up
|
||||
|
||||
let updated = session_configuration
|
||||
.apply(&SessionSettingsUpdate {
|
||||
cwd: Some(next_cwd.clone()),
|
||||
environments: Some(TurnEnvironmentSelections::new(next_cwd.clone(), Vec::new())),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("cwd-only update should succeed");
|
||||
@@ -4447,13 +4455,13 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up
|
||||
assert!(
|
||||
updated
|
||||
.file_system_sandbox_policy()
|
||||
.can_write_path_with_cwd(original_cwd.as_path(), updated.cwd.as_path()),
|
||||
.can_write_path_with_cwd(original_cwd.as_path(), updated.cwd().as_path()),
|
||||
"absolute grant to the old cwd must remain writable"
|
||||
);
|
||||
assert!(
|
||||
!updated
|
||||
.file_system_sandbox_policy()
|
||||
.can_write_path_with_cwd(next_cwd.as_path(), updated.cwd.as_path()),
|
||||
.can_write_path_with_cwd(next_cwd.as_path(), updated.cwd().as_path()),
|
||||
"cwd-only update must not reinterpret an absolute old-cwd grant as :workspace_roots"
|
||||
);
|
||||
}
|
||||
@@ -4463,11 +4471,21 @@ async fn session_update_settings_does_not_rewrite_sticky_environment_cwds() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
#[allow(deprecated)]
|
||||
let updated_cwd = turn_context.cwd.join("project");
|
||||
let current_environments = {
|
||||
let state = session.state.lock().await;
|
||||
state
|
||||
.session_configuration
|
||||
.environment_selections()
|
||||
.to_vec()
|
||||
};
|
||||
std::fs::create_dir_all(updated_cwd.as_path()).expect("create project dir");
|
||||
|
||||
session
|
||||
.update_settings(SessionSettingsUpdate {
|
||||
cwd: Some(updated_cwd.clone()),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
updated_cwd.clone(),
|
||||
current_environments,
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
@@ -4475,7 +4493,7 @@ async fn session_update_settings_does_not_rewrite_sticky_environment_cwds() {
|
||||
|
||||
let session_cwd = {
|
||||
let state = session.state.lock().await;
|
||||
state.session_configuration.cwd.clone()
|
||||
state.session_configuration.cwd().clone()
|
||||
};
|
||||
let config = session.get_config().await;
|
||||
let next_turn = session.new_default_turn().await;
|
||||
@@ -4495,55 +4513,65 @@ async fn relative_cwd_update_without_environments_resolves_under_session_cwd() {
|
||||
let (session, _turn_context) = make_session_and_context().await;
|
||||
let original_cwd = {
|
||||
let mut state = session.state.lock().await;
|
||||
state.session_configuration.environments = Vec::new();
|
||||
state.session_configuration.cwd.clone()
|
||||
state.session_configuration.environments.environments = Vec::new();
|
||||
state.session_configuration.cwd().clone()
|
||||
};
|
||||
let updated_cwd = original_cwd.join("project");
|
||||
std::fs::create_dir_all(updated_cwd.as_path()).expect("create project dir");
|
||||
|
||||
session
|
||||
.update_settings(SessionSettingsUpdate {
|
||||
cwd: Some(updated_cwd.clone()),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
updated_cwd.clone(),
|
||||
Vec::new(),
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.expect("cwd update should succeed");
|
||||
|
||||
let state = session.state.lock().await;
|
||||
assert_eq!(state.session_configuration.cwd, updated_cwd);
|
||||
assert!(state.session_configuration.environments.is_empty());
|
||||
assert_eq!(state.session_configuration.cwd(), &updated_cwd);
|
||||
assert!(
|
||||
state
|
||||
.session_configuration
|
||||
.environment_selections()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cwd_update_does_not_rewrite_sticky_environment_cwd() {
|
||||
async fn cwd_update_rewrites_sticky_environment_cwd() {
|
||||
let (session, _turn_context) = make_session_and_context().await;
|
||||
let (original_cwd, environment_cwd) = {
|
||||
let (original_cwd, environment_cwd, environments) = {
|
||||
let mut state = session.state.lock().await;
|
||||
let original_cwd = state.session_configuration.cwd.clone();
|
||||
let original_cwd = state.session_configuration.cwd().clone();
|
||||
let environment_cwd = original_cwd.join("environment");
|
||||
state.session_configuration.environments = vec![TurnEnvironmentSelection {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: environment_cwd.clone(),
|
||||
}];
|
||||
(original_cwd, environment_cwd)
|
||||
let environments = vec![local(environment_cwd.clone())];
|
||||
state.session_configuration.environments.environments = environments.clone();
|
||||
(original_cwd, environment_cwd, environments)
|
||||
};
|
||||
let updated_cwd = original_cwd.join("project");
|
||||
std::fs::create_dir_all(updated_cwd.as_path()).expect("create project dir");
|
||||
|
||||
session
|
||||
.update_settings(SessionSettingsUpdate {
|
||||
cwd: Some(updated_cwd.clone()),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
updated_cwd.clone(),
|
||||
environments,
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.expect("cwd update should succeed");
|
||||
|
||||
let state = session.state.lock().await;
|
||||
assert_eq!(state.session_configuration.cwd, updated_cwd);
|
||||
assert_eq!(state.session_configuration.cwd(), &updated_cwd);
|
||||
assert_eq!(
|
||||
state.session_configuration.environments[0].cwd,
|
||||
environment_cwd
|
||||
state.session_configuration.environment_selections()[0].cwd,
|
||||
updated_cwd
|
||||
);
|
||||
assert_ne!(environment_cwd, updated_cwd);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -4551,7 +4579,7 @@ async fn absolute_cwd_update_with_turn_environment_is_allowed() {
|
||||
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
|
||||
let absolute_cwd = {
|
||||
let state = session.state.lock().await;
|
||||
state.session_configuration.cwd.join("absolute-turn")
|
||||
state.session_configuration.cwd().join("absolute-turn")
|
||||
};
|
||||
std::fs::create_dir_all(absolute_cwd.as_path()).expect("create absolute turn dir");
|
||||
|
||||
@@ -4559,11 +4587,10 @@ async fn absolute_cwd_update_with_turn_environment_is_allowed() {
|
||||
.new_turn_with_sub_id(
|
||||
"sub-1".to_string(),
|
||||
SessionSettingsUpdate {
|
||||
cwd: Some(absolute_cwd.clone()),
|
||||
environments: Some(vec![TurnEnvironmentSelection {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: absolute_cwd.clone(),
|
||||
}]),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
absolute_cwd.clone(),
|
||||
vec![local(absolute_cwd.clone())],
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -4622,11 +4649,10 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: Vec::new(),
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -4712,10 +4738,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
let default_environments = vec![TurnEnvironmentSelection {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: config.cwd.clone(),
|
||||
}];
|
||||
let default_environments = vec![local(config.cwd.clone())];
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
@@ -4733,11 +4756,10 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: default_environments,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -4751,7 +4773,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
user_shell_override: None,
|
||||
};
|
||||
let per_turn_config =
|
||||
Session::build_per_turn_config(&session_configuration, session_configuration.cwd.clone());
|
||||
Session::build_per_turn_config(&session_configuration, session_configuration.cwd().clone());
|
||||
let model_info = construct_model_info_offline_for_tests(
|
||||
session_configuration.collaboration_mode.model(),
|
||||
&per_turn_config.to_models_manager_config(),
|
||||
@@ -4863,7 +4885,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
.skills_for_config(&skills_input, Some(Arc::clone(&skill_fs)))
|
||||
.await,
|
||||
);
|
||||
let turn_environments = turn_environments_for_tests(&environment, &session_configuration.cwd);
|
||||
let turn_environments = turn_environments_for_tests(&environment, session_configuration.cwd());
|
||||
let turn_context = Session::make_turn_context(
|
||||
thread_id,
|
||||
SessionId::from(thread_id),
|
||||
@@ -4880,7 +4902,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
&models_manager,
|
||||
/*network*/ None,
|
||||
turn_environments,
|
||||
session_configuration.cwd.clone(),
|
||||
session_configuration.cwd().clone(),
|
||||
"turn_id".to_string(),
|
||||
skills_outcome,
|
||||
);
|
||||
@@ -4948,10 +4970,7 @@ async fn make_session_with_config_and_rx(
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
let default_environments = vec![TurnEnvironmentSelection {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: config.cwd.clone(),
|
||||
}];
|
||||
let default_environments = vec![local(config.cwd.clone())];
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
@@ -4969,11 +4988,10 @@ async fn make_session_with_config_and_rx(
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: default_environments,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -5053,10 +5071,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
let default_environments = vec![TurnEnvironmentSelection {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: config.cwd.clone(),
|
||||
}];
|
||||
let default_environments = vec![local(config.cwd.clone())];
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
@@ -5074,11 +5089,10 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: default_environments,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -5937,7 +5951,6 @@ fn submission_dispatch_span_uses_debug_for_realtime_audio() {
|
||||
fn op_kind_for_input_and_context_ops() {
|
||||
assert_eq!(
|
||||
Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
@@ -5969,12 +5982,11 @@ async fn user_turn_updates_approvals_reviewer() {
|
||||
text: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
environments: None,
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd: Some(config.cwd.clone()),
|
||||
environments: Some(local_selections(config.cwd.clone())),
|
||||
approval_policy: Some(config.permissions.approval_policy.value()),
|
||||
approvals_reviewer: Some(codex_config::types::ApprovalsReviewer::AutoReview),
|
||||
sandbox_policy: Some(config.legacy_sandbox_policy()),
|
||||
@@ -6013,10 +6025,10 @@ async fn turn_environments_set_primary_environment() {
|
||||
.new_turn_with_sub_id(
|
||||
"sub-1".to_string(),
|
||||
SessionSettingsUpdate {
|
||||
environments: Some(vec![TurnEnvironmentSelection {
|
||||
environment_id: "local".to_string(),
|
||||
cwd: selected_cwd.clone(),
|
||||
}]),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
selected_cwd.clone(),
|
||||
vec![local(selected_cwd.clone())],
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -6041,7 +6053,7 @@ async fn turn_environments_set_primary_environment() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_turn_overlays_session_cwd_onto_stored_thread_environments() {
|
||||
async fn default_turn_does_not_overlay_legacy_fallback_cwd_onto_stored_thread_environments() {
|
||||
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
|
||||
let session_cwd = session.get_config().await.cwd.clone();
|
||||
let selected_cwd =
|
||||
@@ -6049,10 +6061,7 @@ async fn default_turn_overlays_session_cwd_onto_stored_thread_environments() {
|
||||
|
||||
{
|
||||
let mut state = session.state.lock().await;
|
||||
state.session_configuration.environments = vec![TurnEnvironmentSelection {
|
||||
environment_id: "local".to_string(),
|
||||
cwd: selected_cwd.clone(),
|
||||
}];
|
||||
state.session_configuration.environments.environments = vec![local(selected_cwd.clone())];
|
||||
}
|
||||
|
||||
let turn_context = session.new_default_turn().await;
|
||||
@@ -6069,8 +6078,8 @@ async fn default_turn_overlays_session_cwd_onto_stored_thread_environments() {
|
||||
));
|
||||
#[allow(deprecated)]
|
||||
let turn_cwd = turn_context.cwd.clone();
|
||||
assert_eq!(turn_cwd, session_cwd);
|
||||
assert_eq!(turn_context.config.cwd, session_cwd);
|
||||
assert_eq!(turn_cwd, selected_cwd);
|
||||
assert_eq!(turn_context.config.cwd, selected_cwd);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -6080,7 +6089,7 @@ async fn default_turn_honors_empty_stored_thread_environments() {
|
||||
|
||||
{
|
||||
let mut state = session.state.lock().await;
|
||||
state.session_configuration.environments = Vec::new();
|
||||
state.session_configuration.environments.environments = Vec::new();
|
||||
}
|
||||
|
||||
let turn_context = session.new_default_turn().await;
|
||||
@@ -6143,7 +6152,10 @@ async fn empty_turn_environments_clear_primary_environment() {
|
||||
.new_turn_with_sub_id(
|
||||
"sub-1".to_string(),
|
||||
SessionSettingsUpdate {
|
||||
environments: Some(vec![]),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
session.get_config().await.cwd.clone(),
|
||||
vec![],
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -6170,10 +6182,13 @@ async fn unknown_turn_environment_returns_error() {
|
||||
.new_turn_with_sub_id(
|
||||
"sub-1".to_string(),
|
||||
SessionSettingsUpdate {
|
||||
environments: Some(vec![TurnEnvironmentSelection {
|
||||
environment_id: "missing".to_string(),
|
||||
cwd: original_configuration.cwd.clone(),
|
||||
}]),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
original_configuration.cwd().clone(),
|
||||
vec![TurnEnvironmentSelection {
|
||||
environment_id: "missing".to_string(),
|
||||
cwd: original_configuration.cwd().clone(),
|
||||
}],
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -6186,10 +6201,10 @@ async fn unknown_turn_environment_returns_error() {
|
||||
};
|
||||
assert!(matches!(err, CodexErr::InvalidRequest(_)));
|
||||
assert!(err.to_string().contains("missing"));
|
||||
assert_eq!(current_configuration.cwd, original_configuration.cwd);
|
||||
assert_eq!(current_configuration.cwd(), original_configuration.cwd());
|
||||
assert_eq!(
|
||||
current_configuration.environments,
|
||||
original_configuration.environments
|
||||
current_configuration.environment_selections(),
|
||||
original_configuration.environment_selections()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6205,16 +6220,13 @@ async fn duplicate_turn_environment_returns_error_without_mutating_session() {
|
||||
.new_turn_with_sub_id(
|
||||
"sub-1".to_string(),
|
||||
SessionSettingsUpdate {
|
||||
environments: Some(vec![
|
||||
TurnEnvironmentSelection {
|
||||
environment_id: "local".to_string(),
|
||||
cwd: original_configuration.cwd.clone(),
|
||||
},
|
||||
TurnEnvironmentSelection {
|
||||
environment_id: "local".to_string(),
|
||||
cwd: original_configuration.cwd.join("second"),
|
||||
},
|
||||
]),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
original_configuration.cwd().clone(),
|
||||
vec![
|
||||
local(original_configuration.cwd().clone()),
|
||||
local(original_configuration.cwd().join("second")),
|
||||
],
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -6227,10 +6239,10 @@ async fn duplicate_turn_environment_returns_error_without_mutating_session() {
|
||||
};
|
||||
assert!(matches!(err, CodexErr::InvalidRequest(_)));
|
||||
assert!(err.to_string().contains("duplicate"));
|
||||
assert_eq!(current_configuration.cwd, original_configuration.cwd);
|
||||
assert_eq!(current_configuration.cwd(), original_configuration.cwd());
|
||||
assert_eq!(
|
||||
current_configuration.environments,
|
||||
original_configuration.environments
|
||||
current_configuration.environment_selections(),
|
||||
original_configuration.environment_selections()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6789,10 +6801,7 @@ where
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
let default_environments = vec![TurnEnvironmentSelection {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: config.cwd.clone(),
|
||||
}];
|
||||
let default_environments = vec![local(config.cwd.clone())];
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
collaboration_mode,
|
||||
@@ -6810,11 +6819,10 @@ where
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
permission_profile_state: config.permissions.permission_profile_state().clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
cwd: config.cwd.clone(),
|
||||
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
|
||||
workspace_roots: config.workspace_roots.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environments: default_environments,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -6828,7 +6836,7 @@ where
|
||||
user_shell_override: None,
|
||||
};
|
||||
let per_turn_config =
|
||||
Session::build_per_turn_config(&session_configuration, session_configuration.cwd.clone());
|
||||
Session::build_per_turn_config(&session_configuration, session_configuration.cwd().clone());
|
||||
let model_info = construct_model_info_offline_for_tests(
|
||||
session_configuration.collaboration_mode.model(),
|
||||
&per_turn_config.to_models_manager_config(),
|
||||
@@ -6940,7 +6948,7 @@ where
|
||||
.skills_for_config(&skills_input, Some(Arc::clone(&skill_fs)))
|
||||
.await,
|
||||
);
|
||||
let turn_environments = turn_environments_for_tests(&environment, &session_configuration.cwd);
|
||||
let turn_environments = turn_environments_for_tests(&environment, session_configuration.cwd());
|
||||
let turn_context = Arc::new(Session::make_turn_context(
|
||||
thread_id,
|
||||
SessionId::from(thread_id),
|
||||
@@ -6957,7 +6965,7 @@ where
|
||||
&models_manager,
|
||||
/*network*/ None,
|
||||
turn_environments,
|
||||
session_configuration.cwd.clone(),
|
||||
session_configuration.cwd().clone(),
|
||||
"turn_id".to_string(),
|
||||
skills_outcome,
|
||||
));
|
||||
|
||||
@@ -441,7 +441,7 @@ impl Session {
|
||||
session_configuration: &SessionConfiguration,
|
||||
) -> Config {
|
||||
let mut config =
|
||||
Self::build_per_turn_config(session_configuration, session_configuration.cwd.clone());
|
||||
Self::build_per_turn_config(session_configuration, session_configuration.cwd().clone());
|
||||
config.model = Some(session_configuration.collaboration_mode.model().to_string());
|
||||
config.permissions.approval_policy = session_configuration.approval_policy.clone();
|
||||
config.workspace_roots = session_configuration.workspace_roots.clone();
|
||||
@@ -587,19 +587,9 @@ impl Session {
|
||||
let mut state = self.state.lock().await;
|
||||
match state.session_configuration.clone().apply(&updates) {
|
||||
Ok(next) => {
|
||||
let mut effective_environments = updates
|
||||
.environments
|
||||
.clone()
|
||||
.unwrap_or_else(|| next.environments.clone());
|
||||
if updates.environments.is_none() {
|
||||
Self::overlay_runtime_cwd_on_primary_environment(
|
||||
&mut effective_environments,
|
||||
&next.cwd,
|
||||
);
|
||||
}
|
||||
let turn_environments =
|
||||
self.resolve_turn_environments(&effective_environments)?;
|
||||
let previous_cwd = state.session_configuration.cwd.clone();
|
||||
self.resolve_turn_environments(next.environment_selections())?;
|
||||
let previous_cwd = state.session_configuration.cwd().clone();
|
||||
let previous_permission_profile =
|
||||
state.session_configuration.permission_profile();
|
||||
let next_permission_profile = next.permission_profile();
|
||||
@@ -656,7 +646,7 @@ impl Session {
|
||||
self.emit_config_changed_contributors(previous_config.as_ref(), new_config.as_ref());
|
||||
self.maybe_refresh_shell_snapshot_for_cwd(
|
||||
&previous_cwd,
|
||||
&session_configuration.cwd,
|
||||
session_configuration.cwd(),
|
||||
&codex_home,
|
||||
&session_source,
|
||||
);
|
||||
@@ -731,7 +721,7 @@ impl Session {
|
||||
let cwd = primary_turn_environment
|
||||
.as_ref()
|
||||
.map(|turn_environment| turn_environment.cwd.clone())
|
||||
.unwrap_or_else(|| session_configuration.cwd.clone());
|
||||
.unwrap_or_else(|| session_configuration.cwd().clone());
|
||||
let per_turn_config = Self::build_per_turn_config(&session_configuration, cwd.clone());
|
||||
{
|
||||
let mcp_connection_manager = self.services.mcp_connection_manager.read().await;
|
||||
@@ -871,29 +861,14 @@ impl Session {
|
||||
let state = self.state.lock().await;
|
||||
state.session_configuration.clone()
|
||||
};
|
||||
let mut effective_environments = session_configuration.environments.clone();
|
||||
Self::overlay_runtime_cwd_on_primary_environment(
|
||||
&mut effective_environments,
|
||||
&session_configuration.cwd,
|
||||
);
|
||||
let turn_environments = match self.resolve_turn_environments(&effective_environments) {
|
||||
Ok(turn_environments) => turn_environments,
|
||||
Err(err) => {
|
||||
warn!("failed to resolve stored session environments: {err}");
|
||||
ResolvedTurnEnvironments::default()
|
||||
}
|
||||
};
|
||||
let turn_environments =
|
||||
match self.resolve_turn_environments(session_configuration.environment_selections()) {
|
||||
Ok(turn_environments) => turn_environments,
|
||||
Err(err) => {
|
||||
warn!("failed to resolve stored session environments: {err}");
|
||||
ResolvedTurnEnvironments::default()
|
||||
}
|
||||
};
|
||||
(session_configuration, turn_environments)
|
||||
}
|
||||
|
||||
fn overlay_runtime_cwd_on_primary_environment(
|
||||
environments: &mut [TurnEnvironmentSelection],
|
||||
runtime_cwd: &AbsolutePathBuf,
|
||||
) {
|
||||
if let Some(turn_environment) = environments.first_mut()
|
||||
&& turn_environment.cwd != *runtime_cwd
|
||||
{
|
||||
turn_environment.cwd = runtime_cwd.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
use codex_protocol::protocol::TurnStartedEvent;
|
||||
use codex_protocol::protocol::UserMessageEvent;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::PathExt;
|
||||
use core_test_support::responses::mount_models_once;
|
||||
@@ -343,99 +342,6 @@ async fn start_thread_rejects_explicit_local_environment_when_default_provider_i
|
||||
assert!(manager.list_thread_ids().await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_thread_uses_all_default_environments_from_codex_home() {
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
let mut config = test_config().await;
|
||||
config.codex_home = temp_dir.path().join("codex-home").abs();
|
||||
config.cwd = config.codex_home.abs();
|
||||
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
|
||||
std::fs::write(
|
||||
config.codex_home.join("environments.toml"),
|
||||
r#"
|
||||
default = "dev"
|
||||
|
||||
[[environments]]
|
||||
id = "dev"
|
||||
program = "ssh"
|
||||
args = ["dev", "cd /tmp && true"]
|
||||
"#,
|
||||
)
|
||||
.expect("write environments.toml");
|
||||
|
||||
let runtime_paths = codex_exec_server::ExecServerRuntimePaths::new(
|
||||
std::env::current_exe().expect("current exe path"),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("runtime paths");
|
||||
let environment_manager = Arc::new(
|
||||
codex_exec_server::EnvironmentManager::from_codex_home(
|
||||
config.codex_home.clone(),
|
||||
Some(runtime_paths),
|
||||
)
|
||||
.await
|
||||
.expect("environment manager"),
|
||||
);
|
||||
assert_eq!(
|
||||
environment_manager.default_environment_ids(),
|
||||
vec!["dev".to_string(), "local".to_string()]
|
||||
);
|
||||
|
||||
let manager = ThreadManager::with_models_provider_and_home_for_tests(
|
||||
CodexAuth::from_api_key("dummy"),
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
environment_manager,
|
||||
);
|
||||
|
||||
let thread = manager
|
||||
.start_thread(config)
|
||||
.await
|
||||
.expect("thread should start");
|
||||
|
||||
let prompt_items = crate::prompt_debug::build_prompt_input_from_session(
|
||||
thread.thread.codex.session.as_ref(),
|
||||
Vec::<UserInput>::new(),
|
||||
)
|
||||
.await
|
||||
.expect("prompt input");
|
||||
let environment_context = prompt_items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
ResponseItem::Message { content, .. } => Some(content),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
.find_map(|content| match content {
|
||||
ContentItem::InputText { text } if text.contains("<environment_context>") => {
|
||||
Some(text.as_str())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("environment context prompt item");
|
||||
assert!(environment_context.contains("<environments>"));
|
||||
let cwd = thread.session_configured.cwd.display().to_string();
|
||||
let dev_entry = format!(
|
||||
r#"<environment id="dev">
|
||||
<cwd>{cwd}</cwd>
|
||||
<shell>"#
|
||||
);
|
||||
let local_entry = format!(
|
||||
r#"<environment id="local">
|
||||
<cwd>{cwd}</cwd>
|
||||
<shell>"#
|
||||
);
|
||||
let dev_position = environment_context
|
||||
.find(&dev_entry)
|
||||
.expect("dev environment entry");
|
||||
let local_position = environment_context
|
||||
.find(&local_entry)
|
||||
.expect("local environment entry");
|
||||
assert!(dev_position < local_position);
|
||||
assert!(!environment_context.contains("\n <cwd>"));
|
||||
assert!(!environment_context.contains("\n <shell>"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_thread_keeps_internal_threads_hidden_from_normal_lookups() {
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
|
||||
@@ -2676,7 +2676,6 @@ async fn send_input_accepts_structured_items() {
|
||||
.expect("send_input should succeed");
|
||||
|
||||
let expected = Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![
|
||||
UserInput::Mention {
|
||||
name: "drive".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user