mirror of
https://github.com/openai/codex.git
synced 2026-09-09 15:58:47 +00:00
core: expose permission profile to shell tools
This commit is contained in:
@@ -2,11 +2,16 @@ use codex_protocol::ThreadId;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::config_types::EnvironmentVariablePattern;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::shell_environment;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use codex_protocol::shell_environment::CODEX_THREAD_ID_ENV_VAR;
|
||||
|
||||
/// Informational name of the active permission profile. Child processes can
|
||||
/// overwrite this value, so it must not be treated as proof of enforcement.
|
||||
pub const CODEX_PERMISSION_PROFILE_ENV_VAR: &str = "CODEX_PERMISSION_PROFILE";
|
||||
|
||||
/// Construct an environment map based on the rules in the specified policy. The
|
||||
/// resulting map can be passed directly to `Command::envs()` after calling
|
||||
/// `env_clear()` to ensure no unintended variables are leaked to the spawned
|
||||
@@ -25,6 +30,27 @@ pub fn create_env(
|
||||
shell_environment::create_env(policy, thread_id.as_deref())
|
||||
}
|
||||
|
||||
/// Injects the selected named permission profile into a shell tool's environment.
|
||||
///
|
||||
/// This is applied after the shell environment policy so the runtime-selected
|
||||
/// profile wins over inherited or configured values.
|
||||
pub(crate) fn inject_permission_profile_env(
|
||||
env: &mut HashMap<String, String>,
|
||||
active_permission_profile: Option<&ActivePermissionProfile>,
|
||||
) {
|
||||
if cfg!(windows) {
|
||||
env.retain(|key, _| !key.eq_ignore_ascii_case(CODEX_PERMISSION_PROFILE_ENV_VAR));
|
||||
} else {
|
||||
env.remove(CODEX_PERMISSION_PROFILE_ENV_VAR);
|
||||
}
|
||||
if let Some(active_permission_profile) = active_permission_profile {
|
||||
env.insert(
|
||||
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
|
||||
active_permission_profile.id.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_os = "windows"))]
|
||||
fn create_env_from_vars<I>(
|
||||
vars: I,
|
||||
|
||||
@@ -10,6 +10,59 @@ fn make_vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_permission_profile_env_overrides_policy_value() {
|
||||
let mut env = HashMap::from([(
|
||||
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
|
||||
"stale-profile".to_string(),
|
||||
)]);
|
||||
|
||||
inject_permission_profile_env(
|
||||
&mut env,
|
||||
Some(&ActivePermissionProfile::new("current-profile")),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
env.get(CODEX_PERMISSION_PROFILE_ENV_VAR)
|
||||
.map(String::as_str),
|
||||
Some("current-profile")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_permission_profile_env_removes_stale_value_without_active_profile() {
|
||||
let mut env = HashMap::from([(
|
||||
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
|
||||
"stale-profile".to_string(),
|
||||
)]);
|
||||
|
||||
inject_permission_profile_env(&mut env, /*active_permission_profile*/ None);
|
||||
|
||||
assert_eq!(env.get(CODEX_PERMISSION_PROFILE_ENV_VAR), None);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn inject_permission_profile_env_replaces_differently_cased_windows_key() {
|
||||
let mut env = HashMap::from([(
|
||||
"codex_permission_profile".to_string(),
|
||||
"stale-profile".to_string(),
|
||||
)]);
|
||||
|
||||
inject_permission_profile_env(
|
||||
&mut env,
|
||||
Some(&ActivePermissionProfile::new("current-profile")),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
env,
|
||||
HashMap::from([(
|
||||
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
|
||||
"current-profile".to_string(),
|
||||
)])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_core_inherit_defaults_keep_sensitive_vars() {
|
||||
let vars = make_vars(&[
|
||||
|
||||
@@ -6,6 +6,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::exec::ExecParams;
|
||||
use crate::exec_env::create_env;
|
||||
use crate::exec_env::inject_permission_profile_env;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::maybe_emit_implicit_skill_invocation;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
@@ -99,15 +100,19 @@ impl ShellCommandHandler {
|
||||
let use_login_shell = Self::resolve_use_login_shell(params.login, allow_login_shell)?;
|
||||
let command = Self::base_command(shell, ¶ms.command, use_login_shell);
|
||||
|
||||
let mut env = create_env(
|
||||
&turn_context.config.permissions.shell_environment_policy,
|
||||
Some(session.thread_id),
|
||||
);
|
||||
let active_permission_profile = turn_context.config.permissions.active_permission_profile();
|
||||
inject_permission_profile_env(&mut env, active_permission_profile.as_ref());
|
||||
|
||||
Ok(ExecParams {
|
||||
command,
|
||||
cwd,
|
||||
expiration: params.timeout_ms.into(),
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env: create_env(
|
||||
&turn_context.config.permissions.shell_environment_policy,
|
||||
Some(session.thread_id),
|
||||
),
|
||||
env,
|
||||
network: turn_context.network.clone(),
|
||||
network_environment_id: Some(turn_environment.environment_id.clone()),
|
||||
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::ShellCommandToolCallParams;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::config::PermissionProfileSnapshot;
|
||||
use crate::exec_env::CODEX_PERMISSION_PROFILE_ENV_VAR;
|
||||
use crate::exec_env::create_env;
|
||||
use crate::exec_env::inject_permission_profile_env;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::session::step_context::StepContext;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
@@ -71,7 +75,15 @@ fn assert_safe(shell: &Shell, command: &str) {
|
||||
|
||||
#[tokio::test]
|
||||
async fn shell_command_handler_to_exec_params_uses_selected_environment() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let (session, mut turn_context) = make_session_and_context().await;
|
||||
let permission_profile = turn_context.config.permissions.permission_profile().clone();
|
||||
Arc::make_mut(&mut turn_context.config)
|
||||
.permissions
|
||||
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(
|
||||
permission_profile,
|
||||
ActivePermissionProfile::new("test-profile"),
|
||||
))
|
||||
.expect("set active permission profile");
|
||||
|
||||
let command = "echo hello".to_string();
|
||||
let workdir = Some("subdir".to_string());
|
||||
@@ -99,10 +111,12 @@ async fn shell_command_handler_to_exec_params_uses_selected_environment() {
|
||||
PathUri::from_abs_path(&selected_cwd),
|
||||
Some(selected_shell),
|
||||
);
|
||||
let expected_env = create_env(
|
||||
let mut expected_env = create_env(
|
||||
&turn_context.config.permissions.shell_environment_policy,
|
||||
Some(session.thread_id),
|
||||
);
|
||||
let active_permission_profile = turn_context.config.permissions.active_permission_profile();
|
||||
inject_permission_profile_env(&mut expected_env, active_permission_profile.as_ref());
|
||||
|
||||
let params = ShellCommandToolCallParams {
|
||||
command,
|
||||
@@ -129,6 +143,12 @@ async fn shell_command_handler_to_exec_params_uses_selected_environment() {
|
||||
assert_eq!(exec_params.command, expected_command);
|
||||
assert_eq!(exec_params.cwd, expected_cwd);
|
||||
assert_eq!(exec_params.env, expected_env);
|
||||
assert_eq!(
|
||||
exec_params.env.get(CODEX_PERMISSION_PROFILE_ENV_VAR),
|
||||
active_permission_profile
|
||||
.as_ref()
|
||||
.map(|profile| &profile.id)
|
||||
);
|
||||
assert_eq!(exec_params.network, turn_context.network);
|
||||
assert_eq!(
|
||||
exec_params.network_environment_id.as_deref(),
|
||||
|
||||
@@ -4,6 +4,7 @@ Module: runtimes
|
||||
Concrete ToolRuntime implementations for specific tools. Each runtime stays
|
||||
small and focused and reuses the orchestrator for approvals + sandbox + retry.
|
||||
*/
|
||||
use crate::exec_env::CODEX_PERMISSION_PROFILE_ENV_VAR;
|
||||
use crate::exec_env::CODEX_THREAD_ID_ENV_VAR;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::shell::Shell;
|
||||
@@ -287,8 +288,10 @@ pub(crate) fn maybe_wrap_shell_lc_with_snapshot(
|
||||
.map(|arg| format!(" '{}'", shell_single_quote(arg)))
|
||||
.collect::<String>();
|
||||
let mut override_env = explicit_env_overrides.clone();
|
||||
if let Some(thread_id) = env.get(CODEX_THREAD_ID_ENV_VAR) {
|
||||
override_env.insert(CODEX_THREAD_ID_ENV_VAR.to_string(), thread_id.clone());
|
||||
for key in [CODEX_THREAD_ID_ENV_VAR, CODEX_PERMISSION_PROFILE_ENV_VAR] {
|
||||
if let Some(value) = env.get(key) {
|
||||
override_env.insert(key.to_string(), value.clone());
|
||||
}
|
||||
}
|
||||
let (override_captures, override_exports) = build_override_exports(&override_env);
|
||||
let (proxy_captures, proxy_exports) = build_proxy_env_exports();
|
||||
@@ -319,7 +322,9 @@ fn build_override_exports(explicit_env_overrides: &HashMap<String, String>) -> (
|
||||
.map(String::as_str)
|
||||
.filter(|key| is_valid_shell_variable_name(key))
|
||||
.collect::<Vec<_>>();
|
||||
keys.push(CODEX_PERMISSION_PROFILE_ENV_VAR);
|
||||
keys.sort_unstable();
|
||||
keys.dedup();
|
||||
|
||||
build_override_exports_for_keys("__CODEX_SNAPSHOT_OVERRIDE", &keys)
|
||||
}
|
||||
|
||||
@@ -534,6 +534,78 @@ fn maybe_wrap_shell_lc_with_snapshot_restores_codex_thread_id_from_env() {
|
||||
assert_eq!(String::from_utf8_lossy(&output.stdout), "nested-thread");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_wrap_shell_lc_with_snapshot_restores_permission_profile_from_env() {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
let snapshot_path = dir.path().join("snapshot.sh");
|
||||
std::fs::write(
|
||||
&snapshot_path,
|
||||
"# Snapshot file\nexport CODEX_PERMISSION_PROFILE='parent-profile'\n",
|
||||
)
|
||||
.expect("write snapshot");
|
||||
let (session_shell, shell_snapshot) =
|
||||
shell_with_snapshot(ShellType::Bash, "/bin/bash", snapshot_path.abs());
|
||||
let command = vec![
|
||||
"/bin/bash".to_string(),
|
||||
"-lc".to_string(),
|
||||
"printenv CODEX_PERMISSION_PROFILE".to_string(),
|
||||
];
|
||||
let env = HashMap::from([(
|
||||
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
|
||||
"current-profile".to_string(),
|
||||
)]);
|
||||
let rewritten = maybe_wrap_shell_lc_with_snapshot(
|
||||
&command,
|
||||
&session_shell,
|
||||
Some(&shell_snapshot),
|
||||
&HashMap::new(),
|
||||
&env,
|
||||
&RuntimePathPrepends::default(),
|
||||
);
|
||||
let output = Command::new(&rewritten[0])
|
||||
.args(&rewritten[1..])
|
||||
.env(CODEX_PERMISSION_PROFILE_ENV_VAR, "current-profile")
|
||||
.output()
|
||||
.expect("run rewritten command");
|
||||
|
||||
assert!(output.status.success(), "command failed: {output:?}");
|
||||
assert_eq!(String::from_utf8_lossy(&output.stdout), "current-profile\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_wrap_shell_lc_with_snapshot_unsets_absent_permission_profile() {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
let snapshot_path = dir.path().join("snapshot.sh");
|
||||
std::fs::write(
|
||||
&snapshot_path,
|
||||
"# Snapshot file\nexport CODEX_PERMISSION_PROFILE='stale-profile'\n",
|
||||
)
|
||||
.expect("write snapshot");
|
||||
let (session_shell, shell_snapshot) =
|
||||
shell_with_snapshot(ShellType::Bash, "/bin/bash", snapshot_path.abs());
|
||||
let command = vec![
|
||||
"/bin/bash".to_string(),
|
||||
"-lc".to_string(),
|
||||
"printenv CODEX_PERMISSION_PROFILE".to_string(),
|
||||
];
|
||||
let rewritten = maybe_wrap_shell_lc_with_snapshot(
|
||||
&command,
|
||||
&session_shell,
|
||||
Some(&shell_snapshot),
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
&RuntimePathPrepends::default(),
|
||||
);
|
||||
let output = Command::new(&rewritten[0])
|
||||
.args(&rewritten[1..])
|
||||
.env_remove(CODEX_PERMISSION_PROFILE_ENV_VAR)
|
||||
.output()
|
||||
.expect("run rewritten command");
|
||||
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
assert_eq!(output.stdout, b"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_wrap_shell_lc_with_snapshot_restores_proxy_env_from_process_env() {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
|
||||
@@ -13,8 +13,10 @@ use tokio_util::sync::CancellationToken;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::codex_thread::BackgroundTerminalInfo;
|
||||
use crate::exec_env::CODEX_PERMISSION_PROFILE_ENV_VAR;
|
||||
use crate::exec_env::CODEX_THREAD_ID_ENV_VAR;
|
||||
use crate::exec_env::create_env;
|
||||
use crate::exec_env::inject_permission_profile_env;
|
||||
use crate::exec_policy::ExecApprovalRequest;
|
||||
use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::ExecRequest;
|
||||
@@ -109,15 +111,19 @@ fn apply_unified_exec_env(mut env: HashMap<String, String>) -> HashMap<String, S
|
||||
fn exec_env_policy_from_shell_policy(
|
||||
policy: &ShellEnvironmentPolicy,
|
||||
) -> codex_exec_server::ExecEnvPolicy {
|
||||
let mut exclude = policy
|
||||
.exclude
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
exclude.push(CODEX_PERMISSION_PROFILE_ENV_VAR.to_string());
|
||||
let mut r#set = policy.r#set.clone();
|
||||
r#set.retain(|key, _| !key.eq_ignore_ascii_case(CODEX_PERMISSION_PROFILE_ENV_VAR));
|
||||
codex_exec_server::ExecEnvPolicy {
|
||||
inherit: policy.inherit.clone(),
|
||||
ignore_default_excludes: policy.ignore_default_excludes,
|
||||
exclude: policy
|
||||
.exclude
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect(),
|
||||
r#set: policy.r#set.clone(),
|
||||
exclude,
|
||||
r#set,
|
||||
include_only: policy
|
||||
.include_only
|
||||
.iter()
|
||||
@@ -132,7 +138,10 @@ fn env_overlay_for_exec_server(
|
||||
) -> HashMap<String, String> {
|
||||
request_env
|
||||
.iter()
|
||||
.filter(|(key, value)| local_policy_env.get(*key) != Some(*value))
|
||||
.filter(|(key, value)| {
|
||||
key.as_str() == CODEX_PERMISSION_PROFILE_ENV_VAR
|
||||
|| local_policy_env.get(*key) != Some(*value)
|
||||
})
|
||||
.map(|(key, value)| (key.clone(), value.clone()))
|
||||
.collect()
|
||||
}
|
||||
@@ -1110,6 +1119,8 @@ impl UnifiedExecProcessManager {
|
||||
CODEX_THREAD_ID_ENV_VAR.to_string(),
|
||||
context.session.thread_id.to_string(),
|
||||
);
|
||||
let active_permission_profile = context.turn.config.permissions.active_permission_profile();
|
||||
inject_permission_profile_env(&mut env, active_permission_profile.as_ref());
|
||||
let env = apply_unified_exec_env(env);
|
||||
let exec_server_env_config = ExecServerEnvConfig {
|
||||
policy: exec_env_policy_from_shell_policy(
|
||||
|
||||
@@ -42,12 +42,20 @@ fn env_overlay_for_exec_server_keeps_runtime_changes_only() {
|
||||
("HOME".to_string(), "/client-home".to_string()),
|
||||
("PATH".to_string(), "/client-path".to_string()),
|
||||
("SHELL_SET".to_string(), "policy".to_string()),
|
||||
(
|
||||
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
|
||||
"current-profile".to_string(),
|
||||
),
|
||||
]);
|
||||
let request_env = HashMap::from([
|
||||
("HOME".to_string(), "/client-home".to_string()),
|
||||
("PATH".to_string(), "/sandbox-path".to_string()),
|
||||
("SHELL_SET".to_string(), "policy".to_string()),
|
||||
("CODEX_THREAD_ID".to_string(), "thread-1".to_string()),
|
||||
(
|
||||
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
|
||||
"current-profile".to_string(),
|
||||
),
|
||||
(
|
||||
"CODEX_SANDBOX_NETWORK_DISABLED".to_string(),
|
||||
"1".to_string(),
|
||||
@@ -59,6 +67,10 @@ fn env_overlay_for_exec_server_keeps_runtime_changes_only() {
|
||||
HashMap::from([
|
||||
("PATH".to_string(), "/sandbox-path".to_string()),
|
||||
("CODEX_THREAD_ID".to_string(), "thread-1".to_string()),
|
||||
(
|
||||
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
|
||||
"current-profile".to_string(),
|
||||
),
|
||||
(
|
||||
"CODEX_SANDBOX_NETWORK_DISABLED".to_string(),
|
||||
"1".to_string()
|
||||
@@ -67,6 +79,31 @@ fn env_overlay_for_exec_server_keeps_runtime_changes_only() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_env_policy_excludes_runtime_permission_profile() {
|
||||
let policy = ShellEnvironmentPolicy {
|
||||
r#set: HashMap::from([
|
||||
(
|
||||
"codex_permission_profile".to_string(),
|
||||
"stale-profile".to_string(),
|
||||
),
|
||||
("KEEP".to_string(), "value".to_string()),
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
exec_env_policy_from_shell_policy(&policy),
|
||||
codex_exec_server::ExecEnvPolicy {
|
||||
inherit: policy.inherit,
|
||||
ignore_default_excludes: policy.ignore_default_excludes,
|
||||
exclude: vec![CODEX_PERMISSION_PROFILE_ENV_VAR.to_string()],
|
||||
r#set: HashMap::from([("KEEP".to_string(), "value".to_string())]),
|
||||
include_only: Vec::new(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
|
||||
let cwd: codex_utils_absolute_path::AbsolutePathBuf = std::env::current_dir()
|
||||
|
||||
@@ -10,6 +10,7 @@ use codex_features::Feature;
|
||||
use codex_protocol::approvals::NetworkApprovalProtocol;
|
||||
use codex_protocol::approvals::NetworkPolicyAmendment;
|
||||
use codex_protocol::approvals::NetworkPolicyRuleAction;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
@@ -640,6 +641,7 @@ enum ScenarioGroup {
|
||||
UnifiedExec,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
struct CommandResult {
|
||||
exit_code: Option<i64>,
|
||||
stdout: String,
|
||||
@@ -2696,6 +2698,120 @@ async fn matched_prefix_rule_runs_unsandboxed_under_zsh_fork() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[cfg(unix)]
|
||||
async fn allowed_escalated_shell_command_inherits_active_permission_profile() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
fs::write(
|
||||
home.path().join("config.toml"),
|
||||
r#"default_permissions = ":workspace"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let script_dir = tempfile::tempdir_in(std::env::current_dir()?)?;
|
||||
let script_path = script_dir.path().join("print-permission-profile.sh");
|
||||
let outside_path = script_dir.path().join("unsandboxed-marker");
|
||||
fs::write(
|
||||
&script_path,
|
||||
format!("#!/bin/sh\nprintenv CODEX_PERMISSION_PROFILE\ntouch {outside_path:?}\n"),
|
||||
)?;
|
||||
|
||||
let rules_dir = home.path().join("rules");
|
||||
fs::create_dir_all(&rules_dir)?;
|
||||
let script_pattern = serde_json::to_string(&script_path.to_string_lossy())?;
|
||||
fs::write(
|
||||
rules_dir.join("default.rules"),
|
||||
format!(r#"prefix_rule(pattern=["/bin/sh", {script_pattern}], decision="allow")"#),
|
||||
)?;
|
||||
|
||||
let approval_policy = AskForApproval::OnRequest;
|
||||
let mut builder = test_codex().with_home(home).with_config(move |config| {
|
||||
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
assert!(!outside_path.starts_with(test.config.cwd.as_path()));
|
||||
assert_eq!(
|
||||
test.session_configured
|
||||
.active_permission_profile
|
||||
.as_ref()
|
||||
.map(|profile| profile.id.as_str()),
|
||||
Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE)
|
||||
);
|
||||
|
||||
let call_id = "allowed-escalated-shell-inherits-permission-profile";
|
||||
let command = format!("/bin/sh {script_path:?}");
|
||||
let event = shell_event(
|
||||
call_id,
|
||||
&command,
|
||||
/*timeout_ms*/ 5_000,
|
||||
SandboxPermissions::RequireEscalated,
|
||||
)?;
|
||||
let _ = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-escalated-profile-1"),
|
||||
event,
|
||||
ev_completed("resp-escalated-profile-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let results = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_assistant_message("msg-escalated-profile-1", "done"),
|
||||
ev_completed("resp-escalated-profile-2"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let session_model = test.session_configured.model.clone();
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "run the allowed script with escalated permissions".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
environments: Some(local_selections(test.config.cwd.clone())),
|
||||
approval_policy: Some(approval_policy),
|
||||
approvals_reviewer: Some(ApprovalsReviewer::User),
|
||||
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode {
|
||||
mode: codex_protocol::config_types::ModeKind::Default,
|
||||
settings: codex_protocol::config_types::Settings {
|
||||
model: session_model,
|
||||
reasoning_effort: None,
|
||||
developer_instructions: None,
|
||||
},
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
wait_for_completion_without_approval(&test).await;
|
||||
|
||||
let result = parse_result(&results.single_request().function_call_output(call_id));
|
||||
assert_eq!(
|
||||
result,
|
||||
CommandResult {
|
||||
exit_code: Some(0),
|
||||
stdout: format!("{BUILT_IN_PERMISSION_PROFILE_WORKSPACE}\n"),
|
||||
}
|
||||
);
|
||||
assert!(
|
||||
outside_path.exists(),
|
||||
"allowed escalated script should run outside the :workspace sandbox"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[cfg(unix)]
|
||||
async fn invalid_requested_prefix_rule_falls_back_for_compound_command() -> Result<()> {
|
||||
|
||||
Reference in New Issue
Block a user