mirror of
https://github.com/openai/codex.git
synced 2026-11-01 19:33:35 +00:00
Add sticky thread environment selections
Allow thread/start to configure sticky environment selections that are used by turns when no per-turn override is supplied. Per-turn environments continue to take precedence, while omitted thread selections preserve the existing default behavior. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -3074,6 +3074,14 @@ pub struct ThreadStartParams {
|
||||
pub ephemeral: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub session_start_source: Option<ThreadStartSource>,
|
||||
/// Optional sticky environment selections for this thread.
|
||||
///
|
||||
/// Omitted uses EnvironmentManager default behavior. Empty disables
|
||||
/// environment access for turns that do not provide a turn override.
|
||||
/// Non-empty selects the first environment as the current turn environment.
|
||||
#[experimental("thread/start.environments")]
|
||||
#[ts(optional = nullable)]
|
||||
pub environments: Option<Vec<TurnEnvironmentParams>>,
|
||||
#[experimental("thread/start.dynamicTools")]
|
||||
#[ts(optional = nullable)]
|
||||
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
|
||||
@@ -4662,6 +4670,10 @@ pub struct TurnStartParams {
|
||||
#[ts(optional = nullable)]
|
||||
pub responsesapi_client_metadata: Option<HashMap<String, String>>,
|
||||
/// Optional turn-scoped environment selections.
|
||||
///
|
||||
/// Omitted uses the thread sticky environment selections. Empty disables
|
||||
/// environment access for this turn. Non-empty selects the first
|
||||
/// environment as the current turn environment for this turn.
|
||||
#[experimental("turn/start.environments")]
|
||||
#[ts(optional = nullable)]
|
||||
pub environments: Option<Vec<TurnEnvironmentParams>>,
|
||||
|
||||
@@ -2353,8 +2353,18 @@ impl CodexMessageProcessor {
|
||||
personality,
|
||||
ephemeral,
|
||||
session_start_source,
|
||||
environments,
|
||||
persist_extended_history,
|
||||
} = params;
|
||||
let environments = environments.map(|environments| {
|
||||
environments
|
||||
.into_iter()
|
||||
.map(|environment| TurnEnvironmentSelection {
|
||||
environment_id: environment.environment_id,
|
||||
cwd: environment.cwd,
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
let mut typesafe_overrides = self.build_thread_config_overrides(
|
||||
model,
|
||||
model_provider,
|
||||
@@ -2392,6 +2402,7 @@ impl CodexMessageProcessor {
|
||||
typesafe_overrides,
|
||||
dynamic_tools,
|
||||
session_start_source,
|
||||
environments,
|
||||
persist_extended_history,
|
||||
service_name,
|
||||
experimental_raw_events,
|
||||
@@ -2466,6 +2477,7 @@ impl CodexMessageProcessor {
|
||||
typesafe_overrides: ConfigOverrides,
|
||||
dynamic_tools: Option<Vec<ApiDynamicToolSpec>>,
|
||||
session_start_source: Option<codex_app_server_protocol::ThreadStartSource>,
|
||||
environment_selections: Option<Vec<TurnEnvironmentSelection>>,
|
||||
persist_extended_history: bool,
|
||||
service_name: Option<String>,
|
||||
experimental_raw_events: bool,
|
||||
@@ -2613,6 +2625,7 @@ impl CodexMessageProcessor {
|
||||
persist_extended_history,
|
||||
service_name,
|
||||
request_trace,
|
||||
environment_selections,
|
||||
)
|
||||
.instrument(tracing::info_span!(
|
||||
"app_server.thread_start.create_thread",
|
||||
@@ -7219,6 +7232,7 @@ impl CodexMessageProcessor {
|
||||
service_tier: params.service_tier,
|
||||
collaboration_mode,
|
||||
personality: params.personality,
|
||||
environments: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -92,6 +92,7 @@ pub(crate) async fn run_codex_thread_interactive(
|
||||
user_shell_override: None,
|
||||
inherited_exec_policy: Some(Arc::clone(&parent_session.services.exec_policy)),
|
||||
parent_trace: None,
|
||||
environment_selections: None,
|
||||
analytics_events_client: Some(parent_session.services.analytics_events_client.clone()),
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -166,6 +166,7 @@ pub(super) async fn user_input_or_turn_inner(
|
||||
personality,
|
||||
app_server_client_name: None,
|
||||
app_server_client_version: None,
|
||||
environment_selections: None,
|
||||
},
|
||||
None,
|
||||
environments,
|
||||
@@ -1077,6 +1078,7 @@ pub(super) async fn submission_loop(
|
||||
service_tier,
|
||||
collaboration_mode,
|
||||
personality,
|
||||
environments,
|
||||
} => {
|
||||
let collaboration_mode = if let Some(collab_mode) = collaboration_mode {
|
||||
collab_mode
|
||||
@@ -1101,6 +1103,7 @@ pub(super) async fn submission_loop(
|
||||
reasoning_summary: summary,
|
||||
service_tier,
|
||||
personality,
|
||||
environment_selections: environments,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -401,6 +401,7 @@ pub(crate) struct CodexSpawnArgs {
|
||||
pub(crate) inherited_exec_policy: Option<Arc<ExecPolicyManager>>,
|
||||
pub(crate) user_shell_override: Option<shell::Shell>,
|
||||
pub(crate) parent_trace: Option<W3cTraceContext>,
|
||||
pub(crate) environment_selections: Option<Vec<TurnEnvironmentSelection>>,
|
||||
pub(crate) analytics_events_client: Option<AnalyticsEventsClient>,
|
||||
}
|
||||
|
||||
@@ -455,6 +456,7 @@ impl Codex {
|
||||
user_shell_override,
|
||||
inherited_exec_policy,
|
||||
parent_trace: _,
|
||||
environment_selections,
|
||||
analytics_events_client,
|
||||
} = args;
|
||||
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
@@ -617,6 +619,7 @@ impl Codex {
|
||||
cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environment_selections,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name,
|
||||
app_server_client_name: None,
|
||||
@@ -626,6 +629,7 @@ impl Codex {
|
||||
persist_extended_history,
|
||||
inherited_shell_snapshot,
|
||||
user_shell_override,
|
||||
environment_selections,
|
||||
};
|
||||
|
||||
// Generate a unique ID for the lifetime of this Codex session.
|
||||
|
||||
@@ -71,6 +71,9 @@ 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 environment selections for turns that do not provide a turn-local override.
|
||||
pub(super) environment_selections:
|
||||
Option<Vec<codex_protocol::protocol::TurnEnvironmentSelection>>,
|
||||
|
||||
// TODO(pakrym): Remove config from here
|
||||
pub(super) original_config_do_not_use: Arc<Config>,
|
||||
@@ -181,6 +184,9 @@ impl SessionConfiguration {
|
||||
if let Some(app_server_client_version) = updates.app_server_client_version.clone() {
|
||||
next_configuration.app_server_client_version = Some(app_server_client_version);
|
||||
}
|
||||
if let Some(environment_selections) = updates.environment_selections.clone() {
|
||||
next_configuration.environment_selections = Some(environment_selections);
|
||||
}
|
||||
Ok(next_configuration)
|
||||
}
|
||||
}
|
||||
@@ -199,6 +205,8 @@ pub(crate) struct SessionSettingsUpdate {
|
||||
pub(crate) personality: Option<Personality>,
|
||||
pub(crate) app_server_client_name: Option<String>,
|
||||
pub(crate) app_server_client_version: Option<String>,
|
||||
pub(crate) environment_selections:
|
||||
Option<Vec<codex_protocol::protocol::TurnEnvironmentSelection>>,
|
||||
}
|
||||
|
||||
pub(crate) struct AppServerClientMetadata {
|
||||
|
||||
@@ -1688,6 +1688,7 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
|
||||
service_tier: None,
|
||||
collaboration_mode: Some(collaboration_mode),
|
||||
personality: None,
|
||||
environments: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -2321,6 +2322,7 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environment_selections: None,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -2426,6 +2428,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environment_selections: None,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -2781,6 +2784,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environment_selections: None,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -3051,6 +3055,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
|
||||
cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environment_selections: None,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -3151,6 +3156,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environment_selections: None,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -3371,6 +3377,7 @@ async fn make_session_with_config_and_rx(
|
||||
cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environment_selections: None,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
@@ -3850,6 +3857,7 @@ fn op_kind_distinguishes_turn_ops() {
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
environments: None,
|
||||
}
|
||||
.kind(),
|
||||
"override_turn_context"
|
||||
@@ -4381,6 +4389,7 @@ where
|
||||
cwd: config.cwd.clone(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
thread_name: None,
|
||||
environment_selections: None,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
|
||||
@@ -651,6 +651,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
|
||||
inherited_exec_policy: Some(Arc::new(parent_exec_policy)),
|
||||
user_shell_override: None,
|
||||
parent_trace: None,
|
||||
environment_selections: None,
|
||||
analytics_events_client: None,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -467,20 +467,6 @@ impl Session {
|
||||
updates: SessionSettingsUpdate,
|
||||
environment_selections: Option<Vec<TurnEnvironmentSelection>>,
|
||||
) -> CodexResult<Arc<TurnContext>> {
|
||||
let turn_environments = match self.resolve_turn_environments(environment_selections) {
|
||||
Ok(turn_environments) => turn_environments,
|
||||
Err(err) => {
|
||||
self.send_event_raw(Event {
|
||||
id: sub_id.clone(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: err.to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::BadRequest),
|
||||
}),
|
||||
})
|
||||
.await;
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
let update_result = {
|
||||
let mut state = self.state.lock().await;
|
||||
match state.session_configuration.clone().apply(&updates) {
|
||||
@@ -524,6 +510,23 @@ impl Session {
|
||||
return Err(CodexErr::InvalidRequest(message));
|
||||
}
|
||||
};
|
||||
let effective_environment_selections =
|
||||
environment_selections.or_else(|| session_configuration.environment_selections.clone());
|
||||
let turn_environments =
|
||||
match self.resolve_turn_environments(effective_environment_selections) {
|
||||
Ok(turn_environments) => turn_environments,
|
||||
Err(err) => {
|
||||
self.send_event_raw(Event {
|
||||
id: sub_id.clone(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: err.to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::BadRequest),
|
||||
}),
|
||||
})
|
||||
.await;
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
self.maybe_refresh_shell_snapshot_for_cwd(
|
||||
&previous_cwd,
|
||||
|
||||
@@ -501,6 +501,7 @@ impl ThreadManager {
|
||||
persist_extended_history,
|
||||
/*metrics_service_name*/ None,
|
||||
/*parent_trace*/ None,
|
||||
/*environment_selections*/ None,
|
||||
))
|
||||
.await
|
||||
}
|
||||
@@ -513,6 +514,7 @@ impl ThreadManager {
|
||||
persist_extended_history: bool,
|
||||
metrics_service_name: Option<String>,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
environment_selections: Option<Vec<codex_protocol::protocol::TurnEnvironmentSelection>>,
|
||||
) -> CodexResult<NewThread> {
|
||||
Box::pin(self.state.spawn_thread(
|
||||
config,
|
||||
@@ -523,6 +525,7 @@ impl ThreadManager {
|
||||
persist_extended_history,
|
||||
metrics_service_name,
|
||||
parent_trace,
|
||||
environment_selections,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -563,6 +566,7 @@ impl ThreadManager {
|
||||
persist_extended_history,
|
||||
/*metrics_service_name*/ None,
|
||||
parent_trace,
|
||||
/*environment_selections*/ None,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -582,6 +586,7 @@ impl ThreadManager {
|
||||
/*persist_extended_history*/ false,
|
||||
/*metrics_service_name*/ None,
|
||||
/*parent_trace*/ None,
|
||||
/*environment_selections*/ None,
|
||||
/*user_shell_override*/ Some(user_shell_override),
|
||||
))
|
||||
.await
|
||||
@@ -604,6 +609,7 @@ impl ThreadManager {
|
||||
/*persist_extended_history*/ false,
|
||||
/*metrics_service_name*/ None,
|
||||
/*parent_trace*/ None,
|
||||
/*environment_selections*/ None,
|
||||
/*user_shell_override*/ Some(user_shell_override),
|
||||
))
|
||||
.await
|
||||
@@ -712,6 +718,7 @@ impl ThreadManager {
|
||||
persist_extended_history,
|
||||
/*metrics_service_name*/ None,
|
||||
parent_trace,
|
||||
/*environment_selections*/ None,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -813,6 +820,7 @@ impl ThreadManagerState {
|
||||
inherited_shell_snapshot,
|
||||
inherited_exec_policy,
|
||||
/*parent_trace*/ None,
|
||||
/*environment_selections*/ None,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -840,6 +848,7 @@ impl ThreadManagerState {
|
||||
inherited_shell_snapshot,
|
||||
inherited_exec_policy,
|
||||
/*parent_trace*/ None,
|
||||
/*environment_selections*/ None,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -868,6 +877,7 @@ impl ThreadManagerState {
|
||||
inherited_shell_snapshot,
|
||||
inherited_exec_policy,
|
||||
/*parent_trace*/ None,
|
||||
/*environment_selections*/ None,
|
||||
/*user_shell_override*/ None,
|
||||
))
|
||||
.await
|
||||
@@ -885,6 +895,7 @@ impl ThreadManagerState {
|
||||
persist_extended_history: bool,
|
||||
metrics_service_name: Option<String>,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
environment_selections: Option<Vec<codex_protocol::protocol::TurnEnvironmentSelection>>,
|
||||
user_shell_override: Option<crate::shell::Shell>,
|
||||
) -> CodexResult<NewThread> {
|
||||
Box::pin(self.spawn_thread_with_source(
|
||||
@@ -899,6 +910,7 @@ impl ThreadManagerState {
|
||||
/*inherited_shell_snapshot*/ None,
|
||||
/*inherited_exec_policy*/ None,
|
||||
parent_trace,
|
||||
environment_selections,
|
||||
user_shell_override,
|
||||
))
|
||||
.await
|
||||
@@ -918,6 +930,7 @@ impl ThreadManagerState {
|
||||
inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
|
||||
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
environment_selections: Option<Vec<codex_protocol::protocol::TurnEnvironmentSelection>>,
|
||||
user_shell_override: Option<crate::shell::Shell>,
|
||||
) -> CodexResult<NewThread> {
|
||||
let environment = self.environment_manager.default_environment();
|
||||
@@ -955,6 +968,7 @@ impl ThreadManagerState {
|
||||
inherited_exec_policy,
|
||||
user_shell_override,
|
||||
parent_trace,
|
||||
environment_selections,
|
||||
analytics_events_client: self.analytics_events_client.clone(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -567,6 +567,10 @@ pub enum Op {
|
||||
/// Updated personality preference.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
personality: Option<Personality>,
|
||||
|
||||
/// Updated sticky environment selections for future turns.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
environments: Option<Vec<TurnEnvironmentSelection>>,
|
||||
},
|
||||
|
||||
/// Approve a command execution
|
||||
|
||||
Reference in New Issue
Block a user