mirror of
https://github.com/openai/codex.git
synced 2026-05-30 07:50:17 +00:00
Fixes #24093. ## Why `--dangerously-bypass-hook-trust` is a supported CLI flag intended for headless or automated runs where enabled hooks should be allowed to run without requiring persisted trust. In the TUI, startup hook review still opened whenever hooks looked untrusted, so a launch using the bypass could block on the interactive "Hooks need review" prompt. The tricky case is persistent app-server resume: a resume may attach to an already-running thread, where resume config overrides are ignored. In that path, hiding the startup review would be wrong because the existing hook engine may still filter untrusted hooks. ## What Changed - Startup hook review now skips the prompt only when hook trust bypass is actually safe for that launch. - The TUI forwards `bypass_hook_trust` through the app-server request config for fresh thread start/resume/fork paths, and the app-server applies it as a runtime-only `ConfigOverrides` value rather than treating it like a `config.toml` setting. - Persistent app-server resumes keep the startup review prompt so users still have a chance to trust hooks when the running thread cannot receive the bypass override. ## Verification - Added focused coverage for startup hook review with and without `bypass_hook_trust`. - Extended existing TUI/app-server config override tests to cover forwarding and applying `bypass_hook_trust`.
370 lines
12 KiB
Rust
370 lines
12 KiB
Rust
use codex_arg0::Arg0DispatchPaths;
|
|
use codex_cloud_requirements::cloud_requirements_loader;
|
|
use codex_config::CloudRequirementsLoader;
|
|
use codex_config::ConfigLayerStack;
|
|
use codex_config::LoaderOverrides;
|
|
use codex_config::ThreadConfigLoader;
|
|
use codex_config::loader::load_config_layers_state;
|
|
use codex_core::config::Config;
|
|
use codex_core::config::ConfigOverrides;
|
|
use codex_exec_server::LOCAL_FS;
|
|
use codex_features::feature_for_key;
|
|
use codex_login::AuthManager;
|
|
use codex_login::default_client::set_default_client_residency_requirement;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use codex_utils_json_to_toml::json_to_toml;
|
|
use std::collections::BTreeMap;
|
|
use std::collections::BTreeSet;
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use std::sync::RwLock;
|
|
use toml::Value as TomlValue;
|
|
use tracing::warn;
|
|
|
|
/// Shared app-server entry point for loading effective Codex configuration.
|
|
#[derive(Clone)]
|
|
pub(crate) struct ConfigManager {
|
|
codex_home: PathBuf,
|
|
cli_overrides: Arc<RwLock<Vec<(String, TomlValue)>>>,
|
|
runtime_feature_enablement: Arc<RwLock<BTreeMap<String, bool>>>,
|
|
loader_overrides: LoaderOverrides,
|
|
strict_config: bool,
|
|
cloud_requirements: Arc<RwLock<CloudRequirementsLoader>>,
|
|
arg0_paths: Arg0DispatchPaths,
|
|
thread_config_loader: Arc<RwLock<Arc<dyn ThreadConfigLoader>>>,
|
|
}
|
|
|
|
impl ConfigManager {
|
|
pub(crate) fn new(
|
|
codex_home: PathBuf,
|
|
cli_overrides: Vec<(String, TomlValue)>,
|
|
loader_overrides: LoaderOverrides,
|
|
strict_config: bool,
|
|
cloud_requirements: CloudRequirementsLoader,
|
|
arg0_paths: Arg0DispatchPaths,
|
|
thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
|
) -> Self {
|
|
Self {
|
|
codex_home,
|
|
cli_overrides: Arc::new(RwLock::new(cli_overrides)),
|
|
runtime_feature_enablement: Arc::new(RwLock::new(BTreeMap::new())),
|
|
loader_overrides,
|
|
strict_config,
|
|
cloud_requirements: Arc::new(RwLock::new(cloud_requirements)),
|
|
arg0_paths,
|
|
thread_config_loader: Arc::new(RwLock::new(thread_config_loader)),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn codex_home(&self) -> &Path {
|
|
self.codex_home.as_path()
|
|
}
|
|
|
|
pub(crate) fn user_config_path(&self) -> std::io::Result<AbsolutePathBuf> {
|
|
self.loader_overrides.user_config_path(self.codex_home())
|
|
}
|
|
|
|
pub(crate) fn current_cli_overrides(&self) -> Vec<(String, TomlValue)> {
|
|
self.cli_overrides
|
|
.read()
|
|
.map(|guard| guard.clone())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub(crate) fn current_cloud_requirements(&self) -> CloudRequirementsLoader {
|
|
self.cloud_requirements
|
|
.read()
|
|
.map(|guard| guard.clone())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub(crate) fn extend_runtime_feature_enablement<I>(&self, enablement: I) -> Result<(), ()>
|
|
where
|
|
I: IntoIterator<Item = (String, bool)>,
|
|
{
|
|
let mut runtime_feature_enablement =
|
|
self.runtime_feature_enablement.write().map_err(|_| ())?;
|
|
runtime_feature_enablement.extend(enablement);
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn replace_cloud_requirements_loader(
|
|
&self,
|
|
auth_manager: Arc<AuthManager>,
|
|
chatgpt_base_url: String,
|
|
) {
|
|
let loader =
|
|
cloud_requirements_loader(auth_manager, chatgpt_base_url, self.codex_home.clone());
|
|
if let Ok(mut guard) = self.cloud_requirements.write() {
|
|
*guard = loader;
|
|
} else {
|
|
warn!("failed to update cloud requirements loader");
|
|
}
|
|
}
|
|
|
|
pub(crate) fn replace_thread_config_loader(
|
|
&self,
|
|
thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
|
) {
|
|
if let Ok(mut guard) = self.thread_config_loader.write() {
|
|
*guard = thread_config_loader;
|
|
} else {
|
|
warn!("failed to update thread config loader");
|
|
}
|
|
}
|
|
|
|
fn current_thread_config_loader(&self) -> Arc<dyn ThreadConfigLoader> {
|
|
self.thread_config_loader
|
|
.read()
|
|
.map(|guard| Arc::clone(&*guard))
|
|
.unwrap_or_else(|_| Arc::new(codex_config::NoopThreadConfigLoader))
|
|
}
|
|
|
|
pub(crate) async fn sync_default_client_residency_requirement(&self) {
|
|
match self.load_latest_config(/*fallback_cwd*/ None).await {
|
|
Ok(config) => {
|
|
set_default_client_residency_requirement(config.enforce_residency.value());
|
|
}
|
|
Err(err) => warn!(
|
|
error = %err,
|
|
"failed to sync default client residency requirement after auth refresh"
|
|
),
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn load_latest_config(
|
|
&self,
|
|
fallback_cwd: Option<PathBuf>,
|
|
) -> std::io::Result<Config> {
|
|
self.load_with_cli_overrides(
|
|
&self.current_cli_overrides(),
|
|
/*request_overrides*/ None,
|
|
ConfigOverrides::default(),
|
|
fallback_cwd,
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub(crate) async fn load_latest_config_for_thread(
|
|
&self,
|
|
thread_config: &Config,
|
|
) -> std::io::Result<Config> {
|
|
let refreshed_config = self
|
|
.load_latest_config(Some(thread_config.cwd.to_path_buf()))
|
|
.await?;
|
|
let mut config = thread_config
|
|
.rebuild_preserving_session_layers(&refreshed_config)
|
|
.await?;
|
|
self.apply_runtime_feature_enablement(&mut config);
|
|
self.apply_arg0_paths(&mut config);
|
|
Ok(config)
|
|
}
|
|
|
|
pub(crate) async fn load_default_config(&self) -> std::io::Result<Config> {
|
|
let mut config = Config::load_default_with_cli_overrides_for_codex_home(
|
|
self.codex_home.clone(),
|
|
self.current_cli_overrides(),
|
|
)
|
|
.await?;
|
|
if self.loader_overrides.user_config_path.is_some()
|
|
|| self.loader_overrides.user_config_profile.is_some()
|
|
{
|
|
let user_config_path = self.loader_overrides.user_config_path(self.codex_home())?;
|
|
config.config_layer_stack = config.config_layer_stack.with_user_config_profile(
|
|
&user_config_path,
|
|
self.loader_overrides.user_config_profile.as_ref(),
|
|
TomlValue::Table(toml::map::Map::new()),
|
|
);
|
|
}
|
|
self.apply_runtime_feature_enablement(&mut config);
|
|
self.apply_arg0_paths(&mut config);
|
|
Ok(config)
|
|
}
|
|
|
|
pub(crate) async fn load_with_overrides(
|
|
&self,
|
|
request_overrides: Option<HashMap<String, serde_json::Value>>,
|
|
typesafe_overrides: ConfigOverrides,
|
|
) -> std::io::Result<Config> {
|
|
self.load_with_cli_overrides(
|
|
&self.current_cli_overrides(),
|
|
request_overrides,
|
|
typesafe_overrides,
|
|
/*fallback_cwd*/ None,
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub(crate) async fn load_for_cwd(
|
|
&self,
|
|
request_overrides: Option<HashMap<String, serde_json::Value>>,
|
|
typesafe_overrides: ConfigOverrides,
|
|
cwd: Option<PathBuf>,
|
|
) -> std::io::Result<Config> {
|
|
self.load_with_cli_overrides(
|
|
&self.current_cli_overrides(),
|
|
request_overrides,
|
|
typesafe_overrides,
|
|
cwd,
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub(crate) async fn load_with_cli_overrides(
|
|
&self,
|
|
cli_overrides: &[(String, TomlValue)],
|
|
request_overrides: Option<HashMap<String, serde_json::Value>>,
|
|
mut typesafe_overrides: ConfigOverrides,
|
|
fallback_cwd: Option<PathBuf>,
|
|
) -> std::io::Result<Config> {
|
|
let mut request_overrides = request_overrides.unwrap_or_default();
|
|
if let Some(value) = request_overrides.remove("bypass_hook_trust") {
|
|
typesafe_overrides.bypass_hook_trust = Some(value.as_bool().ok_or_else(|| {
|
|
std::io::Error::new(
|
|
std::io::ErrorKind::InvalidData,
|
|
"`bypass_hook_trust` override must be a boolean",
|
|
)
|
|
})?);
|
|
}
|
|
let merged_cli_overrides = cli_overrides
|
|
.iter()
|
|
.cloned()
|
|
.chain(
|
|
request_overrides
|
|
.into_iter()
|
|
.map(|(key, value)| (key, json_to_toml(value))),
|
|
)
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut config = codex_core::config::ConfigBuilder::default()
|
|
.codex_home(self.codex_home.clone())
|
|
.cli_overrides(merged_cli_overrides)
|
|
.loader_overrides(self.loader_overrides.clone())
|
|
.strict_config(self.strict_config)
|
|
.harness_overrides(typesafe_overrides)
|
|
.fallback_cwd(fallback_cwd)
|
|
.cloud_requirements(self.current_cloud_requirements())
|
|
.thread_config_loader(self.current_thread_config_loader())
|
|
.build()
|
|
.await?;
|
|
self.apply_runtime_feature_enablement(&mut config);
|
|
self.apply_arg0_paths(&mut config);
|
|
Ok(config)
|
|
}
|
|
|
|
pub(crate) async fn load_config_layers_for_cwd(
|
|
&self,
|
|
cwd: AbsolutePathBuf,
|
|
) -> std::io::Result<ConfigLayerStack> {
|
|
self.load_config_layers(Some(cwd)).await
|
|
}
|
|
|
|
pub(crate) async fn load_config_layers(
|
|
&self,
|
|
cwd: Option<AbsolutePathBuf>,
|
|
) -> std::io::Result<ConfigLayerStack> {
|
|
let thread_config_loader = self.current_thread_config_loader();
|
|
load_config_layers_state(
|
|
LOCAL_FS.as_ref(),
|
|
&self.codex_home,
|
|
cwd,
|
|
&self.current_cli_overrides(),
|
|
codex_config::ConfigLoadOptions {
|
|
loader_overrides: self.loader_overrides.clone(),
|
|
strict_config: self.strict_config,
|
|
},
|
|
self.current_cloud_requirements(),
|
|
thread_config_loader.as_ref(),
|
|
)
|
|
.await
|
|
}
|
|
|
|
fn apply_runtime_feature_enablement(&self, config: &mut Config) {
|
|
apply_runtime_feature_enablement(config, &self.current_runtime_feature_enablement());
|
|
}
|
|
|
|
fn current_runtime_feature_enablement(&self) -> BTreeMap<String, bool> {
|
|
self.runtime_feature_enablement
|
|
.read()
|
|
.map(|guard| guard.clone())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn apply_arg0_paths(&self, config: &mut Config) {
|
|
config.codex_self_exe = self.arg0_paths.codex_self_exe.clone();
|
|
config.codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone();
|
|
config.main_execve_wrapper_exe = self.arg0_paths.main_execve_wrapper_exe.clone();
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn new_for_tests(
|
|
codex_home: PathBuf,
|
|
cli_overrides: Vec<(String, TomlValue)>,
|
|
loader_overrides: LoaderOverrides,
|
|
cloud_requirements: CloudRequirementsLoader,
|
|
) -> Self {
|
|
Self::new(
|
|
codex_home,
|
|
cli_overrides,
|
|
loader_overrides,
|
|
/*strict_config*/ false,
|
|
cloud_requirements,
|
|
Arg0DispatchPaths::default(),
|
|
Arc::new(codex_config::NoopThreadConfigLoader),
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn without_managed_config_for_tests(codex_home: PathBuf) -> Self {
|
|
Self::new_for_tests(
|
|
codex_home,
|
|
Vec::new(),
|
|
LoaderOverrides::without_managed_config_for_tests(),
|
|
CloudRequirementsLoader::default(),
|
|
)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn protected_feature_keys(config_layer_stack: &ConfigLayerStack) -> BTreeSet<String> {
|
|
let mut protected_features = config_layer_stack
|
|
.effective_config()
|
|
.get("features")
|
|
.and_then(toml::Value::as_table)
|
|
.map(|features| features.keys().cloned().collect::<BTreeSet<_>>())
|
|
.unwrap_or_default();
|
|
|
|
if let Some(feature_requirements) = config_layer_stack
|
|
.requirements_toml()
|
|
.feature_requirements
|
|
.as_ref()
|
|
{
|
|
protected_features.extend(feature_requirements.entries.keys().cloned());
|
|
}
|
|
|
|
protected_features
|
|
}
|
|
|
|
pub(crate) fn apply_runtime_feature_enablement(
|
|
config: &mut Config,
|
|
runtime_feature_enablement: &BTreeMap<String, bool>,
|
|
) {
|
|
let protected_features = protected_feature_keys(&config.config_layer_stack);
|
|
for (name, enabled) in runtime_feature_enablement {
|
|
if protected_features.contains(name) {
|
|
continue;
|
|
}
|
|
let Some(feature) = feature_for_key(name) else {
|
|
continue;
|
|
};
|
|
if let Err(err) = config.features.set_enabled(feature, *enabled) {
|
|
warn!(
|
|
feature = name,
|
|
error = %err,
|
|
"failed to apply runtime feature enablement"
|
|
);
|
|
}
|
|
}
|
|
}
|