permissions: migrate approval and sandbox consumers to profiles (#19393)

## Why

Runtime decisions should not infer permissions from the lossy legacy
sandbox projection once `PermissionProfile` is available. In particular,
`Disabled` and `External` need to remain distinct, and managed profiles
with split filesystem or deny-read rules should not be collapsed before
approval, network, safety, or analytics code makes decisions.

## What Changed

- Changes managed network proxy setup and network approval logic to use
`PermissionProfile` when deciding whether a managed sandbox is active.
- Migrates patch safety, Guardian/user-shell approval paths, Landlock
helper setup, analytics sandbox classification, and selected
turn/session code to profile-backed permissions.
- Validates command-level profile overrides against the constrained
`PermissionProfile` rather than a strict `SandboxPolicy` round trip.
- Preserves configured deny-read restrictions when command profiles are
narrowed.
- Adds coverage for profile-backed trust, network proxy/approval
behavior, patch safety, analytics classification, and command-profile
narrowing.

## Verification

- `cargo test -p codex-core direct_write_roots`
- `cargo test -p codex-core runtime_roots_to_legacy_projection`
- `cargo test -p codex-app-server
requested_permissions_trust_project_uses_permission_profile_intent`




































































---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19393).
* #19395
* #19394
* __->__ #19393
This commit is contained in:
Michael Bolin
2026-04-26 15:30:40 -07:00
committed by GitHub
Unverified
parent 9c3abcd46c
commit dda8199b73
24 changed files with 367 additions and 164 deletions
@@ -315,7 +315,10 @@ fn sample_turn_resolved_config(turn_id: &str) -> TurnResolvedConfigFact {
session_source: SessionSource::Exec,
model: "gpt-5".to_string(),
model_provider: "openai".to_string(),
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: CorePermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_read_only_policy(),
),
permission_profile_cwd: PathBuf::from("/tmp"),
reasoning_effort: None,
reasoning_summary: None,
service_tier: None,
+3 -2
View File
@@ -13,12 +13,12 @@ use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::HookEventName;
use codex_protocol::protocol::HookRunStatus;
use codex_protocol::protocol::HookSource;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SkillScope;
use codex_protocol::protocol::SubAgentSource;
@@ -62,7 +62,8 @@ pub struct TurnResolvedConfigFact {
pub session_source: SessionSource,
pub model: String,
pub model_provider: String,
pub sandbox_policy: SandboxPolicy,
pub permission_profile: PermissionProfile,
pub permission_profile_cwd: PathBuf,
pub reasoning_effort: Option<ReasoningEffort>,
pub reasoning_summary: Option<ReasoningSummary>,
pub service_tier: Option<ServiceTier>,
+20 -8
View File
@@ -61,6 +61,7 @@ use codex_login::default_client::originator;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SkillScope;
@@ -884,7 +885,8 @@ fn codex_turn_event_params(
session_source: _session_source,
model,
model_provider,
sandbox_policy,
permission_profile,
permission_profile_cwd,
reasoning_effort,
reasoning_summary,
service_tier,
@@ -909,7 +911,10 @@ fn codex_turn_event_params(
parent_thread_id: thread_metadata.parent_thread_id.clone(),
model: Some(model),
model_provider,
sandbox_policy: Some(sandbox_policy_mode(&sandbox_policy)),
sandbox_policy: Some(sandbox_policy_mode(
&permission_profile,
permission_profile_cwd.as_path(),
)),
reasoning_effort: reasoning_effort.map(|value| value.to_string()),
reasoning_summary: reasoning_summary_mode(reasoning_summary),
service_tier: service_tier
@@ -954,12 +959,19 @@ fn codex_turn_event_params(
}
}
fn sandbox_policy_mode(sandbox_policy: &SandboxPolicy) -> &'static str {
match sandbox_policy {
SandboxPolicy::DangerFullAccess => "full_access",
SandboxPolicy::ReadOnly { .. } => "read_only",
SandboxPolicy::WorkspaceWrite { .. } => "workspace_write",
SandboxPolicy::ExternalSandbox { .. } => "external_sandbox",
fn sandbox_policy_mode(permission_profile: &PermissionProfile, cwd: &Path) -> &'static str {
match permission_profile {
PermissionProfile::Disabled => "full_access",
PermissionProfile::External { .. } => "external_sandbox",
PermissionProfile::Managed { .. } => {
match permission_profile.to_legacy_sandbox_policy(cwd) {
Ok(SandboxPolicy::DangerFullAccess) => "full_access",
Ok(SandboxPolicy::ReadOnly { .. }) => "read_only",
Ok(SandboxPolicy::WorkspaceWrite { .. }) => "workspace_write",
Ok(SandboxPolicy::ExternalSandbox { .. }) => "external_sandbox",
Err(_) => "workspace_write",
}
}
}
}
@@ -2209,7 +2209,7 @@ impl CodexMessageProcessor {
let started_network_proxy = match self.config.permissions.network.as_ref() {
Some(spec) => match spec
.start_proxy(
self.config.permissions.sandbox_policy.get(),
self.config.permissions.permission_profile.get(),
/*policy_decider*/ None,
/*blocked_request_observer*/ None,
managed_network_requirements_enabled,
@@ -2290,17 +2290,11 @@ impl CodexMessageProcessor {
&file_system_sandbox_policy,
network_sandbox_policy,
);
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
&effective_permission_profile,
&file_system_sandbox_policy,
network_sandbox_policy,
sandbox_cwd.as_path(),
);
match self
.config
.permissions
.sandbox_policy
.can_set(&sandbox_policy)
.permission_profile
.can_set(&effective_permission_profile)
{
Ok(()) => effective_permission_profile,
Err(err) => {
@@ -2320,13 +2314,29 @@ impl CodexMessageProcessor {
codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, &sandbox_cwd);
let network_sandbox_policy =
codex_protocol::permissions::NetworkSandboxPolicy::from(&policy);
codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement(
let permission_profile =
codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement(
codex_protocol::models::SandboxEnforcement::from_legacy_sandbox_policy(
&policy,
),
&file_system_sandbox_policy,
network_sandbox_policy,
)
);
if let Err(err) = self
.config
.permissions
.permission_profile
.can_set(&permission_profile)
{
let error = JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: format!("invalid sandbox policy: {err}"),
data: None,
};
self.outgoing.send_error(request, error).await;
return;
}
permission_profile
}
Err(err) => {
let error = JSONRPCErrorError {
+1 -1
View File
@@ -171,7 +171,7 @@ async fn run_command_under_sandbox(
let network_proxy = match config.permissions.network.as_ref() {
Some(spec) => Some(
spec.start_proxy(
config.permissions.sandbox_policy.get(),
config.permissions.permission_profile.get(),
/*policy_decider*/ None,
/*blocked_request_observer*/ None,
managed_network_requirements_enabled,
+1 -2
View File
@@ -35,11 +35,10 @@ pub(crate) async fn apply_patch(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
action: ApplyPatchAction,
) -> InternalApplyPatchInvocation {
let sandbox_policy = turn_context.sandbox_policy();
match assess_patch_safety(
&action,
turn_context.approval_policy.value(),
&sandbox_policy,
&turn_context.permission_profile(),
file_system_sandbox_policy,
&turn_context.cwd,
turn_context.windows_sandbox_level,
+57
View File
@@ -58,6 +58,7 @@ use codex_model_provider_info::WireApi;
use codex_models_manager::bundled_models_response;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxEnforcement;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
@@ -6775,6 +6776,62 @@ async fn permission_profile_override_falls_back_when_disallowed_by_requirements(
Ok(())
}
#[tokio::test]
async fn permission_profile_override_preserves_split_write_roots() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let cwd = codex_home.path().join("workspace");
let outside_root = codex_home.path().join("outside-write");
std::fs::create_dir_all(&cwd)?;
std::fs::create_dir_all(&outside_root)?;
let outside_root =
AbsolutePathBuf::from_absolute_path(outside_root).expect("outside root is absolute");
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: outside_root.clone(),
},
access: FileSystemAccessMode::Write,
},
]);
let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::Managed,
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
);
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(cwd))
.harness_overrides(ConfigOverrides {
permission_profile: Some(permission_profile),
..Default::default()
})
.build()
.await?;
assert!(
config
.permissions
.file_system_sandbox_policy()
.can_write_path_with_cwd(outside_root.as_path(), config.cwd.as_path())
);
assert!(matches!(
config.permissions.sandbox_policy.get(),
SandboxPolicy::WorkspaceWrite { .. }
));
assert_eq!(
config.permissions.network_sandbox_policy(),
NetworkSandboxPolicy::Restricted
);
Ok(())
}
#[tokio::test]
async fn requirements_web_search_mode_overrides_danger_full_access_default() -> std::io::Result<()>
{
+8 -1
View File
@@ -2396,10 +2396,17 @@ impl Config {
None => (None, None),
};
let has_network_requirements = network_requirements.is_some();
let network_permission_profile = if *constrained_sandbox_policy.get()
== original_sandbox_policy
{
permission_profile.clone()
} else {
PermissionProfile::from_legacy_sandbox_policy(constrained_sandbox_policy.get())
};
let network = NetworkProxySpec::from_config_and_constraints(
configured_network_proxy_config,
network_requirements,
constrained_sandbox_policy.get(),
&network_permission_profile,
)
.map_err(|err| {
if let Some(source) = network_requirements_source.as_ref() {
+19 -24
View File
@@ -16,7 +16,7 @@ use codex_network_proxy::build_config_state;
use codex_network_proxy::host_and_port_from_network_addr;
use codex_network_proxy::normalize_host;
use codex_network_proxy::validate_policy_against_constraints;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::models::PermissionProfile;
use std::collections::HashSet;
use std::sync::Arc;
@@ -89,7 +89,7 @@ impl NetworkProxySpec {
pub(crate) fn from_config_and_constraints(
config: NetworkProxyConfig,
requirements: Option<NetworkConstraints>,
sandbox_policy: &SandboxPolicy,
permission_profile: &PermissionProfile,
) -> std::io::Result<Self> {
let base_config = config.clone();
let hard_deny_allowlist_misses = requirements
@@ -99,7 +99,7 @@ impl NetworkProxySpec {
Self::apply_requirements(
config,
requirements,
sandbox_policy,
permission_profile,
hard_deny_allowlist_misses,
)
} else {
@@ -122,7 +122,7 @@ impl NetworkProxySpec {
pub async fn start_proxy(
&self,
sandbox_policy: &SandboxPolicy,
permission_profile: &PermissionProfile,
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
blocked_request_observer: Option<Arc<dyn BlockedRequestObserver>>,
enable_network_approval_flow: bool,
@@ -133,10 +133,7 @@ impl NetworkProxySpec {
if enable_network_approval_flow && !self.hard_deny_allowlist_misses {
if let Some(policy_decider) = policy_decider {
builder = builder.policy_decider_arc(policy_decider);
} else if matches!(
sandbox_policy,
SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. }
) {
} else if Self::managed_sandbox_active(permission_profile) {
builder = builder
.policy_decider(|_request| async { NetworkDecision::ask("not_allowed") });
}
@@ -154,14 +151,14 @@ impl NetworkProxySpec {
Ok(StartedNetworkProxy::new(proxy, handle))
}
pub(crate) fn recompute_for_sandbox_policy(
pub(crate) fn recompute_for_permission_profile(
&self,
sandbox_policy: &SandboxPolicy,
permission_profile: &PermissionProfile,
) -> std::io::Result<Self> {
Self::from_config_and_constraints(
self.base_config.clone(),
self.requirements.clone(),
sandbox_policy,
permission_profile,
)
}
@@ -216,13 +213,13 @@ impl NetworkProxySpec {
fn apply_requirements(
mut config: NetworkProxyConfig,
requirements: &NetworkConstraints,
sandbox_policy: &SandboxPolicy,
permission_profile: &PermissionProfile,
hard_deny_allowlist_misses: bool,
) -> (NetworkProxyConfig, NetworkProxyConstraints) {
let mut constraints = NetworkProxyConstraints::default();
let allowlist_expansion_enabled =
Self::allowlist_expansion_enabled(sandbox_policy, hard_deny_allowlist_misses);
let denylist_expansion_enabled = Self::denylist_expansion_enabled(sandbox_policy);
Self::allowlist_expansion_enabled(permission_profile, hard_deny_allowlist_misses);
let denylist_expansion_enabled = Self::denylist_expansion_enabled(permission_profile);
if let Some(enabled) = requirements.enabled {
config.network.enabled = enabled;
@@ -322,24 +319,22 @@ impl NetworkProxySpec {
}
fn allowlist_expansion_enabled(
sandbox_policy: &SandboxPolicy,
permission_profile: &PermissionProfile,
hard_deny_allowlist_misses: bool,
) -> bool {
matches!(
sandbox_policy,
SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. }
) && !hard_deny_allowlist_misses
Self::managed_sandbox_active(permission_profile) && !hard_deny_allowlist_misses
}
fn managed_allowed_domains_only(requirements: &NetworkConstraints) -> bool {
requirements.managed_allowed_domains_only.unwrap_or(false)
}
fn denylist_expansion_enabled(sandbox_policy: &SandboxPolicy) -> bool {
matches!(
sandbox_policy,
SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. }
)
fn denylist_expansion_enabled(permission_profile: &PermissionProfile) -> bool {
Self::managed_sandbox_active(permission_profile)
}
fn managed_sandbox_active(permission_profile: &PermissionProfile) -> bool {
matches!(permission_profile, PermissionProfile::Managed { .. })
}
fn merge_domain_lists(mut managed: Vec<String>, user_entries: &[String]) -> Vec<String> {
@@ -2,8 +2,16 @@ use super::*;
use codex_config::NetworkDomainPermissionToml;
use codex_config::NetworkDomainPermissionsToml;
use codex_network_proxy::NetworkDomainPermission;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
use pretty_assertions::assert_eq;
fn permission_profile_for_sandbox_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile {
PermissionProfile::from_legacy_sandbox_policy(sandbox_policy)
}
fn domain_permissions(
entries: impl IntoIterator<Item = (&'static str, NetworkDomainPermissionToml)>,
) -> NetworkDomainPermissionsToml {
@@ -54,7 +62,7 @@ fn requirements_allowed_domains_are_a_baseline_for_user_allowlist() {
let spec = NetworkProxySpec::from_config_and_constraints(
config,
Some(requirements),
&SandboxPolicy::new_read_only_policy(),
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_read_only_policy()),
)
.expect("config should stay within the managed allowlist");
@@ -89,7 +97,7 @@ fn requirements_allowed_domains_do_not_override_user_denies_for_same_pattern() {
let spec = NetworkProxySpec::from_config_and_constraints(
config,
Some(requirements),
&SandboxPolicy::new_workspace_write_policy(),
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
)
.expect("managed allowlist should not erase a user deny");
@@ -121,7 +129,7 @@ fn requirements_allowlist_expansion_keeps_user_entries_mutable() {
let spec = NetworkProxySpec::from_config_and_constraints(
config,
Some(requirements),
&SandboxPolicy::new_workspace_write_policy(),
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
)
.expect("managed baseline should still allow user edits");
@@ -144,6 +152,41 @@ fn requirements_allowlist_expansion_keeps_user_entries_mutable() {
.expect("user allowlist entries should not become managed constraints");
}
#[test]
fn managed_unrestricted_profile_allows_domain_expansion() {
let mut config = NetworkProxyConfig::default();
config
.network
.set_allowed_domains(vec!["api.example.com".to_string()]);
let requirements = NetworkConstraints {
domains: Some(domain_permissions([(
"*.example.com",
NetworkDomainPermissionToml::Allow,
)])),
..Default::default()
};
let permission_profile = PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: NetworkSandboxPolicy::Restricted,
};
let spec = NetworkProxySpec::from_config_and_constraints(
config,
Some(requirements),
&permission_profile,
)
.expect("managed unrestricted filesystem should still use managed network constraints");
assert_eq!(
spec.config.network.allowed_domains(),
Some(vec![
"*.example.com".to_string(),
"api.example.com".to_string()
])
);
assert_eq!(spec.constraints.allowlist_expansion_enabled, Some(true));
}
#[test]
fn danger_full_access_keeps_managed_allowlist_and_denylist_fixed() {
let mut config = NetworkProxyConfig::default();
@@ -164,7 +207,7 @@ fn danger_full_access_keeps_managed_allowlist_and_denylist_fixed() {
let spec = NetworkProxySpec::from_config_and_constraints(
config,
Some(requirements),
&SandboxPolicy::DangerFullAccess,
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
)
.expect("yolo mode should pin the effective policy to the managed baseline");
@@ -198,7 +241,7 @@ fn managed_allowed_domains_only_disables_default_mode_allowlist_expansion() {
let spec = NetworkProxySpec::from_config_and_constraints(
config,
Some(requirements),
&SandboxPolicy::new_workspace_write_policy(),
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
)
.expect("managed baseline should still load");
@@ -227,7 +270,7 @@ fn managed_allowed_domains_only_ignores_user_allowlist_and_hard_denies_misses()
let spec = NetworkProxySpec::from_config_and_constraints(
config,
Some(requirements),
&SandboxPolicy::new_workspace_write_policy(),
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
)
.expect("managed-only allowlist should still load");
@@ -257,7 +300,7 @@ fn managed_allowed_domains_only_without_managed_allowlist_blocks_all_user_domain
let spec = NetworkProxySpec::from_config_and_constraints(
config,
Some(requirements),
&SandboxPolicy::new_workspace_write_policy(),
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
)
.expect("managed-only mode should treat missing managed allowlist as empty");
@@ -281,7 +324,7 @@ fn managed_allowed_domains_only_blocks_all_user_domains_in_full_access_without_m
let spec = NetworkProxySpec::from_config_and_constraints(
config,
Some(requirements),
&SandboxPolicy::DangerFullAccess,
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
)
.expect("managed-only mode should treat missing managed allowlist as empty");
@@ -308,7 +351,7 @@ fn deny_only_requirements_do_not_create_allow_constraints_in_full_access() {
let spec = NetworkProxySpec::from_config_and_constraints(
config,
Some(requirements),
&SandboxPolicy::DangerFullAccess,
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
)
.expect("deny-only requirements should not constrain the allowlist");
@@ -341,7 +384,7 @@ fn allow_only_requirements_do_not_create_deny_constraints_in_full_access() {
let spec = NetworkProxySpec::from_config_and_constraints(
config,
Some(requirements),
&SandboxPolicy::DangerFullAccess,
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
)
.expect("allow-only requirements should not constrain the denylist");
@@ -374,7 +417,7 @@ fn requirements_denied_domains_are_a_baseline_for_default_mode() {
let spec = NetworkProxySpec::from_config_and_constraints(
config,
Some(requirements),
&SandboxPolicy::new_workspace_write_policy(),
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
)
.expect("default mode should merge managed and user deny entries");
@@ -409,7 +452,7 @@ fn requirements_denylist_expansion_keeps_user_entries_mutable() {
let spec = NetworkProxySpec::from_config_and_constraints(
config,
Some(requirements),
&SandboxPolicy::new_workspace_write_policy(),
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
)
.expect("managed baseline should still allow user edits");
+1 -1
View File
@@ -874,7 +874,7 @@ pub(crate) fn build_guardian_review_session_config(
guardian_config.permissions.network = Some(NetworkProxySpec::from_config_and_constraints(
live_network_config,
network_constraints,
&SandboxPolicy::new_read_only_policy(),
guardian_config.permissions.permission_profile.get(),
)?);
}
for feature in [
+6 -3
View File
@@ -27,6 +27,7 @@ use codex_protocol::ThreadId;
use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::models::ContentItem;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
@@ -1942,7 +1943,7 @@ async fn guardian_review_session_config_preserves_parent_network_proxy() {
}),
..Default::default()
}),
parent_config.permissions.sandbox_policy.get(),
parent_config.permissions.permission_profile.get(),
)
.expect("network proxy spec");
parent_config.permissions.network = Some(network.clone());
@@ -2007,7 +2008,7 @@ async fn guardian_review_session_config_uses_live_network_proxy_state() {
NetworkProxySpec::from_config_and_constraints(
parent_network,
/*requirements*/ None,
parent_config.permissions.sandbox_policy.get(),
parent_config.permissions.permission_profile.get(),
)
.expect("parent network proxy spec"),
);
@@ -2032,7 +2033,9 @@ async fn guardian_review_session_config_uses_live_network_proxy_state() {
NetworkProxySpec::from_config_and_constraints(
live_network,
/*requirements*/ None,
&SandboxPolicy::new_read_only_policy(),
&PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_read_only_policy(),
),
)
.expect("live network proxy spec")
)
+14 -12
View File
@@ -2,9 +2,8 @@ use crate::spawn::SpawnChildRequest;
use crate::spawn::StdioPolicy;
use crate::spawn::spawn_child_async;
use codex_network_proxy::NetworkProxy;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::models::PermissionProfile;
use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile;
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
use codex_sandboxing::landlock::allow_network_for_proxy;
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies;
@@ -18,15 +17,15 @@ use tokio::process::Child;
/// isolation plus seccomp for network restrictions.
///
/// Unlike macOS Seatbelt where we directly embed the policy text, the Linux
/// helper is a separate executable. We pass the legacy [`SandboxPolicy`] plus
/// split filesystem/network policies as JSON so the helper can migrate
/// incrementally without breaking older call sites.
/// helper is a separate executable. We pass both the canonical split
/// filesystem/network policies and a compatibility legacy projection as JSON
/// until the helper protocol no longer needs the legacy field.
#[allow(clippy::too_many_arguments)]
pub async fn spawn_command_under_linux_sandbox<P>(
codex_linux_sandbox_exe: P,
command: Vec<String>,
command_cwd: AbsolutePathBuf,
sandbox_policy: &SandboxPolicy,
permission_profile: &PermissionProfile,
sandbox_policy_cwd: &AbsolutePathBuf,
use_legacy_landlock: bool,
stdio_policy: StdioPolicy,
@@ -36,15 +35,18 @@ pub async fn spawn_command_under_linux_sandbox<P>(
where
P: AsRef<Path>,
{
let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
sandbox_policy,
sandbox_policy_cwd,
let (file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
permission_profile,
&file_system_sandbox_policy,
network_sandbox_policy,
sandbox_policy_cwd.as_path(),
);
let network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy);
let args = create_linux_sandbox_command_args_for_policies(
command,
command_cwd.as_path(),
sandbox_policy,
&sandbox_policy,
&file_system_sandbox_policy,
network_sandbox_policy,
sandbox_policy_cwd,
+31 -13
View File
@@ -6,9 +6,9 @@ use crate::util::resolve_path;
use codex_apply_patch::ApplyPatchAction;
use codex_apply_patch::ApplyPatchFileChange;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use codex_sandboxing::SandboxType;
use codex_sandboxing::get_platform_sandbox;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -33,7 +33,7 @@ pub enum SafetyCheck {
pub fn assess_patch_safety(
action: &ApplyPatchAction,
policy: AskForApproval,
sandbox_policy: &SandboxPolicy,
permission_profile: &PermissionProfile,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
cwd: &AbsolutePathBuf,
windows_sandbox_level: WindowsSandboxLevel,
@@ -71,10 +71,11 @@ pub fn assess_patch_safety(
|| matches!(policy, AskForApproval::OnFailure)
{
if matches!(
sandbox_policy,
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
permission_profile,
PermissionProfile::Disabled | PermissionProfile::External { .. }
) {
// DangerFullAccess is intended to bypass sandboxing entirely.
// Disabled and External profiles intentionally do not apply an
// outer Codex filesystem sandbox.
SafetyCheck::AutoApprove {
sandbox_type: SandboxType::None,
user_explicitly_approved: false,
@@ -91,7 +92,12 @@ pub fn assess_patch_safety(
None => {
if rejects_sandbox_approval {
SafetyCheck::Reject {
reason: patch_rejection_reason(sandbox_policy).to_string(),
reason: patch_rejection_reason(
permission_profile,
file_system_sandbox_policy,
cwd,
)
.to_string(),
}
} else {
SafetyCheck::AskUser
@@ -101,19 +107,31 @@ pub fn assess_patch_safety(
}
} else if rejects_sandbox_approval {
SafetyCheck::Reject {
reason: patch_rejection_reason(sandbox_policy).to_string(),
reason: patch_rejection_reason(permission_profile, file_system_sandbox_policy, cwd)
.to_string(),
}
} else {
SafetyCheck::AskUser
}
}
fn patch_rejection_reason(sandbox_policy: &SandboxPolicy) -> &'static str {
match sandbox_policy {
SandboxPolicy::ReadOnly { .. } => PATCH_REJECTED_READ_ONLY_REASON,
SandboxPolicy::WorkspaceWrite { .. }
| SandboxPolicy::DangerFullAccess
| SandboxPolicy::ExternalSandbox { .. } => PATCH_REJECTED_OUTSIDE_PROJECT_REASON,
fn patch_rejection_reason(
permission_profile: &PermissionProfile,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
cwd: &AbsolutePathBuf,
) -> &'static str {
match permission_profile {
PermissionProfile::Managed { .. }
if !file_system_sandbox_policy.has_full_disk_write_access()
&& file_system_sandbox_policy
.get_writable_roots_with_cwd(cwd.as_path())
.is_empty() =>
{
PATCH_REJECTED_READ_ONLY_REASON
}
PermissionProfile::Managed { .. }
| PermissionProfile::Disabled
| PermissionProfile::External { .. } => PATCH_REJECTED_OUTSIDE_PROJECT_REASON,
}
}
+14 -8
View File
@@ -1,14 +1,20 @@
use super::*;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::FileSystemAccessMode;
use codex_protocol::protocol::FileSystemPath;
use codex_protocol::protocol::FileSystemSandboxEntry;
use codex_protocol::protocol::FileSystemSpecialPath;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::PathExt;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
fn permission_profile_for_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile {
PermissionProfile::from_legacy_sandbox_policy(sandbox_policy)
}
#[test]
fn test_writable_roots_constraint() {
// Use a temporary directory as our workspace to avoid touching
@@ -75,7 +81,7 @@ fn external_sandbox_auto_approves_in_on_request() {
assess_patch_safety(
&add_inside,
AskForApproval::OnRequest,
&policy,
&permission_profile_for_policy(&policy),
&FileSystemSandboxPolicy::from(&policy),
&cwd,
WindowsSandboxLevel::Disabled
@@ -105,7 +111,7 @@ fn granular_with_all_flags_true_matches_on_request_for_out_of_root_patch() {
assess_patch_safety(
&add_outside,
AskForApproval::OnRequest,
&policy_workspace_only,
&permission_profile_for_policy(&policy_workspace_only),
&FileSystemSandboxPolicy::from(&policy_workspace_only),
&cwd,
WindowsSandboxLevel::Disabled,
@@ -122,7 +128,7 @@ fn granular_with_all_flags_true_matches_on_request_for_out_of_root_patch() {
request_permissions: true,
mcp_elicitations: true,
}),
&policy_workspace_only,
&permission_profile_for_policy(&policy_workspace_only),
&FileSystemSandboxPolicy::from(&policy_workspace_only),
&cwd,
WindowsSandboxLevel::Disabled,
@@ -155,7 +161,7 @@ fn granular_sandbox_approval_false_rejects_out_of_root_patch() {
request_permissions: true,
mcp_elicitations: true,
}),
&policy_workspace_only,
&permission_profile_for_policy(&policy_workspace_only),
&FileSystemSandboxPolicy::from(&policy_workspace_only),
&cwd,
WindowsSandboxLevel::Disabled,
@@ -185,7 +191,7 @@ fn read_only_policy_rejects_patch_with_read_only_reason() {
assess_patch_safety(
&action,
AskForApproval::Never,
&sandbox_policy,
&permission_profile_for_policy(&sandbox_policy),
&file_system_sandbox_policy,
&cwd,
WindowsSandboxLevel::Disabled,
@@ -229,7 +235,7 @@ fn explicit_unreadable_paths_prevent_auto_approval_for_external_sandbox() {
assess_patch_safety(
&action,
AskForApproval::OnRequest,
&sandbox_policy,
&permission_profile_for_policy(&sandbox_policy),
&file_system_sandbox_policy,
&cwd,
WindowsSandboxLevel::Disabled,
@@ -273,7 +279,7 @@ fn explicit_read_only_subpaths_prevent_auto_approval_for_external_sandbox() {
assess_patch_safety(
&action,
AskForApproval::OnRequest,
&sandbox_policy,
&permission_profile_for_policy(&sandbox_policy),
&file_system_sandbox_policy,
&cwd,
WindowsSandboxLevel::Disabled,
@@ -306,7 +312,7 @@ fn missing_project_dot_codex_config_requires_approval() {
assess_patch_safety(
&action,
AskForApproval::OnRequest,
&sandbox_policy,
&permission_profile_for_policy(&sandbox_policy),
&file_system_sandbox_policy,
&cwd,
WindowsSandboxLevel::Disabled,
+17 -13
View File
@@ -845,8 +845,10 @@ impl Session {
}
}
fn managed_network_proxy_active_for_sandbox_policy(sandbox_policy: &SandboxPolicy) -> bool {
!matches!(sandbox_policy, SandboxPolicy::DangerFullAccess)
fn managed_network_proxy_active_for_permission_profile(
permission_profile: &PermissionProfile,
) -> bool {
!matches!(permission_profile, PermissionProfile::Disabled)
}
/// Builds the `x-codex-beta-features` header value for this session.
@@ -879,7 +881,7 @@ impl Session {
async fn start_managed_network_proxy(
spec: &crate::config::NetworkProxySpec,
exec_policy: &codex_execpolicy::Policy,
sandbox_policy: &SandboxPolicy,
permission_profile: &PermissionProfile,
network_policy_decider: Option<Arc<dyn codex_network_proxy::NetworkPolicyDecider>>,
blocked_request_observer: Option<Arc<dyn codex_network_proxy::BlockedRequestObserver>>,
managed_network_requirements_enabled: bool,
@@ -896,7 +898,7 @@ impl Session {
.unwrap_or_else(|_| spec.clone());
let network_proxy = spec
.start_proxy(
sandbox_policy,
permission_profile,
network_policy_decider,
blocked_request_observer,
managed_network_requirements_enabled,
@@ -914,7 +916,7 @@ impl Session {
Ok((network_proxy, session_network_proxy))
}
async fn refresh_managed_network_proxy_for_current_sandbox_policy(&self) {
async fn refresh_managed_network_proxy_for_current_permission_profile(&self) {
let Some(started_proxy) = self.services.network_proxy.as_ref() else {
return;
};
@@ -935,7 +937,8 @@ impl Session {
return;
};
let spec = match spec.recompute_for_sandbox_policy(&session_configuration.sandbox_policy())
let spec = match spec
.recompute_for_permission_profile(&session_configuration.permission_profile())
{
Ok(spec) => spec,
Err(err) => {
@@ -1285,7 +1288,7 @@ impl Session {
&self,
updates: SessionSettingsUpdate,
) -> ConstraintResult<()> {
let (previous_cwd, sandbox_policy_changed, next_cwd, codex_home, session_source) = {
let (previous_cwd, permission_profile_changed, next_cwd, codex_home, session_source) = {
let mut state = self.state.lock().await;
let updated = match state.session_configuration.apply(&updates) {
Ok(updated) => updated,
@@ -1296,16 +1299,17 @@ impl Session {
};
let previous_cwd = state.session_configuration.cwd.clone();
let previous_sandbox_policy = state.session_configuration.sandbox_policy();
let updated_sandbox_policy = updated.sandbox_policy();
let sandbox_policy_changed = previous_sandbox_policy != updated_sandbox_policy;
let previous_permission_profile = state.session_configuration.permission_profile();
let updated_permission_profile = updated.permission_profile();
let permission_profile_changed =
previous_permission_profile != updated_permission_profile;
let next_cwd = updated.cwd.clone();
let codex_home = updated.codex_home.clone();
let session_source = updated.session_source.clone();
state.session_configuration = updated;
(
previous_cwd,
sandbox_policy_changed,
permission_profile_changed,
next_cwd,
codex_home,
session_source,
@@ -1318,8 +1322,8 @@ impl Session {
&codex_home,
&session_source,
);
if sandbox_policy_changed {
self.refresh_managed_network_proxy_for_current_sandbox_policy()
if permission_profile_changed {
self.refresh_managed_network_proxy_for_current_permission_profile()
.await;
}
+3 -3
View File
@@ -730,7 +730,7 @@ impl Session {
let (network_proxy, session_network_proxy) = Self::start_managed_network_proxy(
spec,
current_exec_policy.as_ref(),
config.permissions.sandbox_policy.get(),
config.permissions.permission_profile.get(),
network_policy_decider.as_ref().map(Arc::clone),
blocked_request_observer.as_ref().map(Arc::clone),
managed_network_requirements_configured,
@@ -885,8 +885,8 @@ impl Session {
history_entry_count,
initial_messages,
network_proxy: session_network_proxy.filter(|_| {
Self::managed_network_proxy_active_for_sandbox_policy(
&session_sandbox_policy,
Self::managed_network_proxy_active_for_permission_profile(
session_configuration.permission_profile.get(),
)
}),
rollout_path,
+39 -23
View File
@@ -159,6 +159,10 @@ use std::time::Duration as StdDuration;
mod guardian_tests;
fn permission_profile_for_sandbox_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile {
PermissionProfile::from_legacy_sandbox_policy(sandbox_policy)
}
struct InstructionsTestCase {
slug: &'static str,
expects_apply_patch_description: bool,
@@ -593,7 +597,7 @@ async fn start_managed_network_proxy_applies_execpolicy_network_rules() -> anyho
let spec = crate::config::NetworkProxySpec::from_config_and_constraints(
NetworkProxyConfig::default(),
/*requirements*/ None,
&SandboxPolicy::new_workspace_write_policy(),
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
)?;
let mut exec_policy = Policy::empty();
exec_policy.add_network_rule(
@@ -606,7 +610,7 @@ async fn start_managed_network_proxy_applies_execpolicy_network_rules() -> anyho
let (started_proxy, _) = Session::start_managed_network_proxy(
&spec,
&exec_policy,
&SandboxPolicy::new_workspace_write_policy(),
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
/*network_policy_decider*/ None,
/*blocked_request_observer*/ None,
/*managed_network_requirements_enabled*/ false,
@@ -637,7 +641,7 @@ async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules()
managed_allowed_domains_only: Some(true),
..Default::default()
}),
&SandboxPolicy::new_workspace_write_policy(),
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
)?;
let mut exec_policy = Policy::empty();
exec_policy.add_network_rule(
@@ -650,7 +654,7 @@ async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules()
let (started_proxy, _) = Session::start_managed_network_proxy(
&spec,
&exec_policy,
&SandboxPolicy::new_workspace_write_policy(),
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
/*network_policy_decider*/ None,
/*blocked_request_observer*/ None,
/*managed_network_requirements_enabled*/ false,
@@ -674,7 +678,7 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R
enabled: Some(true),
..Default::default()
}),
&SandboxPolicy::DangerFullAccess,
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
)?;
let exec_policy = Policy::empty();
let decider_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
@@ -689,7 +693,7 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R
let (started_proxy, _) = Session::start_managed_network_proxy(
&spec,
&exec_policy,
&SandboxPolicy::DangerFullAccess,
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
Some(network_policy_decider),
/*blocked_request_observer*/ None,
/*managed_network_requirements_enabled*/ true,
@@ -697,7 +701,9 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R
)
.await?;
let spec = spec.recompute_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy())?;
let spec = spec.recompute_for_permission_profile(&permission_profile_for_sandbox_policy(
&SandboxPolicy::new_workspace_write_policy(),
))?;
spec.apply_to_started_proxy(&started_proxy).await?;
let current_cfg = started_proxy.proxy().current_cfg().await?;
assert_eq!(current_cfg.network.allowed_domains(), None);
@@ -754,12 +760,12 @@ async fn new_turn_refreshes_managed_network_proxy_for_sandbox_change() -> anyhow
let spec = crate::config::NetworkProxySpec::from_config_and_constraints(
network_config,
Some(requirements),
&initial_policy,
&permission_profile_for_sandbox_policy(&initial_policy),
)?;
let (started_proxy, _) = Session::start_managed_network_proxy(
&spec,
&Policy::empty(),
&initial_policy,
&permission_profile_for_sandbox_policy(&initial_policy),
/*network_policy_decider*/ None,
/*blocked_request_observer*/ None,
/*managed_network_requirements_enabled*/ false,
@@ -832,14 +838,15 @@ async fn danger_full_access_turns_do_not_expose_managed_network_proxy() -> anyho
enabled: Some(true),
..Default::default()
}),
&SandboxPolicy::DangerFullAccess,
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
)?;
let session = make_session_with_config(move |config| {
config.permissions.sandbox_policy =
codex_config::Constrained::allow_any(SandboxPolicy::DangerFullAccess);
config.permissions.permission_profile =
codex_config::Constrained::allow_any(PermissionProfile::Disabled);
let cwd = config.cwd.clone();
config
.permissions
.set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess, cwd.as_path())
.expect("test setup should allow sandbox policy");
config.permissions.network = Some(network_spec);
})
.await?;
@@ -897,14 +904,15 @@ async fn danger_full_access_tool_attempts_do_not_enforce_managed_network() -> an
enabled: Some(true),
..Default::default()
}),
&SandboxPolicy::DangerFullAccess,
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
)?;
let session = make_session_with_config(move |config| {
config.permissions.sandbox_policy =
codex_config::Constrained::allow_any(SandboxPolicy::DangerFullAccess);
config.permissions.permission_profile =
codex_config::Constrained::allow_any(PermissionProfile::Disabled);
let cwd = config.cwd.clone();
config
.permissions
.set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess, cwd.as_path())
.expect("test setup should allow sandbox policy");
config.permissions.network = Some(network_spec);
let layers = config
@@ -971,11 +979,15 @@ async fn workspace_write_turns_continue_to_expose_managed_network_proxy() -> any
enabled: Some(true),
..Default::default()
}),
&sandbox_policy,
&permission_profile_for_sandbox_policy(&sandbox_policy),
)?;
let session = make_session_with_config(move |config| {
config.permissions.sandbox_policy = codex_config::Constrained::allow_any(sandbox_policy);
let cwd = config.cwd.clone();
config
.permissions
.set_legacy_sandbox_policy(sandbox_policy, cwd.as_path())
.expect("test setup should allow sandbox policy");
config.permissions.network = Some(network_spec);
})
.await?;
@@ -994,11 +1006,15 @@ async fn user_shell_commands_do_not_inherit_managed_network_proxy() -> anyhow::R
enabled: Some(true),
..Default::default()
}),
&sandbox_policy,
&permission_profile_for_sandbox_policy(&sandbox_policy),
)?;
let (session, rx) = make_session_with_config_and_rx(move |config| {
config.permissions.sandbox_policy = codex_config::Constrained::allow_any(sandbox_policy);
let cwd = config.cwd.clone();
config
.permissions
.set_legacy_sandbox_policy(sandbox_policy, cwd.as_path())
.expect("test setup should allow sandbox policy");
config.permissions.network = Some(network_spec);
})
.await?;
+2 -1
View File
@@ -692,7 +692,8 @@ async fn track_turn_resolved_config_analytics(
session_source: thread_config.session_source,
model: turn_context.model_info.slug.clone(),
model_provider: turn_context.config.model_provider_id.clone(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: turn_context.permission_profile(),
permission_profile_cwd: turn_context.cwd.to_path_buf(),
reasoning_effort: turn_context.reasoning_effort,
reasoning_summary: Some(turn_context.reasoning_summary),
service_tier: turn_context.config.service_tier,
+11 -9
View File
@@ -538,16 +538,18 @@ impl Session {
let turn_environments =
self.resolve_turn_environments(&effective_environments)?;
let previous_cwd = state.session_configuration.cwd.clone();
let previous_sandbox_policy = state.session_configuration.sandbox_policy();
let next_sandbox_policy = next.sandbox_policy();
let sandbox_policy_changed = previous_sandbox_policy != next_sandbox_policy;
let previous_permission_profile =
state.session_configuration.permission_profile();
let next_permission_profile = next.permission_profile();
let permission_profile_changed =
previous_permission_profile != next_permission_profile;
let codex_home = next.codex_home.clone();
let session_source = next.session_source.clone();
state.session_configuration = next.clone();
Ok((
next,
turn_environments,
sandbox_policy_changed,
permission_profile_changed,
previous_cwd,
codex_home,
session_source,
@@ -560,7 +562,7 @@ impl Session {
let (
session_configuration,
turn_environments,
sandbox_policy_changed,
permission_profile_changed,
previous_cwd,
codex_home,
session_source,
@@ -587,8 +589,8 @@ impl Session {
&session_source,
);
if sandbox_policy_changed {
self.refresh_managed_network_proxy_for_current_sandbox_policy()
if permission_profile_changed {
self.refresh_managed_network_proxy_for_current_permission_profile()
.await;
}
@@ -691,8 +693,8 @@ impl Session {
.network_proxy
.as_ref()
.and_then(|started_proxy| {
Self::managed_network_proxy_active_for_sandbox_policy(
&session_configuration.sandbox_policy(),
Self::managed_network_proxy_active_for_permission_profile(
&session_configuration.permission_profile(),
)
.then(|| started_proxy.proxy())
}),
+19 -1
View File
@@ -3,6 +3,10 @@ use std::time::Duration;
use codex_async_utils::CancelErr;
use codex_async_utils::OrCancelExt;
use codex_network_proxy::PROXY_ACTIVE_ENV_KEY;
use codex_network_proxy::PROXY_ENV_KEYS;
#[cfg(target_os = "macos")]
use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY;
use codex_protocol::user_input::UserInput;
use tokio_util::sync::CancellationToken;
use tracing::error;
@@ -123,10 +127,24 @@ pub(crate) async fn execute_user_shell_command(
let use_login_shell = true;
let session_shell = session.user_shell();
let display_command = session_shell.derive_exec_args(&command, use_login_shell);
let exec_env_map = create_env(
let mut exec_env_map = create_env(
&turn_context.shell_environment_policy,
Some(session.conversation_id),
);
if exec_env_map.contains_key(PROXY_ACTIVE_ENV_KEY) {
for key in PROXY_ENV_KEYS {
exec_env_map.remove(*key);
}
#[cfg(target_os = "macos")]
if exec_env_map
.get(PROXY_GIT_SSH_COMMAND_ENV_KEY)
.is_some_and(|value| {
value.starts_with(codex_network_proxy::CODEX_PROXY_GIT_SSH_COMMAND_MARKER)
})
{
exec_env_map.remove(PROXY_GIT_SSH_COMMAND_ENV_KEY);
}
}
let exec_command = maybe_wrap_shell_lc_with_snapshot(
&display_command,
session_shell.as_ref(),
+4 -8
View File
@@ -21,11 +21,11 @@ use codex_network_proxy::NetworkProxy;
use codex_protocol::approvals::NetworkApprovalContext;
use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::approvals::NetworkPolicyRuleAction;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::WarningEvent;
use indexmap::IndexMap;
use std::collections::HashMap;
@@ -127,11 +127,8 @@ fn allows_network_approval_flow(policy: AskForApproval) -> bool {
!matches!(policy, AskForApproval::Never)
}
fn sandbox_policy_allows_network_approval_flow(policy: &SandboxPolicy) -> bool {
matches!(
policy,
SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. }
)
fn permission_profile_allows_network_approval_flow(permission_profile: &PermissionProfile) -> bool {
matches!(permission_profile, PermissionProfile::Managed { .. })
}
impl PendingApprovalDecision {
@@ -359,8 +356,7 @@ impl NetworkApprovalService {
.await;
return NetworkDecision::deny(REASON_NOT_ALLOWED);
};
let sandbox_policy = turn_context.sandbox_policy();
if !sandbox_policy_allows_network_approval_flow(&sandbox_policy) {
if !permission_profile_allows_network_approval_flow(&turn_context.permission_profile()) {
pending.set_decision(PendingApprovalDecision::Deny).await;
self.pending_host_approvals.lock().await.remove(&key);
self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy(
@@ -1,6 +1,8 @@
use super::*;
use crate::sandboxing::SandboxPermissions;
use codex_network_proxy::BlockedRequestArgs;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use core_test_support::PathBufExt;
@@ -185,14 +187,19 @@ fn only_never_policy_disables_network_approval_flow() {
#[test]
fn network_approval_flow_is_limited_to_restricted_sandbox_modes() {
assert!(sandbox_policy_allows_network_approval_flow(
&SandboxPolicy::new_read_only_policy()
assert!(permission_profile_allows_network_approval_flow(
&PermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::new_read_only_policy())
));
assert!(sandbox_policy_allows_network_approval_flow(
&SandboxPolicy::new_workspace_write_policy()
assert!(permission_profile_allows_network_approval_flow(
&PermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::new_workspace_write_policy())
));
assert!(!sandbox_policy_allows_network_approval_flow(
&SandboxPolicy::DangerFullAccess
assert!(!permission_profile_allows_network_approval_flow(
&PermissionProfile::Disabled
));
assert!(!permission_profile_allows_network_approval_flow(
&PermissionProfile::External {
network: NetworkSandboxPolicy::Restricted,
}
));
}
+4 -1
View File
@@ -89,13 +89,16 @@ async fn spawn_command_under_sandbox(
env: HashMap<String, String>,
) -> std::io::Result<Child> {
use codex_core::spawn_command_under_linux_sandbox;
use codex_protocol::models::PermissionProfile;
let codex_linux_sandbox_exe = core_test_support::find_codex_linux_sandbox_exe()
.map_err(|err| io::Error::new(io::ErrorKind::NotFound, err))?;
let permission_profile = PermissionProfile::from_legacy_sandbox_policy(sandbox_policy);
spawn_command_under_linux_sandbox(
codex_linux_sandbox_exe,
command,
command_cwd,
sandbox_policy,
&permission_profile,
sandbox_cwd,
/*use_legacy_landlock*/ false,
stdio_policy,