Compare commits

...

1 Commits

Author SHA1 Message Date
Richard Lee
1c63f4ac34 Support turn scoped tool config overrides 2026-05-19 13:42:39 -07:00
6 changed files with 94 additions and 1 deletions

View File

@@ -103,6 +103,11 @@ pub struct TurnStartParams {
/// Override the personality for this turn and subsequent turns.
#[ts(optional = nullable)]
pub personality: Option<Personality>,
/// Request-scoped config overrides for this turn and subsequent turns.
/// These mirror thread start/resume config overrides so clients can adjust
/// model-visible tool availability without rebuilding the thread.
#[ts(optional = nullable)]
pub config: Option<HashMap<String, JsonValue>>,
/// Optional JSON Schema used to constrain the final assistant message for
/// this turn.
#[ts(optional = nullable)]

View File

@@ -1,4 +1,5 @@
use super::*;
use codex_protocol::protocol::McpServerRefreshConfig;
#[derive(Clone)]
pub(crate) struct TurnRequestProcessor {
@@ -347,6 +348,7 @@ impl TurnRequestProcessor {
.collaboration_mode
.map(|mode| self.normalize_turn_start_collaboration_mode(mode));
let environment_selections = self.parse_environment_selections(params.environments)?;
let runtime_config_overrides = params.config.clone();
// Map v2 input items to core input items.
let mapped_items: Vec<CoreInputItem> = params
@@ -366,7 +368,8 @@ impl TurnRequestProcessor {
|| params.effort.is_some()
|| params.summary.is_some()
|| collaboration_mode.is_some()
|| params.personality.is_some();
|| params.personality.is_some()
|| runtime_config_overrides.is_some();
if params.sandbox_policy.is_some() && params.permissions.is_some() {
return Err(invalid_request(
@@ -424,6 +427,26 @@ impl TurnRequestProcessor {
let summary = params.summary;
let service_tier = params.service_tier;
let personality = params.personality;
let runtime_config = if let Some(request_overrides) = runtime_config_overrides {
let snapshot = thread.config_snapshot().await;
let config = self
.config_manager
.load_for_cwd(
Some(request_overrides),
ConfigOverrides {
cwd: cwd.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
..Default::default()
},
Some(snapshot.cwd.to_path_buf()),
)
.await
.map_err(|err| config_load_error(&err))?;
Some(config)
} else {
None
};
// If any overrides are provided, validate them synchronously so the
// request can fail before accepting user input. The actual update is
@@ -449,6 +472,34 @@ impl TurnRequestProcessor {
.map_err(|err| invalid_request(format!("invalid turn context override: {err}")))?;
}
if let Some(runtime_config) = runtime_config {
thread.replace_runtime_config(runtime_config.clone()).await;
let configured_mcp_servers = self
.thread_manager
.mcp_manager()
.configured_servers(&runtime_config)
.await;
thread
.submit(Op::RefreshMcpServers {
config: McpServerRefreshConfig {
mcp_servers: serde_json::to_value(configured_mcp_servers)
.map_err(|err| internal_error(format!(
"failed to serialize turn MCP refresh config: {err}"
)))?,
mcp_oauth_credentials_store_mode: serde_json::to_value(
runtime_config.mcp_oauth_credentials_store_mode,
)
.map_err(|err| internal_error(format!(
"failed to serialize turn MCP OAuth store mode: {err}"
)))?,
},
})
.await
.map_err(|err| {
internal_error(format!("failed to queue turn MCP refresh: {err}"))
})?;
}
// Start the turn by submitting the user input. Return its submission id as turn_id.
let turn_op = if has_any_overrides {
Op::UserInputWithTurnContext {

View File

@@ -489,6 +489,10 @@ impl CodexThread {
self.codex.session.refresh_runtime_config(next_config).await;
}
pub async fn replace_runtime_config(&self, next_config: crate::config::Config) {
self.codex.session.replace_runtime_config(next_config).await;
}
pub async fn environment_selections(&self) -> Vec<TurnEnvironmentSelection> {
self.codex.thread_environment_selections().await
}

View File

@@ -1451,6 +1451,37 @@ impl Session {
}
}
pub(crate) async fn replace_runtime_config(&self, next_config: Config) {
let notify_config_contributors = !self.services.extensions.config_contributors().is_empty();
let (previous_config, new_config, config) = {
let mut state = self.state.lock().await;
let previous_config = notify_config_contributors
.then(|| Self::build_effective_session_config(&state.session_configuration));
let config = Arc::new(next_config);
state.session_configuration.original_config_do_not_use = Arc::clone(&config);
let new_config = notify_config_contributors
.then(|| Self::build_effective_session_config(&state.session_configuration));
(previous_config, new_config, config)
};
self.emit_config_changed_contributors(previous_config.as_ref(), new_config.as_ref());
self.services.skills_manager.clear_cache();
self.services.plugins_manager.clear_cache();
let hooks = build_hooks_for_config(
config.as_ref(),
self.services.plugins_manager.as_ref(),
self.services.user_shell.as_ref(),
)
.await;
let state = self.state.lock().await;
if Arc::ptr_eq(
&state.session_configuration.original_config_do_not_use,
&config,
) {
self.services.hooks.store(Arc::new(hooks));
}
}
fn emit_config_changed_contributors(
&self,
previous_config: Option<&Config>,

View File

@@ -793,6 +793,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
effort: default_effort,
summary: None,
personality: None,
config: None,
output_schema,
collaboration_mode: None,
},

View File

@@ -585,6 +585,7 @@ impl AppServerSession {
effort,
summary,
personality,
config: None,
output_schema,
collaboration_mode,
},