mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
dcd5e08269
Resubmit https://github.com/openai/codex/pull/15020 with correct content. 1. Use requirement-resolved config.features as the plugin gate. 2. Guard plugin/list, plugin/read, and related flows behind that gate. 3. Skip bad marketplace.json files instead of failing the whole list. 4. Simplify plugin state and caching.
332 lines
11 KiB
Rust
332 lines
11 KiB
Rust
use crate::config_requirements::ConfigRequirements;
|
|
use crate::config_requirements::ConfigRequirementsToml;
|
|
|
|
use super::fingerprint::record_origins;
|
|
use super::fingerprint::version_for_toml;
|
|
use super::merge::merge_toml_values;
|
|
use codex_app_server_protocol::ConfigLayer;
|
|
use codex_app_server_protocol::ConfigLayerMetadata;
|
|
use codex_app_server_protocol::ConfigLayerSource;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use serde_json::Value as JsonValue;
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
use toml::Value as TomlValue;
|
|
|
|
/// LoaderOverrides overrides managed configuration inputs (primarily for tests).
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct LoaderOverrides {
|
|
pub managed_config_path: Option<PathBuf>,
|
|
//TODO(gt): Add a macos_ prefix to this field and remove the target_os check.
|
|
#[cfg(target_os = "macos")]
|
|
pub managed_preferences_base64: Option<String>,
|
|
pub macos_managed_config_requirements_base64: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct ConfigLayerEntry {
|
|
pub name: ConfigLayerSource,
|
|
pub config: TomlValue,
|
|
pub raw_toml: Option<String>,
|
|
pub version: String,
|
|
pub disabled_reason: Option<String>,
|
|
}
|
|
|
|
impl ConfigLayerEntry {
|
|
pub fn new(name: ConfigLayerSource, config: TomlValue) -> Self {
|
|
let version = version_for_toml(&config);
|
|
Self {
|
|
name,
|
|
config,
|
|
raw_toml: None,
|
|
version,
|
|
disabled_reason: None,
|
|
}
|
|
}
|
|
|
|
pub fn new_with_raw_toml(name: ConfigLayerSource, config: TomlValue, raw_toml: String) -> Self {
|
|
let version = version_for_toml(&config);
|
|
Self {
|
|
name,
|
|
config,
|
|
raw_toml: Some(raw_toml),
|
|
version,
|
|
disabled_reason: None,
|
|
}
|
|
}
|
|
|
|
pub fn new_disabled(
|
|
name: ConfigLayerSource,
|
|
config: TomlValue,
|
|
disabled_reason: impl Into<String>,
|
|
) -> Self {
|
|
let version = version_for_toml(&config);
|
|
Self {
|
|
name,
|
|
config,
|
|
raw_toml: None,
|
|
version,
|
|
disabled_reason: Some(disabled_reason.into()),
|
|
}
|
|
}
|
|
|
|
pub fn is_disabled(&self) -> bool {
|
|
self.disabled_reason.is_some()
|
|
}
|
|
|
|
pub fn raw_toml(&self) -> Option<&str> {
|
|
self.raw_toml.as_deref()
|
|
}
|
|
|
|
pub fn metadata(&self) -> ConfigLayerMetadata {
|
|
ConfigLayerMetadata {
|
|
name: self.name.clone(),
|
|
version: self.version.clone(),
|
|
}
|
|
}
|
|
|
|
pub fn as_layer(&self) -> ConfigLayer {
|
|
ConfigLayer {
|
|
name: self.name.clone(),
|
|
version: self.version.clone(),
|
|
config: serde_json::to_value(&self.config).unwrap_or(JsonValue::Null),
|
|
disabled_reason: self.disabled_reason.clone(),
|
|
}
|
|
}
|
|
|
|
// Get the `.codex/` folder associated with this config layer, if any.
|
|
pub fn config_folder(&self) -> Option<AbsolutePathBuf> {
|
|
match &self.name {
|
|
ConfigLayerSource::Mdm { .. } => None,
|
|
ConfigLayerSource::System { file } => file.parent(),
|
|
ConfigLayerSource::User { file } => file.parent(),
|
|
ConfigLayerSource::Project { dot_codex_folder } => Some(dot_codex_folder.clone()),
|
|
ConfigLayerSource::SessionFlags => None,
|
|
ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => None,
|
|
ConfigLayerSource::LegacyManagedConfigTomlFromMdm => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ConfigLayerStackOrdering {
|
|
LowestPrecedenceFirst,
|
|
HighestPrecedenceFirst,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq)]
|
|
pub struct ConfigLayerStack {
|
|
/// Layers are listed from lowest precedence (base) to highest (top), so
|
|
/// later entries in the Vec override earlier ones.
|
|
layers: Vec<ConfigLayerEntry>,
|
|
|
|
/// Index into [layers] of the user config layer, if any.
|
|
user_layer_index: Option<usize>,
|
|
|
|
/// Constraints that must be enforced when deriving a [Config] from the
|
|
/// layers.
|
|
requirements: ConfigRequirements,
|
|
|
|
/// Raw requirements data as loaded from requirements.toml/MDM/legacy
|
|
/// sources. This preserves the original allow-lists so they can be
|
|
/// surfaced via APIs.
|
|
requirements_toml: ConfigRequirementsToml,
|
|
}
|
|
|
|
impl ConfigLayerStack {
|
|
pub fn new(
|
|
layers: Vec<ConfigLayerEntry>,
|
|
requirements: ConfigRequirements,
|
|
requirements_toml: ConfigRequirementsToml,
|
|
) -> std::io::Result<Self> {
|
|
let user_layer_index = verify_layer_ordering(&layers)?;
|
|
Ok(Self {
|
|
layers,
|
|
user_layer_index,
|
|
requirements,
|
|
requirements_toml,
|
|
})
|
|
}
|
|
|
|
/// Returns the raw user config layer, if any.
|
|
///
|
|
/// This does not merge other config layers or apply any requirements.
|
|
pub fn get_user_layer(&self) -> Option<&ConfigLayerEntry> {
|
|
self.user_layer_index
|
|
.and_then(|index| self.layers.get(index))
|
|
}
|
|
|
|
pub fn requirements(&self) -> &ConfigRequirements {
|
|
&self.requirements
|
|
}
|
|
|
|
pub fn requirements_toml(&self) -> &ConfigRequirementsToml {
|
|
&self.requirements_toml
|
|
}
|
|
|
|
/// Creates a new [ConfigLayerStack] using the specified values to inject a
|
|
/// "user layer" into the stack. If such a layer already exists, it is
|
|
/// replaced; otherwise, it is inserted into the stack at the appropriate
|
|
/// position based on precedence rules.
|
|
pub fn with_user_config(&self, config_toml: &AbsolutePathBuf, user_config: TomlValue) -> Self {
|
|
let user_layer = ConfigLayerEntry::new(
|
|
ConfigLayerSource::User {
|
|
file: config_toml.clone(),
|
|
},
|
|
user_config,
|
|
);
|
|
|
|
let mut layers = self.layers.clone();
|
|
match self.user_layer_index {
|
|
Some(index) => {
|
|
layers[index] = user_layer;
|
|
Self {
|
|
layers,
|
|
user_layer_index: self.user_layer_index,
|
|
requirements: self.requirements.clone(),
|
|
requirements_toml: self.requirements_toml.clone(),
|
|
}
|
|
}
|
|
None => {
|
|
let user_layer_index = match layers
|
|
.iter()
|
|
.position(|layer| layer.name.precedence() > user_layer.name.precedence())
|
|
{
|
|
Some(index) => {
|
|
layers.insert(index, user_layer);
|
|
index
|
|
}
|
|
None => {
|
|
layers.push(user_layer);
|
|
layers.len() - 1
|
|
}
|
|
};
|
|
Self {
|
|
layers,
|
|
user_layer_index: Some(user_layer_index),
|
|
requirements: self.requirements.clone(),
|
|
requirements_toml: self.requirements_toml.clone(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the merged config-layer view.
|
|
///
|
|
/// This only merges ordinary config layers and does not apply requirements
|
|
/// such as cloud requirements.
|
|
pub fn effective_config(&self) -> TomlValue {
|
|
let mut merged = TomlValue::Table(toml::map::Map::new());
|
|
for layer in self.get_layers(
|
|
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
|
/*include_disabled*/ false,
|
|
) {
|
|
merge_toml_values(&mut merged, &layer.config);
|
|
}
|
|
merged
|
|
}
|
|
|
|
/// Returns field origins for the merged config-layer view.
|
|
///
|
|
/// Requirement sources are tracked separately and are not included here.
|
|
pub fn origins(&self) -> HashMap<String, ConfigLayerMetadata> {
|
|
let mut origins = HashMap::new();
|
|
let mut path = Vec::new();
|
|
|
|
for layer in self.get_layers(
|
|
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
|
/*include_disabled*/ false,
|
|
) {
|
|
record_origins(&layer.config, &layer.metadata(), &mut path, &mut origins);
|
|
}
|
|
|
|
origins
|
|
}
|
|
|
|
/// Returns config layers from highest precedence to lowest precedence.
|
|
///
|
|
/// Requirement sources are tracked separately and are not included here.
|
|
pub fn layers_high_to_low(&self) -> Vec<&ConfigLayerEntry> {
|
|
self.get_layers(
|
|
ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
|
/*include_disabled*/ false,
|
|
)
|
|
}
|
|
|
|
/// Returns config layers in the requested precedence order.
|
|
///
|
|
/// Requirement sources are tracked separately and are not included here.
|
|
pub fn get_layers(
|
|
&self,
|
|
ordering: ConfigLayerStackOrdering,
|
|
include_disabled: bool,
|
|
) -> Vec<&ConfigLayerEntry> {
|
|
let mut layers: Vec<&ConfigLayerEntry> = self
|
|
.layers
|
|
.iter()
|
|
.filter(|layer| include_disabled || !layer.is_disabled())
|
|
.collect();
|
|
if ordering == ConfigLayerStackOrdering::HighestPrecedenceFirst {
|
|
layers.reverse();
|
|
}
|
|
layers
|
|
}
|
|
}
|
|
|
|
/// Ensures precedence ordering of config layers is correct. Returns the index
|
|
/// of the user config layer, if any (at most one should exist).
|
|
fn verify_layer_ordering(layers: &[ConfigLayerEntry]) -> std::io::Result<Option<usize>> {
|
|
if !layers.iter().map(|layer| &layer.name).is_sorted() {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidData,
|
|
"config layers are not in correct precedence order",
|
|
));
|
|
}
|
|
|
|
// The previous check ensured `layers` is sorted by precedence, so now we
|
|
// further verify that:
|
|
// 1. There is at most one user config layer.
|
|
// 2. Project layers are ordered from root to cwd.
|
|
let mut user_layer_index: Option<usize> = None;
|
|
let mut previous_project_dot_codex_folder: Option<&AbsolutePathBuf> = None;
|
|
for (index, layer) in layers.iter().enumerate() {
|
|
if matches!(layer.name, ConfigLayerSource::User { .. }) {
|
|
if user_layer_index.is_some() {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidData,
|
|
"multiple user config layers found",
|
|
));
|
|
}
|
|
user_layer_index = Some(index);
|
|
}
|
|
|
|
if let ConfigLayerSource::Project {
|
|
dot_codex_folder: current_project_dot_codex_folder,
|
|
} = &layer.name
|
|
{
|
|
if let Some(previous) = previous_project_dot_codex_folder {
|
|
let Some(parent) = previous.as_path().parent() else {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidData,
|
|
"project layer has no parent directory",
|
|
));
|
|
};
|
|
if previous == current_project_dot_codex_folder
|
|
|| !current_project_dot_codex_folder
|
|
.as_path()
|
|
.ancestors()
|
|
.any(|ancestor| ancestor == parent)
|
|
{
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidData,
|
|
"project layers are not ordered from root to cwd",
|
|
));
|
|
}
|
|
}
|
|
previous_project_dot_codex_folder = Some(current_project_dot_codex_folder);
|
|
}
|
|
}
|
|
|
|
Ok(user_layer_index)
|
|
}
|