mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
b77fe8fefe
## Why Once the repo-local lint exists, `codex-rs` needs to follow the checked-in convention and CI needs to keep it from drifting. This commit applies the fallback `/*param*/` style consistently across existing positional literal call sites without changing those APIs. The longer-term preference is still to avoid APIs that require comments by choosing clearer parameter types and call shapes. This PR is intentionally the mechanical follow-through for the places where the existing signatures stay in place. After rebasing onto newer `main`, the rollout also had to cover newly introduced `tui_app_server` call sites. That made it clear the first cut of the CI job was too expensive for the common path: it was spending almost as much time installing `cargo-dylint` and re-testing the lint crate as a representative test job spends running product tests. The CI update keeps the full workspace enforcement but trims that extra overhead from ordinary `codex-rs` PRs. ## What changed - keep a dedicated `argument_comment_lint` job in `rust-ci` - mechanically annotate remaining opaque positional literals across `codex-rs` with exact `/*param*/` comments, including the rebased `tui_app_server` call sites that now fall under the lint - keep the checked-in style aligned with the lint policy by using `/*param*/` and leaving string and char literals uncommented - cache `cargo-dylint`, `dylint-link`, and the relevant Cargo registry/git metadata in the lint job - split changed-path detection so the lint crate's own `cargo test` step runs only when `tools/argument-comment-lint/*` or `rust-ci.yml` changes - continue to run the repo wrapper over the `codex-rs` workspace, so product-code enforcement is unchanged Most of the code changes in this commit are intentionally mechanical comment rewrites or insertions driven by the lint itself. ## Verification - `./tools/argument-comment-lint/run.sh --workspace` - `cargo test -p codex-tui-app-server -p codex-tui` - parsed `.github/workflows/rust-ci.yml` locally with PyYAML --- * -> #14652 * #14651
321 lines
11 KiB
Rust
321 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 user config layer, if any.
|
|
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(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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 the highest-precedence to lowest-precedence layers, so
|
|
/// `ConfigLayerSource::SessionFlags` would be first, if present.
|
|
pub fn layers_high_to_low(&self) -> Vec<&ConfigLayerEntry> {
|
|
self.get_layers(
|
|
ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
|
/*include_disabled*/ false,
|
|
)
|
|
}
|
|
|
|
/// Returns the highest-precedence to lowest-precedence layers, so
|
|
/// `ConfigLayerSource::SessionFlags` would be first, if present.
|
|
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)
|
|
}
|