Compare commits

...

1 Commits

Author SHA1 Message Date
Anton Panasenko
7134842438 feat: configure network_access independent of sandbox policy 2025-12-17 15:24:30 -08:00
8 changed files with 197 additions and 28 deletions

View File

@@ -3,6 +3,7 @@ use std::path::PathBuf;
use codex_protocol::ConversationId;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::NetworkAccess;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::Verbosity;
@@ -326,6 +327,7 @@ pub struct UserSavedConfig {
pub approval_policy: Option<AskForApproval>,
pub sandbox_mode: Option<SandboxMode>,
pub sandbox_settings: Option<SandboxSettings>,
pub network_access: Option<NetworkAccess>,
pub forced_chatgpt_workspace_id: Option<String>,
pub forced_login_method: Option<ForcedLoginMethod>,
pub model: Option<String>,

View File

@@ -5,6 +5,7 @@ use crate::protocol::common::AuthMode;
use codex_protocol::account::PlanType;
use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::NetworkAccess;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode as CoreSandboxMode;
use codex_protocol::config_types::Verbosity;
@@ -279,6 +280,7 @@ pub struct Config {
pub approval_policy: Option<AskForApproval>,
pub sandbox_mode: Option<SandboxMode>,
pub sandbox_workspace_write: Option<SandboxWorkspaceWrite>,
pub network_access: Option<NetworkAccess>,
pub forced_chatgpt_workspace_id: Option<String>,
pub forced_login_method: Option<ForcedLoginMethod>,
pub tools: Option<ToolsV2>,

View File

@@ -91,6 +91,7 @@ async fn get_config_toml_parses_all_fields() -> Result<()> {
exclude_tmpdir_env_var: Some(true),
exclude_slash_tmp: Some(true),
}),
network_access: None,
forced_chatgpt_workspace_id: Some("12345678-0000-0000-0000-000000000000".into()),
forced_login_method: Some(ForcedLoginMethod::Chatgpt),
model: Some("gpt-5.1-codex-max".into()),
@@ -141,6 +142,7 @@ async fn get_config_toml_empty() -> Result<()> {
approval_policy: None,
sandbox_mode: None,
sandbox_settings: None,
network_access: None,
forced_chatgpt_workspace_id: None,
forced_login_method: None,
model: None,

View File

@@ -60,7 +60,6 @@ sha1 = { workspace = true }
sha2 = { workspace = true }
shlex = { workspace = true }
similar = { workspace = true }
strum_macros = { workspace = true }
tempfile = { workspace = true }
test-case = "3.3.1"
test-log = { workspace = true }

View File

@@ -151,6 +151,7 @@ use crate::util::backoff;
use codex_async_utils::OrCancelExt;
use codex_execpolicy::Policy as ExecPolicy;
use codex_otel::otel_manager::OtelManager;
use codex_protocol::config_types::NetworkAccess;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseInputItem;
@@ -266,6 +267,7 @@ impl Codex {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.approval_policy.clone(),
sandbox_policy: config.sandbox_policy.clone(),
network_access: config.network_access,
cwd: config.cwd.clone(),
original_config_do_not_use: Arc::clone(&config),
exec_policy,
@@ -367,6 +369,7 @@ pub(crate) struct TurnContext {
pub(crate) user_instructions: Option<String>,
pub(crate) approval_policy: AskForApproval,
pub(crate) sandbox_policy: SandboxPolicy,
pub(crate) network_access: Option<NetworkAccess>,
pub(crate) shell_environment_policy: ShellEnvironmentPolicy,
pub(crate) tools_config: ToolsConfig,
pub(crate) ghost_snapshot: GhostSnapshotConfig,
@@ -418,6 +421,8 @@ pub(crate) struct SessionConfiguration {
approval_policy: Constrained<AskForApproval>,
/// How to sandbox commands executed in the system
sandbox_policy: SandboxPolicy,
/// Optional override describing network access to include in environment context.
network_access: Option<NetworkAccess>,
/// Working directory that should be treated as the *root* of the
/// session. All relative paths supplied by the model as well as the
@@ -529,6 +534,7 @@ impl Session {
user_instructions: session_configuration.user_instructions.clone(),
approval_policy: session_configuration.approval_policy.value(),
sandbox_policy: session_configuration.sandbox_policy.clone(),
network_access: session_configuration.network_access,
shell_environment_policy: per_turn_config.shell_environment_policy.clone(),
tools_config,
ghost_snapshot: per_turn_config.ghost_snapshot.clone(),
@@ -1327,6 +1333,7 @@ impl Session {
Some(turn_context.cwd.clone()),
Some(turn_context.approval_policy),
Some(turn_context.sandbox_policy.clone()),
turn_context.network_access,
shell.as_ref().clone(),
)));
items
@@ -2181,6 +2188,7 @@ async fn spawn_review_thread(
compact_prompt: parent_turn_context.compact_prompt.clone(),
approval_policy: parent_turn_context.approval_policy,
sandbox_policy: parent_turn_context.sandbox_policy.clone(),
network_access: parent_turn_context.network_access,
shell_environment_policy: parent_turn_context.shell_environment_policy.clone(),
cwd: parent_turn_context.cwd.clone(),
final_output_json_schema: None,
@@ -2882,6 +2890,7 @@ mod tests {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.approval_policy.clone(),
sandbox_policy: config.sandbox_policy.clone(),
network_access: config.network_access,
cwd: config.cwd.clone(),
original_config_do_not_use: Arc::clone(&config),
exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())),
@@ -2954,6 +2963,7 @@ mod tests {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.approval_policy.clone(),
sandbox_policy: config.sandbox_policy.clone(),
network_access: config.network_access,
cwd: config.cwd.clone(),
original_config_do_not_use: Arc::clone(&config),
exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())),
@@ -3158,6 +3168,7 @@ mod tests {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.approval_policy.clone(),
sandbox_policy: config.sandbox_policy.clone(),
network_access: config.network_access,
cwd: config.cwd.clone(),
original_config_do_not_use: Arc::clone(&config),
exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())),
@@ -3249,6 +3260,7 @@ mod tests {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.approval_policy.clone(),
sandbox_policy: config.sandbox_policy.clone(),
network_access: config.network_access,
cwd: config.cwd.clone(),
original_config_do_not_use: Arc::clone(&config),
exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())),

View File

@@ -29,6 +29,7 @@ use crate::protocol::SandboxPolicy;
use codex_app_server_protocol::Tools;
use codex_app_server_protocol::UserSavedConfig;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::NetworkAccess;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::TrustLevel;
@@ -113,6 +114,9 @@ pub struct Config {
pub sandbox_policy: SandboxPolicy,
/// Optional override for the network status sent in the environment context.
pub network_access: Option<NetworkAccess>,
/// True if the user passed in an override or set a value in config.toml
/// for either of approval_policy or sandbox_mode.
pub did_user_set_custom_approval_policy_or_sandbox_mode: bool,
@@ -560,6 +564,9 @@ pub struct ConfigToml {
/// Sandbox mode to use.
pub sandbox_mode: Option<SandboxMode>,
/// Optional override describing network access to include in environment context.
pub network_access: Option<NetworkAccess>,
/// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`.
pub sandbox_workspace_write: Option<SandboxWorkspaceWrite>,
@@ -712,6 +719,7 @@ impl From<ConfigToml> for UserSavedConfig {
approval_policy: config_toml.approval_policy,
sandbox_mode: config_toml.sandbox_mode,
sandbox_settings: config_toml.sandbox_workspace_write.map(From::from),
network_access: config_toml.network_access,
forced_chatgpt_workspace_id: config_toml.forced_chatgpt_workspace_id,
forced_login_method: config_toml.forced_login_method,
model: config_toml.model,
@@ -1026,6 +1034,7 @@ impl Config {
}
}
}
let network_access = cfg.network_access;
let approval_policy = approval_policy_override
.or(config_profile.approval_policy)
.or(cfg.approval_policy)
@@ -1165,6 +1174,7 @@ impl Config {
cwd: resolved_cwd,
approval_policy,
sandbox_policy,
network_access,
did_user_set_custom_approval_policy_or_sandbox_mode,
forced_auto_mode_downgraded_on_windows,
shell_environment_policy,
@@ -1559,6 +1569,23 @@ trust_level = "trusted"
}
}
#[test]
fn network_access_override_is_loaded() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let config = Config::load_from_base_config_with_overrides(
ConfigToml {
network_access: Some(NetworkAccess::Restricted),
..Default::default()
},
ConfigOverrides::default(),
codex_home.path().to_path_buf(),
)?;
assert_eq!(config.network_access, Some(NetworkAccess::Restricted));
Ok(())
}
#[test]
fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
@@ -2956,6 +2983,7 @@ model_verbosity = "high"
model_provider: fixture.openai_provider.clone(),
approval_policy: Constrained::allow_any(AskForApproval::Never),
sandbox_policy: SandboxPolicy::new_read_only_policy(),
network_access: None,
did_user_set_custom_approval_policy_or_sandbox_mode: true,
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -3031,6 +3059,7 @@ model_verbosity = "high"
model_provider: fixture.openai_chat_completions_provider.clone(),
approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted),
sandbox_policy: SandboxPolicy::new_read_only_policy(),
network_access: None,
did_user_set_custom_approval_policy_or_sandbox_mode: true,
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -3121,6 +3150,7 @@ model_verbosity = "high"
model_provider: fixture.openai_provider.clone(),
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
sandbox_policy: SandboxPolicy::new_read_only_policy(),
network_access: None,
did_user_set_custom_approval_policy_or_sandbox_mode: true,
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -3197,6 +3227,7 @@ model_verbosity = "high"
model_provider: fixture.openai_provider.clone(),
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
sandbox_policy: SandboxPolicy::new_read_only_policy(),
network_access: None,
did_user_set_custom_approval_policy_or_sandbox_mode: true,
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),

View File

@@ -1,12 +1,12 @@
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use serde::Serialize;
use strum_macros::Display as DeriveDisplay;
use crate::codex::TurnContext;
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
use crate::shell::Shell;
use codex_protocol::config_types::NetworkAccess;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
@@ -14,13 +14,6 @@ use codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG;
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, DeriveDisplay)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub enum NetworkAccess {
Restricted,
Enabled,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename = "environment_context", rename_all = "snake_case")]
pub(crate) struct EnvironmentContext {
@@ -37,29 +30,21 @@ impl EnvironmentContext {
cwd: Option<PathBuf>,
approval_policy: Option<AskForApproval>,
sandbox_policy: Option<SandboxPolicy>,
network_access: Option<NetworkAccess>,
shell: Shell,
) -> Self {
let sandbox_mode = sandbox_policy.as_ref().map(|policy| match policy {
SandboxPolicy::DangerFullAccess => SandboxMode::DangerFullAccess,
SandboxPolicy::ReadOnly => SandboxMode::ReadOnly,
SandboxPolicy::WorkspaceWrite { .. } => SandboxMode::WorkspaceWrite,
});
let resolved_network_access =
Self::resolve_network_access(sandbox_policy.as_ref(), network_access);
Self {
cwd,
approval_policy,
sandbox_mode: match sandbox_policy {
Some(SandboxPolicy::DangerFullAccess) => Some(SandboxMode::DangerFullAccess),
Some(SandboxPolicy::ReadOnly) => Some(SandboxMode::ReadOnly),
Some(SandboxPolicy::WorkspaceWrite { .. }) => Some(SandboxMode::WorkspaceWrite),
None => None,
},
network_access: match sandbox_policy {
Some(SandboxPolicy::DangerFullAccess) => Some(NetworkAccess::Enabled),
Some(SandboxPolicy::ReadOnly) => Some(NetworkAccess::Restricted),
Some(SandboxPolicy::WorkspaceWrite { network_access, .. }) => {
if network_access {
Some(NetworkAccess::Enabled)
} else {
Some(NetworkAccess::Restricted)
}
}
None => None,
},
sandbox_mode,
network_access: resolved_network_access,
writable_roots: match sandbox_policy {
Some(SandboxPolicy::WorkspaceWrite { writable_roots, .. }) => {
if writable_roots.is_empty() {
@@ -74,6 +59,27 @@ impl EnvironmentContext {
}
}
fn resolve_network_access(
sandbox_policy: Option<&SandboxPolicy>,
network_access_override: Option<NetworkAccess>,
) -> Option<NetworkAccess> {
match network_access_override {
Some(access) => Some(access),
None => match sandbox_policy {
Some(SandboxPolicy::DangerFullAccess) => Some(NetworkAccess::Enabled),
Some(SandboxPolicy::ReadOnly) => Some(NetworkAccess::Restricted),
Some(SandboxPolicy::WorkspaceWrite { network_access, .. }) => {
if *network_access {
Some(NetworkAccess::Enabled)
} else {
Some(NetworkAccess::Restricted)
}
}
None => None,
},
}
}
/// Compares two environment contexts, ignoring the shell. Useful when
/// comparing turn to turn, since the initial environment_context will
/// include the shell, and then it is not configurable from turn to turn.
@@ -111,7 +117,26 @@ impl EnvironmentContext {
} else {
None
};
EnvironmentContext::new(cwd, approval_policy, sandbox_policy, shell.clone())
let before_network_access = EnvironmentContext::resolve_network_access(
Some(&before.sandbox_policy),
before.network_access,
);
let after_network_access = EnvironmentContext::resolve_network_access(
Some(&after.sandbox_policy),
after.network_access,
);
let network_access = if before_network_access != after_network_access {
after_network_access
} else {
None
};
EnvironmentContext::new(
cwd,
approval_policy,
sandbox_policy,
network_access,
shell.clone(),
)
}
pub fn from_turn_context(turn_context: &TurnContext, shell: &Shell) -> Self {
@@ -119,6 +144,7 @@ impl EnvironmentContext {
Some(turn_context.cwd.clone()),
Some(turn_context.approval_policy),
Some(turn_context.sandbox_policy.clone()),
turn_context.network_access,
shell.clone(),
)
}
@@ -192,6 +218,7 @@ mod tests {
use crate::shell::ShellType;
use super::*;
use crate::codex::make_session_and_context;
use core_test_support::test_path_buf;
use core_test_support::test_tmp_path_buf;
use pretty_assertions::assert_eq;
@@ -231,6 +258,7 @@ mod tests {
vec![cwd_str, writable_root_str],
false,
)),
None,
fake_shell(),
);
@@ -253,12 +281,51 @@ mod tests {
assert_eq!(context.serialize_to_xml(), expected);
}
#[test]
fn serialize_workspace_write_environment_context_with_network_override() {
let cwd = test_path_buf("/repo");
let writable_root = test_tmp_path_buf();
let cwd_str = cwd.to_str().expect("cwd is valid utf-8");
let writable_root_str = writable_root
.to_str()
.expect("writable root is valid utf-8");
let context = EnvironmentContext::new(
Some(cwd.clone()),
Some(AskForApproval::OnRequest),
Some(workspace_write_policy(
vec![cwd_str, writable_root_str],
false,
)),
Some(NetworkAccess::Enabled),
fake_shell(),
);
let expected = format!(
r#"<environment_context>
<cwd>{cwd}</cwd>
<approval_policy>on-request</approval_policy>
<sandbox_mode>workspace-write</sandbox_mode>
<network_access>enabled</network_access>
<writable_roots>
<root>{cwd}</root>
<root>{writable_root}</root>
</writable_roots>
<shell>bash</shell>
</environment_context>"#,
cwd = cwd.display(),
writable_root = writable_root.display(),
);
assert_eq!(context.serialize_to_xml(), expected);
}
#[test]
fn serialize_read_only_environment_context() {
let context = EnvironmentContext::new(
None,
Some(AskForApproval::Never),
Some(SandboxPolicy::ReadOnly),
None,
fake_shell(),
);
@@ -278,6 +345,7 @@ mod tests {
None,
Some(AskForApproval::OnFailure),
Some(SandboxPolicy::DangerFullAccess),
None,
fake_shell(),
);
@@ -291,6 +359,43 @@ mod tests {
assert_eq!(context.serialize_to_xml(), expected);
}
#[test]
fn serialize_full_access_environment_context_with_override() {
let context = EnvironmentContext::new(
None,
Some(AskForApproval::OnFailure),
Some(SandboxPolicy::DangerFullAccess),
Some(NetworkAccess::Restricted),
fake_shell(),
);
let expected = r#"<environment_context>
<approval_policy>on-failure</approval_policy>
<sandbox_mode>danger-full-access</sandbox_mode>
<network_access>restricted</network_access>
<shell>bash</shell>
</environment_context>"#;
assert_eq!(context.serialize_to_xml(), expected);
}
#[test]
fn diff_detects_network_override_changes() {
let (_, mut before) = make_session_and_context();
let (_, mut after) = make_session_and_context();
before.sandbox_policy = SandboxPolicy::ReadOnly;
after.sandbox_policy = SandboxPolicy::ReadOnly;
before.network_access = None;
after.network_access = Some(NetworkAccess::Enabled);
let shell = fake_shell();
let diff = EnvironmentContext::diff(&before, &after, &shell);
let expected =
EnvironmentContext::new(None, None, None, Some(NetworkAccess::Enabled), shell);
assert_eq!(diff, expected);
}
#[test]
fn equals_except_shell_compares_approval_policy() {
// Approval policy
@@ -298,12 +403,14 @@ mod tests {
Some(PathBuf::from("/repo")),
Some(AskForApproval::OnRequest),
Some(workspace_write_policy(vec!["/repo"], false)),
None,
fake_shell(),
);
let context2 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
Some(AskForApproval::Never),
Some(workspace_write_policy(vec!["/repo"], true)),
None,
fake_shell(),
);
assert!(!context1.equals_except_shell(&context2));
@@ -315,12 +422,14 @@ mod tests {
Some(PathBuf::from("/repo")),
Some(AskForApproval::OnRequest),
Some(SandboxPolicy::new_read_only_policy()),
None,
fake_shell(),
);
let context2 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
Some(AskForApproval::OnRequest),
Some(SandboxPolicy::new_workspace_write_policy()),
None,
fake_shell(),
);
@@ -333,12 +442,14 @@ mod tests {
Some(PathBuf::from("/repo")),
Some(AskForApproval::OnRequest),
Some(workspace_write_policy(vec!["/repo", "/tmp", "/var"], false)),
None,
fake_shell(),
);
let context2 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
Some(AskForApproval::OnRequest),
Some(workspace_write_policy(vec!["/repo", "/tmp"], true)),
None,
fake_shell(),
);
@@ -351,6 +462,7 @@ mod tests {
Some(PathBuf::from("/repo")),
Some(AskForApproval::OnRequest),
Some(workspace_write_policy(vec!["/repo"], false)),
None,
Shell {
shell_type: ShellType::Bash,
shell_path: "/bin/bash".into(),
@@ -361,6 +473,7 @@ mod tests {
Some(PathBuf::from("/repo")),
Some(AskForApproval::OnRequest),
Some(workspace_write_policy(vec!["/repo"], false)),
None,
Shell {
shell_type: ShellType::Zsh,
shell_path: "/bin/zsh".into(),

View File

@@ -63,6 +63,14 @@ pub enum SandboxMode {
DangerFullAccess,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub enum NetworkAccess {
Restricted,
Enabled,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]