mirror of
https://github.com/openai/codex.git
synced 2026-04-28 08:34:54 +00:00
1381 lines
49 KiB
Markdown
1381 lines
49 KiB
Markdown
# PR #1929: [config] Onboarding flow with persistence
|
||
|
||
- URL: https://github.com/openai/codex/pull/1929
|
||
- Author: dylan-hurd-oai
|
||
- Created: 2025-08-07 07:20:33 UTC
|
||
- Updated: 2025-08-07 16:27:47 UTC
|
||
- Changes: +434/-189, Files changed: 13, Commits: 20
|
||
|
||
## Description
|
||
|
||
## Summary
|
||
In collaboration with @gpeal: upgrade the onboarding flow, and persist user settings.
|
||
|
||
## Testing
|
||
Tested a few scenarios locally
|
||
|
||
## Full Diff
|
||
|
||
```diff
|
||
diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock
|
||
index eabd9f35db..4eddf7bd7b 100644
|
||
--- a/codex-rs/Cargo.lock
|
||
+++ b/codex-rs/Cargo.lock
|
||
@@ -708,6 +708,7 @@ dependencies = [
|
||
"tokio-test",
|
||
"tokio-util",
|
||
"toml 0.9.4",
|
||
+ "toml_edit 0.23.3",
|
||
"tracing",
|
||
"tree-sitter",
|
||
"tree-sitter-bash",
|
||
@@ -3273,7 +3274,7 @@ version = "3.3.0"
|
||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
|
||
dependencies = [
|
||
- "toml_edit",
|
||
+ "toml_edit 0.22.27",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -4800,7 +4801,7 @@ dependencies = [
|
||
"serde",
|
||
"serde_spanned 0.6.9",
|
||
"toml_datetime 0.6.11",
|
||
- "toml_edit",
|
||
+ "toml_edit 0.22.27",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -4849,11 +4850,24 @@ dependencies = [
|
||
"winnow",
|
||
]
|
||
|
||
+[[package]]
|
||
+name = "toml_edit"
|
||
+version = "0.23.3"
|
||
+source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
+checksum = "17d3b47e6b7a040216ae5302712c94d1cf88c95b47efa80e2c59ce96c878267e"
|
||
+dependencies = [
|
||
+ "indexmap 2.10.0",
|
||
+ "toml_datetime 0.7.0",
|
||
+ "toml_parser",
|
||
+ "toml_writer",
|
||
+ "winnow",
|
||
+]
|
||
+
|
||
[[package]]
|
||
name = "toml_parser"
|
||
-version = "1.0.1"
|
||
+version = "1.0.2"
|
||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
-checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
|
||
+checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
|
||
dependencies = [
|
||
"winnow",
|
||
]
|
||
diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml
|
||
index e9d6970ded..006a218abf 100644
|
||
--- a/codex-rs/core/Cargo.toml
|
||
+++ b/codex-rs/core/Cargo.toml
|
||
@@ -36,6 +36,7 @@ sha1 = "0.10.6"
|
||
shlex = "1.3.0"
|
||
similar = "2.7.0"
|
||
strum_macros = "0.27.2"
|
||
+tempfile = "3"
|
||
thiserror = "2.0.12"
|
||
time = { version = "0.3", features = ["formatting", "local-offset", "macros"] }
|
||
tokio = { version = "1", features = [
|
||
@@ -47,6 +48,7 @@ tokio = { version = "1", features = [
|
||
] }
|
||
tokio-util = "0.7.14"
|
||
toml = "0.9.4"
|
||
+toml_edit = "0.23.3"
|
||
tracing = { version = "0.1.41", features = ["log"] }
|
||
tree-sitter = "0.25.8"
|
||
tree-sitter-bash = "0.25.0"
|
||
diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs
|
||
index 081306dab1..723ee5f817 100644
|
||
--- a/codex-rs/core/src/config.rs
|
||
+++ b/codex-rs/core/src/config.rs
|
||
@@ -22,13 +22,17 @@ use serde::Deserialize;
|
||
use std::collections::HashMap;
|
||
use std::path::Path;
|
||
use std::path::PathBuf;
|
||
+use tempfile::NamedTempFile;
|
||
use toml::Value as TomlValue;
|
||
+use toml_edit::DocumentMut;
|
||
|
||
/// Maximum number of bytes of the documentation that will be embedded. Larger
|
||
/// files are *silently truncated* to this size so we do not take up too much of
|
||
/// the context window.
|
||
pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB
|
||
|
||
+const CONFIG_TOML_FILE: &str = "config.toml";
|
||
+
|
||
/// Application configuration loaded from disk and merged with overrides.
|
||
#[derive(Debug, Clone, PartialEq)]
|
||
pub struct Config {
|
||
@@ -191,10 +195,28 @@ impl Config {
|
||
}
|
||
}
|
||
|
||
+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)?;
|
||
+
|
||
+ for (path, value) in cli_overrides.into_iter() {
|
||
+ apply_toml_override(&mut root_value, &path, value);
|
||
+ }
|
||
+
|
||
+ let cfg: ConfigToml = root_value.try_into().map_err(|e| {
|
||
+ tracing::error!("Failed to deserialize overridden config: {e}");
|
||
+ std::io::Error::new(std::io::ErrorKind::InvalidData, e)
|
||
+ })?;
|
||
+
|
||
+ 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.
|
||
-fn load_config_as_toml(codex_home: &Path) -> std::io::Result<TomlValue> {
|
||
- let config_path = codex_home.join("config.toml");
|
||
+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),
|
||
@@ -214,6 +236,35 @@ fn load_config_as_toml(codex_home: &Path) -> std::io::Result<TomlValue> {
|
||
}
|
||
}
|
||
|
||
+/// Patch `CODEX_HOME/config.toml` project state.
|
||
+/// Use with caution.
|
||
+pub fn set_project_trusted(codex_home: &Path, project_path: &Path) -> anyhow::Result<()> {
|
||
+ let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||
+ // Parse existing config if present; otherwise start a new document.
|
||
+ let mut doc = match std::fs::read_to_string(config_path.clone()) {
|
||
+ Ok(s) => s.parse::<DocumentMut>()?,
|
||
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
|
||
+ Err(e) => return Err(e.into()),
|
||
+ };
|
||
+
|
||
+ // Mark the project as trusted. toml_edit is very good at handling
|
||
+ // missing properties
|
||
+ let project_key = project_path.to_string_lossy().to_string();
|
||
+ doc["projects"][project_key.as_str()]["trust_level"] = toml_edit::value("trusted");
|
||
+
|
||
+ // ensure codex_home exists
|
||
+ std::fs::create_dir_all(codex_home)?;
|
||
+
|
||
+ // create a tmp_file
|
||
+ let tmp_file = NamedTempFile::new_in(codex_home)?;
|
||
+ std::fs::write(tmp_file.path(), doc.to_string())?;
|
||
+
|
||
+ // atomically move the tmp file into config.toml
|
||
+ tmp_file.persist(config_path)?;
|
||
+
|
||
+ Ok(())
|
||
+}
|
||
+
|
||
/// Apply a single dotted-path override onto a TOML value.
|
||
fn apply_toml_override(root: &mut TomlValue, path: &str, value: TomlValue) {
|
||
use toml::value::Table;
|
||
@@ -350,6 +401,13 @@ pub struct ConfigToml {
|
||
|
||
/// The value for the `originator` header included with Responses API requests.
|
||
pub internal_originator: Option<String>,
|
||
+
|
||
+ pub projects: Option<HashMap<String, ProjectConfig>>,
|
||
+}
|
||
+
|
||
+#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
|
||
+pub struct ProjectConfig {
|
||
+ pub trust_level: Option<String>,
|
||
}
|
||
|
||
impl ConfigToml {
|
||
@@ -377,6 +435,36 @@ impl ConfigToml {
|
||
SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess,
|
||
}
|
||
}
|
||
+
|
||
+ pub fn is_cwd_trusted(&self, resolved_cwd: &Path) -> bool {
|
||
+ let projects = self.projects.clone().unwrap_or_default();
|
||
+
|
||
+ projects
|
||
+ .get(&resolved_cwd.to_string_lossy().to_string())
|
||
+ .map(|p| p.trust_level.clone().unwrap_or("".to_string()) == "trusted")
|
||
+ .unwrap_or(false)
|
||
+ }
|
||
+
|
||
+ pub fn get_config_profile(
|
||
+ &self,
|
||
+ override_profile: Option<String>,
|
||
+ ) -> Result<ConfigProfile, std::io::Error> {
|
||
+ let profile = override_profile.or_else(|| self.profile.clone());
|
||
+
|
||
+ match profile {
|
||
+ Some(key) => {
|
||
+ if let Some(profile) = self.profiles.get(key.as_str()) {
|
||
+ return Ok(profile.clone());
|
||
+ }
|
||
+
|
||
+ Err(std::io::Error::new(
|
||
+ std::io::ErrorKind::NotFound,
|
||
+ format!("config profile `{key}` not found"),
|
||
+ ))
|
||
+ }
|
||
+ None => Ok(ConfigProfile::default()),
|
||
+ }
|
||
+ }
|
||
}
|
||
|
||
/// Optional overrides for user configuration (e.g., from CLI flags).
|
||
diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs
|
||
index c789798bcd..9008ad307d 100644
|
||
--- a/codex-rs/core/src/protocol.rs
|
||
+++ b/codex-rs/core/src/protocol.rs
|
||
@@ -139,7 +139,6 @@ pub enum AskForApproval {
|
||
/// Under this policy, only "known safe" commands—as determined by
|
||
/// `is_safe_command()`—that **only read files** are auto‑approved.
|
||
/// Everything else will ask the user to approve.
|
||
- #[default]
|
||
#[serde(rename = "untrusted")]
|
||
#[strum(serialize = "untrusted")]
|
||
UnlessTrusted,
|
||
@@ -151,6 +150,7 @@ pub enum AskForApproval {
|
||
OnFailure,
|
||
|
||
/// The model decides when to ask the user for approval.
|
||
+ #[default]
|
||
OnRequest,
|
||
|
||
/// Never ask the user to approve commands. Failures are immediately returned
|
||
diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs
|
||
index 5d7f1281ee..6ed57898b2 100644
|
||
--- a/codex-rs/exec/src/lib.rs
|
||
+++ b/codex-rs/exec/src/lib.rs
|
||
@@ -181,7 +181,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
|
||
event_processor.print_config_summary(&config, &prompt);
|
||
|
||
if !skip_git_repo_check && !is_inside_git_repo(&config.cwd.to_path_buf()) {
|
||
- eprintln!("Not inside a Git repo and --skip-git-repo-check was not specified.");
|
||
+ eprintln!("Not inside a trusted directory and --skip-git-repo-check was not specified.");
|
||
std::process::exit(1);
|
||
}
|
||
|
||
diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs
|
||
index 1ba8883b0b..d71a331e3b 100644
|
||
--- a/codex-rs/tui/src/app.rs
|
||
+++ b/codex-rs/tui/src/app.rs
|
||
@@ -13,7 +13,6 @@ use codex_core::config::Config;
|
||
use codex_core::protocol::Event;
|
||
use codex_core::protocol::EventMsg;
|
||
use codex_core::protocol::Op;
|
||
-use codex_core::util::is_inside_git_repo;
|
||
use color_eyre::eyre::Result;
|
||
use crossterm::SynchronizedUpdate;
|
||
use crossterm::event::KeyCode;
|
||
@@ -71,7 +70,7 @@ pub(crate) struct App<'a> {
|
||
/// deferred until after the Git warning screen is dismissed.
|
||
#[derive(Clone, Debug)]
|
||
pub(crate) struct ChatWidgetArgs {
|
||
- config: Config,
|
||
+ pub(crate) config: Config,
|
||
initial_prompt: Option<String>,
|
||
initial_images: Vec<PathBuf>,
|
||
enhanced_keys_supported: bool,
|
||
@@ -81,8 +80,8 @@ impl App<'_> {
|
||
pub(crate) fn new(
|
||
config: Config,
|
||
initial_prompt: Option<String>,
|
||
- skip_git_repo_check: bool,
|
||
initial_images: Vec<std::path::PathBuf>,
|
||
+ show_trust_screen: bool,
|
||
) -> Self {
|
||
let (app_event_tx, app_event_rx) = channel();
|
||
let app_event_tx = AppEventSender::new(app_event_tx);
|
||
@@ -134,9 +133,7 @@ impl App<'_> {
|
||
}
|
||
|
||
let show_login_screen = should_show_login_screen(&config);
|
||
- let show_git_warning =
|
||
- !skip_git_repo_check && !is_inside_git_repo(&config.cwd.to_path_buf());
|
||
- let app_state = if show_login_screen || show_git_warning {
|
||
+ let app_state = if show_login_screen || show_trust_screen {
|
||
let chat_widget_args = ChatWidgetArgs {
|
||
config: config.clone(),
|
||
initial_prompt,
|
||
@@ -149,7 +146,7 @@ impl App<'_> {
|
||
codex_home: config.codex_home.clone(),
|
||
cwd: config.cwd.clone(),
|
||
show_login_screen,
|
||
- show_git_warning,
|
||
+ show_trust_screen,
|
||
chat_widget_args,
|
||
}),
|
||
}
|
||
diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs
|
||
index 078936dc33..91ee9cfdc7 100644
|
||
--- a/codex-rs/tui/src/cli.rs
|
||
+++ b/codex-rs/tui/src/cli.rs
|
||
@@ -54,10 +54,6 @@ pub struct Cli {
|
||
#[clap(long = "cd", short = 'C', value_name = "DIR")]
|
||
pub cwd: Option<PathBuf>,
|
||
|
||
- /// Allow running Codex outside a Git repository.
|
||
- #[arg(long = "skip-git-repo-check", default_value_t = false)]
|
||
- pub skip_git_repo_check: bool,
|
||
-
|
||
#[clap(skip)]
|
||
pub config_overrides: CliConfigOverrides,
|
||
}
|
||
diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs
|
||
index 0e809afdbe..057d25168b 100644
|
||
--- a/codex-rs/tui/src/lib.rs
|
||
+++ b/codex-rs/tui/src/lib.rs
|
||
@@ -6,8 +6,12 @@ use app::App;
|
||
use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID;
|
||
use codex_core::config::Config;
|
||
use codex_core::config::ConfigOverrides;
|
||
+use codex_core::config::ConfigToml;
|
||
+use codex_core::config::find_codex_home;
|
||
+use codex_core::config::load_config_as_toml_with_cli_overrides;
|
||
use codex_core::config_types::SandboxMode;
|
||
use codex_core::protocol::AskForApproval;
|
||
+use codex_core::protocol::SandboxPolicy;
|
||
use codex_login::load_auth;
|
||
use codex_ollama::DEFAULT_OSS_MODEL;
|
||
use log_layer::TuiLogLayer;
|
||
@@ -89,33 +93,38 @@ pub async fn run_main(
|
||
None
|
||
};
|
||
|
||
- let config = {
|
||
+ // canonicalize the cwd
|
||
+ let cwd = cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p));
|
||
+
|
||
+ let overrides = ConfigOverrides {
|
||
+ model,
|
||
+ approval_policy,
|
||
+ sandbox_mode,
|
||
+ cwd,
|
||
+ model_provider: model_provider_override,
|
||
+ config_profile: cli.config_profile.clone(),
|
||
+ codex_linux_sandbox_exe,
|
||
+ base_instructions: None,
|
||
+ include_plan_tool: Some(true),
|
||
+ disable_response_storage: cli.oss.then_some(true),
|
||
+ show_raw_agent_reasoning: cli.oss.then_some(true),
|
||
+ };
|
||
+
|
||
+ // Parse `-c` overrides from the CLI.
|
||
+ let cli_kv_overrides = match cli.config_overrides.parse_overrides() {
|
||
+ Ok(v) => v,
|
||
+ #[allow(clippy::print_stderr)]
|
||
+ Err(e) => {
|
||
+ eprintln!("Error parsing -c overrides: {e}");
|
||
+ std::process::exit(1);
|
||
+ }
|
||
+ };
|
||
+
|
||
+ let mut config = {
|
||
// Load configuration and support CLI overrides.
|
||
- let overrides = ConfigOverrides {
|
||
- model,
|
||
- approval_policy,
|
||
- sandbox_mode,
|
||
- cwd: cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)),
|
||
- model_provider: model_provider_override,
|
||
- config_profile: cli.config_profile.clone(),
|
||
- codex_linux_sandbox_exe,
|
||
- base_instructions: None,
|
||
- include_plan_tool: Some(true),
|
||
- disable_response_storage: cli.oss.then_some(true),
|
||
- show_raw_agent_reasoning: cli.oss.then_some(true),
|
||
- };
|
||
- // Parse `-c` overrides from the CLI.
|
||
- let cli_kv_overrides = match cli.config_overrides.parse_overrides() {
|
||
- Ok(v) => v,
|
||
- #[allow(clippy::print_stderr)]
|
||
- Err(e) => {
|
||
- eprintln!("Error parsing -c overrides: {e}");
|
||
- std::process::exit(1);
|
||
- }
|
||
- };
|
||
|
||
#[allow(clippy::print_stderr)]
|
||
- match Config::load_with_cli_overrides(cli_kv_overrides, overrides) {
|
||
+ match Config::load_with_cli_overrides(cli_kv_overrides.clone(), overrides) {
|
||
Ok(config) => config,
|
||
Err(err) => {
|
||
eprintln!("Error loading configuration: {err}");
|
||
@@ -124,6 +133,34 @@ pub async fn run_main(
|
||
}
|
||
};
|
||
|
||
+ // we load config.toml here to determine project state.
|
||
+ #[allow(clippy::print_stderr)]
|
||
+ let config_toml = {
|
||
+ let codex_home = match find_codex_home() {
|
||
+ Ok(codex_home) => codex_home,
|
||
+ Err(err) => {
|
||
+ eprintln!("Error finding codex home: {err}");
|
||
+ std::process::exit(1);
|
||
+ }
|
||
+ };
|
||
+
|
||
+ match load_config_as_toml_with_cli_overrides(&codex_home, cli_kv_overrides) {
|
||
+ Ok(config_toml) => config_toml,
|
||
+ Err(err) => {
|
||
+ eprintln!("Error loading config.toml: {err}");
|
||
+ std::process::exit(1);
|
||
+ }
|
||
+ }
|
||
+ };
|
||
+
|
||
+ let should_show_trust_screen = determine_repo_trust_state(
|
||
+ &mut config,
|
||
+ &config_toml,
|
||
+ approval_policy,
|
||
+ sandbox_mode,
|
||
+ cli.config_profile.clone(),
|
||
+ )?;
|
||
+
|
||
let log_dir = codex_core::config::log_dir(&config)?;
|
||
std::fs::create_dir_all(&log_dir)?;
|
||
// Open (or create) your log file, appending to it.
|
||
@@ -204,12 +241,14 @@ pub async fn run_main(
|
||
eprintln!("");
|
||
}
|
||
|
||
- run_ratatui_app(cli, config, log_rx).map_err(|err| std::io::Error::other(err.to_string()))
|
||
+ run_ratatui_app(cli, config, should_show_trust_screen, log_rx)
|
||
+ .map_err(|err| std::io::Error::other(err.to_string()))
|
||
}
|
||
|
||
fn run_ratatui_app(
|
||
cli: Cli,
|
||
config: Config,
|
||
+ should_show_trust_screen: bool,
|
||
mut log_rx: tokio::sync::mpsc::UnboundedReceiver<String>,
|
||
) -> color_eyre::Result<codex_core::protocol::TokenUsage> {
|
||
color_eyre::install()?;
|
||
@@ -227,7 +266,7 @@ fn run_ratatui_app(
|
||
terminal.clear()?;
|
||
|
||
let Cli { prompt, images, .. } = cli;
|
||
- let mut app = App::new(config.clone(), prompt, cli.skip_git_repo_check, images);
|
||
+ let mut app = App::new(config.clone(), prompt, images, should_show_trust_screen);
|
||
|
||
// Bridge log receiver into the AppEvent channel so latest log lines update the UI.
|
||
{
|
||
@@ -277,3 +316,39 @@ fn should_show_login_screen(config: &Config) -> bool {
|
||
false
|
||
}
|
||
}
|
||
+
|
||
+/// Determine if user has configured a sandbox / approval policy,
|
||
+/// or if the current cwd project is trusted, and updates the config
|
||
+/// accordingly.
|
||
+fn determine_repo_trust_state(
|
||
+ config: &mut Config,
|
||
+ config_toml: &ConfigToml,
|
||
+ approval_policy_overide: Option<AskForApproval>,
|
||
+ sandbox_mode_override: Option<SandboxMode>,
|
||
+ config_profile_override: Option<String>,
|
||
+) -> std::io::Result<bool> {
|
||
+ let config_profile = config_toml.get_config_profile(config_profile_override)?;
|
||
+
|
||
+ if approval_policy_overide.is_some() || sandbox_mode_override.is_some() {
|
||
+ // if the user has overridden either approval policy or sandbox mode,
|
||
+ // skip the trust flow
|
||
+ Ok(false)
|
||
+ } else if config_profile.approval_policy.is_some() {
|
||
+ // if the user has specified settings in a config profile, skip the trust flow
|
||
+ // todo: profile sandbox mode?
|
||
+ Ok(false)
|
||
+ } else if config_toml.approval_policy.is_some() || config_toml.sandbox_mode.is_some() {
|
||
+ // if the user has specified either approval policy or sandbox mode in config.toml
|
||
+ // skip the trust flow
|
||
+ Ok(false)
|
||
+ } else if config_toml.is_cwd_trusted(&config.cwd) {
|
||
+ // if the current cwd project is trusted and no config has been set
|
||
+ // skip the trust flow and set the approval policy and sandbox mode
|
||
+ config.approval_policy = AskForApproval::OnRequest;
|
||
+ config.sandbox_policy = SandboxPolicy::new_workspace_write_policy();
|
||
+ Ok(false)
|
||
+ } else {
|
||
+ // if none of the above conditions are met, show the trust screen
|
||
+ Ok(true)
|
||
+ }
|
||
+}
|
||
diff --git a/codex-rs/tui/src/onboarding/continue_to_chat.rs b/codex-rs/tui/src/onboarding/continue_to_chat.rs
|
||
index 071d0851da..01e31d900a 100644
|
||
--- a/codex-rs/tui/src/onboarding/continue_to_chat.rs
|
||
+++ b/codex-rs/tui/src/onboarding/continue_to_chat.rs
|
||
@@ -8,12 +8,14 @@ use crate::app_event_sender::AppEventSender;
|
||
use crate::onboarding::onboarding_screen::StepStateProvider;
|
||
|
||
use super::onboarding_screen::StepState;
|
||
+use std::sync::Arc;
|
||
+use std::sync::Mutex;
|
||
|
||
/// This doesn't render anything explicitly but serves as a signal that we made it to the end and
|
||
/// we should continue to the chat.
|
||
pub(crate) struct ContinueToChatWidget {
|
||
pub event_tx: AppEventSender,
|
||
- pub chat_widget_args: ChatWidgetArgs,
|
||
+ pub chat_widget_args: Arc<Mutex<ChatWidgetArgs>>,
|
||
}
|
||
|
||
impl StepStateProvider for ContinueToChatWidget {
|
||
@@ -24,7 +26,9 @@ impl StepStateProvider for ContinueToChatWidget {
|
||
|
||
impl WidgetRef for &ContinueToChatWidget {
|
||
fn render_ref(&self, _area: Rect, _buf: &mut Buffer) {
|
||
- self.event_tx
|
||
- .send(AppEvent::OnboardingComplete(self.chat_widget_args.clone()));
|
||
+ if let Ok(args) = self.chat_widget_args.lock() {
|
||
+ self.event_tx
|
||
+ .send(AppEvent::OnboardingComplete(args.clone()));
|
||
+ }
|
||
}
|
||
}
|
||
diff --git a/codex-rs/tui/src/onboarding/git_warning.rs b/codex-rs/tui/src/onboarding/git_warning.rs
|
||
deleted file mode 100644
|
||
index e4e5747404..0000000000
|
||
--- a/codex-rs/tui/src/onboarding/git_warning.rs
|
||
+++ /dev/null
|
||
@@ -1,126 +0,0 @@
|
||
-use std::path::PathBuf;
|
||
-
|
||
-use codex_core::util::is_inside_git_repo;
|
||
-use crossterm::event::KeyCode;
|
||
-use crossterm::event::KeyEvent;
|
||
-use ratatui::buffer::Buffer;
|
||
-use ratatui::layout::Rect;
|
||
-use ratatui::prelude::Widget;
|
||
-use ratatui::style::Modifier;
|
||
-use ratatui::style::Style;
|
||
-use ratatui::style::Stylize;
|
||
-use ratatui::text::Line;
|
||
-use ratatui::text::Span;
|
||
-use ratatui::widgets::Paragraph;
|
||
-use ratatui::widgets::WidgetRef;
|
||
-use ratatui::widgets::Wrap;
|
||
-
|
||
-use crate::app_event::AppEvent;
|
||
-use crate::app_event_sender::AppEventSender;
|
||
-use crate::colors::LIGHT_BLUE;
|
||
-
|
||
-use crate::onboarding::onboarding_screen::KeyboardHandler;
|
||
-use crate::onboarding::onboarding_screen::StepStateProvider;
|
||
-
|
||
-use super::onboarding_screen::StepState;
|
||
-
|
||
-pub(crate) struct GitWarningWidget {
|
||
- pub event_tx: AppEventSender,
|
||
- pub cwd: PathBuf,
|
||
- pub selection: Option<GitWarningSelection>,
|
||
- pub highlighted: GitWarningSelection,
|
||
-}
|
||
-
|
||
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
-pub(crate) enum GitWarningSelection {
|
||
- Continue,
|
||
- Exit,
|
||
-}
|
||
-
|
||
-impl WidgetRef for &GitWarningWidget {
|
||
- fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||
- let mut lines: Vec<Line> = vec![
|
||
- Line::from(vec![
|
||
- Span::raw("> "),
|
||
- Span::raw("You are running Codex in "),
|
||
- Span::styled(
|
||
- self.cwd.to_string_lossy().to_string(),
|
||
- Style::default().add_modifier(Modifier::BOLD),
|
||
- ),
|
||
- Span::raw(". This folder is not version controlled."),
|
||
- ]),
|
||
- Line::from(""),
|
||
- Line::from(" Do you want to continue?"),
|
||
- Line::from(""),
|
||
- ];
|
||
-
|
||
- let create_option =
|
||
- |idx: usize, option: GitWarningSelection, text: &str| -> Line<'static> {
|
||
- let is_selected = self.highlighted == option;
|
||
- if is_selected {
|
||
- Line::from(vec![
|
||
- Span::styled(
|
||
- format!("> {}. ", idx + 1),
|
||
- Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM),
|
||
- ),
|
||
- Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)),
|
||
- ])
|
||
- } else {
|
||
- Line::from(format!(" {}. {}", idx + 1, text))
|
||
- }
|
||
- };
|
||
-
|
||
- lines.push(create_option(0, GitWarningSelection::Continue, "Yes"));
|
||
- lines.push(create_option(1, GitWarningSelection::Exit, "No"));
|
||
- lines.push(Line::from(""));
|
||
- lines.push(Line::from(" Press Enter to continue").add_modifier(Modifier::DIM));
|
||
-
|
||
- Paragraph::new(lines)
|
||
- .wrap(Wrap { trim: false })
|
||
- .render(area, buf);
|
||
- }
|
||
-}
|
||
-
|
||
-impl KeyboardHandler for GitWarningWidget {
|
||
- fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||
- match key_event.code {
|
||
- KeyCode::Up | KeyCode::Char('k') => {
|
||
- self.highlighted = GitWarningSelection::Continue;
|
||
- }
|
||
- KeyCode::Down | KeyCode::Char('j') => {
|
||
- self.highlighted = GitWarningSelection::Exit;
|
||
- }
|
||
- KeyCode::Char('1') => self.handle_continue(),
|
||
- KeyCode::Char('2') => self.handle_quit(),
|
||
- KeyCode::Enter => match self.highlighted {
|
||
- GitWarningSelection::Continue => self.handle_continue(),
|
||
- GitWarningSelection::Exit => self.handle_quit(),
|
||
- },
|
||
- _ => {}
|
||
- }
|
||
- }
|
||
-}
|
||
-
|
||
-impl StepStateProvider for GitWarningWidget {
|
||
- fn get_step_state(&self) -> StepState {
|
||
- let is_git_repo = is_inside_git_repo(&self.cwd);
|
||
- match is_git_repo {
|
||
- true => StepState::Hidden,
|
||
- false => match self.selection {
|
||
- Some(_) => StepState::Complete,
|
||
- None => StepState::InProgress,
|
||
- },
|
||
- }
|
||
- }
|
||
-}
|
||
-
|
||
-impl GitWarningWidget {
|
||
- fn handle_continue(&mut self) {
|
||
- self.selection = Some(GitWarningSelection::Continue);
|
||
- }
|
||
-
|
||
- fn handle_quit(&mut self) {
|
||
- self.highlighted = GitWarningSelection::Exit;
|
||
- self.event_tx.send(AppEvent::ExitRequest);
|
||
- }
|
||
-}
|
||
diff --git a/codex-rs/tui/src/onboarding/mod.rs b/codex-rs/tui/src/onboarding/mod.rs
|
||
index 645cda22d9..c116936851 100644
|
||
--- a/codex-rs/tui/src/onboarding/mod.rs
|
||
+++ b/codex-rs/tui/src/onboarding/mod.rs
|
||
@@ -1,5 +1,5 @@
|
||
mod auth;
|
||
mod continue_to_chat;
|
||
-mod git_warning;
|
||
pub mod onboarding_screen;
|
||
+mod trust_directory;
|
||
mod welcome;
|
||
diff --git a/codex-rs/tui/src/onboarding/onboarding_screen.rs b/codex-rs/tui/src/onboarding/onboarding_screen.rs
|
||
index 7ce7d16c47..a104f777c2 100644
|
||
--- a/codex-rs/tui/src/onboarding/onboarding_screen.rs
|
||
+++ b/codex-rs/tui/src/onboarding/onboarding_screen.rs
|
||
@@ -1,3 +1,4 @@
|
||
+use codex_core::util::is_inside_git_repo;
|
||
use crossterm::event::KeyEvent;
|
||
use ratatui::buffer::Buffer;
|
||
use ratatui::layout::Rect;
|
||
@@ -11,16 +12,18 @@ use crate::app_event_sender::AppEventSender;
|
||
use crate::onboarding::auth::AuthModeWidget;
|
||
use crate::onboarding::auth::SignInState;
|
||
use crate::onboarding::continue_to_chat::ContinueToChatWidget;
|
||
-use crate::onboarding::git_warning::GitWarningSelection;
|
||
-use crate::onboarding::git_warning::GitWarningWidget;
|
||
+use crate::onboarding::trust_directory::TrustDirectorySelection;
|
||
+use crate::onboarding::trust_directory::TrustDirectoryWidget;
|
||
use crate::onboarding::welcome::WelcomeWidget;
|
||
use std::path::PathBuf;
|
||
+use std::sync::Arc;
|
||
+use std::sync::Mutex;
|
||
|
||
#[allow(clippy::large_enum_variant)]
|
||
enum Step {
|
||
Welcome(WelcomeWidget),
|
||
Auth(AuthModeWidget),
|
||
- GitWarning(GitWarningWidget),
|
||
+ TrustDirectory(TrustDirectoryWidget),
|
||
ContinueToChat(ContinueToChatWidget),
|
||
}
|
||
|
||
@@ -49,7 +52,7 @@ pub(crate) struct OnboardingScreenArgs {
|
||
pub codex_home: PathBuf,
|
||
pub cwd: PathBuf,
|
||
pub show_login_screen: bool,
|
||
- pub show_git_warning: bool,
|
||
+ pub show_trust_screen: bool,
|
||
}
|
||
|
||
impl OnboardingScreen {
|
||
@@ -60,7 +63,7 @@ impl OnboardingScreen {
|
||
codex_home,
|
||
cwd,
|
||
show_login_screen,
|
||
- show_git_warning,
|
||
+ show_trust_screen,
|
||
} = args;
|
||
let mut steps: Vec<Step> = vec![Step::Welcome(WelcomeWidget {
|
||
is_logged_in: !show_login_screen,
|
||
@@ -71,20 +74,33 @@ impl OnboardingScreen {
|
||
highlighted_mode: AuthMode::ChatGPT,
|
||
error: None,
|
||
sign_in_state: SignInState::PickMode,
|
||
- codex_home,
|
||
+ codex_home: codex_home.clone(),
|
||
}))
|
||
}
|
||
- if show_git_warning {
|
||
- steps.push(Step::GitWarning(GitWarningWidget {
|
||
- event_tx: event_tx.clone(),
|
||
+ let is_git_repo = is_inside_git_repo(&cwd);
|
||
+ let highlighted = if is_git_repo {
|
||
+ TrustDirectorySelection::Trust
|
||
+ } else {
|
||
+ // Default to not trusting the directory if it's not a git repo.
|
||
+ TrustDirectorySelection::DontTrust
|
||
+ };
|
||
+ // Share ChatWidgetArgs between steps so changes in the TrustDirectory step
|
||
+ // are reflected when continuing to chat.
|
||
+ let shared_chat_args = Arc::new(Mutex::new(chat_widget_args));
|
||
+ if show_trust_screen {
|
||
+ steps.push(Step::TrustDirectory(TrustDirectoryWidget {
|
||
cwd,
|
||
+ codex_home,
|
||
+ is_git_repo,
|
||
selection: None,
|
||
- highlighted: GitWarningSelection::Continue,
|
||
+ highlighted,
|
||
+ error: None,
|
||
+ chat_widget_args: shared_chat_args.clone(),
|
||
}))
|
||
}
|
||
steps.push(Step::ContinueToChat(ContinueToChatWidget {
|
||
event_tx: event_tx.clone(),
|
||
- chat_widget_args,
|
||
+ chat_widget_args: shared_chat_args,
|
||
}));
|
||
// TODO: add git warning.
|
||
Self { event_tx, steps }
|
||
@@ -215,7 +231,7 @@ impl KeyboardHandler for Step {
|
||
match self {
|
||
Step::Welcome(_) | Step::ContinueToChat(_) => (),
|
||
Step::Auth(widget) => widget.handle_key_event(key_event),
|
||
- Step::GitWarning(widget) => widget.handle_key_event(key_event),
|
||
+ Step::TrustDirectory(widget) => widget.handle_key_event(key_event),
|
||
}
|
||
}
|
||
}
|
||
@@ -225,7 +241,7 @@ impl StepStateProvider for Step {
|
||
match self {
|
||
Step::Welcome(w) => w.get_step_state(),
|
||
Step::Auth(w) => w.get_step_state(),
|
||
- Step::GitWarning(w) => w.get_step_state(),
|
||
+ Step::TrustDirectory(w) => w.get_step_state(),
|
||
Step::ContinueToChat(w) => w.get_step_state(),
|
||
}
|
||
}
|
||
@@ -240,7 +256,7 @@ impl WidgetRef for Step {
|
||
Step::Auth(widget) => {
|
||
widget.render_ref(area, buf);
|
||
}
|
||
- Step::GitWarning(widget) => {
|
||
+ Step::TrustDirectory(widget) => {
|
||
widget.render_ref(area, buf);
|
||
}
|
||
Step::ContinueToChat(widget) => {
|
||
diff --git a/codex-rs/tui/src/onboarding/trust_directory.rs b/codex-rs/tui/src/onboarding/trust_directory.rs
|
||
new file mode 100644
|
||
index 0000000000..3be9bac1ac
|
||
--- /dev/null
|
||
+++ b/codex-rs/tui/src/onboarding/trust_directory.rs
|
||
@@ -0,0 +1,179 @@
|
||
+use std::path::PathBuf;
|
||
+
|
||
+use codex_core::config::set_project_trusted;
|
||
+use codex_core::protocol::AskForApproval;
|
||
+use codex_core::protocol::SandboxPolicy;
|
||
+use crossterm::event::KeyCode;
|
||
+use crossterm::event::KeyEvent;
|
||
+use ratatui::buffer::Buffer;
|
||
+use ratatui::layout::Rect;
|
||
+use ratatui::prelude::Widget;
|
||
+use ratatui::style::Color;
|
||
+use ratatui::style::Modifier;
|
||
+use ratatui::style::Style;
|
||
+use ratatui::style::Stylize;
|
||
+use ratatui::text::Line;
|
||
+use ratatui::text::Span;
|
||
+use ratatui::widgets::Paragraph;
|
||
+use ratatui::widgets::WidgetRef;
|
||
+use ratatui::widgets::Wrap;
|
||
+
|
||
+use crate::colors::LIGHT_BLUE;
|
||
+
|
||
+use crate::onboarding::onboarding_screen::KeyboardHandler;
|
||
+use crate::onboarding::onboarding_screen::StepStateProvider;
|
||
+
|
||
+use super::onboarding_screen::StepState;
|
||
+use crate::app::ChatWidgetArgs;
|
||
+use std::sync::Arc;
|
||
+use std::sync::Mutex;
|
||
+
|
||
+pub(crate) struct TrustDirectoryWidget {
|
||
+ pub codex_home: PathBuf,
|
||
+ pub cwd: PathBuf,
|
||
+ pub is_git_repo: bool,
|
||
+ pub selection: Option<TrustDirectorySelection>,
|
||
+ pub highlighted: TrustDirectorySelection,
|
||
+ pub error: Option<String>,
|
||
+ pub chat_widget_args: Arc<Mutex<ChatWidgetArgs>>,
|
||
+}
|
||
+
|
||
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
+pub(crate) enum TrustDirectorySelection {
|
||
+ Trust,
|
||
+ DontTrust,
|
||
+}
|
||
+
|
||
+impl WidgetRef for &TrustDirectoryWidget {
|
||
+ fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||
+ let mut lines: Vec<Line> = vec![
|
||
+ Line::from(vec![
|
||
+ Span::raw("> "),
|
||
+ Span::styled(
|
||
+ "You are running Codex in ",
|
||
+ Style::default().add_modifier(Modifier::BOLD),
|
||
+ ),
|
||
+ Span::raw(self.cwd.to_string_lossy().to_string()),
|
||
+ ]),
|
||
+ Line::from(""),
|
||
+ ];
|
||
+
|
||
+ if self.is_git_repo {
|
||
+ lines.push(Line::from(
|
||
+ " Since this folder is version controlled, you may wish to allow Codex",
|
||
+ ));
|
||
+ lines.push(Line::from(
|
||
+ " to work in this folder without asking for approval.",
|
||
+ ));
|
||
+ } else {
|
||
+ lines.push(Line::from(
|
||
+ " Since this folder is not version controlled, we recommend requiring",
|
||
+ ));
|
||
+ lines.push(Line::from(" approval of all edits and commands."));
|
||
+ }
|
||
+ lines.push(Line::from(""));
|
||
+
|
||
+ let create_option =
|
||
+ |idx: usize, option: TrustDirectorySelection, text: &str| -> Line<'static> {
|
||
+ let is_selected = self.highlighted == option;
|
||
+ if is_selected {
|
||
+ Line::from(vec![
|
||
+ Span::styled(
|
||
+ format!("> {}. ", idx + 1),
|
||
+ Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM),
|
||
+ ),
|
||
+ Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)),
|
||
+ ])
|
||
+ } else {
|
||
+ Line::from(format!(" {}. {}", idx + 1, text))
|
||
+ }
|
||
+ };
|
||
+
|
||
+ if self.is_git_repo {
|
||
+ lines.push(create_option(
|
||
+ 0,
|
||
+ TrustDirectorySelection::Trust,
|
||
+ "Yes, allow Codex to work in this folder without asking for approval",
|
||
+ ));
|
||
+ lines.push(create_option(
|
||
+ 1,
|
||
+ TrustDirectorySelection::DontTrust,
|
||
+ "No, ask me to approve edits and commands",
|
||
+ ));
|
||
+ } else {
|
||
+ lines.push(create_option(
|
||
+ 0,
|
||
+ TrustDirectorySelection::Trust,
|
||
+ "Allow Codex to work in this folder without asking for approval",
|
||
+ ));
|
||
+ lines.push(create_option(
|
||
+ 1,
|
||
+ TrustDirectorySelection::DontTrust,
|
||
+ "Require approval of edits and commands",
|
||
+ ));
|
||
+ }
|
||
+ lines.push(Line::from(""));
|
||
+ if let Some(error) = &self.error {
|
||
+ lines.push(Line::from(format!(" {error}")).fg(Color::Red));
|
||
+ lines.push(Line::from(""));
|
||
+ }
|
||
+ lines.push(Line::from(" Press Enter to continue").add_modifier(Modifier::DIM));
|
||
+
|
||
+ Paragraph::new(lines)
|
||
+ .wrap(Wrap { trim: false })
|
||
+ .render(area, buf);
|
||
+ }
|
||
+}
|
||
+
|
||
+impl KeyboardHandler for TrustDirectoryWidget {
|
||
+ fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||
+ match key_event.code {
|
||
+ KeyCode::Up | KeyCode::Char('k') => {
|
||
+ self.highlighted = TrustDirectorySelection::Trust;
|
||
+ }
|
||
+ KeyCode::Down | KeyCode::Char('j') => {
|
||
+ self.highlighted = TrustDirectorySelection::DontTrust;
|
||
+ }
|
||
+ KeyCode::Char('1') => self.handle_trust(),
|
||
+ KeyCode::Char('2') => self.handle_dont_trust(),
|
||
+ KeyCode::Enter => match self.highlighted {
|
||
+ TrustDirectorySelection::Trust => self.handle_trust(),
|
||
+ TrustDirectorySelection::DontTrust => self.handle_dont_trust(),
|
||
+ },
|
||
+ _ => {}
|
||
+ }
|
||
+ }
|
||
+}
|
||
+
|
||
+impl StepStateProvider for TrustDirectoryWidget {
|
||
+ fn get_step_state(&self) -> StepState {
|
||
+ match self.selection {
|
||
+ Some(_) => StepState::Complete,
|
||
+ None => StepState::InProgress,
|
||
+ }
|
||
+ }
|
||
+}
|
||
+
|
||
+impl TrustDirectoryWidget {
|
||
+ fn handle_trust(&mut self) {
|
||
+ if let Err(e) = set_project_trusted(&self.codex_home, &self.cwd) {
|
||
+ tracing::error!("Failed to set project trusted: {e:?}");
|
||
+ self.error = Some(e.to_string());
|
||
+ // self.error = Some("Failed to set project trusted".to_string());
|
||
+ }
|
||
+
|
||
+ // Update the in-memory chat config for this session to a more permissive
|
||
+ // policy suitable for a trusted workspace.
|
||
+ if let Ok(mut args) = self.chat_widget_args.lock() {
|
||
+ args.config.approval_policy = AskForApproval::OnRequest;
|
||
+ args.config.sandbox_policy = SandboxPolicy::new_workspace_write_policy();
|
||
+ }
|
||
+
|
||
+ self.selection = Some(TrustDirectorySelection::Trust);
|
||
+ }
|
||
+
|
||
+ fn handle_dont_trust(&mut self) {
|
||
+ self.highlighted = TrustDirectorySelection::DontTrust;
|
||
+ self.selection = Some(TrustDirectorySelection::DontTrust);
|
||
+ }
|
||
+}
|
||
```
|
||
|
||
## Review Comments
|
||
|
||
### codex-rs/core/Cargo.toml
|
||
|
||
- Created: 2025-08-07 15:46:51 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2260738622
|
||
|
||
```diff
|
||
@@ -53,6 +53,8 @@ tree-sitter-bash = "0.25.0"
|
||
uuid = { version = "1", features = ["serde", "v4"] }
|
||
whoami = "1.6.0"
|
||
wildmatch = "2.4.0"
|
||
+toml_edit = "0.23.3"
|
||
```
|
||
|
||
> alpha :P
|
||
|
||
### codex-rs/core/src/config.rs
|
||
|
||
- Created: 2025-08-07 09:11:39 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259655004
|
||
|
||
```diff
|
||
@@ -214,6 +225,33 @@ fn load_config_as_toml(codex_home: &Path) -> std::io::Result<TomlValue> {
|
||
}
|
||
}
|
||
|
||
+/// Patch `CODEX_HOME/config.toml` project state.
|
||
+/// Use with caution.
|
||
+pub fn set_project_trusted(
|
||
+ codex_home: &Path,
|
||
+ project_path: &Path,
|
||
+ trusted: bool,
|
||
+) -> anyhow::Result<()> {
|
||
+ let config_path = codex_home.join("config.toml");
|
||
+
|
||
+ // Parse existing config if present; otherwise start a new document.
|
||
+ let mut doc = match std::fs::read_to_string(&config_path) {
|
||
+ Ok(s) => s.parse::<DocumentMut>()?,
|
||
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
|
||
+ Err(e) => return Err(e.into()),
|
||
+ };
|
||
+
|
||
+ let project_key = project_path.to_string_lossy().to_string();
|
||
+ doc["projects"][project_key.as_str()]["trusted"] = toml_edit::value(trusted);
|
||
+
|
||
+ if let Some(parent) = config_path.parent() {
|
||
+ std::fs::create_dir_all(parent)?;
|
||
+ }
|
||
+ std::fs::write(config_path, doc.to_string())?;
|
||
```
|
||
|
||
> We should maybe write to a temp file in the folder and then `mv` it so writes are atomic.
|
||
|
||
- Created: 2025-08-07 09:12:03 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259656059
|
||
|
||
```diff
|
||
@@ -214,6 +225,33 @@ fn load_config_as_toml(codex_home: &Path) -> std::io::Result<TomlValue> {
|
||
}
|
||
}
|
||
|
||
+/// Patch `CODEX_HOME/config.toml` project state.
|
||
+/// Use with caution.
|
||
+pub fn set_project_trusted(
|
||
+ codex_home: &Path,
|
||
+ project_path: &Path,
|
||
+ trusted: bool,
|
||
+) -> anyhow::Result<()> {
|
||
+ let config_path = codex_home.join("config.toml");
|
||
+
|
||
+ // Parse existing config if present; otherwise start a new document.
|
||
+ let mut doc = match std::fs::read_to_string(&config_path) {
|
||
+ Ok(s) => s.parse::<DocumentMut>()?,
|
||
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
|
||
+ Err(e) => return Err(e.into()),
|
||
+ };
|
||
+
|
||
+ let project_key = project_path.to_string_lossy().to_string();
|
||
+ doc["projects"][project_key.as_str()]["trusted"] = toml_edit::value(trusted);
|
||
+
|
||
+ if let Some(parent) = config_path.parent() {
|
||
+ std::fs::create_dir_all(parent)?;
|
||
+ }
|
||
```
|
||
|
||
> By construction, `parent` is `codex_home`, so just use that?
|
||
|
||
- Created: 2025-08-07 09:12:51 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259658005
|
||
|
||
```diff
|
||
@@ -214,6 +225,33 @@ fn load_config_as_toml(codex_home: &Path) -> std::io::Result<TomlValue> {
|
||
}
|
||
}
|
||
|
||
+/// Patch `CODEX_HOME/config.toml` project state.
|
||
+/// Use with caution.
|
||
+pub fn set_project_trusted(
|
||
+ codex_home: &Path,
|
||
+ project_path: &Path,
|
||
+ trusted: bool,
|
||
+) -> anyhow::Result<()> {
|
||
+ let config_path = codex_home.join("config.toml");
|
||
+
|
||
+ // Parse existing config if present; otherwise start a new document.
|
||
+ let mut doc = match std::fs::read_to_string(&config_path) {
|
||
+ Ok(s) => s.parse::<DocumentMut>()?,
|
||
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
|
||
+ Err(e) => return Err(e.into()),
|
||
+ };
|
||
+
|
||
+ let project_key = project_path.to_string_lossy().to_string();
|
||
+ doc["projects"][project_key.as_str()]["trusted"] = toml_edit::value(trusted);
|
||
```
|
||
|
||
> What happens if `doc` does not have a `"projects"` key: does it create an entry on demand? How does this not panic?
|
||
|
||
- Created: 2025-08-07 09:18:05 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259670414
|
||
|
||
```diff
|
||
@@ -518,6 +565,41 @@ impl Config {
|
||
Self::get_base_instructions(experimental_instructions_path, &resolved_cwd)?;
|
||
let base_instructions = base_instructions.or(file_base_instructions);
|
||
|
||
+ // Let's begin.
|
||
+ // First: load approval_policy and sandbox_mode from overrides, config
|
||
+ // profile, or config.toml.
|
||
+ let mut approval_policy = approval_policy
|
||
+ .or(config_profile.approval_policy)
|
||
+ .or(cfg.approval_policy.clone());
|
||
+ // TODO: Add sandbox_mode to the config profile? Doesn't
|
||
+ // appear to exist right now
|
||
+ let mut sandbox_mode = sandbox_mode.or(cfg.sandbox_mode.clone());
|
||
+ let mut projects = cfg.projects.clone().unwrap_or_default();
|
||
+
|
||
+ // Second: Default to "trusted" if the user has configured approval policy
|
||
```
|
||
|
||
> What does "trusted" mean, particularly if sandbox mode is "read only"?
|
||
|
||
- Created: 2025-08-07 09:19:15 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259673214
|
||
|
||
```diff
|
||
@@ -518,6 +565,41 @@ impl Config {
|
||
Self::get_base_instructions(experimental_instructions_path, &resolved_cwd)?;
|
||
let base_instructions = base_instructions.or(file_base_instructions);
|
||
|
||
+ // Let's begin.
|
||
+ // First: load approval_policy and sandbox_mode from overrides, config
|
||
+ // profile, or config.toml.
|
||
+ let mut approval_policy = approval_policy
|
||
+ .or(config_profile.approval_policy)
|
||
+ .or(cfg.approval_policy.clone());
|
||
+ // TODO: Add sandbox_mode to the config profile? Doesn't
|
||
+ // appear to exist right now
|
||
+ let mut sandbox_mode = sandbox_mode.or(cfg.sandbox_mode.clone());
|
||
+ let mut projects = cfg.projects.clone().unwrap_or_default();
|
||
+
|
||
+ // Second: Default to "trusted" if the user has configured approval policy
|
||
+ // or sandbox mode, regardless of projects config. If you've modified the
|
||
+ // config.toml, we'll respect your decision.
|
||
+ //
|
||
+ // This is a bit of a hack, but it allows us to skip tui onboarding
|
||
+ // for now.
|
||
+ if approval_policy.is_some() || sandbox_mode.is_some() {
|
||
+ projects.insert(
|
||
```
|
||
|
||
> What if `projects` has an existing entry for `cwd` where `trusted=false`? Should this override it? Should it write it back?
|
||
|
||
- Created: 2025-08-07 09:30:40 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259702973
|
||
|
||
```diff
|
||
@@ -214,6 +225,33 @@ fn load_config_as_toml(codex_home: &Path) -> std::io::Result<TomlValue> {
|
||
}
|
||
}
|
||
|
||
+/// Patch `CODEX_HOME/config.toml` project state.
|
||
+/// Use with caution.
|
||
+pub fn set_project_trusted(
|
||
+ codex_home: &Path,
|
||
+ project_path: &Path,
|
||
+ trusted: bool,
|
||
+) -> anyhow::Result<()> {
|
||
+ let config_path = codex_home.join("config.toml");
|
||
+
|
||
+ // Parse existing config if present; otherwise start a new document.
|
||
+ let mut doc = match std::fs::read_to_string(&config_path) {
|
||
+ Ok(s) => s.parse::<DocumentMut>()?,
|
||
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
|
||
+ Err(e) => return Err(e.into()),
|
||
+ };
|
||
+
|
||
+ let project_key = project_path.to_string_lossy().to_string();
|
||
+ doc["projects"][project_key.as_str()]["trusted"] = toml_edit::value(trusted);
|
||
```
|
||
|
||
> I see, `DocumentMut` implements `IndexMut<&str>`.
|
||
|
||
- Created: 2025-08-07 09:34:45 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259712484
|
||
|
||
```diff
|
||
@@ -518,6 +565,41 @@ impl Config {
|
||
Self::get_base_instructions(experimental_instructions_path, &resolved_cwd)?;
|
||
let base_instructions = base_instructions.or(file_base_instructions);
|
||
|
||
+ // Let's begin.
|
||
+ // First: load approval_policy and sandbox_mode from overrides, config
|
||
+ // profile, or config.toml.
|
||
+ let mut approval_policy = approval_policy
|
||
+ .or(config_profile.approval_policy)
|
||
+ .or(cfg.approval_policy);
|
||
+ // TODO: Add sandbox_mode to the config profile? Doesn't
|
||
+ // appear to exist right now
|
||
+ let mut sandbox_mode = sandbox_mode.or(cfg.sandbox_mode);
|
||
+ let mut projects = cfg.projects.clone().unwrap_or_default();
|
||
+
|
||
+ // Second: Default to "trusted" if the user has configured approval policy
|
||
+ // or sandbox mode, regardless of projects config. If you've modified the
|
||
+ // config.toml, we'll respect your decision.
|
||
+ //
|
||
+ // This is a bit of a hack, but it allows us to skip tui onboarding
|
||
+ // for now.
|
||
+ if approval_policy.is_some() || sandbox_mode.is_some() {
|
||
+ projects.insert(
|
||
+ resolved_cwd.to_string_lossy().to_string(),
|
||
+ ProjectConfig {
|
||
+ trusted: Some(true),
|
||
+ },
|
||
+ );
|
||
+ } else if let Some(project) = projects.get(&resolved_cwd.to_string_lossy().to_string()) {
|
||
+ // Third: If we have a project config, and it's trusted, set the approval policy and sandbox mode
|
||
+ if project.trusted.unwrap_or(false) {
|
||
+ approval_policy = Some(AskForApproval::OnRequest);
|
||
+ sandbox_mode = Some(SandboxMode::WorkspaceWrite);
|
||
```
|
||
|
||
> So if I run the Codex CLI once with `--sandbox read-only`, we would set it as trusted, and the next time, if I didn't run any flags, we would run it with workplace-write?
|
||
|
||
- Created: 2025-08-07 09:40:22 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259725983
|
||
|
||
```diff
|
||
@@ -518,6 +565,41 @@ impl Config {
|
||
Self::get_base_instructions(experimental_instructions_path, &resolved_cwd)?;
|
||
let base_instructions = base_instructions.or(file_base_instructions);
|
||
|
||
+ // Let's begin.
|
||
+ // First: load approval_policy and sandbox_mode from overrides, config
|
||
+ // profile, or config.toml.
|
||
+ let mut approval_policy = approval_policy
|
||
+ .or(config_profile.approval_policy)
|
||
+ .or(cfg.approval_policy.clone());
|
||
+ // TODO: Add sandbox_mode to the config profile? Doesn't
|
||
+ // appear to exist right now
|
||
+ let mut sandbox_mode = sandbox_mode.or(cfg.sandbox_mode.clone());
|
||
+ let mut projects = cfg.projects.clone().unwrap_or_default();
|
||
+
|
||
+ // Second: Default to "trusted" if the user has configured approval policy
|
||
```
|
||
|
||
> What if I ran `codex exec --sandbox read-only`?
|
||
|
||
- Created: 2025-08-07 09:40:56 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259727288
|
||
|
||
```diff
|
||
@@ -518,6 +565,41 @@ impl Config {
|
||
Self::get_base_instructions(experimental_instructions_path, &resolved_cwd)?;
|
||
let base_instructions = base_instructions.or(file_base_instructions);
|
||
|
||
+ // Let's begin.
|
||
+ // First: load approval_policy and sandbox_mode from overrides, config
|
||
+ // profile, or config.toml.
|
||
+ let mut approval_policy = approval_policy
|
||
+ .or(config_profile.approval_policy)
|
||
+ .or(cfg.approval_policy.clone());
|
||
+ // TODO: Add sandbox_mode to the config profile? Doesn't
|
||
+ // appear to exist right now
|
||
+ let mut sandbox_mode = sandbox_mode.or(cfg.sandbox_mode.clone());
|
||
+ let mut projects = cfg.projects.clone().unwrap_or_default();
|
||
+
|
||
+ // Second: Default to "trusted" if the user has configured approval policy
|
||
```
|
||
|
||
> This logic is in `core`, but it seems you are making strong assumptions about the UI here?
|
||
|
||
- Created: 2025-08-07 15:49:16 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2260744829
|
||
|
||
```diff
|
||
@@ -666,6 +715,28 @@ pub fn find_codex_home() -> std::io::Result<PathBuf> {
|
||
Ok(p)
|
||
}
|
||
|
||
+pub fn resolve_cwd(cwd: Option<PathBuf>) -> std::io::Result<PathBuf> {
|
||
```
|
||
|
||
> I think this function can go away?
|
||
|
||
### codex-rs/core/src/protocol.rs
|
||
|
||
- Created: 2025-08-07 09:20:26 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259675989
|
||
|
||
```diff
|
||
@@ -151,6 +150,7 @@ pub enum AskForApproval {
|
||
OnFailure,
|
||
|
||
/// The model decides when to ask the user for approval.
|
||
+ #[default]
|
||
```
|
||
|
||
> Update docs?
|
||
|
||
### codex-rs/exec/src/lib.rs
|
||
|
||
- Created: 2025-08-07 09:21:30 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259679636
|
||
|
||
```diff
|
||
@@ -180,11 +178,6 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
|
||
// is using.
|
||
event_processor.print_config_summary(&config, &prompt);
|
||
|
||
- if !skip_git_repo_check && !is_inside_git_repo(&config.cwd.to_path_buf()) {
|
||
```
|
||
|
||
> I thought there was still going to be a case where we exit for `codex exec` even if not in a Git repo?
|
||
|
||
### codex-rs/tui/src/lib.rs
|
||
|
||
- Created: 2025-08-07 15:48:51 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2260743769
|
||
|
||
```diff
|
||
@@ -277,3 +322,33 @@ fn should_show_login_screen(config: &Config) -> bool {
|
||
false
|
||
}
|
||
}
|
||
+
|
||
+fn should_show_trust_screen(
|
||
+ config: &mut Config,
|
||
+ config_toml: &ConfigToml,
|
||
+ approval_policy_overide: Option<AskForApproval>,
|
||
+ sandbox_mode_override: Option<SandboxMode>,
|
||
+ cwd: Option<PathBuf>,
|
||
```
|
||
|
||
> Why does this take `cwd` as an arg instead of using `config.cwd`?
|
||
>
|
||
> We should be sure we are honoring `--cwd` if the user passed it in...
|
||
|
||
- Created: 2025-08-07 15:52:25 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2260751522
|
||
|
||
```diff
|
||
@@ -277,3 +322,33 @@ fn should_show_login_screen(config: &Config) -> bool {
|
||
false
|
||
}
|
||
}
|
||
+
|
||
+fn should_show_trust_screen(
|
||
+ config: &mut Config,
|
||
+ config_toml: &ConfigToml,
|
||
+ approval_policy_overide: Option<AskForApproval>,
|
||
+ sandbox_mode_override: Option<SandboxMode>,
|
||
+ cwd: Option<PathBuf>,
|
||
+) -> std::io::Result<bool> {
|
||
+ let cwd = cwd.map(|p| p.canonicalize().unwrap_or(p));
|
||
+ let resolved_cwd = resolve_cwd(cwd)?;
|
||
+
|
||
+ if approval_policy_overide.is_some() || sandbox_mode_override.is_some() {
|
||
```
|
||
|
||
> Note the user _could have_ passed `-c sandbox_mode=read-only`...
|
||
|
||
### codex-rs/tui/src/onboarding/continue_to_chat.rs
|
||
|
||
- Created: 2025-08-07 09:24:44 UTC | Link: https://github.com/openai/codex/pull/1929#discussion_r2259687584
|
||
|
||
```diff
|
||
@@ -8,12 +8,14 @@ use crate::app_event_sender::AppEventSender;
|
||
use crate::onboarding::onboarding_screen::StepStateProvider;
|
||
|
||
use super::onboarding_screen::StepState;
|
||
+use std::sync::Arc;
|
||
+use std::sync::Mutex;
|
||
|
||
/// This doesn't render anything explicitly but serves as a signal that we made it to the end and
|
||
/// we should continue to the chat.
|
||
pub(crate) struct ContinueToChatWidget {
|
||
pub event_tx: AppEventSender,
|
||
- pub chat_widget_args: ChatWidgetArgs,
|
||
+ pub chat_widget_args: Arc<Mutex<ChatWidgetArgs>>,
|
||
```
|
||
|
||
> Maybe I'm missing where this happens, but I don't see where this is mutated (though it is cloned), so does it need `Arc<Mutex>`? |