mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Honor managed config during project discovery (#39306)
## What changed - Include legacy managed-file and MDM settings when resolving project root markers and project trust in both canonical and executor-local config loads. - Preserve the existing managed-layer precedence in the final config stack and resolve managed relative paths against the same bases used by the final loader. - Update explicit-untrusted warnings to describe the effective configuration and direct users to an administrator when the setting is managed. ## Testing - Cover managed root markers, managed trust overrides, MDM precedence, app-server config reads, and the remote TUI warning. GitOrigin-RevId: 9b5ddd54b4530f73589d6842a15cbab7e6d6bbb9
This commit is contained in:
committed by
copyberry
parent
e51a91b2f4
commit
6ec012668b
@@ -856,6 +856,79 @@ model_reasoning_effort = "high"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_read_respects_managed_project_root_markers() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_config(&codex_home, "model_context_window = 16384\n")?;
|
||||
let workspace = TempDir::new()?;
|
||||
let ancestor_config = workspace.path().join(".codex");
|
||||
let child = workspace.path().join("child");
|
||||
let child_config = child.join(".codex");
|
||||
for dir in [
|
||||
workspace.path().join(".git"),
|
||||
ancestor_config.clone(),
|
||||
child_config.clone(),
|
||||
] {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
}
|
||||
std::fs::write(
|
||||
ancestor_config.join("config.toml"),
|
||||
"model_context_window = 32768\n",
|
||||
)?;
|
||||
std::fs::write(
|
||||
child_config.join("config.toml"),
|
||||
"model_reasoning_effort = \"high\"\n",
|
||||
)?;
|
||||
set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?;
|
||||
let managed_path = codex_home.path().join("managed_config.toml");
|
||||
std::fs::write(&managed_path, "project_root_markers = []\n")?;
|
||||
let managed_path = managed_path.to_string_lossy().into_owned();
|
||||
|
||||
let mut app_server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("CODEX_APP_SERVER_MANAGED_CONFIG_PATH", Some(&managed_path))])
|
||||
.build_initialized_with_timeout(DEFAULT_READ_TIMEOUT)
|
||||
.await?;
|
||||
let request_id = app_server
|
||||
.send_config_read_request(ConfigReadParams {
|
||||
include_layers: true,
|
||||
cwd: Some(child.to_string_lossy().into_owned()),
|
||||
})
|
||||
.await?;
|
||||
let ConfigReadResponse { config, layers, .. } =
|
||||
timeout(DEFAULT_READ_TIMEOUT, app_server.read_response(request_id)).await??;
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
config.additional.get("project_root_markers"),
|
||||
config.model_context_window,
|
||||
config.model_reasoning_effort,
|
||||
),
|
||||
(Some(&json!([])), Some(16384), Some(ReasoningEffort::High))
|
||||
);
|
||||
let project_layers = layers
|
||||
.expect("layers present")
|
||||
.into_iter()
|
||||
.filter_map(|layer| {
|
||||
if let ConfigLayerSource::Project { dot_codex_folder } = layer.name {
|
||||
Some((dot_codex_folder, layer.config, layer.disabled_reason))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
project_layers,
|
||||
vec![(
|
||||
AbsolutePathBuf::try_from(child_config)?,
|
||||
json!({"model_reasoning_effort": "high"}),
|
||||
None,
|
||||
)]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_read_includes_system_layer_and_overrides() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -39,6 +39,12 @@ precedence first, highest precedence last, so later layers override earlier
|
||||
layers when folded. Thread config entries supplied by `thread_config_loader` are
|
||||
inserted according to their translated `ConfigLayerSource` precedence.
|
||||
|
||||
Project-root discovery and project trust use the applicable non-project layers,
|
||||
including legacy managed-file and MDM config at their normal precedence. The
|
||||
managed layers remain above project and session layers in the final stack.
|
||||
Executor-local reads use their own system, base-user, and legacy managed sources;
|
||||
they do not include cloud config, selected profiles, or session flags.
|
||||
|
||||
Layers with a `disabled_reason` are still surfaced for UI, but are ignored when
|
||||
computing the effective config and origins metadata. This is what
|
||||
`ConfigLayerStack::effective_config()` implements.
|
||||
@@ -77,6 +83,7 @@ Implementation is split by concern:
|
||||
|
||||
- `state.rs`: public types (`ConfigLayerEntry`, `ConfigLayerStack`) + merge/origins convenience methods.
|
||||
- `layer_io.rs`: reading `config.toml`, managed config, and managed preferences inputs.
|
||||
- `project_discovery.rs`: shared managed-config composition for project-root and trust discovery.
|
||||
- `overrides.rs`: CLI dotted-path overrides → TOML “session flags” layer.
|
||||
- `merge.rs`: recursive TOML merge.
|
||||
- `fingerprint.rs`: stable per-layer hashing and per-key origins traversal.
|
||||
|
||||
@@ -3,6 +3,7 @@ use super::layer_io;
|
||||
use super::load_config_toml_for_required_layer_raw;
|
||||
use super::load_requirements_toml;
|
||||
use super::load_root_checkout_project_config;
|
||||
use super::project_discovery;
|
||||
use super::project_root_markers_from_config;
|
||||
use super::project_trust_context;
|
||||
use super::requirements_layers_from_legacy_scheme;
|
||||
@@ -86,8 +87,8 @@ pub struct LocalTomlLayer<S> {
|
||||
/// config reads.
|
||||
///
|
||||
/// Cloud, selected profiles, session flags, and thread-provided layers are not
|
||||
/// included. Project discovery uses only the executor's system and base-user
|
||||
/// configuration.
|
||||
/// included. Project discovery uses the executor's system, base-user, and
|
||||
/// legacy managed configuration.
|
||||
pub async fn load_local_config_layers(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
codex_home: &Path,
|
||||
@@ -121,6 +122,13 @@ pub(super) async fn load_local_config_layers_with_overrides(
|
||||
let mut discovery_config = TomlValue::Table(toml::map::Map::new());
|
||||
merge_toml_values(&mut discovery_config, &system.toml);
|
||||
merge_toml_values(&mut discovery_config, &user.toml);
|
||||
// Managed file and MDM values also govern the project boundary and trust.
|
||||
// Only this snapshot is resolved; the returned local layers stay raw.
|
||||
project_discovery::merge_managed_config_for_discovery(
|
||||
&mut discovery_config,
|
||||
&loaded_managed,
|
||||
codex_home.as_path(),
|
||||
)?;
|
||||
let project_root_markers = project_root_markers_from_config(&discovery_config)?
|
||||
.unwrap_or_else(default_project_root_markers);
|
||||
let trust_context = project_trust_context(
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
use crate::ConfigLayerSource;
|
||||
use crate::ConfigLayerStack;
|
||||
use crate::LoaderOverrides;
|
||||
use crate::NoopThreadConfigLoader;
|
||||
use crate::loader::LocalConfigLayers;
|
||||
use crate::loader::load_config_layers_state;
|
||||
use crate::loader::local::load_local_config_layers_with_overrides;
|
||||
use crate::loader::project_trust_key;
|
||||
use crate::loader::tests::TestFileSystem;
|
||||
use crate::merge_toml_values;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
struct Fixture {
|
||||
_temp: TempDir,
|
||||
home: PathBuf,
|
||||
repo: AbsolutePathBuf,
|
||||
project: AbsolutePathBuf,
|
||||
cwd: AbsolutePathBuf,
|
||||
managed: PathBuf,
|
||||
overrides: LoaderOverrides,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
fn new() -> anyhow::Result<Self> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let home = temp.path().join("home");
|
||||
let repo = AbsolutePathBuf::from_absolute_path(temp.path().join("repo"))?;
|
||||
let project = repo.join("project");
|
||||
let cwd = project.join("child");
|
||||
std::fs::create_dir_all(&home)?;
|
||||
std::fs::create_dir_all(repo.join(".git"))?;
|
||||
for (dir, model) in [(&repo, "ancestor"), (&project, "project"), (&cwd, "child")] {
|
||||
std::fs::create_dir_all(dir.join(".codex"))?;
|
||||
std::fs::write(
|
||||
dir.join(".codex/config.toml"),
|
||||
format!("model = \"{model}\"\n"),
|
||||
)?;
|
||||
}
|
||||
std::fs::write(project.join(".company-root"), "")?;
|
||||
let managed = temp.path().join("managed_config.toml");
|
||||
let mut overrides = LoaderOverrides::with_managed_config_path_for_tests(managed.clone());
|
||||
overrides.system_config_path = Some(temp.path().join("system.toml"));
|
||||
let fixture = Self {
|
||||
_temp: temp,
|
||||
home,
|
||||
repo,
|
||||
project,
|
||||
cwd,
|
||||
managed,
|
||||
overrides,
|
||||
};
|
||||
std::fs::write(fixture.home.join("config.toml"), fixture.trust("trusted"))?;
|
||||
Ok(fixture)
|
||||
}
|
||||
|
||||
fn trust(&self, level: &str) -> String {
|
||||
let key = TomlValue::String(project_trust_key(self.repo.as_path()));
|
||||
format!("[projects.{key}]\ntrust_level = \"{level}\"\n")
|
||||
}
|
||||
|
||||
async fn load(&self) -> anyhow::Result<(ConfigLayerStack, LocalConfigLayers)> {
|
||||
let canonical = load_config_layers_state(
|
||||
&TestFileSystem,
|
||||
&self.home,
|
||||
Some(self.cwd.clone()),
|
||||
&[
|
||||
(
|
||||
"project_root_markers".into(),
|
||||
toml::Value::try_from([".git"])?,
|
||||
),
|
||||
("model".into(), TomlValue::String("session".into())),
|
||||
],
|
||||
self.overrides.clone(),
|
||||
&NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
let local = load_local_config_layers_with_overrides(
|
||||
&TestFileSystem,
|
||||
&self.home,
|
||||
&self.cwd,
|
||||
&self.overrides,
|
||||
)
|
||||
.await?;
|
||||
Ok((canonical, local))
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_discovery(
|
||||
canonical: &ConfigLayerStack,
|
||||
local: &LocalConfigLayers,
|
||||
expected_dirs: &[&AbsolutePathBuf],
|
||||
markers: &[&str],
|
||||
) -> anyhow::Result<()> {
|
||||
let project_dirs = |sources: Vec<&ConfigLayerSource>| {
|
||||
sources
|
||||
.into_iter()
|
||||
.filter_map(|source| match source {
|
||||
ConfigLayerSource::Project { dot_codex_folder } => Some(dot_codex_folder.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let expected_dirs = expected_dirs
|
||||
.iter()
|
||||
.map(|dir| dir.join(".codex"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
(
|
||||
project_dirs(
|
||||
canonical
|
||||
.layers_low_to_high()
|
||||
.map(|layer| &layer.name)
|
||||
.collect()
|
||||
),
|
||||
project_dirs(
|
||||
local
|
||||
.config
|
||||
.layers
|
||||
.iter()
|
||||
.map(|layer| &layer.source)
|
||||
.collect()
|
||||
),
|
||||
),
|
||||
(expected_dirs.clone(), expected_dirs)
|
||||
);
|
||||
let effective = canonical.effective_config();
|
||||
let mut local_effective = TomlValue::Table(toml::map::Map::new());
|
||||
for layer in &local.config.layers {
|
||||
merge_toml_values(&mut local_effective, &layer.toml);
|
||||
}
|
||||
let expected_markers = TomlValue::try_from(markers)?;
|
||||
assert_eq!(
|
||||
(
|
||||
effective.get("project_root_markers"),
|
||||
local_effective.get("project_root_markers"),
|
||||
),
|
||||
(Some(&expected_markers), Some(&expected_markers))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_project_discovery_uses_file_markers_without_changing_precedence()
|
||||
-> anyhow::Result<()> {
|
||||
let fixture = Fixture::new()?;
|
||||
for (markers, dirs) in [
|
||||
(
|
||||
vec![".git"],
|
||||
vec![&fixture.repo, &fixture.project, &fixture.cwd],
|
||||
),
|
||||
(vec![], vec![&fixture.cwd]),
|
||||
(vec![".company-root"], vec![&fixture.project, &fixture.cwd]),
|
||||
] {
|
||||
let markers_toml = TomlValue::try_from(&markers)?;
|
||||
std::fs::write(
|
||||
&fixture.managed,
|
||||
format!("project_root_markers = {markers_toml}\nmodel = \"managed\"\n"),
|
||||
)?;
|
||||
let (canonical, local) = fixture.load().await?;
|
||||
assert_discovery(&canonical, &local, &dirs, &markers)?;
|
||||
let managed_source = ConfigLayerSource::LegacyManagedConfigTomlFromFile {
|
||||
file: AbsolutePathBuf::from_absolute_path(&fixture.managed)?,
|
||||
};
|
||||
assert_eq!(
|
||||
(
|
||||
canonical.effective_config().get("model").cloned(),
|
||||
&canonical.origins()["model"].name,
|
||||
&local.config.layers.last().expect("managed layer").source,
|
||||
),
|
||||
(
|
||||
Some(TomlValue::String("managed".into())),
|
||||
&managed_source,
|
||||
&managed_source,
|
||||
)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_project_discovery_uses_managed_project_trust() -> anyhow::Result<()> {
|
||||
let fixture = Fixture::new()?;
|
||||
let trust_key = project_trust_key(fixture.repo.as_path());
|
||||
let user_config = fixture.home.join("config.toml");
|
||||
let untrusted_reason = format!(
|
||||
"{trust_key} is marked as untrusted in the effective configuration. To load project-local config, hooks, and exec policies, update its trust setting. If that setting is managed by your organization, contact your administrator."
|
||||
);
|
||||
let unknown_reason = format!(
|
||||
"To load project-local config, hooks, and exec policies, add {trust_key} as a trusted project in {}.",
|
||||
user_config.display()
|
||||
);
|
||||
for (user, managed, dirs, expected_reason) in [
|
||||
(
|
||||
fixture.trust("trusted"),
|
||||
fixture.trust("untrusted"),
|
||||
vec![],
|
||||
Some(untrusted_reason.as_str()),
|
||||
),
|
||||
(
|
||||
fixture.trust("untrusted"),
|
||||
fixture.trust("trusted"),
|
||||
vec![&fixture.cwd],
|
||||
None,
|
||||
),
|
||||
(
|
||||
String::new(),
|
||||
String::new(),
|
||||
vec![],
|
||||
Some(unknown_reason.as_str()),
|
||||
),
|
||||
] {
|
||||
std::fs::write(&user_config, user)?;
|
||||
std::fs::write(
|
||||
&fixture.managed,
|
||||
format!("project_root_markers = []\n{managed}"),
|
||||
)?;
|
||||
let (canonical, local) = fixture.load().await?;
|
||||
assert_discovery(&canonical, &local, &dirs, &[])?;
|
||||
assert_eq!(
|
||||
canonical
|
||||
.all_layers_low_to_high()
|
||||
.filter(|layer| matches!(layer.name, ConfigLayerSource::Project { .. }))
|
||||
.map(|layer| layer.disabled_reason.as_deref())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![expected_reason]
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn managed_project_discovery_mdm_overrides_file_markers() -> anyhow::Result<()> {
|
||||
use base64::Engine;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
|
||||
let mut fixture = Fixture::new()?;
|
||||
std::fs::write(&fixture.managed, "project_root_markers = [\".git\"]\n")?;
|
||||
for (markers, dirs) in [
|
||||
(vec![], vec![&fixture.cwd]),
|
||||
(vec![".company-root"], vec![&fixture.project, &fixture.cwd]),
|
||||
] {
|
||||
let markers_toml = TomlValue::try_from(&markers)?;
|
||||
fixture.overrides.managed_preferences_base64 =
|
||||
Some(BASE64_STANDARD.encode(format!("project_root_markers = {markers_toml}\n")));
|
||||
let (canonical, local) = fixture.load().await?;
|
||||
assert_discovery(&canonical, &local, &dirs, &markers)?;
|
||||
assert_eq!(
|
||||
(
|
||||
canonical
|
||||
.layers_high_to_low()
|
||||
.next()
|
||||
.map(|layer| &layer.name),
|
||||
local.config.layers.last().map(|layer| &layer.source),
|
||||
),
|
||||
(
|
||||
Some(&ConfigLayerSource::LegacyManagedConfigTomlFromMdm),
|
||||
Some(&ConfigLayerSource::LegacyManagedConfigTomlFromMdm),
|
||||
)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,6 +2,7 @@ mod layer_io;
|
||||
mod local;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
mod project_discovery;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -364,6 +365,13 @@ pub async fn load_config_layers_state(
|
||||
if let Some(cli_overrides_layer) = cli_overrides_layer.as_ref() {
|
||||
merge_toml_values(&mut merged_so_far, cli_overrides_layer);
|
||||
}
|
||||
// Managed config wins over CLI config. Apply it before choosing the
|
||||
// project root and trust, but keep its final layers above project config.
|
||||
project_discovery::merge_managed_config_for_discovery(
|
||||
&mut merged_so_far,
|
||||
&loaded_config_layers,
|
||||
codex_home,
|
||||
)?;
|
||||
|
||||
let project_root_markers = match project_root_markers_from_config(&merged_so_far) {
|
||||
Ok(markers) => markers.unwrap_or_else(default_project_root_markers),
|
||||
@@ -1053,9 +1061,11 @@ impl ProjectTrustContext {
|
||||
let gated_features = "project-local config, hooks, and exec policies";
|
||||
let trust_key = decision.trust_key.as_str();
|
||||
let user_config_file = self.user_config_file.as_path().display();
|
||||
// Trust may come from managed config. Keep the explicit-untrusted prefix
|
||||
// stable because the remote TUI uses it to recognize existing decisions.
|
||||
match decision.trust_level {
|
||||
Some(TrustLevel::Untrusted) => Some(format!(
|
||||
"{trust_key} is marked as untrusted in {user_config_file}. To load {gated_features}, mark it trusted."
|
||||
"{trust_key} is marked as untrusted in the effective configuration. To load {gated_features}, update its trust setting. If that setting is managed by your organization, contact your administrator."
|
||||
)),
|
||||
_ => Some(format!(
|
||||
"To load {gated_features}, add {trust_key} as a trusted project in {user_config_file}."
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
//! Shared configuration composition for project-root and trust discovery.
|
||||
|
||||
use super::layer_io::LoadedConfigLayers;
|
||||
use super::resolve_relative_paths_in_config_toml;
|
||||
use crate::merge_toml_values;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
/// Overlay the already-read managed sources at their normal precedence: file,
|
||||
/// then MDM. Resolve paths as the final loader does, without changing the raw
|
||||
/// layers or reading either source again.
|
||||
pub(super) fn merge_managed_config_for_discovery(
|
||||
discovery_config: &mut TomlValue,
|
||||
loaded: &LoadedConfigLayers,
|
||||
codex_home: &Path,
|
||||
) -> io::Result<()> {
|
||||
if let Some(config) = &loaded.managed_config {
|
||||
let base_dir = config.file.as_path().parent().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Managed config file {} has no parent directory",
|
||||
config.file.as_path().display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let resolved =
|
||||
resolve_relative_paths_in_config_toml(config.managed_config.clone(), base_dir)?;
|
||||
merge_toml_values(discovery_config, &resolved);
|
||||
}
|
||||
if let Some(config) = &loaded.managed_config_from_mdm {
|
||||
let resolved =
|
||||
resolve_relative_paths_in_config_toml(config.managed_config.clone(), codex_home)?;
|
||||
merge_toml_values(discovery_config, &resolved);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "managed_project_discovery_tests.rs"]
|
||||
mod tests;
|
||||
@@ -11,7 +11,7 @@ use codex_utils_path_uri::PathUri;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::tempdir;
|
||||
|
||||
struct TestFileSystem;
|
||||
pub(super) struct TestFileSystem;
|
||||
|
||||
impl ExecutorFileSystem for TestFileSystem {
|
||||
fn canonicalize<'a>(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use crate::legacy_core::config::ConfigOverrides;
|
||||
use codex_app_server_client::AppServerClient;
|
||||
@@ -108,6 +109,39 @@ async fn remote_project_trust_guards_thread_start_and_preserves_repository_decis
|
||||
None
|
||||
);
|
||||
|
||||
let response: JsonValue = app_server
|
||||
.request_typed(ClientRequest::ConfigRead {
|
||||
request_id: RequestId::String("untrusted-project-warning".to_string()),
|
||||
params: ConfigReadParams {
|
||||
include_layers: true,
|
||||
cwd: Some(project_cwd.to_string_lossy().into_owned()),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
let reason = response["layers"]
|
||||
.as_array()
|
||||
.expect("config layers")
|
||||
.iter()
|
||||
.find(|layer| layer["name"]["type"] == "project")
|
||||
.and_then(|layer| layer["disabledReason"].as_str())
|
||||
.expect("explicitly untrusted project warning");
|
||||
let warning = crate::history_cell::new_warning_event(
|
||||
reason.replace(&project_trust_key(&project_root), "<PROJECT>"),
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
warning
|
||||
.display_lines(/*width*/ 80)
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
@r"
|
||||
⚠ <PROJECT> is marked as untrusted in the effective configuration. To load
|
||||
project-local config, hooks, and exec policies, update its trust setting. If
|
||||
that setting is managed by your organization, contact your administrator.
|
||||
"
|
||||
);
|
||||
|
||||
std::fs::remove_file(project_cwd.join(".codex/config.toml"))?;
|
||||
std::fs::remove_dir(project_cwd.join(".codex"))?;
|
||||
let canonical_project_cwd = PathBuf::from(project_trust_key(&project_root)).join("nested");
|
||||
|
||||
Reference in New Issue
Block a user