mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
Support Codex turn context overrides
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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()),
|
||||
¤t_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()),
|
||||
¤t_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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user