-
Notifications
You must be signed in to change notification settings - Fork 6.9k
feat: migrate to new constraint-based loading strategy #8251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
@codex review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| match tokio::fs::read_to_string(&user_file).await { | ||
| Ok(contents) => { | ||
| let user_config: TomlValue = | ||
| toml::from_str(&contents).unwrap_or(TomlValue::Table(toml::map::Map::new())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Propagate parse errors for user config
When $CODEX_HOME/config.toml exists but is malformed, the loader now parses with toml::from_str(...).unwrap_or(...), which silently falls back to an empty table. That bypasses the error propagation promised in the comment and means a user’s invalid config is ignored rather than surfaced, so their settings disappear without any feedback. This is a regression from the previous behavior where invalid TOML resulted in an error, and it can hide configuration mistakes in production.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed locally: fix available when I update this PR.
codex-rs/core/src/config/service.rs
Outdated
| let user_layer = layers.get_user_layer().ok_or_else(|| { | ||
| // TODO(mbolin): Support creating the user layer if it does not exist. | ||
| ConfigServiceError::write( | ||
| ConfigWriteErrorCode::UserLayerNotFound, | ||
| "user layer not found and we do not support creating it yet", | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allow writes when no user config exists
With the new layering, load_config_layers_state no longer creates a user layer when config.toml is missing. apply_edits now treats a missing user layer as an error and returns UserLayerNotFound, which means config writes cannot create the initial file. This breaks first‑time configuration updates (e.g., UI settings on a fresh install) that previously succeeded by writing an empty base layer.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed locally: fix available when I update this PR.
gt-oai
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for doing this and for the detailed 'how to review' guide!
| impl ConfigLayerSource { | ||
| pub fn precedence(&self) -> i16 { | ||
| match self { | ||
| ConfigLayerSource::Mdm { .. } => 0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps start at 10, not 0, to allow for room below? you can go negative, ignore me
| } | ||
|
|
||
| impl ConfigLayerSource { | ||
| pub fn precedence(&self) -> i16 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest adding a small comment here about what lower / higher precedence means in practice. I'd probably explain it multiple ways that mean the same thing such that readers can pick the most understandable one.
"Higher precedence configs are applied last."
"Higher precedence configs overwrite values from lower precendence configs". etc.
| /// Base config deserialized from /etc/codex/requirements.toml or MDM. | ||
| #[derive(Deserialize, Debug, Clone, Default, PartialEq)] | ||
| pub struct ConfigRequirementsToml { | ||
| pub approval_policy: Option<Vec<AskForApproval>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
approval_policy or allowed_approval_policies? I worry that reusing the same field name here will have the following undesirable effects:
- Users may assume, because they have the same field names, that requirements.toml has the same shape as config.toml, or that all fields from config.toml are supported. I think it's confusing that the fields have the same name but different types.
- It may set expectations that there should be a bijection between fields in config.toml and fields in requirements.toml. But I foresee a future where there are things you could set in requirements.toml that don't map cleanly to individual values in config.toml.
- Name collisions make it harder to grep, for us and for Codex.
In summary: (1) users will expect things in config.toml to be in requirements.toml (2) users will expect things in requirements.toml to be in config.toml
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, happy to change!
|
|
||
| fn try_from(toml: ConfigRequirementsToml) -> Result<Self, Self::Error> { | ||
| let approval_policy: Constrained<AskForApproval> = match toml.approval_policy { | ||
| Some(policies) => Constrained::allow_values(AskForApproval::default(), policies)?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this imply that, if the default is not in the allowed policies, we just fail?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like we aren't using this right now. But we should be wary of this when we start reading from requirements.toml. I expect this to look more like "pick a sensible allowed default for the user" rather than "crash" -- might be worth adding a todo here so codex can pick it up when we implement requirements.toml reading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good point. I'll change that so:
- If
AskForApproval::default()is inpolicies, make it the default (since that would be Codex's default even ifrequirements.tomldid not exist). - Else, choose the first item in
policies.
| // a config layer on top of everything else. Note users can still | ||
| // override these values on a per-turn basis in the TUI and VS Code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think "users can still override..." is strictly true because you're pulling out the approval_policy from the managed config and constraining it with Constrained::allow_only, so I expect that to actually take effect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clarified the comment. Currently, this is true for all fields except approval_policy.
Over in `config_loader/macos.rs`, we were doing this complicated `mod` thing to expose one version of `load_managed_admin_config_layer()` for Mac: https://github.com/openai/codex/blob/580c59aa9af61cb4bffb5b204bd16a5dcc4bc911/codex-rs/core/src/config_loader/macos.rs#L4-L5 While exposing a trivial implementation for non-Mac: https://github.com/openai/codex/blob/580c59aa9af61cb4bffb5b204bd16a5dcc4bc911/codex-rs/core/src/config_loader/macos.rs#L110-L117 That was being used like this: https://github.com/openai/codex/blob/580c59aa9af61cb4bffb5b204bd16a5dcc4bc911/codex-rs/core/src/config_loader/layer_io.rs#L47-L48 This PR simplifies that callsite in `layer_io.rs` to just be: ```rust #[cfg(not(target_os = "macos"))] let managed_preferences = None; ``` And updates `config_loader/mod.rs` so we only pull in `macos.rs` on Mac: ```rust #[cfg(target_os = "macos")] mod macos; ``` This simplifies `macos.rs` considerably, though it looks like a big change because everything gets unindented and reformatted because we can drop the whole `mod native` thing now. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/8248). * #8251 * #8249 * __->__ #8248
a271ed5 to
d5a6b56
Compare
This is a significant change to how layers of configuration are applied. In particular, the `ConfigLayerStack` now has two important fields: - `layers: Vec<ConfigLayerEntry>` - `requirements: ConfigRequirements` We merge `TomlValue`s across the layers, but they are subject to `ConfigRequirements` before creating a `Config`.
|
@gt-oai I believe all comments have been addressed (including the ones from codexbot) if you want to take another look! |
This is a significant change to how layers of configuration are applied. In particular, the
ConfigLayerStacknow has two important fields:layers: Vec<ConfigLayerEntry>requirements: ConfigRequirementsWe merge
TomlValues across the layers, but they are subject toConfigRequirementsbefore creating aConfig.How I would review this PR:
codex-rs/app-server-protocol/src/protocol/v2.rsand note the new variants added to theConfigLayerSourceenum:LegacyManagedConfigTomlFromFileandLegacyManagedConfigTomlFromMdmConfigLayerSourcenow has aprecedence()method and implementsPartialOrdcodex-rs/core/src/config_loader/layer_io.rsis responsible for loading "admin" preferences from/etc/codex/managed_config.tomland MDM. Because/etc/codex/managed_config.tomlis now deprecated in favor of/etc/codex/requirements.tomland/etc/codex/config.toml, we now include some extra information on theLoadedConfigLayersreturned inlayer_io.rs.codex-rs/core/src/config_loader/mod.rshas major changes toload_config_layers_state(), which is what producesConfigLayerStack. The docstring has the new specification and describes the various layers that will be loaded and the precedence order.LoaderOverrides"twice," both in the spirit of legacy support:ConfigRequirements. Currently, the only field inmanaged_config.tomlthat contributes toConfigRequirementsisapproval_policy. This PR introducesConstrained::allow_only()to support this.LoaderOverridesto deriveConfigLayerSource::LegacyManagedConfigTomlFromFileandConfigLayerSource::LegacyManagedConfigTomlFromMdmlayers, as appropriate. As before, this ends up being a "best effort" at enterprise controls, but is enforcement is not guaranteed like it is forConfigRequirements.$CODEX_HOME/config.tomlexists. (Previously, a user layer was always created forConfigLayerStack.)config_loader/state.rscontains the updated implementation forConfigLayerStack. Note the public API is largely the same as before, but the implementation is quite different. We leverage the fact thatConfigLayerSourceis nowPartialOrdto ensure layers are in the correct order.Configconstructed viaConfigBuilder.build()will useload_config_layers_state()to create theConfigLayerStackand use the associatedConfigRequirementswhen constructing theConfigobject.Configconstructed viaConfig::load_from_base_config_with_overrides()does not yet useConfigBuilder, so it creates aConfigRequirements::default()instead of loading a properConfigRequirements. I will fix this in a subsequent PR.Then the following files are mostly test changes:
Again, because we do not always include "user" and "session flags" layers when the contents are empty,
ConfigLayerStacksometimes has fewer layers than before (and the precedence order changed slightly), which is the main reason integration tests changed.