Compare commits

...

12 Commits

Author SHA1 Message Date
Fouad Matin
2c3874d5fd fix 2025-09-11 11:16:46 -07:00
Fouad Matin
23b4a8c027 fix 2025-09-11 11:12:02 -07:00
Fouad Matin
1bafe46731 fix 2025-09-11 10:29:46 -07:00
Fouad Matin
3e773fbcfc Merge branch 'main' into add-enterprise-config 2025-09-11 10:17:46 -07:00
Fouad Matin
82982f1466 Merge branch 'main' into add-enterprise-config 2025-09-10 13:53:02 -07:00
Fouad Matin
8d9369a650 add: log file 2025-09-10 01:28:41 -07:00
Fouad Matin
4a6a63d6c4 fix: PR feedback 2025-09-09 17:55:51 -07:00
Fouad Matin
6e772e993a - 2025-09-09 15:55:29 -07:00
Fouad Matin
848bc6c94a Merge branch 'main' into add-enterprise-config 2025-09-05 16:59:10 -07:00
Fouad Matin
eab3d8e328 - 2025-09-04 22:25:35 -07:00
Fouad Matin
609d69d3ba - 2025-09-04 13:13:42 -07:00
Fouad Matin
dd2c9ed676 add: admin config 2025-09-04 10:37:31 -07:00
19 changed files with 994 additions and 39 deletions

20
codex-rs/Cargo.lock generated
View File

@@ -614,6 +614,7 @@ dependencies = [
"codex-apply-patch",
"codex-mcp-client",
"codex-protocol",
"core-foundation 0.9.4",
"core_test_support",
"dirs",
"env-flags",
@@ -652,6 +653,7 @@ dependencies = [
"uuid",
"walkdir",
"which",
"whoami",
"wildmatch",
"wiremock",
]
@@ -2544,6 +2546,7 @@ checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0"
dependencies = [
"bitflags 2.9.1",
"libc",
"redox_syscall",
]
[[package]]
@@ -5099,6 +5102,12 @@ dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "wasite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
@@ -5227,6 +5236,17 @@ dependencies = [
"winsafe",
]
[[package]]
name = "whoami"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
dependencies = [
"libredox",
"wasite",
"web-sys",
]
[[package]]
name = "wildmatch"
version = "2.4.0"

View File

@@ -2,8 +2,11 @@ use std::path::PathBuf;
use clap::Parser;
use codex_common::CliConfigOverrides;
use codex_core::admin_controls::ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE;
use codex_core::config::AdminAuditContext;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::audit_admin_run_without_prompt;
use crate::chatgpt_token::init_chatgpt_token_from_auth;
use crate::get_task::GetTaskResponse;
@@ -31,6 +34,28 @@ pub async fn run_apply_command(
ConfigOverrides::default(),
)?;
if config
.admin_danger_prompt
.as_ref()
.is_some_and(|prompt| prompt.needs_prompt())
{
anyhow::bail!(ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE);
}
audit_admin_run_without_prompt(
&config.admin_controls,
AdminAuditContext {
sandbox_policy: &config.sandbox_policy,
approval_policy: &config.approval_policy,
cwd: &config.cwd,
command: None,
dangerously_bypass_requested: false,
dangerous_mode_justification: None,
record_command_event: false,
},
)
.await?;
init_chatgpt_token_from_auth(&config.codex_home).await?;
let task_response = get_task(&config, apply_cli.task_id).await?;

View File

@@ -1,8 +1,11 @@
use std::path::PathBuf;
use codex_common::CliConfigOverrides;
use codex_core::admin_controls::ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE;
use codex_core::config::AdminAuditContext;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::audit_admin_run_without_prompt;
use codex_core::exec_env::create_env;
use codex_core::landlock::spawn_command_under_linux_sandbox;
use codex_core::seatbelt::spawn_command_under_seatbelt;
@@ -75,6 +78,28 @@ async fn run_command_under_sandbox(
..Default::default()
},
)?;
if config
.admin_danger_prompt
.as_ref()
.is_some_and(|prompt| prompt.needs_prompt())
{
anyhow::bail!(ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE);
}
audit_admin_run_without_prompt(
&config.admin_controls,
AdminAuditContext {
sandbox_policy: &config.sandbox_policy,
approval_policy: &config.approval_policy,
cwd: &config.cwd,
command: None,
dangerously_bypass_requested: false,
dangerous_mode_justification: None,
record_command_event: false,
},
)
.await?;
let stdio_policy = StdioPolicy::Inherit;
let env = create_env(&config.shell_environment_policy);

View File

@@ -1,10 +1,13 @@
use codex_common::CliConfigOverrides;
use codex_core::CodexAuth;
use codex_core::admin_controls::ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE;
use codex_core::auth::CLIENT_ID;
use codex_core::auth::login_with_api_key;
use codex_core::auth::logout;
use codex_core::config::AdminAuditContext;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::audit_admin_run_without_prompt;
use codex_login::ServerOptions;
use codex_login::run_login_server;
use codex_protocol::mcp_protocol::AuthMode;
@@ -23,7 +26,7 @@ pub async fn login_with_chatgpt(codex_home: PathBuf) -> std::io::Result<()> {
}
pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) -> ! {
let config = load_config_or_exit(cli_config_overrides);
let config = load_config_or_exit(cli_config_overrides).await;
match login_with_chatgpt(config.codex_home).await {
Ok(_) => {
@@ -41,7 +44,7 @@ pub async fn run_login_with_api_key(
cli_config_overrides: CliConfigOverrides,
api_key: String,
) -> ! {
let config = load_config_or_exit(cli_config_overrides);
let config = load_config_or_exit(cli_config_overrides).await;
match login_with_api_key(&config.codex_home, &api_key) {
Ok(_) => {
@@ -56,7 +59,7 @@ pub async fn run_login_with_api_key(
}
pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
let config = load_config_or_exit(cli_config_overrides);
let config = load_config_or_exit(cli_config_overrides).await;
match CodexAuth::from_codex_home(&config.codex_home) {
Ok(Some(auth)) => match auth.mode {
@@ -87,7 +90,7 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
}
pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! {
let config = load_config_or_exit(cli_config_overrides);
let config = load_config_or_exit(cli_config_overrides).await;
match logout(&config.codex_home) {
Ok(true) => {
@@ -105,7 +108,7 @@ pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! {
}
}
fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config {
async fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config {
let cli_overrides = match cli_config_overrides.parse_overrides() {
Ok(v) => v,
Err(e) => {
@@ -115,13 +118,42 @@ fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config {
};
let config_overrides = ConfigOverrides::default();
match Config::load_with_cli_overrides(cli_overrides, config_overrides) {
let config = match Config::load_with_cli_overrides(cli_overrides, config_overrides) {
Ok(config) => config,
Err(e) => {
eprintln!("Error loading configuration: {e}");
std::process::exit(1);
}
};
if config
.admin_danger_prompt
.as_ref()
.is_some_and(|prompt| prompt.needs_prompt())
{
eprintln!("{ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE}");
std::process::exit(1);
}
if let Err(err) = audit_admin_run_without_prompt(
&config.admin_controls,
AdminAuditContext {
sandbox_policy: &config.sandbox_policy,
approval_policy: &config.approval_policy,
cwd: &config.cwd,
command: None,
dangerously_bypass_requested: false,
dangerous_mode_justification: None,
record_command_event: false,
},
)
.await
{
eprintln!("{err}");
std::process::exit(1);
}
config
}
fn safe_format_key(key: &str) -> String {

View File

@@ -5,8 +5,11 @@ use codex_common::CliConfigOverrides;
use codex_core::AuthManager;
use codex_core::ConversationManager;
use codex_core::NewConversation;
use codex_core::admin_controls::ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE;
use codex_core::config::AdminAuditContext;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::audit_admin_run_without_prompt;
use codex_core::protocol::Event;
use codex_core::protocol::EventMsg;
use codex_core::protocol::Submission;
@@ -36,6 +39,28 @@ pub async fn run_main(opts: ProtoCli) -> anyhow::Result<()> {
.map_err(anyhow::Error::msg)?;
let config = Config::load_with_cli_overrides(overrides_vec, ConfigOverrides::default())?;
if config
.admin_danger_prompt
.as_ref()
.is_some_and(|prompt| prompt.needs_prompt())
{
anyhow::bail!(ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE);
}
audit_admin_run_without_prompt(
&config.admin_controls,
AdminAuditContext {
sandbox_policy: &config.sandbox_policy,
approval_policy: &config.approval_policy,
cwd: &config.cwd,
command: None,
dangerously_bypass_requested: false,
dangerous_mode_justification: None,
record_command_event: false,
},
)
.await?;
// Use conversation_manager API to start a conversation
let conversation_manager =
ConversationManager::new(AuthManager::shared(config.codex_home.clone()));

View File

@@ -56,12 +56,16 @@ tree-sitter-bash = "0.25.0"
uuid = { version = "1", features = ["serde", "v4"] }
which = "6"
wildmatch = "2.4.0"
whoami = "1"
[target.'cfg(target_os = "linux")'.dependencies]
landlock = "0.4.1"
seccompiler = "0.5.0"
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9"
# Build OpenSSL from source for musl builds.
[target.x86_64-unknown-linux-musl.dependencies]
openssl-sys = { version = "*", features = ["vendored"] }

View File

@@ -0,0 +1,348 @@
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
use chrono::Utc;
use reqwest::Client;
use serde::Deserialize;
use serde::Serialize;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
pub const ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE: &str = "Running Codex with --dangerously-bypass-approvals-and-sandbox or --sandbox danger-full-access has been disabled by your administrator. Please contact your system administrator or try: codex --full-auto";
pub const ADMIN_DANGEROUS_SANDBOX_DISABLED_PROMPT_LINES: &[&str] = &[
"Running Codex with --dangerously-bypass-approvals-and-sandbox or",
"--sandbox danger-full-access has been disabled by your administrator.",
"\nPlease contact your system administrator or try with sandboxed mode:",
"codex --full-auto",
];
#[derive(Debug, Clone, Deserialize, Default)]
pub struct AdminConfigToml {
#[serde(default)]
pub disallow_dangerous_sandbox: Option<bool>,
#[serde(default)]
pub disallow_dangerously_bypass_approvals_and_sandbox: Option<bool>,
#[serde(default)]
pub allow_danger_with_reason: Option<bool>,
#[serde(default)]
pub audit: Option<AdminAuditToml>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct AdminAuditToml {
pub endpoint: Option<String>,
#[serde(default)]
pub dangerous_sandbox: Option<bool>,
#[serde(default)]
pub all_commands: Option<bool>,
#[serde(default)]
pub audit_log_file: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct AdminControls {
pub disallow_dangerous_sandbox: bool,
pub disallow_dangerously_bypass_approvals_and_sandbox: bool,
pub allow_danger_with_reason: bool,
pub audit: Option<AdminAudit>,
}
impl AdminControls {
pub fn from_toml(admin: Option<AdminConfigToml>) -> Self {
let mut controls = AdminControls::default();
if let Some(section) = admin {
if let Some(value) = section.disallow_dangerous_sandbox {
controls.disallow_dangerous_sandbox = value;
}
if let Some(value) = section.disallow_dangerously_bypass_approvals_and_sandbox {
controls.disallow_dangerously_bypass_approvals_and_sandbox = value;
}
if let Some(value) = section.allow_danger_with_reason {
controls.allow_danger_with_reason = value;
}
controls.audit = section.audit.and_then(AdminAudit::from_toml);
}
controls
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdminAudit {
pub endpoint: String,
pub events: AdminAuditEvents,
pub audit_log_file: Option<PathBuf>,
}
impl AdminAudit {
fn from_toml(config: AdminAuditToml) -> Option<Self> {
let AdminAuditToml {
endpoint,
dangerous_sandbox,
all_commands,
audit_log_file,
} = config;
let endpoint = endpoint?.trim().to_string();
if endpoint.is_empty() {
return None;
}
let events = AdminAuditEvents {
dangerous_sandbox: dangerous_sandbox.unwrap_or(false),
all_commands: all_commands.unwrap_or(false),
};
Some(Self {
endpoint,
events,
audit_log_file: audit_log_file
.as_deref()
.and_then(resolve_admin_audit_log_file),
})
}
}
fn resolve_admin_audit_log_file(path: &str) -> Option<PathBuf> {
let trimmed = path.trim();
if trimmed.is_empty() {
return None;
}
if let Some(home) = trimmed.strip_prefix("~/") {
let mut expanded = dirs::home_dir()?;
expanded.push(home);
Some(expanded)
} else if trimmed == "~" {
dirs::home_dir()
} else {
Some(PathBuf::from(trimmed))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct AdminAuditEvents {
pub dangerous_sandbox: bool,
pub all_commands: bool,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct AdminDangerPrompt {
pub sandbox: bool,
pub dangerously_bypass: bool,
}
impl AdminDangerPrompt {
pub fn needs_prompt(&self) -> bool {
self.sandbox || self.dangerously_bypass
}
}
#[derive(Debug, Clone, Copy)]
pub struct AdminAuditContext<'a> {
pub sandbox_policy: &'a SandboxPolicy,
pub approval_policy: &'a AskForApproval,
pub cwd: &'a Path,
pub command: Option<&'a [String]>,
pub dangerously_bypass_requested: bool,
pub dangerous_mode_justification: Option<&'a str>,
pub record_command_event: bool,
}
#[derive(Debug, Clone, Copy)]
enum AdminAuditEventKind {
CommandInvoked,
DangerousSandbox,
}
impl AdminAuditEventKind {
fn label(self) -> &'static str {
match self {
Self::CommandInvoked => "command-invoked",
Self::DangerousSandbox => "dangerous-sandbox",
}
}
}
#[derive(Serialize)]
struct AdminAuditPayload<'a> {
event: &'a str,
sandbox_mode: &'a str,
username: String,
dangerously_bypass_requested: bool,
timestamp: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
justification: Option<&'a str>,
approval_policy: &'a str,
cwd: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
command: Option<&'a [String]>,
}
#[derive(Debug, Clone, Copy)]
struct AdminAuditEventDetails<'a> {
sandbox_mode: &'a str,
dangerously_bypass_requested: bool,
justification: Option<&'a str>,
approval_policy: &'a str,
cwd: &'a str,
command: Option<&'a [String]>,
}
pub async fn maybe_post_admin_audit_events(
admin_controls: &AdminControls,
context: AdminAuditContext<'_>,
) {
let Some(audit) = admin_controls.audit.as_ref() else {
return;
};
if !audit.events.all_commands && !audit.events.dangerous_sandbox {
return;
}
let sandbox_mode = context.sandbox_policy.to_string();
let approval_policy_display = context.approval_policy.to_string();
let cwd_display = context.cwd.display().to_string();
let command_args = context.command;
let client = Client::new();
let event_details = AdminAuditEventDetails {
sandbox_mode: &sandbox_mode,
dangerously_bypass_requested: context.dangerously_bypass_requested,
justification: context.dangerous_mode_justification,
approval_policy: &approval_policy_display,
cwd: &cwd_display,
command: command_args,
};
if audit.events.all_commands && context.record_command_event {
post_admin_audit_event(
&client,
&audit.endpoint,
audit.audit_log_file.as_deref(),
AdminAuditEventKind::CommandInvoked,
event_details,
)
.await;
}
let dangerous_requested = context.dangerously_bypass_requested
|| matches!(context.sandbox_policy, SandboxPolicy::DangerFullAccess);
if dangerous_requested && audit.events.dangerous_sandbox {
post_admin_audit_event(
&client,
&audit.endpoint,
audit.audit_log_file.as_deref(),
AdminAuditEventKind::DangerousSandbox,
event_details,
)
.await;
}
}
pub async fn audit_admin_run_with_prompt(
admin_controls: &AdminControls,
context: AdminAuditContext<'_>,
prompt_result: Result<Option<String>, io::Error>,
) -> Result<Option<String>, io::Error> {
let (justification, prompt_error) = match prompt_result {
Ok(reason) => (reason, None),
Err(err) => (None, Some(err)),
};
maybe_post_admin_audit_events(
admin_controls,
AdminAuditContext {
dangerous_mode_justification: justification.as_deref(),
..context
},
)
.await;
if let Some(err) = prompt_error {
Err(err)
} else {
Ok(justification)
}
}
pub async fn audit_admin_run_without_prompt(
admin_controls: &AdminControls,
context: AdminAuditContext<'_>,
) -> io::Result<()> {
maybe_post_admin_audit_events(admin_controls, context).await;
Ok(())
}
async fn post_admin_audit_event(
client: &Client,
endpoint: &str,
audit_log_file: Option<&Path>,
kind: AdminAuditEventKind,
details: AdminAuditEventDetails<'_>,
) {
let username = whoami::username();
let timestamp_string;
let payload = {
timestamp_string = Utc::now().to_rfc3339();
AdminAuditPayload {
event: kind.label(),
sandbox_mode: details.sandbox_mode,
username,
dangerously_bypass_requested: details.dangerously_bypass_requested,
timestamp: &timestamp_string,
justification: details.justification,
approval_policy: details.approval_policy,
cwd: details.cwd,
command: details.command,
}
};
if let Err(err) = client.post(endpoint).json(&payload).send().await {
tracing::warn!("Failed to POST admin audit event {}: {err}", kind.label());
}
if let Some(path) = audit_log_file
&& let Err(err) = append_admin_audit_log(path, &payload).await
{
tracing::warn!(
"Failed to write admin audit event {} to {}: {err}",
kind.label(),
path.display()
);
}
}
async fn append_admin_audit_log(path: &Path, payload: &AdminAuditPayload<'_>) -> io::Result<()> {
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let mut file = OpenOptions::new()
.create(true)
.write(true)
.append(true)
.open(path)
.await?;
let mut json_line = serde_json::to_vec(payload).map_err(io::Error::other)?;
json_line.push(b'\n');
file.write_all(&json_line).await?;
Ok(())
}

View File

@@ -46,7 +46,9 @@ use crate::apply_patch::convert_apply_patch_to_protocol;
use crate::client::ModelClient;
use crate::client_common::Prompt;
use crate::client_common::ResponseEvent;
use crate::config::AdminAuditContext;
use crate::config::Config;
use crate::config::maybe_post_admin_audit_events;
use crate::config_types::ShellEnvironmentPolicy;
use crate::conversation_history::ConversationHistory;
use crate::environment_context::EnvironmentContext;
@@ -278,6 +280,7 @@ struct State {
pub(crate) struct Session {
conversation_id: ConversationId,
tx_event: Sender<Event>,
config: Arc<Config>,
/// Manager for external MCP servers/tools.
mcp_connection_manager: McpConnectionManager,
@@ -486,6 +489,7 @@ impl Session {
let sess = Arc::new(Session {
conversation_id,
tx_event: tx_event.clone(),
config: config.clone(),
mcp_connection_manager,
session_manager: ExecSessionManager::default(),
unified_exec_manager: UnifiedExecSessionManager::default(),
@@ -854,12 +858,38 @@ impl Session {
self.on_exec_command_begin(turn_diff_tracker, begin_ctx.clone())
.await;
let ExecInvokeArgs {
params,
sandbox_type,
sandbox_policy,
approval_policy,
codex_linux_sandbox_exe,
stdout_stream,
record_admin_command_event,
} = exec_args;
if record_admin_command_event {
maybe_post_admin_audit_events(
&self.config.admin_controls,
AdminAuditContext {
sandbox_policy,
approval_policy,
cwd: &params.cwd,
command: Some(params.command.as_slice()),
dangerously_bypass_requested: matches!(approval_policy, AskForApproval::Never),
dangerous_mode_justification: None,
record_command_event: true,
},
)
.await;
}
let result = process_exec_tool_call(
exec_args.params,
exec_args.sandbox_type,
exec_args.sandbox_policy,
exec_args.codex_linux_sandbox_exe,
exec_args.stdout_stream,
params,
sandbox_type,
sandbox_policy,
codex_linux_sandbox_exe,
stdout_stream,
)
.await;
@@ -2454,8 +2484,10 @@ pub struct ExecInvokeArgs<'a> {
pub params: ExecParams,
pub sandbox_type: SandboxType,
pub sandbox_policy: &'a SandboxPolicy,
pub approval_policy: &'a AskForApproval,
pub codex_linux_sandbox_exe: &'a Option<PathBuf>,
pub stdout_stream: Option<StdoutStream>,
pub record_admin_command_event: bool,
}
fn maybe_translate_shell_command(
@@ -2645,6 +2677,7 @@ async fn handle_container_exec_with_params(
params: params.clone(),
sandbox_type,
sandbox_policy: &turn_context.sandbox_policy,
approval_policy: &turn_context.approval_policy,
codex_linux_sandbox_exe: &sess.codex_linux_sandbox_exe,
stdout_stream: if exec_command_context.apply_patch.is_some() {
None
@@ -2655,6 +2688,7 @@ async fn handle_container_exec_with_params(
tx_event: sess.tx_event.clone(),
})
},
record_admin_command_event: true,
},
)
.await;
@@ -2782,6 +2816,7 @@ async fn handle_sandbox_error(
params,
sandbox_type: SandboxType::None,
sandbox_policy: &turn_context.sandbox_policy,
approval_policy: &turn_context.approval_policy,
codex_linux_sandbox_exe: &sess.codex_linux_sandbox_exe,
stdout_stream: if exec_command_context.apply_patch.is_some() {
None
@@ -2792,6 +2827,7 @@ async fn handle_sandbox_error(
tx_event: sess.tx_event.clone(),
})
},
record_admin_command_event: true,
},
)
.await;

View File

@@ -1,3 +1,5 @@
use crate::admin_controls::ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE;
use crate::admin_controls::AdminControls;
use crate::config_profile::ConfigProfile;
use crate::config_types::History;
use crate::config_types::McpServerConfig;
@@ -181,6 +183,10 @@ pub struct Config {
/// All characters are inserted as they are received, and no buffering
/// or placeholder replacement will occur for fast keypress bursts.
pub disable_paste_burst: bool,
pub admin_controls: AdminControls,
pub admin_danger_prompt: Option<AdminDangerPrompt>,
}
impl Config {
@@ -200,7 +206,7 @@ impl Config {
let codex_home = find_codex_home()?;
// Step 1: parse `config.toml` into a generic JSON value.
let mut root_value = load_config_as_toml(&codex_home)?;
let mut root_value = crate::config_loader::load_config_as_toml(&codex_home)?;
// Step 2: apply the `-c` overrides.
for (path, value) in cli_overrides.into_iter() {
@@ -223,7 +229,7 @@ pub fn load_config_as_toml_with_cli_overrides(
codex_home: &Path,
cli_overrides: Vec<(String, TomlValue)>,
) -> std::io::Result<ConfigToml> {
let mut root_value = load_config_as_toml(codex_home)?;
let mut root_value = crate::config_loader::load_config_as_toml(codex_home)?;
for (path, value) in cli_overrides.into_iter() {
apply_toml_override(&mut root_value, &path, value);
@@ -237,28 +243,12 @@ pub fn load_config_as_toml_with_cli_overrides(
Ok(cfg)
}
/// Read `CODEX_HOME/config.toml` and return it as a generic TOML value. Returns
/// an empty TOML table when the file does not exist.
pub fn load_config_as_toml(codex_home: &Path) -> std::io::Result<TomlValue> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
match std::fs::read_to_string(&config_path) {
Ok(contents) => match toml::from_str::<TomlValue>(&contents) {
Ok(val) => Ok(val),
Err(e) => {
tracing::error!("Failed to parse config.toml: {e}");
Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
},
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
tracing::info!("config.toml not found, using defaults");
Ok(TomlValue::Table(Default::default()))
}
Err(e) => {
tracing::error!("Failed to read config.toml: {e}");
Err(e)
}
}
}
pub use crate::config_loader::load_config_as_toml;
pub use crate::admin_controls::AdminAuditContext;
pub use crate::admin_controls::audit_admin_run_with_prompt;
pub use crate::admin_controls::audit_admin_run_without_prompt;
pub use crate::admin_controls::maybe_post_admin_audit_events;
fn set_project_trusted_inner(doc: &mut DocumentMut, project_path: &Path) -> anyhow::Result<()> {
// Ensure we render a human-friendly structure:
@@ -394,6 +384,10 @@ fn apply_toml_override(root: &mut TomlValue, path: &str, value: TomlValue) {
}
}
pub use crate::admin_controls::AdminAuditEvents;
pub use crate::admin_controls::AdminConfigToml;
pub use crate::admin_controls::AdminDangerPrompt;
/// Base config deserialized from ~/.codex/config.toml.
#[derive(Deserialize, Debug, Clone, Default)]
pub struct ConfigToml {
@@ -497,6 +491,9 @@ pub struct ConfigToml {
/// All characters are inserted as they are received, and no buffering
/// or placeholder replacement will occur for fast keypress bursts.
pub disable_paste_burst: Option<bool>,
/// Administrative controls configured by the enterprise administrator.
pub admin: Option<AdminConfigToml>,
}
impl From<ConfigToml> for UserSavedConfig {
@@ -685,6 +682,44 @@ impl Config {
let sandbox_policy = cfg.derive_sandbox_policy(sandbox_mode);
let admin_controls = AdminControls::from_toml(cfg.admin.clone());
let resolved_approval_policy = approval_policy
.or(config_profile.approval_policy)
.or(cfg.approval_policy)
.unwrap_or_else(AskForApproval::default);
let sandbox_is_dangerous = sandbox_policy.has_full_network_access();
let dangerously_bypass_requested =
matches!(sandbox_policy, SandboxPolicy::DangerFullAccess)
&& resolved_approval_policy == AskForApproval::Never;
let mut admin_danger_prompt = AdminDangerPrompt::default();
if sandbox_is_dangerous && admin_controls.disallow_dangerous_sandbox {
if admin_controls.allow_danger_with_reason {
admin_danger_prompt.sandbox = true;
} else {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE,
));
}
}
if dangerously_bypass_requested
&& admin_controls.disallow_dangerously_bypass_approvals_and_sandbox
{
if admin_controls.allow_danger_with_reason {
admin_danger_prompt.dangerously_bypass = true;
} else {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE,
));
}
}
let mut model_providers = built_in_model_providers();
// Merge user-defined providers into the built-in list.
for (key, provider) in cfg.model_providers.into_iter() {
@@ -789,10 +824,7 @@ impl Config {
model_provider_id,
model_provider,
cwd: resolved_cwd,
approval_policy: approval_policy
.or(config_profile.approval_policy)
.or(cfg.approval_policy)
.unwrap_or_else(AskForApproval::default),
approval_policy: resolved_approval_policy,
sandbox_policy,
shell_environment_policy,
notify: cfg.notify,
@@ -839,6 +871,10 @@ impl Config {
include_view_image_tool,
active_profile: active_profile_name,
disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false),
admin_controls,
admin_danger_prompt: admin_danger_prompt
.needs_prompt()
.then_some(admin_danger_prompt),
};
Ok(config)
}
@@ -1214,6 +1250,8 @@ model_verbosity = "high"
include_view_image_tool: true,
active_profile: Some("o3".to_string()),
disable_paste_burst: false,
admin_controls: AdminControls::default(),
admin_danger_prompt: None,
},
o3_profile_config
);
@@ -1271,6 +1309,8 @@ model_verbosity = "high"
include_view_image_tool: true,
active_profile: Some("gpt3".to_string()),
disable_paste_burst: false,
admin_controls: AdminControls::default(),
admin_danger_prompt: None,
};
assert_eq!(expected_gpt3_profile_config, gpt3_profile_config);
@@ -1343,6 +1383,8 @@ model_verbosity = "high"
include_view_image_tool: true,
active_profile: Some("zdr".to_string()),
disable_paste_burst: false,
admin_controls: AdminControls::default(),
admin_danger_prompt: None,
};
assert_eq!(expected_zdr_profile_config, zdr_profile_config);
@@ -1401,6 +1443,8 @@ model_verbosity = "high"
include_view_image_tool: true,
active_profile: Some("gpt5".to_string()),
disable_paste_burst: false,
admin_controls: AdminControls::default(),
admin_danger_prompt: None,
};
assert_eq!(expected_gpt5_profile_config, gpt5_profile_config);

View File

@@ -0,0 +1,180 @@
#[cfg(target_os = "macos")]
use base64::Engine;
#[cfg(target_os = "macos")]
use base64::prelude::BASE64_STANDARD;
use dirs::home_dir;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::thread;
use toml::Value as TomlValue;
const CONFIG_TOML_FILE: &str = "config.toml";
pub fn load_config_as_toml(codex_home: &Path) -> io::Result<TomlValue> {
let user_config_path = codex_home.join(CONFIG_TOML_FILE);
let global_config_path = home_dir().map(|mut path| {
path.push(".codex");
path.push("global.toml");
path
});
let system_config_path = PathBuf::from("/etc/opt/codex/config.toml");
thread::scope(|scope| {
let user_handle = scope.spawn(|| read_config_from_path(&user_config_path, true));
let global_handle = scope.spawn(move || match global_config_path {
Some(path) => read_config_from_path(&path, false),
None => Ok(None),
});
let system_handle = scope.spawn(move || read_config_from_path(&system_config_path, false));
let managed_handle = scope.spawn(load_managed_admin_config);
let user_config = join_config_result(user_handle, "user config.toml")?;
let global_config = join_config_result(global_handle, "~/.codex/global.toml")?;
let system_config = join_config_result(system_handle, "/etc/opt/codex/config.toml")?;
let managed_config = join_config_result(managed_handle, "managed preferences")?;
let mut merged = user_config.unwrap_or_else(default_empty_table);
for overlay in [global_config, system_config, managed_config]
.into_iter()
.flatten()
{
merge_toml_values(&mut merged, &overlay);
}
Ok(merged)
})
}
fn default_empty_table() -> TomlValue {
TomlValue::Table(Default::default())
}
fn join_config_result(
handle: thread::ScopedJoinHandle<'_, io::Result<Option<TomlValue>>>,
label: &str,
) -> io::Result<Option<TomlValue>> {
match handle.join() {
Ok(result) => result,
Err(panic) => {
if let Some(msg) = panic.downcast_ref::<&str>() {
tracing::error!("Configuration loader for {label} panicked: {msg}");
} else if let Some(msg) = panic.downcast_ref::<String>() {
tracing::error!("Configuration loader for {label} panicked: {msg}");
} else {
tracing::error!("Configuration loader for {label} panicked");
}
Err(io::Error::other(format!(
"Failed to load {label} configuration"
)))
}
}
}
fn read_config_from_path(path: &Path, log_missing_as_info: bool) -> io::Result<Option<TomlValue>> {
match std::fs::read_to_string(path) {
Ok(contents) => match toml::from_str::<TomlValue>(&contents) {
Ok(value) => Ok(Some(value)),
Err(err) => {
tracing::error!("Failed to parse {}: {err}", path.display());
Err(io::Error::new(io::ErrorKind::InvalidData, err))
}
},
Err(err) if err.kind() == io::ErrorKind::NotFound => {
if log_missing_as_info {
tracing::info!("{} not found, using defaults", path.display());
} else {
tracing::debug!("{} not found", path.display());
}
Ok(None)
}
Err(err) => {
tracing::error!("Failed to read {}: {err}", path.display());
Err(err)
}
}
}
fn merge_toml_values(base: &mut TomlValue, overlay: &TomlValue) {
if let TomlValue::Table(overlay_table) = overlay
&& let TomlValue::Table(base_table) = base
{
for (key, value) in overlay_table {
if let Some(existing) = base_table.get_mut(key) {
merge_toml_values(existing, value);
} else {
base_table.insert(key.clone(), value.clone());
}
}
return;
}
*base = overlay.clone();
}
fn load_managed_admin_config() -> io::Result<Option<TomlValue>> {
load_managed_admin_config_impl()
}
#[cfg(target_os = "macos")]
fn load_managed_admin_config_impl() -> io::Result<Option<TomlValue>> {
use core_foundation::base::TCFType;
use core_foundation::string::CFString;
use core_foundation::string::CFStringRef;
use std::ffi::c_void;
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFPreferencesCopyAppValue(key: CFStringRef, application_id: CFStringRef) -> *mut c_void;
}
const MANAGED_PREFERENCES_APPLICATION_ID: &str = "com.openai.codex";
const MANAGED_PREFERENCES_CONFIG_KEY: &str = "config_toml_base64";
let application_id = CFString::new(MANAGED_PREFERENCES_APPLICATION_ID);
let key = CFString::new(MANAGED_PREFERENCES_CONFIG_KEY);
let value_ref = unsafe {
CFPreferencesCopyAppValue(
key.as_concrete_TypeRef(),
application_id.as_concrete_TypeRef(),
)
};
if value_ref.is_null() {
tracing::debug!(
"Managed preferences for {} key {} not found",
MANAGED_PREFERENCES_APPLICATION_ID,
MANAGED_PREFERENCES_CONFIG_KEY
);
return Ok(None);
}
let value = unsafe { CFString::wrap_under_create_rule(value_ref as _) };
let contents = value.to_string();
let trimmed = contents.trim();
let decoded = BASE64_STANDARD.decode(trimmed.as_bytes()).map_err(|err| {
tracing::error!("Failed to decode managed preferences as base64: {err}");
io::Error::new(io::ErrorKind::InvalidData, err)
})?;
let decoded_str = String::from_utf8(decoded).map_err(|err| {
tracing::error!("Managed preferences base64 contents were not valid UTF-8: {err}");
io::Error::new(io::ErrorKind::InvalidData, err)
})?;
match toml::from_str::<TomlValue>(&decoded_str) {
Ok(parsed) => Ok(Some(parsed)),
Err(err) => {
tracing::error!("Failed to parse managed preferences TOML: {err}");
Err(io::Error::new(io::ErrorKind::InvalidData, err))
}
}
}
#[cfg(not(target_os = "macos"))]
fn load_managed_admin_config_impl() -> io::Result<Option<TomlValue>> {
Ok(None)
}

View File

@@ -5,6 +5,7 @@
// the TUI or the tracing stack).
#![deny(clippy::print_stdout, clippy::print_stderr)]
pub mod admin_controls;
mod apply_patch;
pub mod auth;
pub mod bash;
@@ -17,6 +18,7 @@ pub mod token_data;
pub use codex_conversation::CodexConversation;
pub mod config;
pub mod config_edit;
pub mod config_loader;
pub mod config_profile;
pub mod config_types;
mod conversation_history;

22
codex-rs/docs/global.toml Normal file
View File

@@ -0,0 +1,22 @@
# Example administrator configuration applied to every Codex user on this host.
[admin]
# Disallow --sandbox danger-full-access and --dangerously-bypass-approvals-and-sandbox.
disallow_dangerous_sandbox = true
disallow_dangerously_bypass_approvals_and_sandbox = true
# Permit end users to proceed after providing a justification, which will be
# logged via the admin audit hook below.
allow_danger_with_reason = true
[admin.audit]
# Optional webhook to receive audit events whenever Codex commands run.
endpoint = "http://localhost:5000/collect"
# Optional file path to append audit entries as JSON Lines. Paths starting with
# `~/` expand to the current user's home directory.
# audit_log_file = "~/Library/Logs/com.openai.codex/codex_audit.jsonl"
# Record executions that request danger-full-access sandboxing.
dangerous_sandbox = true
# Emit audit entries for every Codex invocation (set to false to log only
# dangerous sandbox usage).
all_commands = true

View File

@@ -12,8 +12,11 @@ use codex_core::AuthManager;
use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID;
use codex_core::ConversationManager;
use codex_core::NewConversation;
use codex_core::admin_controls::ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE;
use codex_core::config::AdminAuditContext;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::audit_admin_run_without_prompt;
use codex_core::git_info::get_git_repo_root;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::Event;
@@ -162,6 +165,30 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
};
let config = Config::load_with_cli_overrides(cli_kv_overrides, overrides)?;
if config
.admin_danger_prompt
.as_ref()
.is_some_and(|prompt| prompt.needs_prompt())
{
eprintln!("{ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE}");
std::process::exit(1);
}
audit_admin_run_without_prompt(
&config.admin_controls,
AdminAuditContext {
sandbox_policy: &config.sandbox_policy,
approval_policy: &config.approval_policy,
cwd: &config.cwd,
command: None,
dangerously_bypass_requested: dangerously_bypass_approvals_and_sandbox,
dangerous_mode_justification: None,
record_command_event: false,
},
)
.await
.map_err(|err| anyhow::anyhow!("{err}"))?;
let mut event_processor: Box<dyn EventProcessor> = if json_mode {
Box::new(EventProcessorWithJsonOutput::new(last_message_file.clone()))
} else {

View File

@@ -10,6 +10,7 @@ use codex_core::Cursor as RolloutCursor;
use codex_core::NewConversation;
use codex_core::RolloutRecorder;
use codex_core::SessionMeta;
use codex_core::admin_controls::ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE;
use codex_core::auth::CLIENT_ID;
use codex_core::auth::get_auth_file;
use codex_core::auth::login_with_api_key;
@@ -24,12 +25,14 @@ use codex_core::exec_env::create_env;
use codex_core::get_platform_sandbox;
use codex_core::git_info::git_diff_to_remote;
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::Event;
use codex_core::protocol::EventMsg;
use codex_core::protocol::ExecApprovalRequestEvent;
use codex_core::protocol::InputItem as CoreInputItem;
use codex_core::protocol::Op;
use codex_core::protocol::ReviewDecision;
use codex_core::protocol::SandboxPolicy;
use codex_login::ServerOptions as LoginServerOptions;
use codex_login::ShutdownHandle;
use codex_login::run_login_server;
@@ -898,6 +901,23 @@ impl CodexMessageProcessor {
})
.collect();
let controls = &self.config.admin_controls;
let sandbox_policy = self.config.sandbox_policy.clone();
let approval_policy = self.config.approval_policy;
let sandbox_is_dangerous = sandbox_policy.has_full_network_access();
let dangerously_bypass_requested =
matches!(sandbox_policy, SandboxPolicy::DangerFullAccess)
&& approval_policy == AskForApproval::Never;
if (sandbox_is_dangerous && controls.disallow_dangerous_sandbox)
|| (dangerously_bypass_requested
&& controls.disallow_dangerously_bypass_approvals_and_sandbox)
{
self.outgoing
.send_admin_warning(Some(&request_id), ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE)
.await;
}
// Submit user input to the conversation.
let _ = conversation
.submit(Op::UserInput {

View File

@@ -12,6 +12,7 @@ use crate::patch_approval::handle_patch_approval_request;
use codex_core::CodexConversation;
use codex_core::ConversationManager;
use codex_core::NewConversation;
use codex_core::admin_controls::ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE;
use codex_core::config::Config as CodexConfig;
use codex_core::protocol::AgentMessageEvent;
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
@@ -44,6 +45,11 @@ pub async fn run_codex_tool_session(
conversation_manager: Arc<ConversationManager>,
running_requests_id_to_codex_uuid: Arc<Mutex<HashMap<RequestId, ConversationId>>>,
) {
let admin_prompt_required = config
.admin_danger_prompt
.as_ref()
.is_some_and(|prompt| prompt.needs_prompt());
let NewConversation {
conversation_id,
conversation,
@@ -77,6 +83,12 @@ pub async fn run_codex_tool_session(
)
.await;
if admin_prompt_required {
outgoing
.send_admin_warning(Some(&id), ADMIN_DANGEROUS_SANDBOX_DISABLED_MESSAGE)
.await;
}
// Use the original MCP request ID as the `sub_id` for the Codex submission so that
// any events emitted for this tool-call can be correlated with the
// originating `tools/call` request.

View File

@@ -6,8 +6,10 @@ use std::io::Result as IoResult;
use std::path::PathBuf;
use codex_common::CliConfigOverrides;
use codex_core::config::AdminAuditContext;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::audit_admin_run_without_prompt;
use mcp_types::JSONRPCMessage;
use tokio::io::AsyncBufReadExt;
@@ -97,6 +99,20 @@ pub async fn run_main(
std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}"))
})?;
audit_admin_run_without_prompt(
&config.admin_controls,
AdminAuditContext {
sandbox_policy: &config.sandbox_policy,
approval_policy: &config.approval_policy,
cwd: &config.cwd,
command: None,
dangerously_bypass_requested: false,
dangerous_mode_justification: None,
record_command_event: false,
},
)
.await?;
// Task: process incoming messages.
let processor_handle = tokio::spawn({
let outgoing_message_sender = OutgoingMessageSender::new(outgoing_tx);

View File

@@ -11,9 +11,14 @@ use mcp_types::JSONRPCMessage;
use mcp_types::JSONRPCNotification;
use mcp_types::JSONRPCRequest;
use mcp_types::JSONRPCResponse;
use mcp_types::LoggingLevel;
use mcp_types::LoggingMessageNotification;
use mcp_types::LoggingMessageNotificationParams;
use mcp_types::ModelContextProtocolNotification;
use mcp_types::RequestId;
use mcp_types::Result;
use serde::Serialize;
use serde_json::json;
use tokio::sync::Mutex;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
@@ -125,6 +130,31 @@ impl OutgoingMessageSender {
.await;
}
pub(crate) async fn send_admin_warning(&self, request_id: Option<&RequestId>, message: &str) {
let data = if let Some(id) = request_id {
json!({
"message": message,
"requestId": id,
})
} else {
json!({ "message": message })
};
let params = serde_json::to_value(LoggingMessageNotificationParams {
data,
level: LoggingLevel::Warning,
logger: Some("codex-admin".to_string()),
})
.ok();
let notification = OutgoingNotification {
method: LoggingMessageNotification::METHOD.to_string(),
params,
};
self.send_notification(notification).await;
}
pub(crate) async fn send_server_notification(&self, notification: ServerNotification) {
let _ = self
.sender

View File

@@ -0,0 +1,65 @@
use codex_core::config::Config;
use std::io::IsTerminal;
use std::io::Write;
use std::io::{self};
pub fn prompt_for_admin_danger_reason(config: &Config) -> std::io::Result<Option<String>> {
let Some(prompt) = config.admin_danger_prompt.as_ref() else {
return Ok(None);
};
if !prompt.needs_prompt() {
return Ok(None);
}
if !io::stdin().is_terminal() {
return Err(io::Error::other(
"Administrator requires a justification for dangerous sandbox usage, but stdin is not interactive.",
));
}
let red = "\x1b[31m";
let reset = "\x1b[0m";
let blue = "\x1b[34m";
let mut stderr = io::stderr();
writeln!(stderr)?;
writeln!(
stderr,
"{red}╔══════════════════════════════════════════════════════════════╗{reset}"
)?;
writeln!(
stderr,
"{red}║ DANGEROUS USAGE WARNING ║{reset}"
)?;
writeln!(
stderr,
"{red}╚══════════════════════════════════════════════════════════════╝{reset}"
)?;
for line in codex_core::admin_controls::ADMIN_DANGEROUS_SANDBOX_DISABLED_PROMPT_LINES {
writeln!(stderr, "{red}{line}{reset}")?;
}
writeln!(stderr)?;
write!(
stderr,
"{red}?{reset} {blue}Or provide a justification to continue anyway:{reset} "
)?;
stderr.flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let justification = input.trim();
if justification.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Justification is required by your administrator to proceed with dangerous usage.",
));
}
if justification.len() < 4 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Justification must be at least 4 characters long and is required by your administrator to proceed with dangerous usage.",
));
}
Ok(Some(justification.to_string()))
}

View File

@@ -8,9 +8,11 @@ use codex_core::AuthManager;
use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID;
use codex_core::CodexAuth;
use codex_core::RolloutRecorder;
use codex_core::config::AdminAuditContext;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::ConfigToml;
use codex_core::config::audit_admin_run_with_prompt;
use codex_core::config::find_codex_home;
use codex_core::config::load_config_as_toml_with_cli_overrides;
use codex_core::protocol::AskForApproval;
@@ -25,6 +27,7 @@ use tracing_appender::non_blocking;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::prelude::*;
mod admin_prompt;
mod app;
mod app_backtrack;
mod app_event;
@@ -67,6 +70,7 @@ mod updates;
pub use cli::Cli;
use crate::admin_prompt::prompt_for_admin_danger_reason;
use crate::onboarding::TrustDirectorySelection;
use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
use crate::onboarding::onboarding_screen::run_onboarding_app;
@@ -154,6 +158,24 @@ pub async fn run_main(
}
};
let admin_audit_context = AdminAuditContext {
sandbox_policy: &config.sandbox_policy,
approval_policy: &config.approval_policy,
cwd: &config.cwd,
command: None,
dangerously_bypass_requested: cli.dangerously_bypass_approvals_and_sandbox,
dangerous_mode_justification: None,
record_command_event: false,
};
let _admin_override_reason = audit_admin_run_with_prompt(
&config.admin_controls,
admin_audit_context,
prompt_for_admin_danger_reason(&config),
)
.await
.map_err(|err| std::io::Error::other(err.to_string()))?;
// we load config.toml here to determine project state.
#[allow(clippy::print_stderr)]
let config_toml = {