Files
codex/codex-rs/core/src/session/review.rs
T
Adam Perry @ OpenAI 1f52d40704 Bind permission grants to the originating turn (#49880)
## Why

Background code-mode cells can continue running after a new turn starts. Looking up grants and strict review requirements through the active turn can make those cells lose their approved access or use another turn's permissions.

## What changed

- Keep turn-scoped grants and strict review state in `TurnContext`, retained by captured steps and background cells.
- Record permission responses against the requesting turn, including requests made after a later turn starts.
- Share live session-scoped grants across turns while keeping turn-scoped grants isolated.
- Use the originating turn's permissions and review requirements for tool execution, patches, MCP calls, file uploads, and terminal input.

## Testing

Add regression coverage for background cells retaining grants across turn changes, turn versus session scope, permission requests after the next turn starts, and strict review enabled while `write_stdin` is queued.

GitOrigin-RevId: 641502fbfd43553c2c42424d4aaa53ae8020c850
2026-10-01 05:33:36 +00:00

183 lines
7.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::step_settings::ResolvedStepSettings;
use super::*;
/// Spawn a review thread using the given prompt.
pub(super) async fn spawn_review_thread(
sess: Arc<Session>,
config: Arc<Config>,
parent_turn_context: Arc<TurnContext>,
sub_id: String,
resolved: crate::review_prompts::ResolvedReviewRequest,
) {
let model = config
.review_model
.clone()
.unwrap_or_else(|| parent_turn_context.model_info().slug.clone());
let available_models = sess
.services
.models_manager
.list_models(
RefreshStrategy::OnlineIfUncached,
config.http_client_factory(),
)
.await;
let review_model_info = sess
.services
.models_manager
.get_model_info(&model, &config.to_models_manager_config())
.await;
// For reviews, disable web_search and view_image regardless of global settings.
let mut review_features = sess.features.clone();
let _ = review_features.disable(Feature::WebSearchRequest);
let _ = review_features.disable(Feature::WebSearchCached);
let _ = review_features.disable(Feature::Goals);
let review_web_search_mode = WebSearchMode::Disabled;
let unified_exec_shell_mode = UnifiedExecShellMode::for_session(
review_features.get(),
crate::tools::tool_user_shell_type(sess.services.user_shell.as_ref()),
sess.services.shell_zsh_path.as_ref(),
sess.services.main_execve_wrapper_exe.as_ref(),
);
let review_prompt = resolved.prompt.clone();
let model_info = review_model_info.clone();
let mut selected = parent_turn_context.initial_settings.selected().clone();
let mut reasoning_effort = selected.collaboration_mode.reasoning_effort();
// Build per‑turn client with the requested model/family.
let mut per_turn_config = (*parent_turn_context.config).clone();
// Preserve configured overrides without carrying over the parent model's defaults.
per_turn_config.token_budget = config.token_budget.clone();
per_turn_config.features = review_features.clone();
if let Some(current_effort) = reasoning_effort.as_ref()
&& review_model_info.slug != parent_turn_context.model_info().slug
&& !review_model_info.used_fallback_model_metadata
&& !review_model_info
.supported_reasoning_levels
.iter()
.any(|preset| &preset.effort == current_effort)
{
let supported_reasoning_levels = &review_model_info.supported_reasoning_levels;
reasoning_effort = supported_reasoning_levels
.get(supported_reasoning_levels.len().saturating_sub(1) / 2)
.map(|preset| preset.effort.clone())
.or_else(|| review_model_info.default_reasoning_level.clone());
}
if let Err(err) = per_turn_config.web_search_mode.set(review_web_search_mode) {
let fallback_value = per_turn_config.web_search_mode.value();
tracing::warn!(
error = %err,
?review_web_search_mode,
?fallback_value,
"review web_search_mode is disallowed by requirements; keeping constrained value"
);
}
let session_source = parent_turn_context.session_source.clone();
let (forked_from_thread_id, thread_source, service_tier) = {
let state = sess.state.lock().await;
(
state.session_configuration.forked_from_thread_id,
state.session_configuration.thread_source.clone(),
state
.session_configuration
.step_settings
.service_tier
.clone()
.or_else(|| config.service_tier.clone()),
)
};
let auto_review_enabled = crate::guardian::routes_approval_policy_to_guardian(
per_turn_config.permissions.approval_policy.value(),
per_turn_config.approvals_reviewer,
);
// The original config may retain a requested tier unsupported by the parent
// model. Resolve it against the review model and the shared feature gate.
selected.collaboration_mode.settings.model = model.clone();
selected.collaboration_mode.settings.reasoning_effort = reasoning_effort.clone();
selected.service_tier = service_tier;
let step_settings = Arc::new(ResolvedStepSettings::new(
Arc::new(selected),
Arc::new(model_info.clone()),
review_features.enabled(Feature::FastMode),
));
per_turn_config.model = Some(model);
per_turn_config.model_reasoning_effort = reasoning_effort;
per_turn_config.service_tier = step_settings.service_tier.clone();
let per_turn_config = Arc::new(per_turn_config);
let review_turn_id = sub_id.to_string();
#[allow(deprecated)]
let windows_sandbox_selection = parent_turn_context
.initial_environments
.primary()
.map(TurnEnvironment::windows_sandbox_selection_for_turn_metadata)
.unwrap_or_else(|| {
crate::tools::sandboxing::configured_windows_sandbox_selection(
parent_turn_context
.config
.effective_local_windows_sandbox_type(),
parent_turn_context.windows_sandbox_level,
&codex_utils_path_uri::PathUri::from_abs_path(&parent_turn_context.cwd),
)
});
let turn_metadata_state = Arc::new(TurnMetadataState::new(
sess.session_id().to_string(),
sess.thread_id().to_string(),
forked_from_thread_id,
parent_turn_context.parent_thread_id,
&session_source,
thread_source,
review_turn_id.clone(),
#[allow(deprecated)]
parent_turn_context.cwd.clone(),
&parent_turn_context.permission_profile(),
windows_sandbox_selection,
parent_turn_context.network.is_some(),
auto_review_enabled,
&model_info,
));
let review_turn_context = parent_turn_context.for_review(
review_turn_id,
per_turn_config,
step_settings,
available_models,
unified_exec_shell_mode,
turn_metadata_state,
);
// Seed the child task with the review prompt as the initial user message.
let input = vec![TurnInput::UserInput {
metadata: Default::default(),
content: vec![UserInput::Text {
text: review_prompt,
// Review prompt is synthesized; no UI element ranges to preserve.
text_elements: Vec::new(),
}],
client_id: None,
}];
let tc = Arc::new(review_turn_context);
if tc
.initial_environments
.single_local_environment_cwd()
.is_some()
{
tc.turn_metadata_state
.spawn_git_enrichment_task(Arc::clone(&sess.services.git_root_discovery));
}
// TODO(ccunningham): Review turns currently rely on `spawn_task` for TurnComplete but do not
// emit a parent TurnStarted. Consider giving review a full parent turn lifecycle
// (TurnStarted + TurnComplete) for consistency with other standalone tasks.
sess.spawn_task(Arc::clone(&tc), input, ReviewTask::new())
.await;
// Announce entering review mode so UIs can switch modes.
let item = TurnItem::EnteredReviewMode(EnteredReviewModeItem {
id: uuid::Uuid::now_v7().to_string(),
target: resolved.target,
user_facing_hint: resolved.user_facing_hint,
});
sess.emit_turn_item_started(&tc, &item).await;
sess.emit_turn_item_completed(&tc, item).await;
}