Keep step settings and approval environments consistent (#46556)

## Why

Background processes can outlive their launching turn. Their approval reviews need the originating environment's filesystem restrictions, even when the active turn uses a different environment. Remote restrictions must also be resolved using executor paths rather than the local filesystem.

## What changed

- Capture settings and environment selections together in `StepInputs`. Preserve the captured selection during active model updates while allowing environment startup to finish.
- Carry environment IDs into Guardian network and `execve` requests, and use the owning process's environment with current review settings. Refresh environment readiness for network requests that fall back to the active turn.
- Resolve remote denied paths and globs using the executor's policy context. Decline automatic approval when the request's environment is unavailable or an explicitly denied temporary directory cannot be resolved.

## Testing

Add regression coverage for active model updates retaining their environment while the next turn adopts a new selection, background approvals using their owning environment, newly ready network environments, and remote denied paths and globs, including Windows paths and missing temporary-directory metadata.

GitOrigin-RevId: b620d4b030c597a4f430fe0500864fa5469db3d4
This commit is contained in:
sayan-oai
2026-09-19 01:27:26 +00:00
committed by copyberry
parent 328feb0c29
commit c7828dd010
44 changed files with 872 additions and 255 deletions
+17 -8
View File
@@ -119,7 +119,7 @@ async fn exec_command_with_tty(
/*network_policy_decider*/ None,
tty,
Box::new(NoopSpawnLifecycle),
turn.environments
turn.initial_environments
.primary()
.expect("turn environment")
.environment
@@ -147,7 +147,9 @@ async fn exec_command_with_tty(
tty,
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
permissions: TerminalPermissions::for_launch(
turn.environments.primary().expect("turn environment"),
turn.initial_environments
.primary()
.expect("turn environment"),
turn,
TerminalSandboxSource::Native,
SandboxPermissions::UseDefault,
@@ -621,7 +623,9 @@ async fn terminating_initial_exec_command_rechecks_initial_response_state() -> a
tty: true,
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
permissions: TerminalPermissions::for_launch(
turn.environments.primary().expect("turn environment"),
turn.initial_environments
.primary()
.expect("turn environment"),
&turn,
TerminalSandboxSource::Native,
SandboxPermissions::UseDefault,
@@ -704,7 +708,9 @@ async fn terminating_during_stdin_poll_returns_exited_response() -> anyhow::Resu
tty: true,
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
permissions: TerminalPermissions::for_launch(
turn.environments.primary().expect("turn environment"),
turn.initial_environments
.primary()
.expect("turn environment"),
&turn,
TerminalSandboxSource::Native,
SandboxPermissions::UseDefault,
@@ -850,7 +856,8 @@ async fn remote_exec_server_rejects_inherited_fd_launches() -> anyhow::Result<()
let remote_test_env = remote_test_env().await?;
let (_, mut turn) = make_session_and_context().await;
let TurnEnvironmentState::Ready(environment) = &mut turn.environments.environments[0] else {
let TurnEnvironmentState::Ready(environment) = &mut turn.initial_environments.environments[0]
else {
panic!("expected ready primary environment");
};
environment.environment = Arc::new(remote_test_env.environment().clone());
@@ -876,7 +883,7 @@ async fn remote_exec_server_rejects_inherited_fd_launches() -> anyhow::Result<()
Box::new(TestSpawnLifecycle {
inherited_fds: vec![42],
}),
turn.environments
turn.initial_environments
.primary()
.expect("turn environment")
.environment
@@ -928,7 +935,9 @@ async fn stdin_approval_preserves_the_reviewed_terminal() -> anyhow::Result<()>
let mut store = manager.process_store.lock().await;
let entry = store.processes.get_mut(&process_id).unwrap();
entry.permissions = TerminalPermissions::for_launch(
turn.environments.primary().expect("turn environment"),
turn.initial_environments
.primary()
.expect("turn environment"),
&turn,
TerminalSandboxSource::Native,
SandboxPermissions::RequireEscalated,
@@ -984,7 +993,7 @@ async fn stdin_approval_preserves_the_reviewed_terminal() -> anyhow::Result<()>
.get_mut(&process_id)
.unwrap()
.environment_id = turn
.environments
.initial_environments
.primary()
.unwrap()
.selection
@@ -463,7 +463,7 @@ async fn failed_initial_end_for_unstored_process_uses_fallback_output() {
#[allow(deprecated)]
sandbox_cwd: turn.cwd.clone().into(),
turn_environment: turn
.environments
.initial_environments
.primary()
.cloned()
.expect("primary environment"),
@@ -631,7 +631,9 @@ async fn pruning_does_not_evict_live_process_while_exited_process_is_finalizing(
tty: false,
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
permissions: super::super::TerminalPermissions::for_launch(
turn.environments.primary().expect("turn environment"),
turn.initial_environments
.primary()
.expect("turn environment"),
&turn,
super::super::TerminalSandboxSource::Native,
crate::sandboxing::SandboxPermissions::UseDefault,
@@ -98,7 +98,11 @@ fn denied_reads_reject_file_system_changes_but_only_review_network_changes() {
#[tokio::test]
async fn captured_network_changes_require_review() -> anyhow::Result<()> {
let (_session, mut turn) = make_session_and_context().await;
let mut environment = turn.environments.primary().expect("environment").clone();
let mut environment = turn
.initial_environments
.primary()
.expect("environment")
.clone();
environment.config_mut().permission_profile =
PermissionProfileSnapshot::legacy(PermissionProfile::read_only());
let mut proxy = NetworkProxyConfig {
@@ -159,7 +163,11 @@ async fn captured_network_changes_require_review() -> anyhow::Result<()> {
#[tokio::test]
async fn internal_grants_require_review_without_exposing_paths() -> anyhow::Result<()> {
let (_session, turn) = make_session_and_context().await;
let mut environment = turn.environments.primary().expect("environment").clone();
let mut environment = turn
.initial_environments
.primary()
.expect("environment")
.clone();
environment.config_mut().permission_profile =
PermissionProfileSnapshot::legacy(PermissionProfile::read_only());
let grants = serde_json::from_value(json!({
@@ -195,7 +203,11 @@ async fn internal_grants_require_review_without_exposing_paths() -> anyhow::Resu
#[tokio::test]
async fn readable_snapshot_does_not_require_stdin_approval() -> anyhow::Result<()> {
let (_session, turn) = make_session_and_context().await;
let mut environment = turn.environments.primary().expect("environment").clone();
let mut environment = turn
.initial_environments
.primary()
.expect("environment")
.clone();
environment.config_mut().permission_profile =
PermissionProfileSnapshot::legacy(PermissionProfile::read_only());
let snapshot = turn.config.cwd.join("shell-snapshot.sh");
@@ -230,7 +242,11 @@ async fn enabling_windows_sandbox_respects_the_launch_backend(
expected: SandboxPermissions,
) -> anyhow::Result<()> {
let (_session, turn) = make_session_and_context().await;
let mut environment = turn.environments.primary().expect("environment").clone();
let mut environment = turn
.initial_environments
.primary()
.expect("environment")
.clone();
environment.selection.cwd = PathUri::parse("file:///C:/workspace")?;
environment.config_mut().permission_profile =
PermissionProfileSnapshot::legacy(PermissionProfile::read_only());