mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
12 Commits
dh--git-in
...
add-enterp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c3874d5fd | ||
|
|
23b4a8c027 | ||
|
|
1bafe46731 | ||
|
|
3e773fbcfc | ||
|
|
82982f1466 | ||
|
|
8d9369a650 | ||
|
|
4a6a63d6c4 | ||
|
|
6e772e993a | ||
|
|
848bc6c94a | ||
|
|
eab3d8e328 | ||
|
|
609d69d3ba | ||
|
|
dd2c9ed676 |
20
codex-rs/Cargo.lock
generated
20
codex-rs/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
348
codex-rs/core/src/admin_controls.rs
Normal file
348
codex-rs/core/src/admin_controls.rs
Normal 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: ×tamp_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(())
|
||||
}
|
||||
@@ -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: ¶ms.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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
180
codex-rs/core/src/config_loader.rs
Normal file
180
codex-rs/core/src/config_loader.rs
Normal 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)
|
||||
}
|
||||
@@ -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
22
codex-rs/docs/global.toml
Normal 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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
65
codex-rs/tui/src/admin_prompt.rs
Normal file
65
codex-rs/tui/src/admin_prompt.rs
Normal 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()))
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user