mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
Use native permission profiles for diagnostics and sandbox checks (#61369)
## What changed - Derive sampling diagnostic and rollout trace labels directly from permission profiles instead of constructing legacy sandbox policies. Rename the trace metadata field from `sandbox_policy` to `permissions_profile`. - Extract `with_workspace_write_cwd` to share default workspace metadata protections between legacy policy conversion and the TUI's sandbox-mode compatibility check. Build the check's workspace-write profile from filesystem and network permissions directly. ## Testing Add a regression test confirming that the sandbox-mode compatibility check accepts standard workspace metadata protections and rejects a profile with an additional writable `.git` root. GitOrigin-RevId: f20d23afd80dff6347865df3d6a94d6f5e90e1e0
This commit is contained in:
@@ -265,17 +265,6 @@ impl SessionConfiguration {
|
||||
.set_legacy_permission_profile(permission_profile)
|
||||
}
|
||||
|
||||
pub(super) fn sandbox_policy(
|
||||
&self,
|
||||
environments: &[TurnEnvironmentSelection],
|
||||
) -> SandboxPolicy {
|
||||
let permission_profile = self.materialized_permission_profile(environments);
|
||||
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
self.cwd(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn file_system_sandbox_policy(
|
||||
&self,
|
||||
environments: &[TurnEnvironmentSelection],
|
||||
@@ -1334,10 +1323,10 @@ impl Session {
|
||||
.approval_policy
|
||||
.value()
|
||||
.to_string(),
|
||||
sandbox_policy: format!(
|
||||
"{:?}",
|
||||
session_configuration.sandbox_policy(environment_selections)
|
||||
),
|
||||
permissions_profile: session_configuration
|
||||
.materialized_permission_profile(environment_selections)
|
||||
.legacy_sandbox_policy_label(session_configuration.cwd())
|
||||
.to_string(),
|
||||
};
|
||||
let mut post_session_configured_events = Vec::<Event>::new();
|
||||
|
||||
|
||||
@@ -5636,7 +5636,7 @@ async fn session_configuration_apply_permission_profile_accepts_direct_write_roo
|
||||
let file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: external_write_path.clone().into(),
|
||||
path: external_write_path.into(),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
missing_path_behavior: None,
|
||||
@@ -5661,15 +5661,6 @@ async fn session_configuration_apply_permission_profile_accepts_direct_write_roo
|
||||
updated.file_system_sandbox_policy(&[]),
|
||||
file_system_sandbox_policy
|
||||
);
|
||||
assert_eq!(
|
||||
updated.sandbox_policy(&[]),
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![external_write_path],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test_case::test_case(false; "ordinary_proxy")]
|
||||
@@ -8877,7 +8868,7 @@ async fn submission_loop_channel_close_runs_full_thread_teardown() -> anyhow::Re
|
||||
model: turn_context.model_info().slug.clone(),
|
||||
provider_name: config.model_provider_id.clone(),
|
||||
approval_policy: "never".to_string(),
|
||||
sandbox_policy: "danger-full-access".to_string(),
|
||||
permissions_profile: "danger-full-access".to_string(),
|
||||
},
|
||||
)?;
|
||||
let live_thread = LiveThread::create(
|
||||
|
||||
@@ -2909,10 +2909,16 @@ async fn try_run_sampling_request(
|
||||
draining_for_mailbox: &mut bool,
|
||||
) -> CodexResult<SamplingRequestResult> {
|
||||
let turn_context = Arc::clone(&step_context.turn);
|
||||
#[expect(deprecated, reason = "Classify feedback using the initial turn cwd.")]
|
||||
let diagnostic_cwd = &turn_context.cwd;
|
||||
feedback_tags!(
|
||||
model = step_context.settings.model_info.slug.clone(),
|
||||
approval_policy = turn_context.approval_policy(),
|
||||
sandbox_policy = &turn_context.sandbox_policy(),
|
||||
sandbox_policy = tracing::field::display(
|
||||
turn_context
|
||||
.permission_profile()
|
||||
.legacy_sandbox_policy_label(diagnostic_cwd)
|
||||
),
|
||||
effort = step_context.settings.reasoning_effort(),
|
||||
auth_mode = sess.services.auth_manager.auth_mode(),
|
||||
tags_json = tracing::field::display(serde_json::json!(crate::feedback_config::usage_tags(
|
||||
|
||||
@@ -796,14 +796,6 @@ impl TurnContext {
|
||||
self.permission_profile().network_sandbox_policy()
|
||||
}
|
||||
|
||||
pub(crate) fn sandbox_policy(&self) -> SandboxPolicy {
|
||||
#[allow(deprecated)]
|
||||
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
|
||||
&self.permission_profile(),
|
||||
&self.cwd,
|
||||
)
|
||||
}
|
||||
|
||||
/// Legacy: returns the frozen initial-turn reasoning effort, including the initial model default.
|
||||
/// Step-scoped consumers should use their captured `StepContext::settings`.
|
||||
pub(crate) fn effective_reasoning_effort(&self) -> Option<ReasoningEffortConfig> {
|
||||
|
||||
@@ -421,7 +421,7 @@ fn attach_test_trace(session: &mut Session, turn: &TurnContext, root: &Path) ->
|
||||
model: "gpt-test".to_string(),
|
||||
provider_name: "test-provider".to_string(),
|
||||
approval_policy: "never".to_string(),
|
||||
sandbox_policy: "danger-full-access".to_string(),
|
||||
permissions_profile: "danger-full-access".to_string(),
|
||||
},
|
||||
)?;
|
||||
rollout_thread_trace.record_codex_turn_started(turn.sub_id.as_str());
|
||||
|
||||
@@ -892,16 +892,7 @@ impl FileSystemSandboxPolicy {
|
||||
pub fn from_legacy_sandbox_policy_for_cwd(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self {
|
||||
let mut file_system_policy = Self::from(sandbox_policy);
|
||||
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = sandbox_policy {
|
||||
if let Ok(cwd_root) = AbsolutePathBuf::from_absolute_path(cwd) {
|
||||
for protected_path in default_read_only_subpaths_for_writable_root(
|
||||
&cwd_root, /*protect_missing_dot_codex*/ true,
|
||||
) {
|
||||
append_default_read_only_path_if_no_explicit_rule(
|
||||
&mut file_system_policy.entries,
|
||||
protected_path,
|
||||
);
|
||||
}
|
||||
}
|
||||
file_system_policy = file_system_policy.with_workspace_write_cwd(cwd);
|
||||
for writable_root in writable_roots {
|
||||
for protected_path in default_read_only_subpaths_for_writable_root(
|
||||
writable_root,
|
||||
@@ -918,6 +909,21 @@ impl FileSystemSandboxPolicy {
|
||||
file_system_policy
|
||||
}
|
||||
|
||||
/// Binds the default workspace metadata protections for a native working directory.
|
||||
pub fn with_workspace_write_cwd(mut self, cwd: &Path) -> Self {
|
||||
if let Ok(cwd_root) = AbsolutePathBuf::from_absolute_path(cwd) {
|
||||
for protected_path in default_read_only_subpaths_for_writable_root(
|
||||
&cwd_root, /*protect_missing_dot_codex*/ true,
|
||||
) {
|
||||
append_default_read_only_path_if_no_explicit_rule(
|
||||
&mut self.entries,
|
||||
protected_path,
|
||||
);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns true when filesystem reads are unrestricted on this host.
|
||||
pub fn has_full_disk_read_access(&self) -> bool {
|
||||
// TODO(anp) Migrate callers to select the executor's path convention explicitly.
|
||||
|
||||
@@ -62,7 +62,7 @@ pub struct ThreadStartedTraceMetadata {
|
||||
pub model: String,
|
||||
pub provider_name: String,
|
||||
pub approval_policy: String,
|
||||
pub sandbox_policy: String,
|
||||
pub permissions_profile: String,
|
||||
}
|
||||
|
||||
/// Trace-only payload for a child completion notification delivered to its parent.
|
||||
|
||||
@@ -49,7 +49,7 @@ fn create_in_root_writes_replayable_lifecycle_events() -> anyhow::Result<()> {
|
||||
model: "gpt-test".to_string(),
|
||||
provider_name: "test-provider".to_string(),
|
||||
approval_policy: "never".to_string(),
|
||||
sandbox_policy: "DangerFullAccess".to_string(),
|
||||
permissions_profile: "danger-full-access".to_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -162,7 +162,7 @@ fn spawned_thread_start_appends_to_root_bundle() -> anyhow::Result<()> {
|
||||
model: "gpt-test".to_string(),
|
||||
provider_name: "test-provider".to_string(),
|
||||
approval_policy: "never".to_string(),
|
||||
sandbox_policy: "DangerFullAccess".to_string(),
|
||||
permissions_profile: "danger-full-access".to_string(),
|
||||
})?;
|
||||
child_trace.record_ended(RolloutStatus::Completed);
|
||||
let bundle_dir = single_bundle_dir(temp.path())?;
|
||||
@@ -458,7 +458,7 @@ fn minimal_metadata(thread_id: ThreadId) -> ThreadStartedTraceMetadata {
|
||||
model: "gpt-test".to_string(),
|
||||
provider_name: "test-provider".to_string(),
|
||||
approval_policy: "never".to_string(),
|
||||
sandbox_policy: "danger-full-access".to_string(),
|
||||
permissions_profile: "danger-full-access".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -162,6 +162,8 @@ use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelServiceTier;
|
||||
use codex_protocol::openai_models::ModelUpgrade;
|
||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
@@ -2603,9 +2605,14 @@ pub(crate) fn permission_profile_is_safely_represented_by_sandbox_mode(
|
||||
let projected_profile = match sandbox_mode {
|
||||
codex_app_server_protocol::SandboxMode::ReadOnly => PermissionProfile::read_only(),
|
||||
codex_app_server_protocol::SandboxMode::WorkspaceWrite => {
|
||||
PermissionProfile::from_legacy_sandbox_policy_for_cwd(
|
||||
&codex_protocol::protocol::SandboxPolicy::new_workspace_write_policy(),
|
||||
cwd,
|
||||
PermissionProfile::from_runtime_permissions(
|
||||
&FileSystemSandboxPolicy::workspace_write(
|
||||
&[],
|
||||
/*exclude_tmpdir_env_var*/ false,
|
||||
/*exclude_slash_tmp*/ false,
|
||||
)
|
||||
.with_workspace_write_cwd(cwd),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
)
|
||||
}
|
||||
codex_app_server_protocol::SandboxMode::DangerFullAccess => PermissionProfile::Disabled,
|
||||
@@ -4240,6 +4247,32 @@ mod tests {
|
||||
assert_eq!(resume.runtime_workspace_roots, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_mode_preserves_workspace_metadata_at_the_destination() {
|
||||
let workspace = tempfile::tempdir().expect("workspace");
|
||||
let cwd = workspace.path();
|
||||
std::fs::create_dir(cwd.join(".git")).expect("git metadata");
|
||||
let profile = PermissionProfile::from_legacy_sandbox_policy_for_cwd(
|
||||
&codex_protocol::protocol::SandboxPolicy::new_workspace_write_policy(),
|
||||
cwd,
|
||||
);
|
||||
assert!(permission_profile_is_safely_represented_by_sandbox_mode(
|
||||
&profile, cwd
|
||||
));
|
||||
let policy = profile
|
||||
.file_system_sandbox_policy()
|
||||
.with_additional_writable_roots(
|
||||
cwd,
|
||||
&[AbsolutePathBuf::from_absolute_path(cwd.join(".git"))
|
||||
.expect("absolute metadata")],
|
||||
);
|
||||
let profile =
|
||||
PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted);
|
||||
assert!(!permission_profile_is_safely_represented_by_sandbox_mode(
|
||||
&profile, cwd
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_mode_does_not_project_non_cwd_write_roots_for_remote_sessions() {
|
||||
let cwd = test_path_buf("/workspace/project").abs();
|
||||
|
||||
Reference in New Issue
Block a user