Support Codex turn context overrides

This commit is contained in:
raju
2026-05-13 20:05:15 -07:00
parent bd42660cb4
commit b4ecb42966
13 changed files with 174 additions and 1 deletions
@@ -3409,6 +3409,8 @@ fn turn_start_params_preserve_explicit_null_service_tier() {
responsesapi_client_metadata: None,
environments: None,
cwd: None,
current_date: None,
timezone: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
@@ -64,6 +64,14 @@ pub struct TurnStartParams {
/// Override the working directory for this turn and subsequent turns.
#[ts(optional = nullable)]
pub cwd: Option<PathBuf>,
/// Override the current date shown in the model-visible environment context
/// for this turn and subsequent turns.
#[ts(optional = nullable)]
pub current_date: Option<String>,
/// Override the timezone shown in the model-visible environment context for
/// this turn and subsequent turns.
#[ts(optional = nullable)]
pub timezone: Option<String>,
/// Override the approval policy for this turn and subsequent turns.
#[experimental(nested)]
#[ts(optional = nullable)]
@@ -658,6 +658,8 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
}],
responsesapi_client_metadata: None,
cwd: None,
current_date: None,
timezone: None,
approval_policy: None,
sandbox_policy: None,
permissions: None,
@@ -354,6 +354,8 @@ impl TurnRequestProcessor {
let turn_has_input = !mapped_items.is_empty();
let has_any_overrides = params.cwd.is_some()
|| params.current_date.is_some()
|| params.timezone.is_some()
|| params.approval_policy.is_some()
|| params.approvals_reviewer.is_some()
|| params.sandbox_policy.is_some()
@@ -372,6 +374,8 @@ impl TurnRequestProcessor {
}
let cwd = params.cwd;
let current_date = params.current_date;
let timezone = params.timezone;
let approval_policy = params.approval_policy.map(AskForApproval::to_core);
let approvals_reviewer = params
.approvals_reviewer
@@ -429,6 +433,8 @@ impl TurnRequestProcessor {
thread
.validate_turn_context_overrides(CodexThreadTurnContextOverrides {
cwd: cwd.clone(),
current_date: current_date.clone(),
timezone: timezone.clone(),
approval_policy,
approvals_reviewer,
sandbox_policy: sandbox_policy.clone(),
@@ -454,6 +460,8 @@ impl TurnRequestProcessor {
final_output_json_schema: params.output_schema,
responsesapi_client_metadata: params.responsesapi_client_metadata,
cwd,
current_date,
timezone,
approval_policy,
approvals_reviewer,
sandbox_policy,
@@ -1890,6 +1890,8 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
}],
responsesapi_client_metadata: None,
cwd: Some(first_cwd.clone()),
current_date: None,
timezone: None,
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
@@ -1931,6 +1933,8 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
}],
responsesapi_client_metadata: None,
cwd: Some(second_cwd.clone()),
current_date: None,
timezone: None,
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess),
+6
View File
@@ -81,6 +81,8 @@ impl ThreadConfigSnapshot {
#[derive(Clone, Default)]
pub struct CodexThreadTurnContextOverrides {
pub cwd: Option<PathBuf>,
pub current_date: Option<String>,
pub timezone: Option<String>,
pub approval_policy: Option<AskForApproval>,
pub approvals_reviewer: Option<ApprovalsReviewer>,
pub sandbox_policy: Option<SandboxPolicy>,
@@ -240,6 +242,8 @@ impl CodexThread {
) -> ConstraintResult<()> {
let CodexThreadTurnContextOverrides {
cwd,
current_date,
timezone,
approval_policy,
approvals_reviewer,
sandbox_policy,
@@ -265,6 +269,8 @@ impl CodexThread {
let updates = SessionSettingsUpdate {
cwd,
current_date,
timezone,
approval_policy,
approvals_reviewer,
sandbox_policy,
@@ -69,6 +69,21 @@ fn build_permissions_update_item(
)
}
fn build_developer_instructions_update_item(
previous: Option<&TurnContextItem>,
next: &TurnContext,
) -> Option<String> {
let prev = previous?;
if prev.developer_instructions == next.developer_instructions {
return None;
}
next.developer_instructions
.as_deref()
.filter(|instructions| !instructions.trim().is_empty())
.map(str::to_string)
}
fn build_collaboration_mode_update_item(
previous: Option<&TurnContextItem>,
next: &TurnContext,
@@ -218,6 +233,7 @@ pub(crate) fn build_settings_update_items(
// Keep model-switch instructions first so model-specific guidance is read before
// any other context diffs on this turn.
build_model_instructions_update_item(previous_turn_settings, next),
build_developer_instructions_update_item(previous, next),
build_permissions_update_item(previous, next, exec_policy),
build_collaboration_mode_update_item(previous, next),
build_realtime_update_item(previous, previous_turn_settings, next),
+6
View File
@@ -143,6 +143,8 @@ pub(super) async fn user_input_or_turn_inner(
items,
SessionSettingsUpdate {
cwd: Some(cwd),
current_date: None,
timezone: None,
approval_policy: Some(approval_policy),
approvals_reviewer,
sandbox_policy: Some(sandbox_policy),
@@ -163,6 +165,8 @@ pub(super) async fn user_input_or_turn_inner(
}
Op::UserInputWithTurnContext {
cwd,
current_date,
timezone,
approval_policy,
approvals_reviewer,
sandbox_policy,
@@ -195,6 +199,8 @@ pub(super) async fn user_input_or_turn_inner(
items,
SessionSettingsUpdate {
cwd,
current_date,
timezone,
approval_policy,
approvals_reviewer,
sandbox_policy,
+2
View File
@@ -620,6 +620,8 @@ impl Codex {
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
current_date: None,
timezone: None,
codex_home: config.codex_home.clone(),
thread_name: None,
environments: environment_selections.to_selections(),
+12
View File
@@ -75,6 +75,10 @@ pub(crate) struct SessionConfiguration {
/// execution sandbox are resolved against this directory **instead** of
/// the process-wide current working directory.
pub(super) cwd: AbsolutePathBuf,
/// Optional current date override for the model-visible environment context.
pub(super) current_date: Option<String>,
/// Optional timezone override for the model-visible environment context.
pub(super) timezone: Option<String>,
/// Directory containing all Codex state for this session.
pub(super) codex_home: AbsolutePathBuf,
/// Optional user-facing name for the thread, updated during the session.
@@ -199,6 +203,12 @@ impl SessionConfiguration {
if let Some(approval_policy) = updates.approval_policy {
next_configuration.approval_policy.set(approval_policy)?;
}
if let Some(current_date) = updates.current_date.clone() {
next_configuration.current_date = Some(current_date);
}
if let Some(timezone) = updates.timezone.clone() {
next_configuration.timezone = Some(timezone);
}
if let Some(approvals_reviewer) = updates.approvals_reviewer {
next_configuration.approvals_reviewer = approvals_reviewer;
}
@@ -309,6 +319,8 @@ impl SessionConfiguration {
#[derive(Default, Clone)]
pub(crate) struct SessionSettingsUpdate {
pub(crate) cwd: Option<PathBuf>,
pub(crate) current_date: Option<String>,
pub(crate) timezone: Option<String>,
pub(crate) approval_policy: Option<AskForApproval>,
pub(crate) approvals_reviewer: Option<ApprovalsReviewer>,
pub(crate) sandbox_policy: Option<SandboxPolicy>,
+91
View File
@@ -2581,6 +2581,8 @@ async fn set_rate_limits_retains_previous_credits() {
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
current_date: None,
timezone: None,
codex_home: config.codex_home.clone(),
thread_name: None,
environments: Vec::new(),
@@ -2685,6 +2687,8 @@ async fn set_rate_limits_updates_plan_type_when_present() {
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
current_date: None,
timezone: None,
codex_home: config.codex_home.clone(),
thread_name: None,
environments: Vec::new(),
@@ -3126,6 +3130,22 @@ async fn session_settings_legacy_fast_service_tier_update_uses_priority_request_
);
}
#[tokio::test]
async fn session_configuration_apply_updates_environment_context_overrides() {
let session_configuration = make_session_configuration_for_tests().await;
let updated = session_configuration
.apply(&SessionSettingsUpdate {
current_date: Some("2026-05-13".to_string()),
timezone: Some("America/Los_Angeles".to_string()),
..Default::default()
})
.expect("time context update should succeed");
assert_eq!(updated.current_date.as_deref(), Some("2026-05-13"));
assert_eq!(updated.timezone.as_deref(), Some("America/Los_Angeles"));
}
pub(crate) async fn make_session_configuration_for_tests() -> SessionConfiguration {
let codex_home = tempfile::tempdir().expect("create temp dir");
let config = build_test_config(codex_home.path()).await;
@@ -3162,6 +3182,8 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
current_date: None,
timezone: None,
codex_home: config.codex_home.clone(),
thread_name: None,
environments: Vec::new(),
@@ -3688,6 +3710,8 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
current_date: None,
timezone: None,
codex_home: config.codex_home.clone(),
thread_name: None,
environments: Vec::new(),
@@ -3797,6 +3821,8 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
current_date: None,
timezone: None,
codex_home: config.codex_home.clone(),
thread_name: None,
environments: default_environments,
@@ -4026,6 +4052,8 @@ async fn make_session_with_config_and_rx(
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
current_date: None,
timezone: None,
codex_home: config.codex_home.clone(),
thread_name: None,
environments: default_environments,
@@ -4129,6 +4157,8 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
current_date: None,
timezone: None,
codex_home: config.codex_home.clone(),
thread_name: None,
environments: default_environments,
@@ -4776,6 +4806,8 @@ fn op_kind_distinguishes_turn_ops() {
final_output_json_schema: None,
responsesapi_client_metadata: None,
cwd: None,
current_date: None,
timezone: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
@@ -5516,6 +5548,8 @@ where
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
current_date: None,
timezone: None,
codex_home: config.codex_home.clone(),
thread_name: None,
environments: default_environments,
@@ -6004,6 +6038,63 @@ async fn build_settings_update_items_omits_environment_item_when_disabled() {
);
}
#[tokio::test]
async fn build_settings_update_items_emits_developer_item_for_developer_instruction_changes() {
let (session, mut previous_context) = make_session_and_context().await;
previous_context.developer_instructions = Some("old developer guidance".to_string());
let previous_context = Arc::new(previous_context);
let mut current_context = previous_context
.with_model(
previous_context.model_info.slug.clone(),
&session.services.models_manager,
)
.await;
current_context.developer_instructions = Some("new developer guidance".to_string());
let update_items = session
.build_settings_update_items(
Some(&previous_context.to_turn_context_item()),
&current_context,
)
.await;
let developer_texts = developer_input_texts(&update_items);
assert!(
developer_texts
.iter()
.any(|text| text.contains("new developer guidance")),
"expected changed developer instructions to be appended, got {developer_texts:?}"
);
assert!(
!developer_texts
.iter()
.any(|text| text.contains("old developer guidance")),
"did not expect previous developer instructions to be re-appended, got {developer_texts:?}"
);
}
#[tokio::test]
async fn build_settings_update_items_omits_unchanged_developer_instruction_item() {
let (session, mut previous_context) = make_session_and_context().await;
previous_context.developer_instructions = Some("stable developer guidance".to_string());
let previous_context = Arc::new(previous_context);
let current_context = previous_context
.with_model(
previous_context.model_info.slug.clone(),
&session.services.models_manager,
)
.await;
let update_items = session
.build_settings_update_items(
Some(&previous_context.to_turn_context_item()),
&current_context,
)
.await;
assert!(developer_input_texts(&update_items).is_empty());
}
#[tokio::test]
async fn build_settings_update_items_emits_realtime_start_when_session_becomes_live() {
let (session, previous_context) = make_session_and_context().await;
+9 -1
View File
@@ -536,7 +536,15 @@ impl Session {
session_configuration.windows_sandbox_level,
network.is_some(),
));
let (current_date, timezone) = local_time_context();
let (local_current_date, local_timezone) = local_time_context();
let current_date = session_configuration
.current_date
.clone()
.unwrap_or(local_current_date);
let timezone = session_configuration
.timezone
.clone()
.unwrap_or(local_timezone);
TurnContext {
sub_id,
trace_id: current_span_trace_id(),
+8
View File
@@ -462,6 +462,14 @@ pub enum Op {
#[serde(skip_serializing_if = "Option::is_none")]
cwd: Option<PathBuf>,
/// Updated current date shown in the model-visible environment context.
#[serde(skip_serializing_if = "Option::is_none")]
current_date: Option<String>,
/// Updated timezone shown in the model-visible environment context.
#[serde(skip_serializing_if = "Option::is_none")]
timezone: Option<String>,
/// Updated command approval policy.
#[serde(skip_serializing_if = "Option::is_none")]
approval_policy: Option<AskForApproval>,