mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
## Why Managed network configures commands to use local HTTP and SOCKS proxies. For commands delegated to the exec server, the proxy environment and the sandbox policy were prepared separately. On macOS, that meant a command could receive `HTTPS_PROXY=http://127.0.0.1:43123` while Seatbelt still denied access to port `43123`. ## What changed `NetworkProxy` now prepares the command environment and sandbox context together from the same runtime snapshot: ```text Prepared managed network ├── command environment: HTTPS_PROXY=http://127.0.0.1:43123 └── sandbox context: allow outbound to 127.0.0.1:43123 ``` That context travels with remote exec requests. The exec server preserves the managed proxy and CA environment, and macOS Seatbelt allows only the prepared loopback proxy ports without enabling broad network access or local binding. The protocol field is optional and the existing enforcement flag remains in place, preserving compatibility with callers that do not send the new context.
287 lines
9.4 KiB
Rust
287 lines
9.4 KiB
Rust
use super::*;
|
|
use crate::sandboxing::SandboxPermissions;
|
|
use crate::tools::hook_names::HookToolName;
|
|
use codex_network_proxy::ManagedNetworkSandboxContext;
|
|
use codex_protocol::permissions::FileSystemAccessMode;
|
|
use codex_protocol::permissions::FileSystemPath;
|
|
use codex_protocol::permissions::FileSystemSandboxEntry;
|
|
use codex_protocol::protocol::GranularApprovalConfig;
|
|
use codex_sandboxing::SandboxCommand;
|
|
use codex_sandboxing::SandboxManager;
|
|
use codex_sandboxing::SandboxType;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use codex_utils_path_uri::PathUri;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
use std::collections::HashMap;
|
|
|
|
#[test]
|
|
fn bash_permission_request_payload_omits_missing_description() {
|
|
assert_eq!(
|
|
PermissionRequestPayload::bash("echo hi".to_string(), /*description*/ None),
|
|
PermissionRequestPayload {
|
|
tool_name: HookToolName::bash(),
|
|
tool_input: json!({ "command": "echo hi" }),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn bash_permission_request_payload_includes_description_when_present() {
|
|
assert_eq!(
|
|
PermissionRequestPayload::bash(
|
|
"echo hi".to_string(),
|
|
Some("network-access example.com".to_string()),
|
|
),
|
|
PermissionRequestPayload {
|
|
tool_name: HookToolName::bash(),
|
|
tool_input: json!({
|
|
"command": "echo hi",
|
|
"description": "network-access example.com",
|
|
}),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn external_sandbox_skips_exec_approval_on_request() {
|
|
assert_eq!(
|
|
default_exec_approval_requirement(
|
|
AskForApproval::OnRequest,
|
|
&FileSystemSandboxPolicy::external_sandbox(),
|
|
),
|
|
ExecApprovalRequirement::Skip {
|
|
bypass_sandbox: false,
|
|
proposed_execpolicy_amendment: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn restricted_sandbox_requires_exec_approval_on_request() {
|
|
assert_eq!(
|
|
default_exec_approval_requirement(
|
|
AskForApproval::OnRequest,
|
|
&FileSystemSandboxPolicy::default()
|
|
),
|
|
ExecApprovalRequirement::NeedsApproval {
|
|
reason: None,
|
|
proposed_execpolicy_amendment: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn default_exec_approval_requirement_rejects_sandbox_prompt_when_granular_disables_it() {
|
|
let policy = AskForApproval::Granular(GranularApprovalConfig {
|
|
sandbox_approval: false,
|
|
rules: true,
|
|
skill_approval: true,
|
|
request_permissions: true,
|
|
mcp_elicitations: true,
|
|
});
|
|
|
|
let requirement =
|
|
default_exec_approval_requirement(policy, &FileSystemSandboxPolicy::default());
|
|
|
|
assert_eq!(
|
|
requirement,
|
|
ExecApprovalRequirement::Forbidden {
|
|
reason: "approval policy disallowed sandbox approval prompt".to_string(),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn default_exec_approval_requirement_keeps_prompt_when_granular_allows_sandbox_approval() {
|
|
let policy = AskForApproval::Granular(GranularApprovalConfig {
|
|
sandbox_approval: true,
|
|
rules: false,
|
|
skill_approval: true,
|
|
request_permissions: true,
|
|
mcp_elicitations: false,
|
|
});
|
|
|
|
let requirement =
|
|
default_exec_approval_requirement(policy, &FileSystemSandboxPolicy::default());
|
|
|
|
assert_eq!(
|
|
requirement,
|
|
ExecApprovalRequirement::NeedsApproval {
|
|
reason: None,
|
|
proposed_execpolicy_amendment: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn additional_permissions_allow_bypass_sandbox_first_attempt_when_execpolicy_skips() {
|
|
assert_eq!(
|
|
sandbox_override_for_first_attempt(
|
|
SandboxPermissions::WithAdditionalPermissions,
|
|
&ExecApprovalRequirement::Skip {
|
|
bypass_sandbox: true,
|
|
proposed_execpolicy_amendment: None,
|
|
},
|
|
&FileSystemSandboxPolicy::default(),
|
|
),
|
|
SandboxOverride::BypassSandboxFirstAttempt
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn guardian_bypasses_sandbox_for_explicit_escalation_on_first_attempt() {
|
|
assert_eq!(
|
|
sandbox_override_for_first_attempt(
|
|
SandboxPermissions::RequireEscalated,
|
|
&ExecApprovalRequirement::Skip {
|
|
bypass_sandbox: false,
|
|
proposed_execpolicy_amendment: None,
|
|
},
|
|
&FileSystemSandboxPolicy::default(),
|
|
),
|
|
SandboxOverride::BypassSandboxFirstAttempt
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn deny_read_blocks_explicit_escalation_and_policy_bypass() {
|
|
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
|
path: FileSystemPath::GlobPattern {
|
|
pattern: "**/*.env".to_string(),
|
|
},
|
|
access: FileSystemAccessMode::Deny,
|
|
}]);
|
|
|
|
assert_eq!(
|
|
sandbox_override_for_first_attempt(
|
|
SandboxPermissions::RequireEscalated,
|
|
&ExecApprovalRequirement::Skip {
|
|
bypass_sandbox: false,
|
|
proposed_execpolicy_amendment: None,
|
|
},
|
|
&file_system_policy,
|
|
),
|
|
SandboxOverride::NoOverride,
|
|
"explicit escalation would drop deny-read filesystem policy, so keep the first attempt sandboxed",
|
|
);
|
|
assert!(!unsandboxed_execution_allowed(&file_system_policy));
|
|
assert_eq!(
|
|
sandbox_permissions_preserving_denied_reads(
|
|
SandboxPermissions::RequireEscalated,
|
|
&file_system_policy,
|
|
),
|
|
SandboxPermissions::UseDefault,
|
|
);
|
|
assert_eq!(
|
|
sandbox_permissions_preserving_denied_reads(
|
|
SandboxPermissions::WithAdditionalPermissions,
|
|
&file_system_policy,
|
|
),
|
|
SandboxPermissions::WithAdditionalPermissions,
|
|
);
|
|
assert_eq!(
|
|
sandbox_permissions_preserving_denied_reads(
|
|
SandboxPermissions::RequireEscalated,
|
|
&FileSystemSandboxPolicy::default(),
|
|
),
|
|
SandboxPermissions::RequireEscalated,
|
|
);
|
|
assert_eq!(
|
|
sandbox_override_for_first_attempt(
|
|
SandboxPermissions::WithAdditionalPermissions,
|
|
&ExecApprovalRequirement::Skip {
|
|
bypass_sandbox: true,
|
|
proposed_execpolicy_amendment: None,
|
|
},
|
|
&file_system_policy,
|
|
),
|
|
SandboxOverride::NoOverride,
|
|
"exec-policy allow rules would drop deny-read filesystem policy, so keep the first attempt sandboxed",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
|
|
let cwd: AbsolutePathBuf = std::env::current_dir()
|
|
.expect("current dir")
|
|
.try_into()
|
|
.expect("absolute cwd");
|
|
let cwd_uri = PathUri::from_abs_path(&cwd);
|
|
let exec_server_permissions = codex_protocol::models::PermissionProfile::workspace_write();
|
|
let permissions = exec_server_permissions
|
|
.clone()
|
|
.materialize_project_roots_with_workspace_roots(std::slice::from_ref(&cwd));
|
|
let manager = SandboxManager::new();
|
|
let mut attempt = SandboxAttempt {
|
|
sandbox: SandboxType::None,
|
|
sandbox_requested: true,
|
|
permissions: &permissions,
|
|
exec_server_permissions: &exec_server_permissions,
|
|
enforce_managed_network: true,
|
|
manager: &manager,
|
|
sandbox_cwd: &cwd_uri,
|
|
workspace_roots: std::slice::from_ref(&cwd),
|
|
codex_linux_sandbox_exe: None,
|
|
use_legacy_landlock: false,
|
|
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
|
|
windows_sandbox_private_desktop: false,
|
|
network_denial_cancellation_token: None,
|
|
};
|
|
let managed_network = ManagedNetworkSandboxContext {
|
|
loopback_ports: vec![43123],
|
|
allow_local_binding: false,
|
|
};
|
|
let command = || SandboxCommand {
|
|
program: "/bin/bash".into(),
|
|
args: vec!["-lc".to_string(), "pwd".to_string()],
|
|
cwd: cwd_uri.clone(),
|
|
env: HashMap::new(),
|
|
managed_network: Some(managed_network.clone()),
|
|
additional_permissions: None,
|
|
};
|
|
let options = || crate::sandboxing::ExecOptions {
|
|
expiration: crate::exec::ExecExpiration::DefaultTimeout,
|
|
capture_policy: crate::exec::ExecCapturePolicy::ShellTool,
|
|
};
|
|
let request = attempt
|
|
.env_for_exec_server(command(), options(), /*network*/ None, Some("remote"))
|
|
.expect("prepare remote exec request");
|
|
|
|
assert_eq!(
|
|
request.command,
|
|
vec![
|
|
"/bin/bash".to_string(),
|
|
"-lc".to_string(),
|
|
"pwd".to_string()
|
|
]
|
|
);
|
|
assert_eq!(request.arg0, None);
|
|
assert_eq!(request.sandbox, SandboxType::None);
|
|
assert_eq!(
|
|
request.exec_server_sandbox,
|
|
Some(codex_exec_server::FileSystemSandboxContext {
|
|
permissions: exec_server_permissions.clone().into(),
|
|
cwd: Some(cwd_uri.clone()),
|
|
workspace_roots: Vec::new(),
|
|
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
|
|
windows_sandbox_private_desktop: false,
|
|
use_legacy_landlock: false,
|
|
})
|
|
);
|
|
assert!(request.exec_server_enforce_managed_network);
|
|
assert_eq!(
|
|
request.exec_server_managed_network,
|
|
Some(managed_network.clone())
|
|
);
|
|
|
|
attempt.sandbox_requested = false;
|
|
let request = attempt
|
|
.env_for_exec_server(command(), options(), /*network*/ None, Some("remote"))
|
|
.expect("prepare unsandboxed remote exec request");
|
|
|
|
assert_eq!(request.exec_server_sandbox, None);
|
|
assert!(!request.exec_server_enforce_managed_network);
|
|
assert_eq!(request.exec_server_managed_network, Some(managed_network));
|
|
}
|