Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ codex-responses-api-proxy = { workspace = true }
codex-rmcp-client = { workspace = true }
codex-rollout-trace = { workspace = true }
codex-sandboxing = { workspace = true }
codex-shell-command = { workspace = true }
codex-state = { workspace = true }
codex-stdio-to-uds = { workspace = true }
codex-terminal-detection = { workspace = true }
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/cli/src/debug_sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@ async fn run_command_under_sandbox(
// sandbox policy. In the future, we could add a CLI option to set them
// separately.
let sandbox_policy_cwd = cwd.clone();
if let Some(reason) = codex_shell_command::metadata_write_forbidden_reason(
&command,
cwd.as_path(),
sandbox_policy_cwd.as_path(),
&config.permissions.file_system_sandbox_policy(),
) {
anyhow::bail!("{reason}");
}

let env = create_env(
&config.permissions.shell_environment_policy,
Expand Down
16 changes: 15 additions & 1 deletion codex-rs/core/src/tools/handlers/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ use crate::tools::runtimes::shell::ShellRequest;
use crate::tools::runtimes::shell::ShellRuntime;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
use crate::tools::sandboxing::ToolCtx;
use crate::tools::sandboxing::apply_protected_metadata_write_preflight;
use codex_features::Feature;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::protocol::ExecCommandSource;
use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
use codex_shell_command::is_safe_command::is_known_safe_command;
use codex_tools::ShellCommandBackendConfig;

Expand Down Expand Up @@ -513,7 +515,11 @@ impl ShellHandler {
);
emitter.begin(event_ctx).await;

let file_system_sandbox_policy = turn.file_system_sandbox_policy();
let base_file_system_sandbox_policy = turn.file_system_sandbox_policy();
let file_system_sandbox_policy = effective_file_system_sandbox_policy(
&base_file_system_sandbox_policy,
normalized_additional_permissions.as_ref(),
);
let exec_approval_requirement = session
.services
.exec_policy
Expand All @@ -531,6 +537,14 @@ impl ShellHandler {
prefix_rule,
})
.await;
let exec_approval_requirement = apply_protected_metadata_write_preflight(
exec_approval_requirement,
effective_additional_permissions.sandbox_permissions,
&exec_params.command,
&exec_params.cwd,
turn.cwd.as_path(),
&file_system_sandbox_policy,
);

let req = ShellRequest {
command: exec_params.command.clone(),
Expand Down
40 changes: 39 additions & 1 deletion codex-rs/core/src/tools/handlers/unified_exec.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::exec_policy::ExecApprovalRequest;
use crate::function_tool::FunctionCallError;
use crate::maybe_emit_implicit_skill_invocation;
use crate::sandboxing::SandboxPermissions;
Expand All @@ -19,6 +20,7 @@ use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::sandboxing::apply_protected_metadata_write_preflight;
use crate::unified_exec::ExecCommandRequest;
use crate::unified_exec::UnifiedExecContext;
use crate::unified_exec::UnifiedExecError;
Expand All @@ -32,6 +34,7 @@ use codex_otel::TOOL_CALL_UNIFIED_EXEC_METRIC;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::TerminalInteractionEvent;
use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
use codex_shell_command::is_safe_command::is_known_safe_command;
use codex_tools::UnifiedExecShellMode;
use codex_utils_output_truncation::TruncationPolicy;
Expand Down Expand Up @@ -330,6 +333,40 @@ impl ToolHandler for UnifiedExecHandler {
});
}

let base_file_system_sandbox_policy = context.turn.file_system_sandbox_policy();
let file_system_sandbox_policy = effective_file_system_sandbox_policy(
&base_file_system_sandbox_policy,
normalized_additional_permissions.as_ref(),
);
let exec_approval_requirement = context
.session
.services
.exec_policy
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
command: &command,
approval_policy: context.turn.approval_policy.value(),
permission_profile: context.turn.permission_profile(),
file_system_sandbox_policy: &file_system_sandbox_policy,
sandbox_cwd: context.turn.cwd.as_path(),
sandbox_permissions: if effective_additional_permissions
.permissions_preapproved
{
SandboxPermissions::UseDefault
} else {
effective_additional_permissions.sandbox_permissions
},
prefix_rule: prefix_rule.clone(),
})
.await;
let exec_approval_requirement = apply_protected_metadata_write_preflight(
exec_approval_requirement,
effective_additional_permissions.sandbox_permissions,
&command,
&cwd,
context.turn.cwd.as_path(),
&file_system_sandbox_policy,
);

emit_unified_exec_tty_metric(&turn.session_telemetry, tty);
match manager
.exec_command(
Expand All @@ -345,10 +382,11 @@ impl ToolHandler for UnifiedExecHandler {
sandbox_permissions: effective_additional_permissions
.sandbox_permissions,
additional_permissions: normalized_additional_permissions,
#[cfg(unix)]
additional_permissions_preapproved: effective_additional_permissions
.permissions_preapproved,
justification,
prefix_rule,
exec_approval_requirement,
},
&context,
)
Expand Down
45 changes: 45 additions & 0 deletions codex-rs/core/src/tools/sandboxing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use serde::Serialize;
use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::Hash;
use std::path::Path;
use std::sync::Arc;

#[derive(Clone, Default, Debug)]
Expand Down Expand Up @@ -194,6 +195,50 @@ impl ExecApprovalRequirement {
}
}

/// Applies an early UX check for protected metadata writes only to commands
/// that would otherwise run in the sandbox. Explicit sandbox-bypass approval
/// paths keep their existing approval semantics.
pub(crate) fn apply_protected_metadata_write_preflight(
exec_approval_requirement: ExecApprovalRequirement,
sandbox_permissions: SandboxPermissions,
command: &[String],
command_cwd: &Path,
sandbox_policy_cwd: &Path,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
) -> ExecApprovalRequirement {
let bypasses_sandbox = matches!(
&exec_approval_requirement,
ExecApprovalRequirement::Skip {
bypass_sandbox: true,
..
}
);
let approval_is_for_sandbox_override = sandbox_permissions.requests_sandbox_override()
&& matches!(
&exec_approval_requirement,
ExecApprovalRequirement::NeedsApproval { .. }
);
if bypasses_sandbox
|| approval_is_for_sandbox_override
|| matches!(
&exec_approval_requirement,
ExecApprovalRequirement::Forbidden { .. }
)
{
return exec_approval_requirement;
}

codex_shell_command::metadata_write_forbidden_reason(
command,
command_cwd,
sandbox_policy_cwd,
file_system_sandbox_policy,
)
.map_or(exec_approval_requirement, |reason| {
ExecApprovalRequirement::Forbidden { reason }
})
}

/// - Never, OnFailure: do not ask
/// - OnRequest: ask unless filesystem access is unrestricted
/// - Granular: ask unless filesystem access is unrestricted, but auto-reject
Expand Down
105 changes: 105 additions & 0 deletions codex-rs/core/src/tools/sandboxing_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::NetworkAccess;
use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::tempdir;

#[test]
fn bash_permission_request_payload_omits_missing_description() {
Expand Down Expand Up @@ -138,3 +139,107 @@ fn guardian_bypasses_sandbox_for_explicit_escalation_on_first_attempt() {
SandboxOverride::BypassSandboxFirstAttempt
);
}

fn metadata_redirect_command(target: &str) -> Vec<String> {
vec![
"/bin/bash".to_string(),
"-lc".to_string(),
format!("printf pwned > {target}"),
]
}

fn workspace_write_file_system_sandbox_policy() -> FileSystemSandboxPolicy {
FileSystemSandboxPolicy::workspace_write(&[], false, false)
}

#[test]
fn protected_metadata_write_preflight_forbids_only_sandboxed_skip() {
let repo = tempdir().expect("create tempdir");
std::fs::create_dir(repo.path().join(".git")).expect("create .git");
let file_system_sandbox_policy = workspace_write_file_system_sandbox_policy();

let requirement = apply_protected_metadata_write_preflight(
ExecApprovalRequirement::Skip {
bypass_sandbox: false,
proposed_execpolicy_amendment: None,
},
SandboxPermissions::UseDefault,
&metadata_redirect_command(".git/config"),
repo.path(),
repo.path(),
&file_system_sandbox_policy,
);

assert_eq!(
requirement,
ExecApprovalRequirement::Forbidden {
reason: "command targets protected workspace metadata path `.git`".to_string()
}
);
}

#[test]
fn protected_metadata_write_preflight_preserves_approval_and_bypass_paths() {
let repo = tempdir().expect("create tempdir");
std::fs::create_dir(repo.path().join(".git")).expect("create .git");
let file_system_sandbox_policy = workspace_write_file_system_sandbox_policy();
let command = metadata_redirect_command(".git/config");

let needs_approval = ExecApprovalRequirement::NeedsApproval {
reason: Some("requires approval".to_string()),
proposed_execpolicy_amendment: None,
};
assert_eq!(
apply_protected_metadata_write_preflight(
needs_approval.clone(),
SandboxPermissions::RequireEscalated,
&command,
repo.path(),
repo.path(),
&file_system_sandbox_policy,
),
needs_approval
);

let bypass_sandbox = ExecApprovalRequirement::Skip {
bypass_sandbox: true,
proposed_execpolicy_amendment: None,
};
assert_eq!(
apply_protected_metadata_write_preflight(
bypass_sandbox.clone(),
SandboxPermissions::UseDefault,
&command,
repo.path(),
repo.path(),
&file_system_sandbox_policy,
),
bypass_sandbox
);
}

#[test]
fn protected_metadata_write_preflight_forbids_sandboxed_approval_prompts() {
let repo = tempdir().expect("create tempdir");
std::fs::create_dir(repo.path().join(".git")).expect("create .git");
let file_system_sandbox_policy = workspace_write_file_system_sandbox_policy();

let requirement = apply_protected_metadata_write_preflight(
ExecApprovalRequirement::NeedsApproval {
reason: Some("dangerous command".to_string()),
proposed_execpolicy_amendment: None,
},
SandboxPermissions::UseDefault,
&metadata_redirect_command(".git/config"),
repo.path(),
repo.path(),
&file_system_sandbox_policy,
);

assert_eq!(
requirement,
ExecApprovalRequirement::Forbidden {
reason: "command targets protected workspace metadata path `.git`".to_string()
}
);
}
4 changes: 3 additions & 1 deletion codex-rs/core/src/unified_exec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use tokio::sync::Mutex;
use crate::sandboxing::SandboxPermissions;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
use crate::tools::sandboxing::ExecApprovalRequirement;

mod async_watcher;
mod errors;
Expand Down Expand Up @@ -97,9 +98,10 @@ pub(crate) struct ExecCommandRequest {
pub tty: bool,
pub sandbox_permissions: SandboxPermissions,
pub additional_permissions: Option<AdditionalPermissionProfile>,
#[cfg(unix)]
pub additional_permissions_preapproved: bool,
pub justification: Option<String>,
pub prefix_rule: Option<Vec<String>>,
pub exec_approval_requirement: ExecApprovalRequirement,
}

#[derive(Debug)]
Expand Down
22 changes: 1 addition & 21 deletions codex-rs/core/src/unified_exec/process_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ use tokio_util::sync::CancellationToken;

use crate::exec_env::CODEX_THREAD_ID_ENV_VAR;
use crate::exec_env::create_env;
use crate::exec_policy::ExecApprovalRequest;
use crate::sandboxing::ExecRequest;
use crate::sandboxing::ExecServerEnvConfig;
use crate::tools::context::ExecCommandToolOutput;
Expand Down Expand Up @@ -789,25 +788,6 @@ impl UnifiedExecProcessManager {
self,
context.turn.tools_config.unified_exec_shell_mode.clone(),
);
let file_system_sandbox_policy = context.turn.file_system_sandbox_policy();
let exec_approval_requirement = context
.session
.services
.exec_policy
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
command: &request.command,
approval_policy: context.turn.approval_policy.value(),
permission_profile: context.turn.permission_profile(),
file_system_sandbox_policy: &file_system_sandbox_policy,
sandbox_cwd: context.turn.cwd.as_path(),
sandbox_permissions: if request.additional_permissions_preapproved {
crate::sandboxing::SandboxPermissions::UseDefault
} else {
request.sandbox_permissions
},
prefix_rule: request.prefix_rule.clone(),
})
.await;
let req = UnifiedExecToolRequest {
command: request.command.clone(),
hook_command: request.hook_command.clone(),
Expand All @@ -823,7 +803,7 @@ impl UnifiedExecProcessManager {
#[cfg(unix)]
additional_permissions_preapproved: request.additional_permissions_preapproved,
justification: request.justification.clone(),
exec_approval_requirement,
exec_approval_requirement: request.exec_approval_requirement.clone(),
};
let tool_ctx = ToolCtx {
session: context.session.clone(),
Expand Down
Loading
Loading