mirror of
https://github.com/openai/codex.git
synced 2026-11-10 16:44:38 +00:00
Preserve Windows sandbox proxy settings in guardian sessions (#35036)
## Why Guardian review commands can run without the proxy-port environment used to configure the parent session. Reconciling persistent Windows sandbox settings for those commands can discard the parent's proxy configuration. ## What changed - Add a session-level Windows sandbox proxy-settings mode and use `Preserve` for guardian review sessions while keeping `Reconcile` as the default. - Carry the mode through unified exec and the exec-server sandbox context to Windows process launches. - In preserve mode, reuse the proxy settings recorded by the existing sandbox setup marker. ## Testing Added coverage for guardian session configuration, exec-server transport, and preserving an existing setup marker when proxy-port environment variables are absent. GitOrigin-RevId: 214655c6e6d97360906431773dc0de0fdda2db6e
This commit is contained in:
1 parent
d45055ae58
commit
b115de97d7
23 files changed
+181
-35
No files matched your search
@@ -87,6 +87,7 @@ pub(crate) async fn run_codex_thread_interactive(
|
|||||||
subagent_source: SubAgentSource,
|
subagent_source: SubAgentSource,
|
||||||
initial_history: Option<InitialHistory>,
|
initial_history: Option<InitialHistory>,
|
||||||
git_enrichment_policy: GitEnrichmentPolicy,
|
git_enrichment_policy: GitEnrichmentPolicy,
|
||||||
|
windows_sandbox_proxy_settings_mode: codex_sandboxing::WindowsSandboxProxySettingsMode,
|
||||||
) -> Result<(Arc<Session>, SessionIo), CodexErr> {
|
) -> Result<(Arc<Session>, SessionIo), CodexErr> {
|
||||||
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||||
let (tx_ops, rx_ops) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
let (tx_ops, rx_ops) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||||
@@ -139,6 +140,7 @@ pub(crate) async fn run_codex_thread_interactive(
|
|||||||
external_time_provider: Some(Arc::clone(&parent_session.services.time_provider)),
|
external_time_provider: Some(Arc::clone(&parent_session.services.time_provider)),
|
||||||
inherited_multi_agent_version: Some(MultiAgentVersion::Disabled),
|
inherited_multi_agent_version: Some(MultiAgentVersion::Disabled),
|
||||||
git_enrichment_policy,
|
git_enrichment_policy,
|
||||||
|
windows_sandbox_proxy_settings_mode,
|
||||||
}))
|
}))
|
||||||
.or_cancel(&cancel_token)
|
.or_cancel(&cancel_token)
|
||||||
.await??;
|
.await??;
|
||||||
@@ -224,6 +226,7 @@ pub(crate) async fn run_codex_thread_one_shot(
|
|||||||
subagent_source,
|
subagent_source,
|
||||||
initial_history,
|
initial_history,
|
||||||
GitEnrichmentPolicy::Fresh,
|
GitEnrichmentPolicy::Fresh,
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ async fn run_codex_thread_interactive_respects_pre_cancelled_spawn() {
|
|||||||
SubAgentSource::Review,
|
SubAgentSource::Review,
|
||||||
/*initial_history*/ None,
|
/*initial_history*/ None,
|
||||||
crate::session::GitEnrichmentPolicy::Fresh,
|
crate::session::GitEnrichmentPolicy::Fresh,
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -680,6 +680,7 @@ async fn spawn_guardian_review_session(
|
|||||||
SubAgentSource::Other(GUARDIAN_REVIEWER_NAME.to_string()),
|
SubAgentSource::Other(GUARDIAN_REVIEWER_NAME.to_string()),
|
||||||
initial_history,
|
initial_history,
|
||||||
GitEnrichmentPolicy::Skip,
|
GitEnrichmentPolicy::Skip,
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode::Preserve,
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -1254,6 +1255,31 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn spawned_guardian_session_preserves_windows_sandbox_proxy_settings() {
|
||||||
|
let params = test_review_params().await;
|
||||||
|
let manager = GuardianReviewSessionManager::default();
|
||||||
|
manager
|
||||||
|
.initialize(params.parent_session, params.parent_turn)
|
||||||
|
.await
|
||||||
|
.expect("initialize Guardian session");
|
||||||
|
let mode = manager
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.trunk
|
||||||
|
.as_ref()
|
||||||
|
.expect("Guardian session")
|
||||||
|
.session
|
||||||
|
.windows_sandbox_proxy_settings_mode;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
mode,
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode::Preserve
|
||||||
|
);
|
||||||
|
manager.shutdown().await;
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn guardian_review_session_config_change_invalidates_cached_session() {
|
async fn guardian_review_session_config_change_invalidates_cached_session() {
|
||||||
let parent_config = crate::config::test_config().await;
|
let parent_config = crate::config::test_config().await;
|
||||||
|
|||||||
@@ -455,6 +455,8 @@ pub(crate) struct SessionSpawnArgs {
|
|||||||
pub(crate) external_time_provider: Option<Arc<dyn TimeProvider>>,
|
pub(crate) external_time_provider: Option<Arc<dyn TimeProvider>>,
|
||||||
pub(crate) inherited_multi_agent_version: Option<MultiAgentVersion>,
|
pub(crate) inherited_multi_agent_version: Option<MultiAgentVersion>,
|
||||||
pub(crate) git_enrichment_policy: GitEnrichmentPolicy,
|
pub(crate) git_enrichment_policy: GitEnrichmentPolicy,
|
||||||
|
pub(crate) windows_sandbox_proxy_settings_mode:
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_multi_agent_version(
|
pub(crate) fn resolve_multi_agent_version(
|
||||||
@@ -544,6 +546,7 @@ impl Session {
|
|||||||
external_time_provider,
|
external_time_provider,
|
||||||
inherited_multi_agent_version,
|
inherited_multi_agent_version,
|
||||||
git_enrichment_policy,
|
git_enrichment_policy,
|
||||||
|
windows_sandbox_proxy_settings_mode,
|
||||||
} = args;
|
} = args;
|
||||||
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||||
let (tx_event, rx_event) = async_channel::unbounded();
|
let (tx_event, rx_event) = async_channel::unbounded();
|
||||||
@@ -732,6 +735,7 @@ impl Session {
|
|||||||
external_time_provider,
|
external_time_provider,
|
||||||
multi_agent_version,
|
multi_agent_version,
|
||||||
git_enrichment_policy,
|
git_enrichment_policy,
|
||||||
|
windows_sandbox_proxy_settings_mode,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ pub(crate) struct Session {
|
|||||||
/// The set of enabled features should be invariant for the lifetime of the
|
/// The set of enabled features should be invariant for the lifetime of the
|
||||||
/// session.
|
/// session.
|
||||||
pub(super) features: ManagedFeatures,
|
pub(super) features: ManagedFeatures,
|
||||||
|
pub(crate) windows_sandbox_proxy_settings_mode:
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode,
|
||||||
pub(super) multi_agent_version: OnceLock<MultiAgentVersion>,
|
pub(super) multi_agent_version: OnceLock<MultiAgentVersion>,
|
||||||
pub(super) mcp_refresh_pending: std::sync::atomic::AtomicBool,
|
pub(super) mcp_refresh_pending: std::sync::atomic::AtomicBool,
|
||||||
/// Serializes runtime refreshes without blocking calls that own a snapshot.
|
/// Serializes runtime refreshes without blocking calls that own a snapshot.
|
||||||
@@ -514,6 +516,7 @@ impl Session {
|
|||||||
external_time_provider: Option<Arc<dyn TimeProvider>>,
|
external_time_provider: Option<Arc<dyn TimeProvider>>,
|
||||||
multi_agent_version: Option<MultiAgentVersion>,
|
multi_agent_version: Option<MultiAgentVersion>,
|
||||||
git_enrichment_policy: GitEnrichmentPolicy,
|
git_enrichment_policy: GitEnrichmentPolicy,
|
||||||
|
windows_sandbox_proxy_settings_mode: codex_sandboxing::WindowsSandboxProxySettingsMode,
|
||||||
) -> anyhow::Result<Arc<Self>> {
|
) -> anyhow::Result<Arc<Self>> {
|
||||||
debug!(
|
debug!(
|
||||||
"Configuring session: model={}; provider={:?}",
|
"Configuring session: model={}; provider={:?}",
|
||||||
@@ -1150,6 +1153,7 @@ impl Session {
|
|||||||
state: Mutex::new(state),
|
state: Mutex::new(state),
|
||||||
managed_network_proxy_refresh_lock: Semaphore::new(/*permits*/ 1),
|
managed_network_proxy_refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||||
features: config.features.clone(),
|
features: config.features.clone(),
|
||||||
|
windows_sandbox_proxy_settings_mode,
|
||||||
multi_agent_version,
|
multi_agent_version,
|
||||||
mcp_refresh_pending: std::sync::atomic::AtomicBool::new(false),
|
mcp_refresh_pending: std::sync::atomic::AtomicBool::new(false),
|
||||||
mcp_refresh_lock: Semaphore::new(/*permits*/ 1),
|
mcp_refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||||
|
|||||||
@@ -5288,6 +5288,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
|
|||||||
/*external_time_provider*/ None,
|
/*external_time_provider*/ None,
|
||||||
Some(config.multi_agent_version_from_features()),
|
Some(config.multi_agent_version_from_features()),
|
||||||
GitEnrichmentPolicy::Fresh,
|
GitEnrichmentPolicy::Fresh,
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -5539,6 +5540,8 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
|||||||
state: Mutex::new(state),
|
state: Mutex::new(state),
|
||||||
managed_network_proxy_refresh_lock: Semaphore::new(/*permits*/ 1),
|
managed_network_proxy_refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||||
features: config.features.clone(),
|
features: config.features.clone(),
|
||||||
|
windows_sandbox_proxy_settings_mode:
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
||||||
multi_agent_version: OnceLock::from(config.multi_agent_version_from_features()),
|
multi_agent_version: OnceLock::from(config.multi_agent_version_from_features()),
|
||||||
mcp_refresh_pending: std::sync::atomic::AtomicBool::new(true),
|
mcp_refresh_pending: std::sync::atomic::AtomicBool::new(true),
|
||||||
mcp_refresh_lock: Semaphore::new(/*permits*/ 1),
|
mcp_refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||||
@@ -5673,6 +5676,7 @@ async fn make_session_with_config_and_rx(
|
|||||||
/*external_time_provider*/ None,
|
/*external_time_provider*/ None,
|
||||||
Some(config.multi_agent_version_from_features()),
|
Some(config.multi_agent_version_from_features()),
|
||||||
GitEnrichmentPolicy::Fresh,
|
GitEnrichmentPolicy::Fresh,
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -5788,6 +5792,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
|||||||
/*external_time_provider*/ None,
|
/*external_time_provider*/ None,
|
||||||
Some(config.multi_agent_version_from_features()),
|
Some(config.multi_agent_version_from_features()),
|
||||||
GitEnrichmentPolicy::Fresh,
|
GitEnrichmentPolicy::Fresh,
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -7702,6 +7707,8 @@ where
|
|||||||
state: Mutex::new(state),
|
state: Mutex::new(state),
|
||||||
managed_network_proxy_refresh_lock: Semaphore::new(/*permits*/ 1),
|
managed_network_proxy_refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||||
features: config.features.clone(),
|
features: config.features.clone(),
|
||||||
|
windows_sandbox_proxy_settings_mode:
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
||||||
multi_agent_version: OnceLock::from(config.multi_agent_version_from_features()),
|
multi_agent_version: OnceLock::from(config.multi_agent_version_from_features()),
|
||||||
mcp_refresh_pending: std::sync::atomic::AtomicBool::new(true),
|
mcp_refresh_pending: std::sync::atomic::AtomicBool::new(true),
|
||||||
mcp_refresh_lock: Semaphore::new(/*permits*/ 1),
|
mcp_refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||||
|
|||||||
@@ -766,6 +766,8 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
|
|||||||
external_time_provider: None,
|
external_time_provider: None,
|
||||||
inherited_multi_agent_version: None,
|
inherited_multi_agent_version: None,
|
||||||
git_enrichment_policy: GitEnrichmentPolicy::Skip,
|
git_enrichment_policy: GitEnrichmentPolicy::Skip,
|
||||||
|
windows_sandbox_proxy_settings_mode:
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode::Preserve,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("spawn guardian subagent");
|
.expect("spawn guardian subagent");
|
||||||
|
|||||||
@@ -360,6 +360,7 @@ impl TurnContext {
|
|||||||
.config
|
.config
|
||||||
.permissions
|
.permissions
|
||||||
.windows_sandbox_private_desktop,
|
.windows_sandbox_private_desktop,
|
||||||
|
windows_sandbox_proxy_settings_mode: None,
|
||||||
use_legacy_landlock: self.config.features.use_legacy_landlock(),
|
use_legacy_landlock: self.config.features.use_legacy_landlock(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1676,6 +1676,8 @@ impl ThreadManagerState {
|
|||||||
external_time_provider: self.external_time_provider.clone(),
|
external_time_provider: self.external_time_provider.clone(),
|
||||||
inherited_multi_agent_version: multi_agent_version,
|
inherited_multi_agent_version: multi_agent_version,
|
||||||
git_enrichment_policy: GitEnrichmentPolicy::Fresh,
|
git_enrichment_policy: GitEnrichmentPolicy::Fresh,
|
||||||
|
windows_sandbox_proxy_settings_mode:
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
let new_thread = self
|
let new_thread = self
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ impl ApplyPatchRuntime {
|
|||||||
workspace_roots: attempt.workspace_roots.to_vec(),
|
workspace_roots: attempt.workspace_roots.to_vec(),
|
||||||
windows_sandbox_level: attempt.windows_sandbox_level,
|
windows_sandbox_level: attempt.windows_sandbox_level,
|
||||||
windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop,
|
windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop,
|
||||||
|
windows_sandbox_proxy_settings_mode: None,
|
||||||
use_legacy_landlock: attempt.use_legacy_landlock,
|
use_legacy_landlock: attempt.use_legacy_landlock,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
|||||||
ctx: &ToolCtx,
|
ctx: &ToolCtx,
|
||||||
) -> Result<UnifiedExecProcess, ToolError> {
|
) -> Result<UnifiedExecProcess, ToolError> {
|
||||||
let base_command = &req.command;
|
let base_command = &req.command;
|
||||||
|
let windows_sandbox_proxy_settings_mode = ctx.session.windows_sandbox_proxy_settings_mode;
|
||||||
let session_shell = ctx.session.user_shell();
|
let session_shell = ctx.session.user_shell();
|
||||||
let shell = req
|
let shell = req
|
||||||
.turn_environment
|
.turn_environment
|
||||||
@@ -464,6 +465,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
|||||||
.open_session_with_prepared_exec_env(
|
.open_session_with_prepared_exec_env(
|
||||||
req.process_id,
|
req.process_id,
|
||||||
&prepared.exec_request,
|
&prepared.exec_request,
|
||||||
|
windows_sandbox_proxy_settings_mode,
|
||||||
req.tty,
|
req.tty,
|
||||||
prepared.spawn_lifecycle,
|
prepared.spawn_lifecycle,
|
||||||
req.turn_environment.environment.as_ref(),
|
req.turn_environment.environment.as_ref(),
|
||||||
@@ -510,6 +512,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
|||||||
network_proxy_launch,
|
network_proxy_launch,
|
||||||
/*environment_id*/ Some(&req.turn_environment.environment_id),
|
/*environment_id*/ Some(&req.turn_environment.environment_id),
|
||||||
req.exec_server_env_config.clone(),
|
req.exec_server_env_config.clone(),
|
||||||
|
windows_sandbox_proxy_settings_mode,
|
||||||
req.tty,
|
req.tty,
|
||||||
Box::new(NoopSpawnLifecycle),
|
Box::new(NoopSpawnLifecycle),
|
||||||
req.turn_environment.environment.as_ref(),
|
req.turn_environment.environment.as_ref(),
|
||||||
|
|||||||
@@ -507,6 +507,7 @@ impl<'a> SandboxAttempt<'a> {
|
|||||||
workspace_roots: self.workspace_roots.to_vec(),
|
workspace_roots: self.workspace_roots.to_vec(),
|
||||||
windows_sandbox_level: self.windows_sandbox_level,
|
windows_sandbox_level: self.windows_sandbox_level,
|
||||||
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
|
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
|
||||||
|
windows_sandbox_proxy_settings_mode: None,
|
||||||
use_legacy_landlock: self.use_legacy_landlock,
|
use_legacy_landlock: self.use_legacy_landlock,
|
||||||
});
|
});
|
||||||
exec_request.exec_server_enforce_managed_network = self.enforce_managed_network;
|
exec_request.exec_server_enforce_managed_network = self.enforce_managed_network;
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
|
|||||||
workspace_roots: vec![cwd_uri.clone()],
|
workspace_roots: vec![cwd_uri.clone()],
|
||||||
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
|
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
|
||||||
windows_sandbox_private_desktop: false,
|
windows_sandbox_private_desktop: false,
|
||||||
|
windows_sandbox_proxy_settings_mode: None,
|
||||||
use_legacy_landlock: false,
|
use_legacy_landlock: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ async fn exec_command_with_tty(
|
|||||||
.open_session_with_prepared_exec_env(
|
.open_session_with_prepared_exec_env(
|
||||||
process_id,
|
process_id,
|
||||||
&request,
|
&request,
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
||||||
tty,
|
tty,
|
||||||
Box::new(NoopSpawnLifecycle),
|
Box::new(NoopSpawnLifecycle),
|
||||||
turn.environments
|
turn.environments
|
||||||
@@ -753,6 +754,7 @@ async fn completed_pipe_commands_preserve_exit_code() -> anyhow::Result<()> {
|
|||||||
.open_session_with_prepared_exec_env(
|
.open_session_with_prepared_exec_env(
|
||||||
/*process_id*/ 1234,
|
/*process_id*/ 1234,
|
||||||
&request,
|
&request,
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
||||||
/*tty*/ false,
|
/*tty*/ false,
|
||||||
Box::new(NoopSpawnLifecycle),
|
Box::new(NoopSpawnLifecycle),
|
||||||
&environment,
|
&environment,
|
||||||
@@ -793,6 +795,7 @@ async fn unified_exec_uses_remote_exec_server_when_configured() -> anyhow::Resul
|
|||||||
.open_session_with_prepared_exec_env(
|
.open_session_with_prepared_exec_env(
|
||||||
/*process_id*/ 1234,
|
/*process_id*/ 1234,
|
||||||
&request,
|
&request,
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
||||||
/*tty*/ true,
|
/*tty*/ true,
|
||||||
Box::new(NoopSpawnLifecycle),
|
Box::new(NoopSpawnLifecycle),
|
||||||
remote_test_env.environment(),
|
remote_test_env.environment(),
|
||||||
@@ -851,6 +854,7 @@ async fn remote_exec_server_rejects_inherited_fd_launches() -> anyhow::Result<()
|
|||||||
.open_session_with_prepared_exec_env(
|
.open_session_with_prepared_exec_env(
|
||||||
/*process_id*/ 1234,
|
/*process_id*/ 1234,
|
||||||
&request,
|
&request,
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
||||||
/*tty*/ true,
|
/*tty*/ true,
|
||||||
Box::new(TestSpawnLifecycle {
|
Box::new(TestSpawnLifecycle {
|
||||||
inherited_fds: vec![42],
|
inherited_fds: vec![42],
|
||||||
|
|||||||
@@ -177,9 +177,14 @@ fn exec_server_env_for_request(
|
|||||||
fn exec_server_params_for_request(
|
fn exec_server_params_for_request(
|
||||||
process_id: i32,
|
process_id: i32,
|
||||||
request: &ExecRequest,
|
request: &ExecRequest,
|
||||||
|
windows_sandbox_proxy_settings_mode: codex_sandboxing::WindowsSandboxProxySettingsMode,
|
||||||
tty: bool,
|
tty: bool,
|
||||||
) -> codex_exec_server::ExecParams {
|
) -> codex_exec_server::ExecParams {
|
||||||
let (env_policy, env) = exec_server_env_for_request(request);
|
let (env_policy, env) = exec_server_env_for_request(request);
|
||||||
|
let sandbox = request.exec_server_sandbox.clone().map(|mut sandbox| {
|
||||||
|
sandbox.windows_sandbox_proxy_settings_mode = Some(windows_sandbox_proxy_settings_mode);
|
||||||
|
sandbox
|
||||||
|
});
|
||||||
// Sandbox retries reuse the unified-exec ID but start a distinct executor process.
|
// Sandbox retries reuse the unified-exec ID but start a distinct executor process.
|
||||||
let exec_server_process_id = if request.exec_server_sandbox.is_some() {
|
let exec_server_process_id = if request.exec_server_sandbox.is_some() {
|
||||||
format!("{process_id}-{}", Uuid::new_v4())
|
format!("{process_id}-{}", Uuid::new_v4())
|
||||||
@@ -195,7 +200,7 @@ fn exec_server_params_for_request(
|
|||||||
tty,
|
tty,
|
||||||
pipe_stdin: false,
|
pipe_stdin: false,
|
||||||
arg0: request.arg0.clone(),
|
arg0: request.arg0.clone(),
|
||||||
sandbox: request.exec_server_sandbox.clone(),
|
sandbox,
|
||||||
enforce_managed_network: request.exec_server_enforce_managed_network,
|
enforce_managed_network: request.exec_server_enforce_managed_network,
|
||||||
managed_network: request.exec_server_managed_network.clone(),
|
managed_network: request.exec_server_managed_network.clone(),
|
||||||
network_proxy: request.exec_server_network_proxy.clone(),
|
network_proxy: request.exec_server_network_proxy.clone(),
|
||||||
@@ -1003,6 +1008,7 @@ impl UnifiedExecProcessManager {
|
|||||||
network_proxy_launch: Option<codex_network_proxy::RemoteNetworkProxyLaunchConfig>,
|
network_proxy_launch: Option<codex_network_proxy::RemoteNetworkProxyLaunchConfig>,
|
||||||
environment_id: Option<&str>,
|
environment_id: Option<&str>,
|
||||||
exec_server_env_config: Option<ExecServerEnvConfig>,
|
exec_server_env_config: Option<ExecServerEnvConfig>,
|
||||||
|
windows_sandbox_proxy_settings_mode: codex_sandboxing::WindowsSandboxProxySettingsMode,
|
||||||
tty: bool,
|
tty: bool,
|
||||||
spawn_lifecycle: SpawnLifecycleHandle,
|
spawn_lifecycle: SpawnLifecycleHandle,
|
||||||
environment: &codex_exec_server::Environment,
|
environment: &codex_exec_server::Environment,
|
||||||
@@ -1018,6 +1024,7 @@ impl UnifiedExecProcessManager {
|
|||||||
self.open_session_with_prepared_exec_env(
|
self.open_session_with_prepared_exec_env(
|
||||||
process_id,
|
process_id,
|
||||||
&request,
|
&request,
|
||||||
|
windows_sandbox_proxy_settings_mode,
|
||||||
tty,
|
tty,
|
||||||
spawn_lifecycle,
|
spawn_lifecycle,
|
||||||
environment,
|
environment,
|
||||||
@@ -1038,6 +1045,7 @@ impl UnifiedExecProcessManager {
|
|||||||
&self,
|
&self,
|
||||||
process_id: i32,
|
process_id: i32,
|
||||||
request: &ExecRequest,
|
request: &ExecRequest,
|
||||||
|
windows_sandbox_proxy_settings_mode: codex_sandboxing::WindowsSandboxProxySettingsMode,
|
||||||
tty: bool,
|
tty: bool,
|
||||||
mut spawn_lifecycle: SpawnLifecycleHandle,
|
mut spawn_lifecycle: SpawnLifecycleHandle,
|
||||||
environment: &codex_exec_server::Environment,
|
environment: &codex_exec_server::Environment,
|
||||||
@@ -1053,7 +1061,12 @@ impl UnifiedExecProcessManager {
|
|||||||
|
|
||||||
let started = environment
|
let started = environment
|
||||||
.get_exec_backend()
|
.get_exec_backend()
|
||||||
.start(exec_server_params_for_request(process_id, request, tty))
|
.start(exec_server_params_for_request(
|
||||||
|
process_id,
|
||||||
|
request,
|
||||||
|
windows_sandbox_proxy_settings_mode,
|
||||||
|
tty,
|
||||||
|
))
|
||||||
.await
|
.await
|
||||||
.map_err(|err| UnifiedExecError::create_process(err.to_string()))?;
|
.map_err(|err| UnifiedExecError::create_process(err.to_string()))?;
|
||||||
spawn_lifecycle.after_spawn();
|
spawn_lifecycle.after_spawn();
|
||||||
@@ -1100,22 +1113,21 @@ impl UnifiedExecProcessManager {
|
|||||||
None::<String>
|
None::<String>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let windows_sandbox = if request.sandbox
|
let windows_sandbox =
|
||||||
== codex_sandboxing::SandboxType::WindowsRestrictedToken
|
if request.sandbox == codex_sandboxing::SandboxType::WindowsRestrictedToken {
|
||||||
{
|
Some(codex_sandboxing::WindowsSandboxSpawnRequest {
|
||||||
Some(codex_sandboxing::WindowsSandboxSpawnRequest {
|
permission_profile: &request.permission_profile,
|
||||||
permission_profile: &request.permission_profile,
|
workspace_roots: &request.windows_sandbox_workspace_roots,
|
||||||
workspace_roots: &request.windows_sandbox_workspace_roots,
|
windows_sandbox_level: request.windows_sandbox_level,
|
||||||
windows_sandbox_level: request.windows_sandbox_level,
|
proxy_enforced: request.network.is_some(),
|
||||||
proxy_enforced: request.network.is_some(),
|
network_proxy_restricting_sid: network_proxy_restricting_sid.as_deref(),
|
||||||
network_proxy_restricting_sid: network_proxy_restricting_sid.as_deref(),
|
proxy_settings_mode: windows_sandbox_proxy_settings_mode,
|
||||||
proxy_settings_mode: codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
filesystem_overrides: request.windows_sandbox_filesystem_overrides.as_ref(),
|
||||||
filesystem_overrides: request.windows_sandbox_filesystem_overrides.as_ref(),
|
use_private_desktop: request.windows_sandbox_private_desktop,
|
||||||
use_private_desktop: request.windows_sandbox_private_desktop,
|
})
|
||||||
})
|
} else {
|
||||||
} else {
|
None
|
||||||
None
|
};
|
||||||
};
|
|
||||||
let spawn_result = codex_sandboxing::spawn_process(codex_sandboxing::SpawnRequest {
|
let spawn_result = codex_sandboxing::spawn_process(codex_sandboxing::SpawnRequest {
|
||||||
command: &request.command,
|
command: &request.command,
|
||||||
cwd: native_cwd.as_path(),
|
cwd: native_cwd.as_path(),
|
||||||
|
|||||||
@@ -177,8 +177,16 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
|
|||||||
exec_server_network_proxy: None,
|
exec_server_network_proxy: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let params =
|
let proxy_settings_mode = codex_sandboxing::WindowsSandboxProxySettingsMode::Preserve;
|
||||||
exec_server_params_for_request(/*process_id*/ 123, &request, /*tty*/ true);
|
let params_for_request = |request: &ExecRequest| {
|
||||||
|
exec_server_params_for_request(
|
||||||
|
/*process_id*/ 123,
|
||||||
|
request,
|
||||||
|
proxy_settings_mode,
|
||||||
|
/*tty*/ true,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let params = params_for_request(&request);
|
||||||
|
|
||||||
assert_eq!(params.process_id.as_str(), "123");
|
assert_eq!(params.process_id.as_str(), "123");
|
||||||
assert_eq!(params.cwd, request.cwd);
|
assert_eq!(params.cwd, request.cwd);
|
||||||
@@ -200,10 +208,15 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
|
|||||||
request.exec_server_sandbox = Some(
|
request.exec_server_sandbox = Some(
|
||||||
codex_exec_server::FileSystemSandboxContext::from_permission_profile(permission_profile),
|
codex_exec_server::FileSystemSandboxContext::from_permission_profile(permission_profile),
|
||||||
);
|
);
|
||||||
let first =
|
let first = params_for_request(&request);
|
||||||
exec_server_params_for_request(/*process_id*/ 123, &request, /*tty*/ true);
|
let second = params_for_request(&request);
|
||||||
let second =
|
assert_eq!(
|
||||||
exec_server_params_for_request(/*process_id*/ 123, &request, /*tty*/ true);
|
first
|
||||||
|
.sandbox
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|sandbox| sandbox.windows_sandbox_proxy_settings_mode),
|
||||||
|
Some(codex_sandboxing::WindowsSandboxProxySettingsMode::Preserve)
|
||||||
|
);
|
||||||
assert!(first.process_id.as_str().starts_with("123-"));
|
assert!(first.process_id.as_str().starts_with("123-"));
|
||||||
assert!(second.process_id.as_str().starts_with("123-"));
|
assert!(second.process_id.as_str().starts_with("123-"));
|
||||||
assert_ne!(first.process_id, second.process_id);
|
assert_ne!(first.process_id, second.process_id);
|
||||||
|
|||||||
@@ -743,6 +743,7 @@ mod tests {
|
|||||||
use codex_network_proxy::NetworkProxyConfig;
|
use codex_network_proxy::NetworkProxyConfig;
|
||||||
use codex_network_proxy::RemoteNetworkProxyConfig;
|
use codex_network_proxy::RemoteNetworkProxyConfig;
|
||||||
use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
|
use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
|
||||||
|
use codex_protocol::config_types::WindowsSandboxProxySettingsMode;
|
||||||
use codex_protocol::models::ManagedFileSystemPermissions;
|
use codex_protocol::models::ManagedFileSystemPermissions;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_protocol::permissions::FileSystemAccessMode;
|
use codex_protocol::permissions::FileSystemAccessMode;
|
||||||
@@ -938,6 +939,17 @@ mod tests {
|
|||||||
.expect("deserialize sandbox"),
|
.expect("deserialize sandbox"),
|
||||||
sandbox
|
sandbox
|
||||||
);
|
);
|
||||||
|
let preserve = FileSystemSandboxContext {
|
||||||
|
windows_sandbox_proxy_settings_mode: Some(WindowsSandboxProxySettingsMode::Preserve),
|
||||||
|
..sandbox
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_value(&preserve).expect("serialize preserve mode");
|
||||||
|
assert_eq!(serialized["windowsSandboxProxySettingsMode"], "preserve");
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_value::<FileSystemSandboxContext>(serialized)
|
||||||
|
.expect("deserialize preserve mode"),
|
||||||
|
preserve
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ struct PreparedWindowsSandboxRequest {
|
|||||||
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
|
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
|
||||||
proxy_enforced: bool,
|
proxy_enforced: bool,
|
||||||
network_proxy_restricting_sid: Option<String>,
|
network_proxy_restricting_sid: Option<String>,
|
||||||
|
proxy_settings_mode: WindowsSandboxProxySettingsMode,
|
||||||
filesystem_overrides: Option<WindowsSandboxFilesystemOverrides>,
|
filesystem_overrides: Option<WindowsSandboxFilesystemOverrides>,
|
||||||
use_private_desktop: bool,
|
use_private_desktop: bool,
|
||||||
}
|
}
|
||||||
@@ -66,7 +67,7 @@ impl PreparedExecRequest {
|
|||||||
windows_sandbox_level: request.windows_sandbox_level,
|
windows_sandbox_level: request.windows_sandbox_level,
|
||||||
proxy_enforced: request.proxy_enforced,
|
proxy_enforced: request.proxy_enforced,
|
||||||
network_proxy_restricting_sid: request.network_proxy_restricting_sid.as_deref(),
|
network_proxy_restricting_sid: request.network_proxy_restricting_sid.as_deref(),
|
||||||
proxy_settings_mode: WindowsSandboxProxySettingsMode::Reconcile,
|
proxy_settings_mode: request.proxy_settings_mode,
|
||||||
filesystem_overrides: request.filesystem_overrides.as_ref(),
|
filesystem_overrides: request.filesystem_overrides.as_ref(),
|
||||||
use_private_desktop: request.use_private_desktop,
|
use_private_desktop: request.use_private_desktop,
|
||||||
})
|
})
|
||||||
@@ -114,6 +115,9 @@ pub(crate) async fn prepare_exec_request(
|
|||||||
windows_sandbox: None,
|
windows_sandbox: None,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
let windows_sandbox_proxy_settings_mode = sandbox_context
|
||||||
|
.windows_sandbox_proxy_settings_mode
|
||||||
|
.unwrap_or_default();
|
||||||
let runtime_paths = runtime_paths
|
let runtime_paths = runtime_paths
|
||||||
.ok_or_else(|| invalid_params("sandbox runtime paths are not configured".to_string()))?;
|
.ok_or_else(|| invalid_params("sandbox runtime paths are not configured".to_string()))?;
|
||||||
// TODO(jif): Transport permissions before orchestrator-local paths are materialized,
|
// TODO(jif): Transport permissions before orchestrator-local paths are materialized,
|
||||||
@@ -212,8 +216,7 @@ pub(crate) async fn prepare_exec_request(
|
|||||||
let (program, args) = (program.into(), args.to_vec());
|
let (program, args) = (program.into(), args.to_vec());
|
||||||
let transform_request = SandboxDirectSpawnTransformRequest {
|
let transform_request = SandboxDirectSpawnTransformRequest {
|
||||||
workspace_roots,
|
workspace_roots,
|
||||||
windows_sandbox_proxy_settings_mode:
|
windows_sandbox_proxy_settings_mode,
|
||||||
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
|
||||||
transform: SandboxTransformRequest {
|
transform: SandboxTransformRequest {
|
||||||
command: SandboxCommand {
|
command: SandboxCommand {
|
||||||
program,
|
program,
|
||||||
@@ -271,6 +274,7 @@ pub(crate) async fn prepare_exec_request(
|
|||||||
windows_sandbox_level: sandbox_context.windows_sandbox_level,
|
windows_sandbox_level: sandbox_context.windows_sandbox_level,
|
||||||
proxy_enforced,
|
proxy_enforced,
|
||||||
network_proxy_restricting_sid,
|
network_proxy_restricting_sid,
|
||||||
|
proxy_settings_mode: windows_sandbox_proxy_settings_mode,
|
||||||
filesystem_overrides,
|
filesystem_overrides,
|
||||||
use_private_desktop: sandbox_context.windows_sandbox_private_desktop,
|
use_private_desktop: sandbox_context.windows_sandbox_private_desktop,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -394,6 +394,8 @@ async fn managed_network_selects_elevated_windows_spawn() {
|
|||||||
cwd_uri.clone(),
|
cwd_uri.clone(),
|
||||||
);
|
);
|
||||||
sandbox.windows_sandbox_level = WindowsSandboxLevel::RestrictedToken;
|
sandbox.windows_sandbox_level = WindowsSandboxLevel::RestrictedToken;
|
||||||
|
sandbox.windows_sandbox_proxy_settings_mode =
|
||||||
|
Some(codex_sandboxing::WindowsSandboxProxySettingsMode::Preserve);
|
||||||
let proxy_config = RemoteNetworkProxyConfig::from_effective_config(&NetworkProxyConfig {
|
let proxy_config = RemoteNetworkProxyConfig::from_effective_config(&NetworkProxyConfig {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
enable_socks5: false,
|
enable_socks5: false,
|
||||||
@@ -434,6 +436,10 @@ async fn managed_network_selects_elevated_windows_spawn() {
|
|||||||
);
|
);
|
||||||
assert!(spawn.proxy_enforced);
|
assert!(spawn.proxy_enforced);
|
||||||
assert!(spawn.network_proxy_restricting_sid.is_some());
|
assert!(spawn.network_proxy_restricting_sid.is_some());
|
||||||
|
assert_eq!(
|
||||||
|
spawn.proxy_settings_mode,
|
||||||
|
codex_sandboxing::WindowsSandboxProxySettingsMode::Preserve
|
||||||
|
);
|
||||||
assert_eq!(spawn.permission_profile, &permissions);
|
assert_eq!(spawn.permission_profile, &permissions);
|
||||||
assert_eq!(spawn.workspace_roots, std::slice::from_ref(&cwd));
|
assert_eq!(spawn.workspace_roots, std::slice::from_ref(&cwd));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod find_up;
|
|||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||||
|
use codex_protocol::config_types::WindowsSandboxProxySettingsMode;
|
||||||
use codex_protocol::models::ManagedFileSystemPermissions;
|
use codex_protocol::models::ManagedFileSystemPermissions;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_protocol::models::SandboxEnforcement;
|
use codex_protocol::models::SandboxEnforcement;
|
||||||
@@ -291,6 +292,8 @@ pub struct FileSystemSandboxContext {
|
|||||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub windows_sandbox_private_desktop: bool,
|
pub windows_sandbox_private_desktop: bool,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub windows_sandbox_proxy_settings_mode: Option<WindowsSandboxProxySettingsMode>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub use_legacy_landlock: bool,
|
pub use_legacy_landlock: bool,
|
||||||
}
|
}
|
||||||
@@ -332,6 +335,7 @@ impl FileSystemSandboxContext {
|
|||||||
workspace_roots,
|
workspace_roots,
|
||||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||||
windows_sandbox_private_desktop: false,
|
windows_sandbox_private_desktop: false,
|
||||||
|
windows_sandbox_proxy_settings_mode: None,
|
||||||
use_legacy_landlock: false,
|
use_legacy_landlock: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,6 +283,16 @@ pub enum WindowsSandboxLevel {
|
|||||||
Elevated,
|
Elevated,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Controls whether a Windows sandbox launch reconciles persistent proxy settings or preserves
|
||||||
|
/// the settings established by another launch.
|
||||||
|
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum WindowsSandboxProxySettingsMode {
|
||||||
|
#[default]
|
||||||
|
Reconcile,
|
||||||
|
Preserve,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
Serialize,
|
Serialize,
|
||||||
|
|||||||
@@ -371,4 +371,36 @@ mod tests {
|
|||||||
vec![8080]
|
vec![8080]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn guardian_preserve_mode_does_not_churn_marker_with_empty_proxy_ports() {
|
||||||
|
let marker = SetupMarker {
|
||||||
|
version: crate::setup::SETUP_VERSION,
|
||||||
|
offline_username: "offline".to_string(),
|
||||||
|
online_username: "online".to_string(),
|
||||||
|
created_at: None,
|
||||||
|
proxy_ports: vec![3128, 8081],
|
||||||
|
allow_local_binding: true,
|
||||||
|
};
|
||||||
|
let env_map = HashMap::new();
|
||||||
|
let reconciled = desired_offline_proxy_settings(
|
||||||
|
Some(&marker),
|
||||||
|
WindowsSandboxProxySettingsMode::Reconcile,
|
||||||
|
&env_map,
|
||||||
|
SandboxNetworkIdentity::Offline,
|
||||||
|
);
|
||||||
|
assert_eq!(reconciled.proxy_ports, Vec::<u16>::new());
|
||||||
|
|
||||||
|
let desired = desired_offline_proxy_settings(
|
||||||
|
Some(&marker),
|
||||||
|
WindowsSandboxProxySettingsMode::Preserve,
|
||||||
|
&env_map,
|
||||||
|
SandboxNetworkIdentity::Offline,
|
||||||
|
);
|
||||||
|
assert_eq!(desired, marker.offline_proxy_settings());
|
||||||
|
assert_eq!(
|
||||||
|
marker.request_mismatch_reason(SandboxNetworkIdentity::Offline, &desired),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -35,14 +35,7 @@ impl fmt::Debug for WindowsSandboxCancellationToken {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Controls whether a Windows sandbox launch reconciles persistent proxy
|
pub use codex_protocol::config_types::WindowsSandboxProxySettingsMode;
|
||||||
/// firewall settings or preserves the settings established by another launch.
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
||||||
pub enum WindowsSandboxProxySettingsMode {
|
|
||||||
#[default]
|
|
||||||
Reconcile,
|
|
||||||
Preserve,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Network settings installed by an administrator during managed Windows sandbox setup.
|
/// Network settings installed by an administrator during managed Windows sandbox setup.
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
|||||||
Reference in new issue
Block a user