mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
permissions: constrain requirements as profiles (#19736)
This commit is contained in:
committed by
GitHub
Unverified
parent
0ccd659b4b
commit
523e4aa8e3
@@ -1,8 +1,8 @@
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -84,7 +84,7 @@ impl<T> std::ops::DerefMut for ConstrainedWithSource<T> {
|
||||
pub struct ConfigRequirements {
|
||||
pub approval_policy: ConstrainedWithSource<AskForApproval>,
|
||||
pub approvals_reviewer: ConstrainedWithSource<ApprovalsReviewer>,
|
||||
pub sandbox_policy: ConstrainedWithSource<SandboxPolicy>,
|
||||
pub permission_profile: ConstrainedWithSource<PermissionProfile>,
|
||||
pub web_search_mode: ConstrainedWithSource<WebSearchMode>,
|
||||
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
|
||||
pub managed_hooks: Option<ConstrainedWithSource<ManagedHooksRequirementsToml>>,
|
||||
@@ -110,8 +110,8 @@ impl Default for ConfigRequirements {
|
||||
Constrained::allow_any_from_default(),
|
||||
/*source*/ None,
|
||||
),
|
||||
sandbox_policy: ConstrainedWithSource::new(
|
||||
Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
permission_profile: ConstrainedWithSource::new(
|
||||
Constrained::allow_any(PermissionProfile::read_only()),
|
||||
/*source*/ None,
|
||||
),
|
||||
web_search_mode: ConstrainedWithSource::new(
|
||||
@@ -967,15 +967,8 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
),
|
||||
};
|
||||
|
||||
// TODO(gt): `ConfigRequirementsToml` should let the author specify the
|
||||
// default `SandboxPolicy`? Should do this for `AskForApproval` too?
|
||||
//
|
||||
// Currently, we force ReadOnly as the default policy because two of
|
||||
// the other variants (WorkspaceWrite, ExternalSandbox) require
|
||||
// additional parameters. Ultimately, we should expand the config
|
||||
// format to allow specifying those parameters.
|
||||
let default_sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let sandbox_policy = match allowed_sandbox_modes {
|
||||
let default_permission_profile = PermissionProfile::read_only();
|
||||
let permission_profile = match allowed_sandbox_modes {
|
||||
Some(Sourced {
|
||||
value: modes,
|
||||
source: requirement_source,
|
||||
@@ -984,23 +977,15 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
return Err(ConstraintError::InvalidValue {
|
||||
field_name: "allowed_sandbox_modes",
|
||||
candidate: format!("{modes:?}"),
|
||||
allowed: "must include 'read-only' to allow any SandboxPolicy".to_string(),
|
||||
allowed: "must include 'read-only' to allow any PermissionProfile"
|
||||
.to_string(),
|
||||
requirement_source,
|
||||
});
|
||||
};
|
||||
|
||||
let requirement_source_for_error = requirement_source.clone();
|
||||
let constrained = Constrained::new(default_sandbox_policy, move |candidate| {
|
||||
let mode = match candidate {
|
||||
SandboxPolicy::ReadOnly { .. } => SandboxModeRequirement::ReadOnly,
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
SandboxModeRequirement::WorkspaceWrite
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess => SandboxModeRequirement::DangerFullAccess,
|
||||
SandboxPolicy::ExternalSandbox { .. } => {
|
||||
SandboxModeRequirement::ExternalSandbox
|
||||
}
|
||||
};
|
||||
let constrained = Constrained::new(default_permission_profile, move |candidate| {
|
||||
let mode = sandbox_mode_requirement_for_permission_profile(candidate);
|
||||
if modes.contains(&mode) {
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -1014,12 +999,10 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
})?;
|
||||
ConstrainedWithSource::new(constrained, Some(requirement_source))
|
||||
}
|
||||
None => {
|
||||
ConstrainedWithSource::new(
|
||||
Constrained::allow_any(default_sandbox_policy),
|
||||
/*source*/ None,
|
||||
)
|
||||
}
|
||||
None => ConstrainedWithSource::new(
|
||||
Constrained::allow_any(default_permission_profile),
|
||||
/*source*/ None,
|
||||
),
|
||||
};
|
||||
let exec_policy = match rules {
|
||||
Some(Sourced { value, source }) => {
|
||||
@@ -1145,7 +1128,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
Ok(ConfigRequirements {
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox_policy,
|
||||
permission_profile,
|
||||
web_search_mode,
|
||||
feature_requirements,
|
||||
managed_hooks,
|
||||
@@ -1159,6 +1142,29 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sandbox_mode_requirement_for_permission_profile(
|
||||
permission_profile: &PermissionProfile,
|
||||
) -> SandboxModeRequirement {
|
||||
match permission_profile {
|
||||
PermissionProfile::Disabled => SandboxModeRequirement::DangerFullAccess,
|
||||
PermissionProfile::External { .. } => SandboxModeRequirement::ExternalSandbox,
|
||||
PermissionProfile::Managed { .. } => {
|
||||
let file_system_policy = permission_profile.file_system_sandbox_policy();
|
||||
if file_system_policy.has_full_disk_write_access() {
|
||||
SandboxModeRequirement::DangerFullAccess
|
||||
} else if file_system_policy
|
||||
.entries
|
||||
.iter()
|
||||
.any(|entry| entry.access.can_write())
|
||||
{
|
||||
SandboxModeRequirement::WorkspaceWrite
|
||||
} else {
|
||||
SandboxModeRequirement::ReadOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1168,6 +1174,7 @@ mod tests {
|
||||
use codex_execpolicy::Evaluation;
|
||||
use codex_execpolicy::RuleMatch;
|
||||
use codex_protocol::protocol::NetworkAccess;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -1183,6 +1190,10 @@ mod tests {
|
||||
)?)
|
||||
}
|
||||
|
||||
fn profile_from_sandbox_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile {
|
||||
PermissionProfile::from_legacy_sandbox_policy(sandbox_policy)
|
||||
}
|
||||
|
||||
fn with_unknown_source(toml: ConfigRequirementsToml) -> ConfigRequirementsWithSources {
|
||||
let ConfigRequirementsToml {
|
||||
allowed_approval_policies,
|
||||
@@ -1724,8 +1735,10 @@ allowed_approvals_reviewers = ["user"]
|
||||
);
|
||||
assert_eq!(
|
||||
requirements
|
||||
.sandbox_policy
|
||||
.can_set(&SandboxPolicy::DangerFullAccess),
|
||||
.permission_profile
|
||||
.can_set(&profile_from_sandbox_policy(
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
)),
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: "DangerFullAccess".into(),
|
||||
@@ -1803,7 +1816,7 @@ allowed_approvals_reviewers = ["user"]
|
||||
Some(source_location.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
requirements.sandbox_policy.source,
|
||||
requirements.permission_profile.source,
|
||||
Some(source_location.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -1869,8 +1882,10 @@ allowed_approvals_reviewers = ["user"]
|
||||
);
|
||||
assert!(
|
||||
requirements
|
||||
.sandbox_policy
|
||||
.can_set(&SandboxPolicy::new_read_only_policy())
|
||||
.permission_profile
|
||||
.can_set(&profile_from_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy()
|
||||
))
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
@@ -1952,25 +1967,30 @@ allowed_approvals_reviewers = ["user"]
|
||||
let root = if cfg!(windows) { "C:\\repo" } else { "/repo" };
|
||||
assert!(
|
||||
requirements
|
||||
.sandbox_policy
|
||||
.can_set(&SandboxPolicy::new_read_only_policy())
|
||||
.permission_profile
|
||||
.can_set(&profile_from_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy()
|
||||
))
|
||||
.is_ok()
|
||||
);
|
||||
let workspace_write_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
};
|
||||
assert!(
|
||||
requirements
|
||||
.sandbox_policy
|
||||
.can_set(&SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
})
|
||||
.permission_profile
|
||||
.can_set(&profile_from_sandbox_policy(&workspace_write_policy))
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(
|
||||
requirements
|
||||
.sandbox_policy
|
||||
.can_set(&SandboxPolicy::DangerFullAccess),
|
||||
.permission_profile
|
||||
.can_set(&profile_from_sandbox_policy(
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
)),
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: "DangerFullAccess".into(),
|
||||
@@ -1980,10 +2000,12 @@ allowed_approvals_reviewers = ["user"]
|
||||
);
|
||||
assert_eq!(
|
||||
requirements
|
||||
.sandbox_policy
|
||||
.can_set(&SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
}),
|
||||
.permission_profile
|
||||
.can_set(&profile_from_sandbox_policy(
|
||||
&SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
}
|
||||
)),
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: "ExternalSandbox".into(),
|
||||
@@ -2064,21 +2086,24 @@ allowed_approvals_reviewers = ["user"]
|
||||
|
||||
let requirements = ConfigRequirements::try_from(requirements_with_sources)?;
|
||||
let root = if cfg!(windows) { "C:\\repo" } else { "/repo" };
|
||||
let workspace_write_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
};
|
||||
assert!(
|
||||
requirements
|
||||
.sandbox_policy
|
||||
.can_set(&SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
})
|
||||
.permission_profile
|
||||
.can_set(&profile_from_sandbox_policy(&workspace_write_policy))
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(
|
||||
requirements
|
||||
.sandbox_policy
|
||||
.can_set(&SandboxPolicy::DangerFullAccess),
|
||||
.permission_profile
|
||||
.can_set(&profile_from_sandbox_policy(
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
)),
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: "DangerFullAccess".into(),
|
||||
@@ -2108,8 +2133,10 @@ allowed_approvals_reviewers = ["user"]
|
||||
|
||||
assert_eq!(
|
||||
requirements
|
||||
.sandbox_policy
|
||||
.can_set(&SandboxPolicy::DangerFullAccess),
|
||||
.permission_profile
|
||||
.can_set(&profile_from_sandbox_policy(
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
)),
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: "DangerFullAccess".into(),
|
||||
@@ -2147,8 +2174,10 @@ allowed_approvals_reviewers = ["user"]
|
||||
|
||||
assert_eq!(
|
||||
requirements
|
||||
.sandbox_policy
|
||||
.can_set(&SandboxPolicy::new_workspace_write_policy()),
|
||||
.permission_profile
|
||||
.can_set(&profile_from_sandbox_policy(
|
||||
&SandboxPolicy::new_workspace_write_policy(),
|
||||
)),
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: "WorkspaceWrite".into(),
|
||||
|
||||
@@ -47,6 +47,7 @@ use codex_protocol::config_types::Verbosity;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::config_types::WebSearchToolConfig;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
@@ -647,7 +648,7 @@ impl ConfigToml {
|
||||
profile_sandbox_mode: Option<SandboxMode>,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
active_project: Option<&ProjectConfig>,
|
||||
sandbox_policy_constraint: Option<&crate::Constrained<SandboxPolicy>>,
|
||||
permission_profile_constraint: Option<&crate::Constrained<PermissionProfile>>,
|
||||
) -> SandboxPolicy {
|
||||
let sandbox_mode_was_explicit = sandbox_mode_override.is_some()
|
||||
|| profile_sandbox_mode.is_some()
|
||||
@@ -707,14 +708,16 @@ impl ConfigToml {
|
||||
downgrade_workspace_write_if_unsupported(&mut sandbox_policy);
|
||||
}
|
||||
if !sandbox_mode_was_explicit
|
||||
&& let Some(constraint) = sandbox_policy_constraint
|
||||
&& let Err(err) = constraint.can_set(&sandbox_policy)
|
||||
&& let Some(constraint) = permission_profile_constraint
|
||||
&& let Err(err) = constraint.can_set(&PermissionProfile::from_legacy_sandbox_policy(
|
||||
&sandbox_policy,
|
||||
))
|
||||
{
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"default sandbox policy is disallowed by requirements; falling back to required default"
|
||||
);
|
||||
sandbox_policy = constraint.get().clone();
|
||||
sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
downgrade_workspace_write_if_unsupported(&mut sandbox_policy);
|
||||
}
|
||||
sandbox_policy
|
||||
|
||||
@@ -53,6 +53,7 @@ pub use config_requirements::ResidencyRequirement;
|
||||
pub use config_requirements::SandboxModeRequirement;
|
||||
pub use config_requirements::Sourced;
|
||||
pub use config_requirements::WebSearchModeRequirement;
|
||||
pub use config_requirements::sandbox_mode_requirement_for_permission_profile;
|
||||
pub use constraint::Constrained;
|
||||
pub use constraint::ConstraintError;
|
||||
pub use constraint::ConstraintResult;
|
||||
|
||||
@@ -27,8 +27,8 @@ use codex_config::version_for_toml;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -577,8 +577,8 @@ allowed_sandbox_modes = ["read-only"]
|
||||
AskForApproval::Never
|
||||
);
|
||||
assert_eq!(
|
||||
*state.requirements().sandbox_policy.get(),
|
||||
SandboxPolicy::new_read_only_policy()
|
||||
state.requirements().permission_profile.get(),
|
||||
&PermissionProfile::read_only()
|
||||
);
|
||||
assert!(
|
||||
state
|
||||
@@ -590,13 +590,15 @@ allowed_sandbox_modes = ["read-only"]
|
||||
assert!(
|
||||
state
|
||||
.requirements()
|
||||
.sandbox_policy
|
||||
.can_set(&SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
})
|
||||
.permission_profile
|
||||
.can_set(&PermissionProfile::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
},
|
||||
))
|
||||
.is_err()
|
||||
);
|
||||
|
||||
@@ -867,6 +869,55 @@ allowed_approval_policies = ["on-request"]
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn system_remote_sandbox_config_keeps_cloud_sandbox_modes() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let requirements_file = tmp.path().join("requirements.toml");
|
||||
tokio::fs::write(
|
||||
&requirements_file,
|
||||
r#"
|
||||
[[remote_sandbox_config]]
|
||||
hostname_patterns = ["*"]
|
||||
allowed_sandbox_modes = ["read-only", "workspace-write"]
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cloud_source = RequirementSource::CloudRequirements;
|
||||
let mut config_requirements_toml = ConfigRequirementsWithSources::default();
|
||||
config_requirements_toml.merge_unset_fields(
|
||||
cloud_source.clone(),
|
||||
toml::from_str(
|
||||
r#"
|
||||
allowed_sandbox_modes = ["read-only"]
|
||||
"#,
|
||||
)?,
|
||||
);
|
||||
load_requirements_toml(
|
||||
LOCAL_FS.as_ref(),
|
||||
&mut config_requirements_toml,
|
||||
&AbsolutePathBuf::try_from(requirements_file)?,
|
||||
)
|
||||
.await?;
|
||||
let config_requirements: ConfigRequirements = config_requirements_toml.try_into()?;
|
||||
|
||||
assert_eq!(
|
||||
config_requirements.permission_profile.can_set(
|
||||
&PermissionProfile::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::new_workspace_write_policy()
|
||||
)
|
||||
),
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: "WorkspaceWrite".into(),
|
||||
allowed: "[ReadOnly]".into(),
|
||||
requirement_source: cloud_source,
|
||||
})
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn load_requirements_toml_resolves_deny_read_against_parent() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
@@ -1088,6 +1139,54 @@ async fn load_config_layers_includes_cloud_hook_requirements() -> anyhow::Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_layers_applies_matching_remote_sandbox_config() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let codex_home = tmp.path().join("home");
|
||||
tokio::fs::create_dir_all(&codex_home).await?;
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?;
|
||||
|
||||
let requirements: ConfigRequirementsToml = toml::from_str(
|
||||
r#"
|
||||
allowed_sandbox_modes = ["read-only"]
|
||||
|
||||
[[remote_sandbox_config]]
|
||||
hostname_patterns = ["*"]
|
||||
allowed_sandbox_modes = ["read-only", "workspace-write"]
|
||||
"#,
|
||||
)?;
|
||||
let cloud_requirements = CloudRequirementsLoader::new(async move { Ok(Some(requirements)) });
|
||||
let layers = load_config_layers_state(
|
||||
LOCAL_FS.as_ref(),
|
||||
&codex_home,
|
||||
Some(cwd),
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
cloud_requirements,
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
layers.requirements_toml().allowed_sandbox_modes,
|
||||
Some(vec![
|
||||
codex_config::SandboxModeRequirement::ReadOnly,
|
||||
codex_config::SandboxModeRequirement::WorkspaceWrite,
|
||||
])
|
||||
);
|
||||
assert!(
|
||||
layers
|
||||
.requirements()
|
||||
.permission_profile
|
||||
.can_set(&PermissionProfile::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::new_workspace_write_policy()
|
||||
))
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_layers_fails_when_cloud_requirements_loader_fails() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
|
||||
@@ -1636,7 +1636,7 @@ network_access = false # This should be ignored.
|
||||
/*profile_sandbox_mode*/ None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
/*active_project*/ None,
|
||||
/*sandbox_policy_constraint*/ None,
|
||||
/*permission_profile_constraint*/ None,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(resolution, SandboxPolicy::DangerFullAccess);
|
||||
@@ -1657,7 +1657,7 @@ network_access = true # This should be ignored.
|
||||
/*profile_sandbox_mode*/ None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
/*active_project*/ None,
|
||||
/*sandbox_policy_constraint*/ None,
|
||||
/*permission_profile_constraint*/ None,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(resolution, SandboxPolicy::new_read_only_policy());
|
||||
@@ -1689,7 +1689,7 @@ trust_level = "trusted"
|
||||
/*profile_sandbox_mode*/ None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
/*active_project*/ None,
|
||||
/*sandbox_policy_constraint*/ None,
|
||||
/*permission_profile_constraint*/ None,
|
||||
)
|
||||
.await;
|
||||
if cfg!(target_os = "windows") {
|
||||
@@ -1729,7 +1729,7 @@ exclude_slash_tmp = true
|
||||
/*profile_sandbox_mode*/ None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
/*active_project*/ None,
|
||||
/*sandbox_policy_constraint*/ None,
|
||||
/*permission_profile_constraint*/ None,
|
||||
)
|
||||
.await;
|
||||
if cfg!(target_os = "windows") {
|
||||
@@ -6316,7 +6316,7 @@ trust_level = "untrusted"
|
||||
/*profile_sandbox_mode*/ None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
Some(&active_project),
|
||||
/*sandbox_policy_constraint*/ None,
|
||||
/*permission_profile_constraint*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -6337,8 +6337,8 @@ trust_level = "untrusted"
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defaults()
|
||||
-> anyhow::Result<()> {
|
||||
async fn derive_sandbox_policy_falls_back_to_read_only_for_implicit_defaults() -> anyhow::Result<()>
|
||||
{
|
||||
let project_dir = TempDir::new()?;
|
||||
let project_path = project_dir.path().to_path_buf();
|
||||
let project_key = project_path.to_string_lossy().to_string();
|
||||
@@ -6354,14 +6354,14 @@ async fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defau
|
||||
let active_project = ProjectConfig {
|
||||
trust_level: Some(TrustLevel::Trusted),
|
||||
};
|
||||
let constrained = Constrained::new(SandboxPolicy::DangerFullAccess, |candidate| {
|
||||
if matches!(candidate, SandboxPolicy::DangerFullAccess) {
|
||||
let constrained = Constrained::new(PermissionProfile::read_only(), |candidate| {
|
||||
if candidate == &PermissionProfile::read_only() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: format!("{candidate:?}"),
|
||||
allowed: "[DangerFullAccess]".to_string(),
|
||||
allowed: "[ReadOnly]".to_string(),
|
||||
requirement_source: RequirementSource::Unknown,
|
||||
})
|
||||
}
|
||||
@@ -6377,7 +6377,7 @@ async fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defau
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(resolution, SandboxPolicy::DangerFullAccess);
|
||||
assert_eq!(resolution, SandboxPolicy::new_read_only_policy());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6399,18 +6399,29 @@ async fn derive_sandbox_policy_preserves_windows_downgrade_for_unsupported_fallb
|
||||
let active_project = ProjectConfig {
|
||||
trust_level: Some(TrustLevel::Trusted),
|
||||
};
|
||||
let constrained = Constrained::new(SandboxPolicy::new_workspace_write_policy(), |candidate| {
|
||||
if matches!(candidate, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: format!("{candidate:?}"),
|
||||
allowed: "[WorkspaceWrite]".to_string(),
|
||||
requirement_source: RequirementSource::Unknown,
|
||||
})
|
||||
}
|
||||
})?;
|
||||
let constrained = Constrained::new(
|
||||
PermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
|
||||
|candidate| {
|
||||
if matches!(
|
||||
candidate,
|
||||
PermissionProfile::Managed {
|
||||
file_system: ManagedFileSystemPermissions::Restricted { entries, .. },
|
||||
..
|
||||
} if entries
|
||||
.iter()
|
||||
.any(|entry| entry.access.can_write())
|
||||
) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: format!("{candidate:?}"),
|
||||
allowed: "[WorkspaceWrite]".to_string(),
|
||||
requirement_source: RequirementSource::Unknown,
|
||||
})
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
let resolution = cfg
|
||||
.derive_sandbox_policy(
|
||||
|
||||
@@ -19,6 +19,7 @@ use codex_config::LoaderOverrides;
|
||||
use codex_config::McpServerIdentity;
|
||||
use codex_config::McpServerRequirement;
|
||||
use codex_config::ResidencyRequirement;
|
||||
use codex_config::SandboxModeRequirement;
|
||||
use codex_config::Sourced;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
@@ -30,6 +31,7 @@ use codex_config::config_toml::validate_model_providers;
|
||||
use codex_config::loader::load_config_layers_state;
|
||||
use codex_config::loader::project_trust_key;
|
||||
use codex_config::profile_toml::ConfigProfile;
|
||||
use codex_config::sandbox_mode_requirement_for_permission_profile;
|
||||
use codex_config::types::ApprovalsReviewer;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_config::types::DEFAULT_OTEL_ENVIRONMENT;
|
||||
@@ -295,25 +297,6 @@ impl Permissions {
|
||||
}
|
||||
}
|
||||
|
||||
fn constrained_permission_profile_from_sandbox_projection(
|
||||
initial_value: PermissionProfile,
|
||||
sandbox_constraint: Constrained<SandboxPolicy>,
|
||||
cwd: AbsolutePathBuf,
|
||||
) -> std::io::Result<Constrained<PermissionProfile>> {
|
||||
Constrained::new(initial_value, move |candidate| {
|
||||
let (file_system_sandbox_policy, network_sandbox_policy) =
|
||||
candidate.to_runtime_permissions();
|
||||
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
candidate,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
cwd.as_path(),
|
||||
);
|
||||
sandbox_constraint.can_set(&sandbox_policy)
|
||||
})
|
||||
.map_err(std::io::Error::from)
|
||||
}
|
||||
|
||||
/// Configured thread persistence backend.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub enum ThreadStoreConfig {
|
||||
@@ -1709,7 +1692,7 @@ impl Config {
|
||||
let ConfigRequirements {
|
||||
approval_policy: mut constrained_approval_policy,
|
||||
approvals_reviewer: mut constrained_approvals_reviewer,
|
||||
sandbox_policy: mut constrained_sandbox_policy,
|
||||
permission_profile: mut constrained_permission_profile,
|
||||
web_search_mode: mut constrained_web_search_mode,
|
||||
feature_requirements,
|
||||
managed_hooks: _,
|
||||
@@ -1881,9 +1864,7 @@ impl Config {
|
||||
let (
|
||||
configured_network_proxy_config,
|
||||
permission_profile,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
) = if let Some(mut permission_profile) = permission_profile {
|
||||
let (mut file_system_sandbox_policy, network_sandbox_policy) =
|
||||
permission_profile.to_runtime_permissions();
|
||||
@@ -1910,7 +1891,7 @@ impl Config {
|
||||
} else {
|
||||
NetworkProxyConfig::default()
|
||||
};
|
||||
let mut sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
@@ -1927,19 +1908,11 @@ impl Config {
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
resolved_cwd.as_path(),
|
||||
);
|
||||
}
|
||||
(
|
||||
configured_network_proxy_config,
|
||||
permission_profile,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
)
|
||||
} else if profiles_are_active {
|
||||
let permissions = cfg.permissions.as_ref().ok_or_else(|| {
|
||||
@@ -1968,7 +1941,7 @@ impl Config {
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
let mut sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
@@ -1984,19 +1957,11 @@ impl Config {
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
resolved_cwd.as_path(),
|
||||
);
|
||||
}
|
||||
(
|
||||
configured_network_proxy_config,
|
||||
permission_profile,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
)
|
||||
} else {
|
||||
let configured_network_proxy_config = NetworkProxyConfig::default();
|
||||
@@ -2006,7 +1971,7 @@ impl Config {
|
||||
config_profile.sandbox_mode,
|
||||
windows_sandbox_level,
|
||||
Some(&active_project),
|
||||
Some(&constrained_sandbox_policy),
|
||||
Some(&constrained_permission_profile),
|
||||
)
|
||||
.await;
|
||||
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy {
|
||||
@@ -2030,9 +1995,7 @@ impl Config {
|
||||
(
|
||||
configured_network_proxy_config,
|
||||
permission_profile,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
)
|
||||
};
|
||||
let approval_policy_was_explicit = approval_policy_override.is_some()
|
||||
@@ -2324,8 +2287,7 @@ impl Config {
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.or_else(|| resolve_sqlite_home_env(&resolved_cwd))
|
||||
.unwrap_or_else(|| codex_home.to_path_buf());
|
||||
let original_sandbox_policy = sandbox_policy.clone();
|
||||
|
||||
let original_permission_profile = permission_profile.clone();
|
||||
apply_requirement_constrained_value(
|
||||
"approval_policy",
|
||||
approval_policy,
|
||||
@@ -2339,17 +2301,22 @@ impl Config {
|
||||
&& !filesystem_requirements.deny_read.is_empty()
|
||||
{
|
||||
let requirement_source = filesystem_requirements_source.clone();
|
||||
constrained_sandbox_policy
|
||||
constrained_permission_profile
|
||||
.value
|
||||
.add_validator(move |policy| match policy {
|
||||
SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } => Ok(()),
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: policy.to_string(),
|
||||
allowed: "[read-only, workspace-write]".to_string(),
|
||||
requirement_source: requirement_source.clone(),
|
||||
})
|
||||
.add_validator(move |permission_profile| {
|
||||
let mode = sandbox_mode_requirement_for_permission_profile(permission_profile);
|
||||
match mode {
|
||||
SandboxModeRequirement::ReadOnly
|
||||
| SandboxModeRequirement::WorkspaceWrite => Ok(()),
|
||||
SandboxModeRequirement::DangerFullAccess
|
||||
| SandboxModeRequirement::ExternalSandbox => {
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: format!("{mode:?}"),
|
||||
allowed: "[read-only, workspace-write]".to_string(),
|
||||
requirement_source: requirement_source.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.map_err(std::io::Error::from)?;
|
||||
@@ -2367,9 +2334,9 @@ impl Config {
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
apply_requirement_constrained_value(
|
||||
"sandbox_mode",
|
||||
sandbox_policy,
|
||||
&mut constrained_sandbox_policy,
|
||||
"permission_profile",
|
||||
permission_profile,
|
||||
&mut constrained_permission_profile,
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
apply_requirement_constrained_value(
|
||||
@@ -2387,13 +2354,7 @@ 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_permission_profile = constrained_permission_profile.get().clone();
|
||||
let network = NetworkProxySpec::from_config_and_constraints(
|
||||
configured_network_proxy_config,
|
||||
network_requirements,
|
||||
@@ -2419,17 +2380,13 @@ impl Config {
|
||||
zsh_path.as_ref(),
|
||||
main_execve_wrapper_exe.as_ref(),
|
||||
);
|
||||
let effective_sandbox_policy = constrained_sandbox_policy.value.get().clone();
|
||||
let mut effective_file_system_sandbox_policy =
|
||||
if effective_sandbox_policy == original_sandbox_policy {
|
||||
file_system_sandbox_policy
|
||||
} else {
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
|
||||
&effective_sandbox_policy,
|
||||
resolved_cwd.as_path(),
|
||||
&file_system_sandbox_policy,
|
||||
)
|
||||
};
|
||||
let effective_permission_profile = constrained_permission_profile.value.get().clone();
|
||||
let (mut effective_file_system_sandbox_policy, effective_network_sandbox_policy) =
|
||||
effective_permission_profile.to_runtime_permissions();
|
||||
if effective_permission_profile != original_permission_profile {
|
||||
effective_file_system_sandbox_policy
|
||||
.preserve_deny_read_restrictions_from(&file_system_sandbox_policy);
|
||||
}
|
||||
if let Some(Sourced {
|
||||
value: filesystem_requirements,
|
||||
..
|
||||
@@ -2442,28 +2399,15 @@ impl Config {
|
||||
}
|
||||
let effective_file_system_sandbox_policy = effective_file_system_sandbox_policy
|
||||
.with_additional_readable_roots(resolved_cwd.as_path(), &helper_readable_roots);
|
||||
let effective_network_sandbox_policy =
|
||||
if effective_sandbox_policy == original_sandbox_policy {
|
||||
network_sandbox_policy
|
||||
} else {
|
||||
NetworkSandboxPolicy::from(&effective_sandbox_policy)
|
||||
};
|
||||
let effective_enforcement = if effective_sandbox_policy == original_sandbox_policy {
|
||||
permission_profile.enforcement()
|
||||
} else {
|
||||
SandboxEnforcement::from_legacy_sandbox_policy(&effective_sandbox_policy)
|
||||
};
|
||||
let effective_permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
effective_enforcement,
|
||||
effective_permission_profile.enforcement(),
|
||||
&effective_file_system_sandbox_policy,
|
||||
effective_network_sandbox_policy,
|
||||
);
|
||||
let constrained_permission_profile =
|
||||
constrained_permission_profile_from_sandbox_projection(
|
||||
effective_permission_profile,
|
||||
constrained_sandbox_policy.value.clone(),
|
||||
resolved_cwd.clone(),
|
||||
)?;
|
||||
constrained_permission_profile
|
||||
.value
|
||||
.set(effective_permission_profile)
|
||||
.map_err(std::io::Error::from)?;
|
||||
let config = Self {
|
||||
model,
|
||||
service_tier,
|
||||
@@ -2476,7 +2420,7 @@ impl Config {
|
||||
startup_warnings,
|
||||
permissions: Permissions {
|
||||
approval_policy: constrained_approval_policy.value,
|
||||
permission_profile: constrained_permission_profile,
|
||||
permission_profile: constrained_permission_profile.value,
|
||||
network,
|
||||
allow_login_shell,
|
||||
shell_environment_policy,
|
||||
|
||||
@@ -126,7 +126,7 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec<Line<'static>> {
|
||||
requirement_lines.push(requirement_line(
|
||||
"allowed_sandbox_modes",
|
||||
value,
|
||||
requirements.sandbox_policy.source.as_ref(),
|
||||
requirements.permission_profile.source.as_ref(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -531,8 +531,8 @@ mod tests {
|
||||
use codex_config::WebSearchModeRequirement;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use ratatui::text::Line;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -622,8 +622,8 @@ mod tests {
|
||||
Constrained::allow_any(ApprovalsReviewer::AutoReview),
|
||||
Some(RequirementSource::LegacyManagedConfigTomlFromMdm),
|
||||
),
|
||||
sandbox_policy: ConstrainedWithSource::new(
|
||||
Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
permission_profile: ConstrainedWithSource::new(
|
||||
Constrained::allow_any(PermissionProfile::read_only()),
|
||||
Some(RequirementSource::SystemRequirementsToml {
|
||||
file: requirements_file.clone(),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user