mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Add an opt-in preference for the local MXC sandbox (#47375)
## What changed Add the default-off `features.prefer_mxc` flag to select MXC for local Windows execution when native support is available and effective network settings do not explicitly forbid local binding. Otherwise, retain the configured backend and legacy setup behavior. Resolve the preference during config loading and use the effective local backend for execution, network proxy setup, sandbox diagnostics, and `windowsSandbox/readiness`. Preserve the configured backend for remote executors and `config/read`. Explicit `windows.sandbox = "mxc"` remains strict, and command failures do not trigger backend fallback. ## Testing Add coverage for preference resolution, startup readiness without rewriting configuration, local and remote backend selection across environment updates and child threads, and workspace-write permission context in an agent turn. GitOrigin-RevId: b51178f0371d38ebb58e41af54a068fca67fa316
This commit is contained in:
@@ -265,10 +265,10 @@ impl CommandExecRequestProcessor {
|
||||
.start_proxy(
|
||||
&network_proxy_permission_profile,
|
||||
managed_proxy_routing_for_windows_sandbox(
|
||||
self.config.permissions.windows_sandbox_type,
|
||||
self.config.effective_local_windows_sandbox_type(),
|
||||
),
|
||||
local_binding_policy_for_sandbox(
|
||||
self.config.permissions.windows_sandbox_type,
|
||||
self.config.effective_local_windows_sandbox_type(),
|
||||
Some(std::env::consts::OS),
|
||||
),
|
||||
/*policy_decider*/ None,
|
||||
@@ -321,7 +321,7 @@ impl CommandExecRequestProcessor {
|
||||
windows_sandbox_workspace_roots.as_slice(),
|
||||
&codex_linux_sandbox_exe,
|
||||
&self.arg0_paths.codex_self_exe,
|
||||
self.config.permissions.windows_sandbox_type,
|
||||
self.config.effective_local_windows_sandbox_type(),
|
||||
use_legacy_landlock,
|
||||
)
|
||||
.map_err(|err| internal_error(format!("exec failed: {err}")))?;
|
||||
|
||||
@@ -34,6 +34,7 @@ impl WindowsSandboxRequestProcessor {
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
#[cfg(target_os = "windows")]
|
||||
if codex_windows_sandbox::registered_core_requested()
|
||||
&& self.config.effective_local_windows_sandbox_type() != SandboxType::WindowsMxc
|
||||
&& matches!(
|
||||
WindowsSandboxLevel::from_config(&self.config),
|
||||
WindowsSandboxLevel::Elevated
|
||||
@@ -306,7 +307,7 @@ fn determine_windows_sandbox_readiness(config: &Config) -> WindowsSandboxReadine
|
||||
};
|
||||
}
|
||||
|
||||
if config.permissions.windows_sandbox_type == SandboxType::WindowsMxc {
|
||||
if config.effective_local_windows_sandbox_type() == SandboxType::WindowsMxc {
|
||||
return WindowsSandboxReadinessResponse {
|
||||
status: WindowsSandboxReadiness::Ready,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@ use app_test_support::to_response;
|
||||
use app_test_support::write_mock_responses_config_toml;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::WindowsSandboxReadiness;
|
||||
use codex_app_server_protocol::WindowsSandboxReadinessResponse;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupCompletedNotification;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupMode;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupStartParams;
|
||||
@@ -17,6 +19,49 @@ use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
#[test_case::test_case(false, false)]
|
||||
#[test_case::test_case(true, false)]
|
||||
#[test_case::test_case(true, true)]
|
||||
#[tokio::test]
|
||||
async fn startup_mxc_preference_is_resolved_before_readiness(
|
||||
prefer_mxc: bool,
|
||||
deny_local_binding: bool,
|
||||
) -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut config_toml = "[features]\nprefer_mxc = false\n".to_string();
|
||||
if deny_local_binding {
|
||||
config_toml
|
||||
.push_str("[features.network_proxy]\nenabled = true\nallow_local_binding = false\n");
|
||||
}
|
||||
let config_path = codex_home.path().join("config.toml");
|
||||
std::fs::write(&config_path, &config_toml)?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_args(&["-c", &format!("features.prefer_mxc={prefer_mxc}")])
|
||||
.build_initialized_with_timeout(DEFAULT_READ_TIMEOUT)
|
||||
.await?;
|
||||
let mxc_selected =
|
||||
prefer_mxc && !deny_local_binding && codex_sandboxing::windows_mxc_available();
|
||||
let request_id = mcp
|
||||
.send_raw_request("windowsSandbox/readiness", /*params*/ None)
|
||||
.await?;
|
||||
let readiness: WindowsSandboxReadinessResponse =
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??;
|
||||
assert_eq!(
|
||||
readiness,
|
||||
WindowsSandboxReadinessResponse {
|
||||
status: if mxc_selected {
|
||||
WindowsSandboxReadiness::Ready
|
||||
} else {
|
||||
WindowsSandboxReadiness::NotConfigured
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(std::fs::read_to_string(config_path)?, config_toml);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn windows_sandbox_setup_start_emits_completion_notification() -> Result<()> {
|
||||
let responses = Vec::new();
|
||||
|
||||
@@ -319,7 +319,8 @@ async fn run_command_under_sandbox(
|
||||
if let SandboxType::Windows = sandbox_type {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if config.permissions.windows_sandbox_type != codex_sandboxing::SandboxType::WindowsMxc
|
||||
if config.effective_local_windows_sandbox_type()
|
||||
!= codex_sandboxing::SandboxType::WindowsMxc
|
||||
{
|
||||
let workspace_roots = config
|
||||
.effective_workspace_roots()
|
||||
@@ -356,9 +357,11 @@ async fn run_command_under_sandbox(
|
||||
Some(spec) => Some(
|
||||
spec.start_proxy(
|
||||
&permission_profile,
|
||||
managed_proxy_routing_for_windows_sandbox(config.permissions.windows_sandbox_type),
|
||||
managed_proxy_routing_for_windows_sandbox(
|
||||
config.effective_local_windows_sandbox_type(),
|
||||
),
|
||||
local_binding_policy_for_sandbox(
|
||||
config.permissions.windows_sandbox_type,
|
||||
config.effective_local_windows_sandbox_type(),
|
||||
Some(std::env::consts::OS),
|
||||
),
|
||||
/*policy_decider*/ None,
|
||||
|
||||
@@ -123,7 +123,7 @@ pub(super) fn sandbox_check(config: &Config, arg0_paths: &Arg0DispatchPaths) ->
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let configured_level = WindowsSandboxLevel::from_config(config);
|
||||
let mxc = config.permissions.windows_sandbox_type
|
||||
let mxc = config.effective_local_windows_sandbox_type()
|
||||
== codex_protocol::sandbox::SandboxType::WindowsMxc;
|
||||
let elevated = !mxc
|
||||
&& configured_level != WindowsSandboxLevel::Disabled
|
||||
|
||||
@@ -951,6 +951,9 @@
|
||||
"powershell_shell_version": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"prefer_mxc": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"prevent_idle_sleep": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -6847,6 +6850,9 @@
|
||||
"powershell_shell_version": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"prefer_mxc": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"prevent_idle_sleep": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -3524,14 +3524,22 @@ async fn default_permissions_profile_can_extend_builtin_read_only() -> std::io::
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case::test_case(false; "legacy")]
|
||||
#[test_case::test_case(true; "prefer_mxc")]
|
||||
#[tokio::test]
|
||||
async fn empty_config_defaults_to_builtin_profile_for_trusted_project() -> std::io::Result<()> {
|
||||
async fn empty_config_defaults_to_builtin_profile_for_trusted_project(
|
||||
prefer_mxc: bool,
|
||||
) -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let project_key = cwd.path().to_string_lossy().to_string();
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
features: Some(FeaturesToml::from(BTreeMap::from([(
|
||||
"prefer_mxc".to_string(),
|
||||
prefer_mxc,
|
||||
)]))),
|
||||
projects: Some(HashMap::from([(
|
||||
project_key,
|
||||
ProjectConfig {
|
||||
@@ -3548,6 +3556,7 @@ async fn empty_config_defaults_to_builtin_profile_for_trusted_project() -> std::
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mxc_selected = prefer_mxc && codex_sandboxing::windows_mxc_available();
|
||||
let policy = config.permissions.file_system_sandbox_policy();
|
||||
assert_eq!(
|
||||
config
|
||||
@@ -3555,13 +3564,13 @@ async fn empty_config_defaults_to_builtin_profile_for_trusted_project() -> std::
|
||||
.active_permission_profile()
|
||||
.as_ref()
|
||||
.map(|active| active.id.as_str()),
|
||||
Some(if cfg!(target_os = "windows") {
|
||||
Some(if cfg!(target_os = "windows") && !mxc_selected {
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY
|
||||
} else {
|
||||
BUILT_IN_PERMISSION_PROFILE_WORKSPACE
|
||||
})
|
||||
);
|
||||
if cfg!(target_os = "windows") {
|
||||
if cfg!(target_os = "windows") && !mxc_selected {
|
||||
assert!(
|
||||
!policy.can_write_local_path_with_cwd(cwd.path(), cwd.path()),
|
||||
"expected trusted project fallback to stay read-only without Windows sandbox support, policy: {policy:?}"
|
||||
@@ -11030,6 +11039,73 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_mxc_preference_preserves_configured_backend() -> anyhow::Result<()> {
|
||||
use codex_sandboxing::SandboxType::WindowsMxc;
|
||||
use codex_sandboxing::SandboxType::WindowsRestrictedToken;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
for (prefer, resolved_preference, binding, mode, expected) in [
|
||||
(true, true, true, "unelevated", WindowsMxc),
|
||||
(true, false, true, "unelevated", WindowsRestrictedToken),
|
||||
(true, false, false, "unelevated", WindowsRestrictedToken),
|
||||
(false, false, true, "unelevated", WindowsRestrictedToken),
|
||||
(false, false, false, "mxc", WindowsMxc),
|
||||
] {
|
||||
let cfg: ConfigToml = toml::from_str(&format!(
|
||||
"[windows]\nsandbox = {mode:?}\n[features]\nprefer_mxc = {prefer}\n\
|
||||
[features.network_proxy]\nenabled = true\nallow_local_binding = {binding}\n"
|
||||
))?;
|
||||
assert_eq!(
|
||||
network_config_allows_mxc(
|
||||
&EffectivePermissionSelection {
|
||||
profiles: None,
|
||||
selected_profile_id: None,
|
||||
persisted_profile_id_was_provided: false,
|
||||
requirements_force_profile_selection: false,
|
||||
},
|
||||
/*profiles_are_active*/ false,
|
||||
/*permission_profile*/ None,
|
||||
/*network_requirements*/ None,
|
||||
cfg.features.as_ref(),
|
||||
/*enable_network_proxy*/ true,
|
||||
)?,
|
||||
binding,
|
||||
);
|
||||
let mut config = Config::load_from_base_config_with_overrides(
|
||||
cfg,
|
||||
ConfigOverrides {
|
||||
cwd: Some(codex_home.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
config.prefer_mxc,
|
||||
prefer && binding && codex_sandboxing::windows_mxc_available(),
|
||||
);
|
||||
// Exercise both resolved decisions independently of the host's native support.
|
||||
config.prefer_mxc = resolved_preference;
|
||||
assert_eq!(
|
||||
(
|
||||
config.windows_sandbox_type_from_config(),
|
||||
config.effective_local_windows_sandbox_type()
|
||||
),
|
||||
(
|
||||
if mode == "mxc" {
|
||||
WindowsMxc
|
||||
} else {
|
||||
WindowsRestrictedToken
|
||||
},
|
||||
expected
|
||||
),
|
||||
"prefer={prefer}, resolved={resolved_preference}, binding={binding}, mode={mode}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn windows_sandbox_mode_falls_back_when_disallowed_by_requirements() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -215,7 +215,9 @@ pub use permissions::resolve_permission_profile;
|
||||
pub(crate) use resolved_permission_profile::PermissionProfileState;
|
||||
pub use token_budget_startup::TokenBudgetStartupConfig;
|
||||
pub use windows_sandbox_config::PreparedWindowsSandboxConfig;
|
||||
use windows_sandbox_config::network_config_allows_mxc;
|
||||
pub use windows_sandbox_config::prepare_windows_sandbox_config;
|
||||
use windows_sandbox_config::resolve_windows_sandbox_type;
|
||||
|
||||
const DEFAULT_IGNORE_LARGE_UNTRACKED_DIRS: i64 = 200;
|
||||
const DEFAULT_IGNORE_LARGE_UNTRACKED_FILES: i64 = 10 * 1024 * 1024;
|
||||
@@ -346,7 +348,7 @@ pub struct Permissions {
|
||||
/// Effective Windows sandbox mode derived from `[windows].sandbox` or
|
||||
/// legacy feature keys.
|
||||
pub windows_sandbox_mode: Option<WindowsSandboxModeToml>,
|
||||
/// Selected Windows sandbox implementation, separate from the legacy setup level.
|
||||
/// Configured Windows backend; use `Config::effective_local_windows_sandbox_type()` for local selection.
|
||||
pub windows_sandbox_type: SandboxType,
|
||||
}
|
||||
|
||||
@@ -1089,6 +1091,9 @@ pub struct Config {
|
||||
/// Centralized feature flags; source of truth for feature gating.
|
||||
pub features: ManagedFeatures,
|
||||
|
||||
/// Local rollout preference after checking network restrictions and native availability.
|
||||
pub prefer_mxc: bool,
|
||||
|
||||
/// When `true`, suppress warnings about unstable (under development) features.
|
||||
pub suppress_unstable_features_warning: bool,
|
||||
|
||||
@@ -3382,10 +3387,6 @@ impl Config {
|
||||
&mut constrained_windows_sandbox_mode,
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
let legacy_windows_sandbox_level = windows_sandbox_level_for_legacy_checks(
|
||||
windows_sandbox_type,
|
||||
windows_sandbox_level,
|
||||
);
|
||||
let resolved_cwd = AbsolutePathBuf::try_from(normalize_for_native_workdir({
|
||||
use std::env;
|
||||
|
||||
@@ -3464,6 +3465,22 @@ impl Config {
|
||||
default_permissions_override.as_deref(),
|
||||
permission_config_syntax,
|
||||
);
|
||||
let prefer_mxc = features.enabled(Feature::PreferMxc)
|
||||
&& network_config_allows_mxc(
|
||||
&effective_permission_selection,
|
||||
profiles_are_active,
|
||||
permission_profile.as_ref(),
|
||||
network_requirements.as_ref(),
|
||||
cfg.features.as_ref(),
|
||||
enable_network_proxy,
|
||||
)?
|
||||
&& codex_sandboxing::windows_mxc_available();
|
||||
let local_windows_sandbox_type =
|
||||
resolve_windows_sandbox_type(windows_sandbox_type, prefer_mxc);
|
||||
let legacy_windows_sandbox_level = windows_sandbox_level_for_legacy_checks(
|
||||
local_windows_sandbox_type,
|
||||
windows_sandbox_level,
|
||||
);
|
||||
let explicit_permission_profile_mode = effective_permission_selection
|
||||
.persisted_profile_id_was_provided
|
||||
|| default_permissions_override.is_some()
|
||||
@@ -4200,6 +4217,7 @@ impl Config {
|
||||
.map_err(std::io::Error::from)?;
|
||||
let otel = otel::resolve_config(cfg.otel.unwrap_or_default(), &mut startup_warnings);
|
||||
let config = Self {
|
||||
prefer_mxc,
|
||||
model,
|
||||
service_tier,
|
||||
review_model,
|
||||
|
||||
@@ -1,12 +1,45 @@
|
||||
//! Windows sandbox mode selection after managed features have been resolved.
|
||||
//! Keep configured mode distinct from the effective level unless policy pins it.
|
||||
//! Configured Windows sandbox modes and the effective local backend.
|
||||
//! Keep the configured backend for remote inheritance; apply the local rollout
|
||||
//! using the preference resolved during config loading.
|
||||
|
||||
use super::Config;
|
||||
use super::EffectivePermissionSelection;
|
||||
use super::apply_requirement_constrained_value;
|
||||
use super::network_proxy_toml_config;
|
||||
use super::permissions::network_proxy_config_for_profile_selection;
|
||||
use super::profile_allows_configured_network_proxy;
|
||||
use codex_config::ConstrainedWithSource;
|
||||
use codex_config::NetworkConstraints;
|
||||
use codex_config::Sourced;
|
||||
use codex_config::types::WindowsSandboxModeToml;
|
||||
use codex_features::FeaturesToml;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_sandboxing::SandboxType;
|
||||
|
||||
impl Config {
|
||||
/// Configured backend, without the local rollout preference. Remote executors inherit this.
|
||||
pub fn windows_sandbox_type_from_config(&self) -> SandboxType {
|
||||
self.permissions.windows_sandbox_type
|
||||
}
|
||||
|
||||
/// Effective local backend, using eligibility and availability resolved at config load.
|
||||
pub fn effective_local_windows_sandbox_type(&self) -> SandboxType {
|
||||
resolve_windows_sandbox_type(self.windows_sandbox_type_from_config(), self.prefer_mxc)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn resolve_windows_sandbox_type(
|
||||
configured: SandboxType,
|
||||
prefer_mxc: bool,
|
||||
) -> SandboxType {
|
||||
if prefer_mxc {
|
||||
SandboxType::WindowsMxc
|
||||
} else {
|
||||
configured
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct PreparedWindowsSandboxConfig {
|
||||
/// Explicit or requirement-constrained mode; excludes feature-only fallback.
|
||||
@@ -58,6 +91,36 @@ pub fn prepare_windows_sandbox_config(
|
||||
})
|
||||
}
|
||||
|
||||
/// Managed requirements take precedence; otherwise preserve explicit
|
||||
/// feature-level or active-profile binding denials during automatic selection.
|
||||
pub(super) fn network_config_allows_mxc(
|
||||
permission_selection: &EffectivePermissionSelection<'_>,
|
||||
profiles_are_active: bool,
|
||||
permission_profile: Option<&PermissionProfile>,
|
||||
network_requirements: Option<&Sourced<NetworkConstraints>>,
|
||||
features: Option<&FeaturesToml>,
|
||||
enable_network_proxy: bool,
|
||||
) -> std::io::Result<bool> {
|
||||
let profile_local_binding = if profiles_are_active
|
||||
&& permission_profile.is_none_or(profile_allows_configured_network_proxy)
|
||||
&& let Some(profile) = permission_selection.selected_profile_id
|
||||
{
|
||||
network_proxy_config_for_profile_selection(permission_selection.profiles.as_ref(), profile)?
|
||||
.allow_local_binding
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let allow_local_binding = network_requirements
|
||||
.and_then(|requirements| requirements.value.allow_local_binding)
|
||||
.or_else(|| {
|
||||
network_proxy_toml_config(features)
|
||||
.filter(|config| enable_network_proxy && config.allow_local_binding == Some(false))
|
||||
.and_then(|config| config.allow_local_binding)
|
||||
})
|
||||
.or(profile_local_binding);
|
||||
Ok(allow_local_binding != Some(false))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "windows_sandbox_config_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -247,7 +247,7 @@ impl ThreadEnvironments {
|
||||
pub(crate) fn new(
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
local_shell: Shell,
|
||||
thread_environment_config: EnvironmentConfig,
|
||||
thread_environment_config: impl Fn(&Environment) -> EnvironmentConfig,
|
||||
shell_snapshot: ShellSnapshot,
|
||||
current: TurnEnvironmentSnapshot,
|
||||
non_blocking_snapshots: bool,
|
||||
@@ -296,7 +296,8 @@ impl ThreadEnvironments {
|
||||
pending_completion: None,
|
||||
};
|
||||
// Child threads re-infer thread-owned config while preserving owner config.
|
||||
inherited_environment.refresh_thread_config(&thread_environment_config);
|
||||
let config = thread_environment_config(&inherited_environment.environment);
|
||||
inherited_environment.refresh_thread_config(&config);
|
||||
Some(inherited_environment)
|
||||
})
|
||||
.collect();
|
||||
@@ -343,7 +344,7 @@ impl ThreadEnvironments {
|
||||
pub(crate) fn update_selections(
|
||||
&self,
|
||||
environments: &[TurnEnvironmentSelection],
|
||||
thread_environment_config: &EnvironmentConfig,
|
||||
thread_environment_config: impl Fn(&Environment) -> EnvironmentConfig,
|
||||
) {
|
||||
let mut current = self
|
||||
.environments
|
||||
@@ -355,8 +356,6 @@ impl ThreadEnvironments {
|
||||
if !seen_environment_ids.insert(selected_environment.environment_id.as_str()) {
|
||||
continue;
|
||||
}
|
||||
let (selected_environment, config_origin) =
|
||||
resolve_selection_config(selected_environment.clone(), thread_environment_config);
|
||||
if let Some(environment) = current.iter().find(|environment| {
|
||||
let previous = &environment.selection;
|
||||
previous.environment_id == selected_environment.environment_id
|
||||
@@ -376,6 +375,10 @@ impl ThreadEnvironments {
|
||||
);
|
||||
|
||||
if !failed && !restarting_as_pending {
|
||||
let (selected_environment, config_origin) = resolve_selection_config(
|
||||
selected_environment.clone(),
|
||||
&thread_environment_config(&environment.environment),
|
||||
);
|
||||
let shell_settings_changed = matches!(
|
||||
(&environment.selection.config, &selected_environment.config),
|
||||
(EnvironmentConfigState::Ready(previous), EnvironmentConfigState::Ready(current))
|
||||
@@ -402,6 +405,10 @@ impl ThreadEnvironments {
|
||||
tracing::warn!("skipping unknown turn environment `{environment_id}`");
|
||||
continue;
|
||||
};
|
||||
let (selected_environment, config_origin) = resolve_selection_config(
|
||||
selected_environment.clone(),
|
||||
&thread_environment_config(&environment),
|
||||
);
|
||||
// Connection state belongs to the environment instance, not its cwd or roots.
|
||||
let connection_events_task = current
|
||||
.iter()
|
||||
@@ -523,12 +530,16 @@ impl ThreadEnvironments {
|
||||
}
|
||||
|
||||
/// Refreshes attachments whose configuration is inferred from the thread.
|
||||
pub(crate) fn update_thread_config(&self, config: &EnvironmentConfig) {
|
||||
pub(crate) fn update_thread_config(
|
||||
&self,
|
||||
thread_environment_config: impl Fn(&Environment) -> EnvironmentConfig,
|
||||
) {
|
||||
let mut environments = self
|
||||
.environments
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner);
|
||||
for environment in environments.iter_mut() {
|
||||
let config = thread_environment_config(&environment.environment);
|
||||
let shell_settings_changed = environment.config_origin
|
||||
== EnvironmentConfigOrigin::Thread
|
||||
&& matches!(
|
||||
@@ -537,7 +548,7 @@ impl ThreadEnvironments {
|
||||
if previous.allow_login_shell != config.allow_login_shell
|
||||
|| previous.shell_environment_policy != config.shell_environment_policy
|
||||
);
|
||||
environment.refresh_thread_config(config);
|
||||
environment.refresh_thread_config(&config);
|
||||
if shell_settings_changed
|
||||
&& !environment.environment.is_remote()
|
||||
&& self.shell_snapshot.should_rebuild_inherited()
|
||||
@@ -1143,6 +1154,7 @@ mod tests {
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicyContext;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::sandbox::SandboxType;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use futures::SinkExt;
|
||||
@@ -1184,12 +1196,12 @@ mod tests {
|
||||
let turn_environments = Arc::new(ThreadEnvironments::new(
|
||||
environment_manager,
|
||||
crate::shell::default_user_shell(),
|
||||
test_environment_config(),
|
||||
|_| test_environment_config(),
|
||||
ShellSnapshot::disabled(),
|
||||
TurnEnvironmentSnapshot::default(),
|
||||
/*non_blocking_snapshots*/ false,
|
||||
));
|
||||
turn_environments.update_selections(selections, &test_environment_config());
|
||||
turn_environments.update_selections(selections, |_| test_environment_config());
|
||||
turn_environments.snapshot().await;
|
||||
turn_environments
|
||||
}
|
||||
@@ -1253,6 +1265,7 @@ mod tests {
|
||||
"userHomeDir": "file:///home/remote",
|
||||
"platformOs": "windows",
|
||||
"temporaryDirectories": ["file:///tmp/remote"],
|
||||
"capabilities": {"windowsMxc": true},
|
||||
}
|
||||
})
|
||||
.to_string()
|
||||
@@ -1362,7 +1375,7 @@ url = "ws://127.0.0.1:8765"
|
||||
let turn_environments = ThreadEnvironments::new(
|
||||
Arc::new(EnvironmentManager::default_for_tests()),
|
||||
local_shell.clone(),
|
||||
expected_config.clone(),
|
||||
|_| expected_config.clone(),
|
||||
ShellSnapshot::disabled(),
|
||||
TurnEnvironmentSnapshot::default(),
|
||||
/*non_blocking_snapshots*/ false,
|
||||
@@ -1374,7 +1387,7 @@ url = "ws://127.0.0.1:8765"
|
||||
workspace_roots: Vec::new(),
|
||||
config: EnvironmentConfigState::FromThread,
|
||||
}],
|
||||
&expected_config,
|
||||
|_| expected_config.clone(),
|
||||
);
|
||||
|
||||
let snapshot = turn_environments.snapshot().await;
|
||||
@@ -1537,13 +1550,14 @@ url = "ws://127.0.0.1:8765"
|
||||
let environments = Arc::new(ThreadEnvironments::new(
|
||||
manager,
|
||||
crate::shell::default_user_shell(),
|
||||
test_environment_config(),
|
||||
|_| test_environment_config(),
|
||||
ShellSnapshot::disabled(),
|
||||
TurnEnvironmentSnapshot::default(),
|
||||
/*non_blocking_snapshots*/ false,
|
||||
));
|
||||
environments
|
||||
.update_selections(std::slice::from_ref(&selection), &test_environment_config());
|
||||
environments.update_selections(std::slice::from_ref(&selection), |_| {
|
||||
test_environment_config()
|
||||
});
|
||||
// Exercise first-use callsite registration from a thread without a subscriber.
|
||||
std::thread::spawn({
|
||||
let environments = Arc::clone(&environments);
|
||||
@@ -1590,8 +1604,15 @@ url = "ws://127.0.0.1:8765"
|
||||
);
|
||||
}
|
||||
|
||||
#[test_case::test_case(SandboxType::None, SandboxType::None, WindowsSandboxLevel::Disabled; "legacy")]
|
||||
#[test_case::test_case(SandboxType::WindowsRestrictedToken, SandboxType::WindowsMxc, WindowsSandboxLevel::Elevated; "local preference")]
|
||||
#[test_case::test_case(SandboxType::WindowsMxc, SandboxType::WindowsMxc, WindowsSandboxLevel::Disabled; "explicit MXC")]
|
||||
#[tokio::test]
|
||||
async fn snapshot_refreshes_readiness_in_selection_order() {
|
||||
async fn snapshot_refreshes_readiness_in_selection_order(
|
||||
configured_sandbox: SandboxType,
|
||||
local_sandbox: SandboxType,
|
||||
legacy_level: WindowsSandboxLevel,
|
||||
) {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("bind websocket listener");
|
||||
@@ -1609,8 +1630,8 @@ url = "ws://127.0.0.1:8765"
|
||||
let expected_config = EnvironmentConfig {
|
||||
allow_login_shell: false,
|
||||
workspace_roots: Vec::new(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_type: codex_protocol::sandbox::SandboxType::None,
|
||||
windows_sandbox_level: legacy_level,
|
||||
windows_sandbox_type: configured_sandbox,
|
||||
use_legacy_landlock: false,
|
||||
permission_profile: PermissionProfileSnapshot::active_with_profile_workspace_roots(
|
||||
PermissionProfile::read_only(),
|
||||
@@ -1623,6 +1644,19 @@ url = "ws://127.0.0.1:8765"
|
||||
network_policy: None,
|
||||
selected_capability_roots: Vec::new(),
|
||||
};
|
||||
let expected_local_config = EnvironmentConfig {
|
||||
windows_sandbox_type: local_sandbox,
|
||||
..expected_config.clone()
|
||||
};
|
||||
let config_for =
|
||||
|config: &EnvironmentConfig, environment: &Environment| EnvironmentConfig {
|
||||
windows_sandbox_type: if environment.is_remote() {
|
||||
config.windows_sandbox_type
|
||||
} else {
|
||||
local_sandbox
|
||||
},
|
||||
..config.clone()
|
||||
};
|
||||
let cwd = PathUri::from_abs_path(&cwd);
|
||||
let remote = TurnEnvironmentSelection {
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
@@ -1637,20 +1671,23 @@ url = "ws://127.0.0.1:8765"
|
||||
config: EnvironmentConfigState::FromThread,
|
||||
};
|
||||
let turn_environments = ThreadEnvironments::new(
|
||||
manager,
|
||||
Arc::clone(&manager),
|
||||
crate::shell::default_user_shell(),
|
||||
test_environment_config(),
|
||||
|environment| config_for(&test_environment_config(), environment),
|
||||
ShellSnapshot::disabled(),
|
||||
TurnEnvironmentSnapshot::default(),
|
||||
/*non_blocking_snapshots*/ true,
|
||||
);
|
||||
turn_environments
|
||||
.update_selections(std::slice::from_ref(&local), &test_environment_config());
|
||||
turn_environments.update_selections(std::slice::from_ref(&local), |environment| {
|
||||
config_for(&test_environment_config(), environment)
|
||||
});
|
||||
let resolution = turn_environments.environments.lock().unwrap()[0]
|
||||
.resolution
|
||||
.clone();
|
||||
resolution.await.expect("local environment should resolve");
|
||||
turn_environments.update_selections(&[remote.clone(), local.clone()], &expected_config);
|
||||
turn_environments.update_selections(&[remote.clone(), local.clone()], |environment| {
|
||||
config_for(&expected_config, environment)
|
||||
});
|
||||
|
||||
let starting = turn_environments.snapshot().await;
|
||||
let resolved_remote = TurnEnvironmentSelection {
|
||||
@@ -1669,7 +1706,7 @@ url = "ws://127.0.0.1:8765"
|
||||
.turn_environments()
|
||||
.map(|environment| environment.config().clone())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![expected_config.clone()]
|
||||
vec![expected_local_config.clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
starting
|
||||
@@ -1689,7 +1726,7 @@ url = "ws://127.0.0.1:8765"
|
||||
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::Disabled),
|
||||
..test_environment_config()
|
||||
};
|
||||
turn_environments.update_thread_config(&next_config);
|
||||
turn_environments.update_thread_config(|environment| config_for(&next_config, environment));
|
||||
let next_starting = turn_environments.snapshot().await;
|
||||
assert!(
|
||||
!next_starting.has_full_access(AskForApproval::Never, &PermissionProfile::Disabled)
|
||||
@@ -1713,6 +1750,27 @@ url = "ws://127.0.0.1:8765"
|
||||
let attached = starting.refresh_readiness();
|
||||
|
||||
assert!(attached.starting().next().is_none());
|
||||
let child = ThreadEnvironments::new(
|
||||
manager,
|
||||
crate::shell::default_user_shell(),
|
||||
|environment| config_for(&next_config, environment),
|
||||
ShellSnapshot::disabled(),
|
||||
attached.clone(),
|
||||
/*non_blocking_snapshots*/ true,
|
||||
);
|
||||
assert_eq!(
|
||||
child
|
||||
.snapshot()
|
||||
.await
|
||||
.turn_environments()
|
||||
.map(|environment| environment.config().clone())
|
||||
.collect::<Vec<_>>(),
|
||||
next_starting
|
||||
.refresh_readiness()
|
||||
.turn_environments()
|
||||
.map(|environment| environment.config().clone())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
assert_eq!(
|
||||
attached
|
||||
.turn_environments()
|
||||
@@ -1725,7 +1783,7 @@ url = "ws://127.0.0.1:8765"
|
||||
.turn_environments()
|
||||
.map(|environment| environment.config().clone())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![expected_config.clone(), expected_config]
|
||||
vec![expected_config, expected_local_config]
|
||||
);
|
||||
assert_eq!(
|
||||
attached
|
||||
@@ -1756,7 +1814,13 @@ url = "ws://127.0.0.1:8765"
|
||||
.turn_environments()
|
||||
.map(|environment| environment.config().clone())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![next_config.clone(), next_config]
|
||||
vec![
|
||||
next_config.clone(),
|
||||
EnvironmentConfig {
|
||||
windows_sandbox_type: local_sandbox,
|
||||
..next_config
|
||||
}
|
||||
]
|
||||
);
|
||||
server.await.expect("server task");
|
||||
}
|
||||
@@ -1779,13 +1843,14 @@ url = "ws://127.0.0.1:8765"
|
||||
let environments = ThreadEnvironments::new(
|
||||
Arc::clone(&manager),
|
||||
crate::shell::default_user_shell(),
|
||||
test_environment_config(),
|
||||
|_| test_environment_config(),
|
||||
ShellSnapshot::disabled(),
|
||||
TurnEnvironmentSnapshot::default(),
|
||||
/*non_blocking_snapshots*/ true,
|
||||
);
|
||||
environments
|
||||
.update_selections(std::slice::from_ref(&selection), &test_environment_config());
|
||||
environments.update_selections(std::slice::from_ref(&selection), |_| {
|
||||
test_environment_config()
|
||||
});
|
||||
let starting = environments.snapshot().await;
|
||||
let failed_resolution = environments.environments.lock().unwrap()[0]
|
||||
.resolution
|
||||
@@ -1833,12 +1898,12 @@ url = "ws://127.0.0.1:8765"
|
||||
allow_login_shell: false,
|
||||
..test_environment_config()
|
||||
};
|
||||
environments.update_thread_config(&next_config);
|
||||
environments.update_thread_config(|_| next_config.clone());
|
||||
assert!(
|
||||
failed_resolution.ptr_eq(&environments.environments.lock().unwrap()[0].resolution),
|
||||
"updating environment config must not retry a failed environment"
|
||||
);
|
||||
environments.update_selections(std::slice::from_ref(&selection), &next_config);
|
||||
environments.update_selections(std::slice::from_ref(&selection), |_| next_config.clone());
|
||||
|
||||
let replacement = environments.snapshot().await;
|
||||
let replacement = replacement
|
||||
@@ -1882,14 +1947,15 @@ url = "ws://127.0.0.1:8765"
|
||||
let environments = Arc::new(ThreadEnvironments::new(
|
||||
Arc::clone(&manager),
|
||||
crate::shell::default_user_shell(),
|
||||
test_environment_config(),
|
||||
|_| test_environment_config(),
|
||||
ShellSnapshot::disabled(),
|
||||
TurnEnvironmentSnapshot::default(),
|
||||
/*non_blocking_snapshots*/ true,
|
||||
));
|
||||
environments.start_connection_event_forwarding(tx_event);
|
||||
environments
|
||||
.update_selections(std::slice::from_ref(&selection), &test_environment_config());
|
||||
environments.update_selections(std::slice::from_ref(&selection), |_| {
|
||||
test_environment_config()
|
||||
});
|
||||
let initial_snapshot = environments.snapshot().await;
|
||||
let second_listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
@@ -1907,15 +1973,16 @@ url = "ws://127.0.0.1:8765"
|
||||
)
|
||||
.expect("replace environment");
|
||||
|
||||
environments
|
||||
.update_selections(std::slice::from_ref(&selection), &test_environment_config());
|
||||
environments.update_selections(std::slice::from_ref(&selection), |_| {
|
||||
test_environment_config()
|
||||
});
|
||||
let reused_snapshot = environments.snapshot().await;
|
||||
environments.update_selections(
|
||||
&[TurnEnvironmentSelection {
|
||||
cwd: PathUri::from_abs_path(&cwd.join("changed")),
|
||||
..selection
|
||||
}],
|
||||
&test_environment_config(),
|
||||
|_| test_environment_config(),
|
||||
);
|
||||
let changed_snapshot = environments.snapshot().await;
|
||||
|
||||
@@ -2014,7 +2081,7 @@ url = "ws://127.0.0.1:8765"
|
||||
let environments = ThreadEnvironments::new(
|
||||
manager,
|
||||
crate::shell::default_user_shell(),
|
||||
child_config.clone(),
|
||||
|_| child_config.clone(),
|
||||
ShellSnapshot::disabled(),
|
||||
TurnEnvironmentSnapshot {
|
||||
environments: vec![TurnEnvironmentState::Ready(inherited)],
|
||||
@@ -2022,7 +2089,7 @@ url = "ws://127.0.0.1:8765"
|
||||
/*non_blocking_snapshots*/ false,
|
||||
);
|
||||
|
||||
environments.update_selections(std::slice::from_ref(&selection), &child_config);
|
||||
environments.update_selections(std::slice::from_ref(&selection), |_| child_config.clone());
|
||||
let snapshot = environments.snapshot().await;
|
||||
|
||||
let inherited = snapshot.primary().expect("inherited environment");
|
||||
@@ -2079,18 +2146,18 @@ url = "ws://127.0.0.1:8765"
|
||||
};
|
||||
let mut owner_selection = selection.clone();
|
||||
owner_selection.config = EnvironmentConfigState::Ready(parent_owner_config.clone());
|
||||
parent.update_selections(
|
||||
std::slice::from_ref(&owner_selection),
|
||||
&test_environment_config(),
|
||||
);
|
||||
parent.update_selections(std::slice::from_ref(&owner_selection), |_| {
|
||||
test_environment_config()
|
||||
});
|
||||
|
||||
let child_thread_config = EnvironmentConfig {
|
||||
windows_sandbox_type: SandboxType::WindowsMxc,
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
PermissionProfile::workspace_write(),
|
||||
),
|
||||
..test_environment_config()
|
||||
};
|
||||
parent.update_thread_config(&child_thread_config);
|
||||
parent.update_thread_config(|_| child_thread_config.clone());
|
||||
assert_eq!(
|
||||
parent
|
||||
.snapshot()
|
||||
@@ -2103,7 +2170,7 @@ url = "ws://127.0.0.1:8765"
|
||||
let child = ThreadEnvironments::new(
|
||||
manager,
|
||||
crate::shell::default_user_shell(),
|
||||
child_thread_config.clone(),
|
||||
|_| child_thread_config.clone(),
|
||||
ShellSnapshot::disabled(),
|
||||
parent.snapshot().await,
|
||||
/*non_blocking_snapshots*/ false,
|
||||
@@ -2116,7 +2183,9 @@ url = "ws://127.0.0.1:8765"
|
||||
..parent_owner_config.clone()
|
||||
};
|
||||
owner_selection.config = EnvironmentConfigState::Ready(cleared_parent_owner_config.clone());
|
||||
parent.update_selections(std::slice::from_ref(&owner_selection), &child_thread_config);
|
||||
parent.update_selections(std::slice::from_ref(&owner_selection), |_| {
|
||||
child_thread_config.clone()
|
||||
});
|
||||
let cleared_snapshot = parent.snapshot().await;
|
||||
assert_eq!(
|
||||
cleared_snapshot
|
||||
@@ -2132,7 +2201,9 @@ url = "ws://127.0.0.1:8765"
|
||||
..selection
|
||||
};
|
||||
let new_thread_config = test_environment_config();
|
||||
parent.update_selections(std::slice::from_ref(&changed_selection), &new_thread_config);
|
||||
parent.update_selections(std::slice::from_ref(&changed_selection), |_| {
|
||||
new_thread_config.clone()
|
||||
});
|
||||
let changed_snapshot = parent.snapshot().await;
|
||||
let changed_environment = changed_snapshot.primary().expect("changed environment");
|
||||
assert_eq!(changed_environment.config(), &new_thread_config);
|
||||
|
||||
@@ -24,12 +24,12 @@ async fn unpolled_snapshot_does_not_delay_canceling_a_removed_environment() {
|
||||
let environments = ThreadEnvironments::new(
|
||||
Arc::new(EnvironmentManager::default_for_tests()),
|
||||
crate::shell::default_user_shell(),
|
||||
config.clone(),
|
||||
|_| config.clone(),
|
||||
ShellSnapshot::disabled(),
|
||||
TurnEnvironmentSnapshot::default(),
|
||||
/*non_blocking_snapshots*/ false,
|
||||
);
|
||||
environments.update_selections(&[selection], &config);
|
||||
environments.update_selections(&[selection], |_| config.clone());
|
||||
let starting = environments
|
||||
.snapshot()
|
||||
.await
|
||||
@@ -39,7 +39,7 @@ async fn unpolled_snapshot_does_not_delay_canceling_a_removed_environment() {
|
||||
.clone();
|
||||
let held_snapshot = environments.snapshot();
|
||||
|
||||
environments.update_selections(&[], &config);
|
||||
environments.update_selections(&[], |_| config.clone());
|
||||
|
||||
let error = timeout(Duration::from_secs(5), starting.wait_until_ready())
|
||||
.await
|
||||
@@ -87,7 +87,7 @@ async fn credential_refresh_does_not_restore_a_removed_environment() {
|
||||
let environments = Arc::new(ThreadEnvironments::new(
|
||||
Arc::new(EnvironmentManager::default_for_tests()),
|
||||
crate::shell::default_user_shell(),
|
||||
config.clone(),
|
||||
|_| config.clone(),
|
||||
shell_snapshot,
|
||||
TurnEnvironmentSnapshot::default(),
|
||||
/*non_blocking_snapshots*/ false,
|
||||
@@ -99,7 +99,7 @@ async fn credential_refresh_does_not_restore_a_removed_environment() {
|
||||
workspace_roots: vec![cwd_uri],
|
||||
config: EnvironmentConfigState::FromThread,
|
||||
}],
|
||||
&config,
|
||||
|_| config.clone(),
|
||||
);
|
||||
let resolution = environments.environments.lock().unwrap()[0]
|
||||
.resolution
|
||||
@@ -128,7 +128,7 @@ async fn credential_refresh_does_not_restore_a_removed_environment() {
|
||||
let removing = Arc::clone(&environments);
|
||||
let mut removal = tokio::task::spawn_blocking(move || {
|
||||
removal_started_tx.send(()).expect("signal removal started");
|
||||
removing.update_selections(&[], &config);
|
||||
removing.update_selections(&[], |_| config.clone());
|
||||
});
|
||||
timeout(Duration::from_secs(/*secs*/ 5), removal_started_rx)
|
||||
.await
|
||||
|
||||
@@ -373,12 +373,14 @@ mod tests {
|
||||
let environments = crate::environment_selection::ThreadEnvironments::new(
|
||||
session.services.turn_environments.environment_manager(),
|
||||
crate::shell::default_user_shell(),
|
||||
environment_config.clone(),
|
||||
|_| environment_config.clone(),
|
||||
crate::shell_snapshot::ShellSnapshot::disabled(),
|
||||
Default::default(),
|
||||
/*non_blocking_snapshots*/ true,
|
||||
);
|
||||
environments.update_selections(std::slice::from_ref(&selection), &environment_config);
|
||||
environments.update_selections(std::slice::from_ref(&selection), |_| {
|
||||
environment_config.clone()
|
||||
});
|
||||
turn_context.initial_environments = environments.snapshot().await;
|
||||
turn_context
|
||||
.initial_environments
|
||||
|
||||
@@ -158,10 +158,11 @@ impl Session {
|
||||
}
|
||||
}
|
||||
if environments != self.services.turn_environments.selections() {
|
||||
self.services.turn_environments.update_selections(
|
||||
&environments,
|
||||
&configuration.inferred_environment_config(),
|
||||
);
|
||||
self.services
|
||||
.turn_environments
|
||||
.update_selections(&environments, |environment| {
|
||||
configuration.inferred_environment_config_for(environment)
|
||||
});
|
||||
}
|
||||
}
|
||||
self.services.turn_environments.snapshot()
|
||||
@@ -256,10 +257,13 @@ impl Session {
|
||||
if update_current {
|
||||
// Invalidate MCP before installed configuration can wake a waiting turn.
|
||||
self.mark_mcp_runtime_dirty();
|
||||
self.services.turn_environments.update_selections(
|
||||
¤t,
|
||||
&state.session_configuration.inferred_environment_config(),
|
||||
);
|
||||
self.services
|
||||
.turn_environments
|
||||
.update_selections(¤t, |environment| {
|
||||
state
|
||||
.session_configuration
|
||||
.inferred_environment_config_for(environment)
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -842,7 +842,7 @@ impl Session {
|
||||
allow_login_shell: config.permissions.allow_login_shell,
|
||||
shell_environment_policy: config.permissions.shell_environment_policy.clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
windows_sandbox_type: config.permissions.windows_sandbox_type,
|
||||
windows_sandbox_type: config.effective_local_windows_sandbox_type(),
|
||||
use_legacy_landlock: config.features.use_legacy_landlock(),
|
||||
legacy_fallback_cwd: config.cwd.clone(),
|
||||
runtime_workspace_roots: config.workspace_roots.clone(),
|
||||
@@ -1908,7 +1908,9 @@ impl Session {
|
||||
if state.session_configuration.inferred_environment_config() != environment_config {
|
||||
self.services
|
||||
.turn_environments
|
||||
.update_thread_config(&environment_config);
|
||||
.update_thread_config(|environment| {
|
||||
updated.inferred_environment_config_for(environment)
|
||||
});
|
||||
}
|
||||
state.session_configuration = updated;
|
||||
if root_service_tier_changed {
|
||||
|
||||
@@ -122,7 +122,9 @@ pub(super) async fn spawn_review_thread(
|
||||
.map(TurnEnvironment::windows_sandbox_selection_for_turn_metadata)
|
||||
.unwrap_or_else(|| {
|
||||
crate::tools::sandboxing::configured_windows_sandbox_selection(
|
||||
parent_turn_context.config.permissions.windows_sandbox_type,
|
||||
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),
|
||||
)
|
||||
|
||||
@@ -178,7 +178,9 @@ impl SessionConfiguration {
|
||||
permission_profile: self.permission_profile_state.snapshot(),
|
||||
shell_environment_policy: self.shell_environment_policy.clone(),
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
windows_sandbox_type: self.windows_sandbox_type,
|
||||
windows_sandbox_type: self
|
||||
.original_config_do_not_use
|
||||
.windows_sandbox_type_from_config(),
|
||||
use_legacy_landlock: self.use_legacy_landlock,
|
||||
exec_policy: None,
|
||||
mcp_policy: None,
|
||||
@@ -187,6 +189,18 @@ impl SessionConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the local rollout only when inferring config for a local environment.
|
||||
pub(super) fn inferred_environment_config_for(
|
||||
&self,
|
||||
environment: &Environment,
|
||||
) -> EnvironmentConfig {
|
||||
let mut config = self.inferred_environment_config();
|
||||
if !environment.is_remote() {
|
||||
config.windows_sandbox_type = self.windows_sandbox_type;
|
||||
}
|
||||
config
|
||||
}
|
||||
|
||||
pub(super) fn permission_profile(&self) -> PermissionProfile {
|
||||
self.permission_profile_state.permission_profile().clone()
|
||||
}
|
||||
@@ -1412,14 +1426,14 @@ impl Session {
|
||||
let turn_environments = Arc::new(ThreadEnvironments::new(
|
||||
environment_manager,
|
||||
default_shell.clone(),
|
||||
session_configuration.inferred_environment_config(),
|
||||
|environment| session_configuration.inferred_environment_config_for(environment),
|
||||
shell_snapshot,
|
||||
inherited_environments.unwrap_or_default(),
|
||||
config.features.enabled(Feature::DeferredExecutor),
|
||||
));
|
||||
turn_environments.update_selections(
|
||||
environment_selections,
|
||||
&session_configuration.inferred_environment_config(),
|
||||
|environment| session_configuration.inferred_environment_config_for(environment),
|
||||
);
|
||||
session_configuration.environments = turn_environments.selections();
|
||||
let resolved_environments = turn_environments.snapshot().await;
|
||||
@@ -1522,7 +1536,7 @@ impl Session {
|
||||
spec,
|
||||
current_exec_policy.as_ref(),
|
||||
config.permissions.permission_profile(),
|
||||
config.permissions.windows_sandbox_type,
|
||||
config.effective_local_windows_sandbox_type(),
|
||||
network_policy_decider.as_ref().map(Arc::clone),
|
||||
blocked_request_observer.as_ref().map(Arc::clone),
|
||||
managed_network_requirements_configured,
|
||||
|
||||
@@ -5002,15 +5002,14 @@ async fn resolved_environments_for_configuration(
|
||||
let turn_environments = ThreadEnvironments::new(
|
||||
Arc::clone(&environment_manager),
|
||||
default_user_shell(),
|
||||
session_configuration.inferred_environment_config(),
|
||||
|environment| session_configuration.inferred_environment_config_for(environment),
|
||||
ShellSnapshot::disabled(),
|
||||
TurnEnvironmentSnapshot::default(),
|
||||
/*non_blocking_snapshots*/ false,
|
||||
);
|
||||
turn_environments.update_selections(
|
||||
environment_selections,
|
||||
&session_configuration.inferred_environment_config(),
|
||||
);
|
||||
turn_environments.update_selections(environment_selections, |environment| {
|
||||
session_configuration.inferred_environment_config_for(environment)
|
||||
});
|
||||
(environment_manager, turn_environments.snapshot().await)
|
||||
}
|
||||
|
||||
@@ -6401,7 +6400,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
let turn_environments = Arc::new(ThreadEnvironments::new(
|
||||
environment_manager,
|
||||
default_user_shell(),
|
||||
session_configuration.inferred_environment_config(),
|
||||
|environment| session_configuration.inferred_environment_config_for(environment),
|
||||
ShellSnapshot::disabled(),
|
||||
resolved_environments,
|
||||
/*non_blocking_snapshots*/ false,
|
||||
@@ -8668,7 +8667,7 @@ where
|
||||
let turn_environments = Arc::new(ThreadEnvironments::new(
|
||||
environment_manager,
|
||||
default_user_shell(),
|
||||
session_configuration.inferred_environment_config(),
|
||||
|environment| session_configuration.inferred_environment_config_for(environment),
|
||||
ShellSnapshot::disabled(),
|
||||
resolved_turn_environments.clone(),
|
||||
/*non_blocking_snapshots*/ false,
|
||||
|
||||
@@ -313,7 +313,7 @@ async fn inactive_profiles_keep_snapshots_but_active_brokers_require_sandbox() -
|
||||
let environments = ThreadEnvironments::new(
|
||||
Arc::clone(&manager),
|
||||
shell.clone(),
|
||||
config.clone(),
|
||||
|_| config.clone(),
|
||||
snapshot_builder.clone(),
|
||||
TurnEnvironmentSnapshot::default(),
|
||||
/*non_blocking_snapshots*/ false,
|
||||
@@ -325,7 +325,7 @@ async fn inactive_profiles_keep_snapshots_but_active_brokers_require_sandbox() -
|
||||
workspace_roots: Vec::new(),
|
||||
config: EnvironmentConfigState::FromThread,
|
||||
}],
|
||||
&config,
|
||||
|_| config.clone(),
|
||||
);
|
||||
for (state, expect_snapshot) in [
|
||||
(SnapshotCredentialBrokerState::Starting, false),
|
||||
@@ -354,7 +354,7 @@ async fn inactive_profiles_keep_snapshots_but_active_brokers_require_sandbox() -
|
||||
.shell_environment_policy
|
||||
.r#set
|
||||
.insert("CORP_REGION".into(), "west".into());
|
||||
environments.update_thread_config(&config);
|
||||
environments.update_thread_config(|_| config.clone());
|
||||
let updated = environments.snapshot().await;
|
||||
assert!(!Arc::ptr_eq(
|
||||
&environment.shell_snapshot_cache,
|
||||
@@ -366,7 +366,7 @@ async fn inactive_profiles_keep_snapshots_but_active_brokers_require_sandbox() -
|
||||
let child = ThreadEnvironments::new(
|
||||
Arc::clone(&manager),
|
||||
shell.clone(),
|
||||
config.clone(),
|
||||
|_| config.clone(),
|
||||
snapshot_builder.clone(),
|
||||
updated.clone(),
|
||||
/*non_blocking_snapshots*/ false,
|
||||
@@ -393,7 +393,7 @@ async fn inactive_profiles_keep_snapshots_but_active_brokers_require_sandbox() -
|
||||
environments.set_snapshot_credential_broker(SnapshotCredentialBrokerState::Ready(
|
||||
started_proxy.proxy(),
|
||||
));
|
||||
environments.update_thread_config(&config);
|
||||
environments.update_thread_config(|_| config.clone());
|
||||
let turn = environments.snapshot().await;
|
||||
let environment = turn.primary().expect("brokered environment");
|
||||
let mut tool_config = crate::config::ConfigBuilder::without_managed_config_for_tests()
|
||||
|
||||
@@ -49,6 +49,26 @@ static TEST_MODEL_PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
|
||||
presets
|
||||
});
|
||||
|
||||
/// Inspect the same resolved environment configurations used to construct sampling steps.
|
||||
pub async fn environment_windows_sandbox_types(
|
||||
thread: &crate::CodexThread,
|
||||
) -> Vec<(String, codex_sandboxing::SandboxType)> {
|
||||
thread
|
||||
.session
|
||||
.services
|
||||
.turn_environments
|
||||
.snapshot()
|
||||
.await
|
||||
.turn_environments()
|
||||
.map(|environment| {
|
||||
(
|
||||
environment.selection.environment_id.clone(),
|
||||
environment.config().windows_sandbox_type,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Reattaches request-only observations to a completed turn's history for capture assertions.
|
||||
/// Tests inspect this separately from the destination-filtered HTTP/WS request.
|
||||
pub async fn history_with_tool_call_metadata(
|
||||
|
||||
@@ -699,7 +699,7 @@ fn detailed_permissions_normalize_paths_and_keep_policy_changes_visible() {
|
||||
"Some additional sandbox guidance. ".repeat(5)
|
||||
);
|
||||
let environment = format!(
|
||||
"<environment_context>\n<cwd>{cwd}</cwd>\n<root>{external}</root>\n</environment_context>"
|
||||
"<environment_context>\n<cwd>{cwd}</cwd>\n<filesystem><root>{external}</root><path>{external}</path><path>{cwd}{separator}{denied}</path></filesystem>\n</environment_context>"
|
||||
);
|
||||
let body = json!({"input": [
|
||||
{"type": "message", "role": "developer", "content": [{"type": "input_text", "text": permissions}]},
|
||||
|
||||
@@ -54,12 +54,12 @@ impl Normalizer {
|
||||
};
|
||||
let text = normalize_line_endings(&text);
|
||||
let segment = known_segment_name(&text, source);
|
||||
let text = self.normalize_values(&text);
|
||||
let text = match segment.as_deref() {
|
||||
Some("PERMISSIONS_INSTRUCTIONS") => self.permissions(&text),
|
||||
Some("ENVIRONMENT_CONTEXT") => self.environment(&text),
|
||||
_ => text,
|
||||
};
|
||||
let text = self.normalize_values(&text);
|
||||
if options.rewrite_known_segments
|
||||
&& let Some(segment) = segment
|
||||
{
|
||||
|
||||
@@ -39,15 +39,17 @@ use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
|
||||
#[test_case(SandboxType::WindowsRestrictedToken, "allow_local_binding = true", Ok(true); "explicit_local_binding")]
|
||||
#[test_case(SandboxType::WindowsMxc, "", Ok(true); "mxc_default_local_binding")]
|
||||
#[test_case(SandboxType::WindowsMxc, "allow_local_binding = false", Err("MXC cannot enforce allow_local_binding=false"); "mxc_rejects_false")]
|
||||
#[test_case(SandboxType::WindowsRestrictedToken, "", Ok(false); "non_mxc_default_local_binding")]
|
||||
#[test_case(SandboxType::WindowsRestrictedToken, "allow_local_binding = true", Ok(true), false; "explicit_local_binding")]
|
||||
#[test_case(SandboxType::WindowsMxc, "", Ok(true), false; "mxc_default_local_binding")]
|
||||
#[test_case(SandboxType::WindowsMxc, "allow_local_binding = false", Err("MXC cannot enforce allow_local_binding=false"), false; "mxc_rejects_false")]
|
||||
#[test_case(SandboxType::WindowsRestrictedToken, "", Ok(false), false; "non_mxc_default_local_binding")]
|
||||
#[test_case(SandboxType::WindowsRestrictedToken, "", Ok(false), true; "local_mxc_preference_keeps_remote_legacy")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_local_binding_policy_and_review_cleanup(
|
||||
sandbox_type: SandboxType,
|
||||
local_binding: &'static str,
|
||||
expected_local_binding: Result<bool, &'static str>,
|
||||
prefer_mxc: bool,
|
||||
) -> Result<()> {
|
||||
let server = start_mock_server().await;
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
@@ -199,12 +201,19 @@ mode = "full"
|
||||
config.project_doc_max_bytes = 0;
|
||||
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
|
||||
config.permissions.windows_sandbox_type = sandbox_type;
|
||||
config.prefer_mxc = prefer_mxc;
|
||||
#[cfg(windows)]
|
||||
config.set_windows_sandbox_enabled(/*value*/ true);
|
||||
})
|
||||
// This test supplies its own fake executor and must not select a CI executor.
|
||||
.build(&server)
|
||||
.await?;
|
||||
if prefer_mxc {
|
||||
assert_eq!(
|
||||
test.config.effective_local_windows_sandbox_type(),
|
||||
SandboxType::WindowsMxc
|
||||
);
|
||||
}
|
||||
let (sandbox_policy, permission_profile) = turn_permission_fields(
|
||||
test.session_configured.permission_profile.clone(),
|
||||
test.config.cwd.as_path(),
|
||||
|
||||
@@ -84,6 +84,9 @@ mod preparation;
|
||||
#[path = "scenarios_shared_instructions.rs"]
|
||||
mod shared_instructions;
|
||||
|
||||
#[path = "scenarios_mxc.rs"]
|
||||
mod mxc;
|
||||
|
||||
fn skills_extensions() -> Arc<ExtensionRegistry<Config>> {
|
||||
let mut extensions = ExtensionRegistryBuilder::<Config>::new();
|
||||
install(&mut extensions, |config: &Config| SkillsExtensionConfig {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Exercises resolved local MXC selection in a real thread and snapshots its permission context.
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::sandbox::SandboxType;
|
||||
use core_test_support::context_snapshot;
|
||||
use core_test_support::context_snapshot::ContextSnapshotOptions;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test_case::test_case(true; "preferred")]
|
||||
#[test_case::test_case(false; "legacy")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn preferred_mxc_reaches_local_thread_and_permission_context(prefer_mxc: bool) -> Result<()> {
|
||||
let server = start_mock_server().await;
|
||||
let mock = mount_sse_once(&server, sse(vec![ev_completed("done")])).await;
|
||||
let test = test_codex()
|
||||
.with_config(move |config| {
|
||||
// Start from the resolved preference; native probing is covered by config tests.
|
||||
config.prefer_mxc = prefer_mxc;
|
||||
config.permissions.windows_sandbox_type = SandboxType::None;
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(if prefer_mxc {
|
||||
PermissionProfile::workspace_write_with(
|
||||
&[],
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
/*exclude_tmpdir_env_var*/ true,
|
||||
/*exclude_slash_tmp*/ true,
|
||||
)
|
||||
} else {
|
||||
PermissionProfile::read_only()
|
||||
})
|
||||
.expect("set test permissions");
|
||||
})
|
||||
// This explicitly exercises host-local selection, including on remote-executor CI.
|
||||
.build(&server)
|
||||
.await?;
|
||||
test.submit_text_turn("Inspect the sandbox.").await?;
|
||||
assert_eq!(
|
||||
codex_core::test_support::environment_windows_sandbox_types(&test.codex).await,
|
||||
vec![(
|
||||
"local".to_string(),
|
||||
if prefer_mxc {
|
||||
SandboxType::WindowsMxc
|
||||
} else {
|
||||
SandboxType::None
|
||||
}
|
||||
)]
|
||||
);
|
||||
if prefer_mxc {
|
||||
insta::assert_snapshot!(
|
||||
"preferred_mxc_permissions",
|
||||
context_snapshot::format_request_history_snapshot(
|
||||
"Resolved local MXC selection preserves workspace-write permissions in a real agent turn.",
|
||||
&mock.requests(),
|
||||
&ContextSnapshotOptions::default(),
|
||||
)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios_mxc.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(\"Resolved local MXC selection preserves workspace-write permissions in a real agent turn.\",\n&mock.requests(), &ContextSnapshotOptions::default(),)"
|
||||
---
|
||||
Scenario: Resolved local MXC selection preserves workspace-write permissions in a real agent turn.
|
||||
|
||||
## Window 1
|
||||
-- request 1 (turn) --
|
||||
00:message/developer:
|
||||
<permissions instructions>
|
||||
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: T...ries requires approval. Network access is restricted. [hash=99963de5837f6638]
|
||||
# Escalation Requests
|
||||
|
||||
Commands are run outside the sandbox if they are approved by the user, or match an existing rule that al...hell control operators, including but not limited to: [hash=d24ebf5fa4e4b9bb]
|
||||
|
||||
- Pipes: |
|
||||
- Logical operators: &&, ||
|
||||
<OMITTED 45 LINES; ~808 TOKENS; hash=587bea404c7facc4>
|
||||
|
||||
### Examples
|
||||
Good examples of prefixes:
|
||||
- ["npm", "run", "dev"]
|
||||
- ["gh", "pr", "check"]
|
||||
- ["cargo", "test"]
|
||||
The writable root is `<WORKSPACE_ROOT 1>`.
|
||||
</permissions instructions>
|
||||
01:message/user:
|
||||
<environment_context>
|
||||
<cwd><CWD></cwd>
|
||||
<shell><HOST_SHELL></shell>
|
||||
<current_date><CURRENT_DATE></current_date>
|
||||
<timezone><HOST_TIMEZONE></timezone>
|
||||
<filesystem><workspace_roots><root><WORKSPACE_ROOT 1></root></workspace_roots><permission_profile type...ntry></file_system></permission_profile></filesystem> [hash=c88e32494c7023a5]
|
||||
</environment_context>
|
||||
02:message/user:
|
||||
Inspect the sandbox.
|
||||
@@ -397,6 +397,8 @@ pub enum Feature {
|
||||
WindowsSandboxElevated,
|
||||
/// Attempt elevated Windows sandbox provisioning through the installed service.
|
||||
WindowsSandboxService,
|
||||
/// Prefer the local native Windows sandbox when available, retaining legacy fallback.
|
||||
PreferMxc,
|
||||
/// Legacy remote models flag kept for backward compatibility.
|
||||
RemoteModels,
|
||||
/// Removed legacy git commit attribution guidance flag.
|
||||
@@ -1254,6 +1256,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::PreferMxc,
|
||||
key: "prefer_mxc",
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::RemoteModels,
|
||||
key: "remote_models",
|
||||
|
||||
@@ -26,6 +26,17 @@ direct non-loopback egress and general inbound network access.
|
||||
Win32k calls and desktop handles remain available for PowerShell startup;
|
||||
clipboard, input-injection, and desktop/system-control restrictions remain.
|
||||
|
||||
## Selection and rollout
|
||||
|
||||
`windows.sandbox = "mxc"` is strict. The default-off `features.prefer_mxc`
|
||||
selects MXC for local execution when available unless local binding is explicitly
|
||||
forbidden; otherwise legacy configuration and setup apply. Remote executors keep
|
||||
their configured backend. Command failures never trigger backend fallback.
|
||||
Desktop can supply `-c features.prefer_mxc=true` from its saved rollout decision
|
||||
on the next local Windows launch. Gate changes require a subsequent launch, and
|
||||
rollback may require legacy setup. `config/read` retains the configured sandbox;
|
||||
`windowsSandbox/readiness` reflects the effective local selection.
|
||||
|
||||
## Launch contract
|
||||
|
||||
`create_command_args()` wraps the command like the Seatbelt backend, using the
|
||||
|
||||
@@ -320,6 +320,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
||||
current_time_reminder: None,
|
||||
sleep_tool_mode: Default::default(),
|
||||
features: Default::default(),
|
||||
prefer_mxc: false,
|
||||
suppress_unstable_features_warning: false,
|
||||
active_project: ProjectConfig { trust_level: None },
|
||||
notices: Notice::default(),
|
||||
|
||||
Reference in New Issue
Block a user