core-agent-ide/codex-rs/config/src/constraint.rs

279 lines
8.1 KiB
Rust
Raw Permalink Normal View History

feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
use std::fmt;
use std::sync::Arc;
Extract `codex-config` from `codex-core` (#11389) `codex-core` had accumulated config loading, requirements parsing, constraint logic, and config-layer state handling in a single crate. This change extracts that subsystem into `codex-config` to reduce `codex-core` rebuild/test surface area and isolate future config work. ## What Changed ### Added `codex-config` - Added new workspace crate `codex-rs/config` (`codex-config`). - Added workspace/build wiring in: - `codex-rs/Cargo.toml` - `codex-rs/config/Cargo.toml` - `codex-rs/config/BUILD.bazel` - Updated lockfiles (`codex-rs/Cargo.lock`, `MODULE.bazel.lock`). - Added `codex-core` -> `codex-config` dependency in `codex-rs/core/Cargo.toml`. ### Moved config internals from `core` into `config` Moved modules to `codex-rs/config/src/`: - `core/src/config/constraint.rs` -> `config/src/constraint.rs` - `core/src/config_loader/cloud_requirements.rs` -> `config/src/cloud_requirements.rs` - `core/src/config_loader/config_requirements.rs` -> `config/src/config_requirements.rs` - `core/src/config_loader/fingerprint.rs` -> `config/src/fingerprint.rs` - `core/src/config_loader/merge.rs` -> `config/src/merge.rs` - `core/src/config_loader/overrides.rs` -> `config/src/overrides.rs` - `core/src/config_loader/requirements_exec_policy.rs` -> `config/src/requirements_exec_policy.rs` - `core/src/config_loader/state.rs` -> `config/src/state.rs` `codex-config` now re-exports this surface from `config/src/lib.rs` at the crate top level. ### Updated `core` to consume/re-export `codex-config` - `core/src/config_loader/mod.rs` now imports/re-exports config-loader types/functions from top-level `codex_config::*`. - Local moved modules were removed from `core/src/config_loader/`. - `core/src/config/mod.rs` now re-exports constraint types from `codex_config`.
2026-02-11 10:02:49 -08:00
use crate::config_requirements::RequirementSource;
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
use thiserror::Error;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum ConstraintError {
#[error(
"invalid value for `{field_name}`: `{candidate}` is not in the allowed set {allowed} (set by {requirement_source})"
)]
InvalidValue {
field_name: &'static str,
candidate: String,
allowed: String,
requirement_source: RequirementSource,
},
#[error("field `{field_name}` cannot be empty")]
EmptyField { field_name: String },
#[error("invalid rules in requirements (set by {requirement_source}): {reason}")]
ExecPolicyParse {
requirement_source: RequirementSource,
reason: String,
},
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
}
impl ConstraintError {
feat: migrate to new constraint-based loading strategy (#8251) 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`. How I would review this PR: - start with `codex-rs/app-server-protocol/src/protocol/v2.rs` and note the new variants added to the `ConfigLayerSource` enum: `LegacyManagedConfigTomlFromFile` and `LegacyManagedConfigTomlFromMdm` - note that `ConfigLayerSource` now has a `precedence()` method and implements `PartialOrd` - `codex-rs/core/src/config_loader/layer_io.rs` is responsible for loading "admin" preferences from `/etc/codex/managed_config.toml` and MDM. Because `/etc/codex/managed_config.toml` is now deprecated in favor of `/etc/codex/requirements.toml` and `/etc/codex/config.toml`, we now include some extra information on the `LoadedConfigLayers` returned in `layer_io.rs`. - `codex-rs/core/src/config_loader/mod.rs` has major changes to `load_config_layers_state()`, which is what produces `ConfigLayerStack`. The docstring has the new specification and describes the various layers that will be loaded and the precedence order. - It uses the information from `LoaderOverrides` "twice," both in the spirit of legacy support: - We use one instances to derive an instance of `ConfigRequirements`. Currently, the only field in `managed_config.toml` that contributes to `ConfigRequirements` is `approval_policy`. This PR introduces `Constrained::allow_only()` to support this. - We use a clone of `LoaderOverrides` to derive `ConfigLayerSource::LegacyManagedConfigTomlFromFile` and `ConfigLayerSource::LegacyManagedConfigTomlFromMdm` layers, as appropriate. As before, this ends up being a "best effort" at enterprise controls, but is enforcement is not guaranteed like it is for `ConfigRequirements`. - Now we only create a "user" layer if `$CODEX_HOME/config.toml` exists. (Previously, a user layer was always created for `ConfigLayerStack`.) - Similarly, we only add a "session flags" layer if there are CLI overrides. - `config_loader/state.rs` contains the updated implementation for `ConfigLayerStack`. Note the public API is largely the same as before, but the implementation is quite different. We leverage the fact that `ConfigLayerSource` is now `PartialOrd` to ensure layers are in the correct order. - A `Config` constructed via `ConfigBuilder.build()` will use `load_config_layers_state()` to create the `ConfigLayerStack` and use the associated `ConfigRequirements` when constructing the `Config` object. - That said, a `Config` constructed via `Config::load_from_base_config_with_overrides()` does _not_ yet use `ConfigBuilder`, so it creates a `ConfigRequirements::default()` instead of loading a proper `ConfigRequirements`. I will fix this in a subsequent PR. Then the following files are mostly test changes: ``` codex-rs/app-server/tests/suite/v2/config_rpc.rs codex-rs/core/src/config/service.rs codex-rs/core/src/config_loader/tests.rs ``` Again, because we do not always include "user" and "session flags" layers when the contents are empty, `ConfigLayerStack` sometimes has fewer layers than before (and the precedence order changed slightly), which is the main reason integration tests changed.
2025-12-18 10:06:05 -08:00
pub fn empty_field(field_name: impl Into<String>) -> Self {
Self::EmptyField {
field_name: field_name.into(),
feat: migrate to new constraint-based loading strategy (#8251) 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`. How I would review this PR: - start with `codex-rs/app-server-protocol/src/protocol/v2.rs` and note the new variants added to the `ConfigLayerSource` enum: `LegacyManagedConfigTomlFromFile` and `LegacyManagedConfigTomlFromMdm` - note that `ConfigLayerSource` now has a `precedence()` method and implements `PartialOrd` - `codex-rs/core/src/config_loader/layer_io.rs` is responsible for loading "admin" preferences from `/etc/codex/managed_config.toml` and MDM. Because `/etc/codex/managed_config.toml` is now deprecated in favor of `/etc/codex/requirements.toml` and `/etc/codex/config.toml`, we now include some extra information on the `LoadedConfigLayers` returned in `layer_io.rs`. - `codex-rs/core/src/config_loader/mod.rs` has major changes to `load_config_layers_state()`, which is what produces `ConfigLayerStack`. The docstring has the new specification and describes the various layers that will be loaded and the precedence order. - It uses the information from `LoaderOverrides` "twice," both in the spirit of legacy support: - We use one instances to derive an instance of `ConfigRequirements`. Currently, the only field in `managed_config.toml` that contributes to `ConfigRequirements` is `approval_policy`. This PR introduces `Constrained::allow_only()` to support this. - We use a clone of `LoaderOverrides` to derive `ConfigLayerSource::LegacyManagedConfigTomlFromFile` and `ConfigLayerSource::LegacyManagedConfigTomlFromMdm` layers, as appropriate. As before, this ends up being a "best effort" at enterprise controls, but is enforcement is not guaranteed like it is for `ConfigRequirements`. - Now we only create a "user" layer if `$CODEX_HOME/config.toml` exists. (Previously, a user layer was always created for `ConfigLayerStack`.) - Similarly, we only add a "session flags" layer if there are CLI overrides. - `config_loader/state.rs` contains the updated implementation for `ConfigLayerStack`. Note the public API is largely the same as before, but the implementation is quite different. We leverage the fact that `ConfigLayerSource` is now `PartialOrd` to ensure layers are in the correct order. - A `Config` constructed via `ConfigBuilder.build()` will use `load_config_layers_state()` to create the `ConfigLayerStack` and use the associated `ConfigRequirements` when constructing the `Config` object. - That said, a `Config` constructed via `Config::load_from_base_config_with_overrides()` does _not_ yet use `ConfigBuilder`, so it creates a `ConfigRequirements::default()` instead of loading a proper `ConfigRequirements`. I will fix this in a subsequent PR. Then the following files are mostly test changes: ``` codex-rs/app-server/tests/suite/v2/config_rpc.rs codex-rs/core/src/config/service.rs codex-rs/core/src/config_loader/tests.rs ``` Again, because we do not always include "user" and "session flags" layers when the contents are empty, `ConfigLayerStack` sometimes has fewer layers than before (and the precedence order changed slightly), which is the main reason integration tests changed.
2025-12-18 10:06:05 -08:00
}
}
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
}
pub type ConstraintResult<T> = Result<T, ConstraintError>;
impl From<ConstraintError> for std::io::Error {
fn from(err: ConstraintError) -> Self {
std::io::Error::new(std::io::ErrorKind::InvalidInput, err)
}
}
type ConstraintValidator<T> = dyn Fn(&T) -> ConstraintResult<()> + Send + Sync;
Restrict MCP servers from `requirements.toml` (#9101) Enterprises want to restrict the MCP servers their users can use. Admins can now specify an allowlist of MCPs in `requirements.toml`. The MCP servers are matched on both Name and Transport (local path or HTTP URL) -- both must match to allow the MCP server. This prevents circumventing the allowlist by renaming MCP servers in user config. (It is still possible to replace the local path e.g. rewrite say `/usr/local/github-mcp` with a nefarious MCP. We could allow hash pinning in the future, but that would break updates. I also think this represents a broader, out-of-scope problem.) We introduce a new field to Constrained: "normalizer". In general, it is a fn(T) -> T and applies when `Constrained<T>.set()` is called. In this particular case, it disables MCP servers which do not match the allowlist. An alternative solution would remove this and instead throw a ConstraintError. That would stop Codex launching if any MCP server was configured which didn't match. I think this is bad. We currently reuse the enabled flag on MCP servers to disable them, but don't propagate any information about why they are disabled. I'd like to add that in a follow up PR, possibly by switching out enabled with an enum. In action: ``` # MCP server config has two MCPs. We are going to allowlist one of them. ➜ codex git:(gt/restrict-mcps) ✗ cat ~/.codex/config.toml | grep mcp_servers -A1 [mcp_servers.hello_world] command = "hello-world-mcp" -- [mcp_servers.docs] command = "docs-mcp" # Restrict the MCPs to the hello_world MCP. ➜ codex git:(gt/restrict-mcps) ✗ defaults read com.openai.codex requirements_toml_base64 | base64 -d [mcp_server_allowlist.hello_world] command = "hello-world-mcp" # List the MCPs, observe hello_world is enabled and docs is disabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - disabled Unsupported hello_world hello-world-mcp - - - enabled Unsupported # Remove the restrictions. ➜ codex git:(gt/restrict-mcps) ✗ defaults delete com.openai.codex requirements_toml_base64 # Observe both MCPs are enabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - enabled Unsupported hello_world hello-world-mcp - - - enabled Unsupported # A new requirements that updates the command to one that does not match. ➜ codex git:(gt/restrict-mcps) ✗ cat ~/requirements.toml [mcp_server_allowlist.hello_world] command = "hello-world-mcp-v2" # Use those requirements. ➜ codex git:(gt/restrict-mcps) ✗ defaults write com.openai.codex requirements_toml_base64 "$(base64 -i /Users/gt/requirements.toml)" # Observe both MCPs are disabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.75s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - disabled Unsupported hello_world hello-world-mcp - - - disabled Unsupported ```
2026-01-13 19:45:00 +00:00
/// A ConstraintNormalizer is a function which transforms a value into another of the same type.
/// `Constrained` uses normalizers to transform values to satisfy constraints or enforce values.
type ConstraintNormalizer<T> = dyn Fn(T) -> T + Send + Sync;
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
#[derive(Clone)]
pub struct Constrained<T> {
value: T,
validator: Arc<ConstraintValidator<T>>,
Restrict MCP servers from `requirements.toml` (#9101) Enterprises want to restrict the MCP servers their users can use. Admins can now specify an allowlist of MCPs in `requirements.toml`. The MCP servers are matched on both Name and Transport (local path or HTTP URL) -- both must match to allow the MCP server. This prevents circumventing the allowlist by renaming MCP servers in user config. (It is still possible to replace the local path e.g. rewrite say `/usr/local/github-mcp` with a nefarious MCP. We could allow hash pinning in the future, but that would break updates. I also think this represents a broader, out-of-scope problem.) We introduce a new field to Constrained: "normalizer". In general, it is a fn(T) -> T and applies when `Constrained<T>.set()` is called. In this particular case, it disables MCP servers which do not match the allowlist. An alternative solution would remove this and instead throw a ConstraintError. That would stop Codex launching if any MCP server was configured which didn't match. I think this is bad. We currently reuse the enabled flag on MCP servers to disable them, but don't propagate any information about why they are disabled. I'd like to add that in a follow up PR, possibly by switching out enabled with an enum. In action: ``` # MCP server config has two MCPs. We are going to allowlist one of them. ➜ codex git:(gt/restrict-mcps) ✗ cat ~/.codex/config.toml | grep mcp_servers -A1 [mcp_servers.hello_world] command = "hello-world-mcp" -- [mcp_servers.docs] command = "docs-mcp" # Restrict the MCPs to the hello_world MCP. ➜ codex git:(gt/restrict-mcps) ✗ defaults read com.openai.codex requirements_toml_base64 | base64 -d [mcp_server_allowlist.hello_world] command = "hello-world-mcp" # List the MCPs, observe hello_world is enabled and docs is disabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - disabled Unsupported hello_world hello-world-mcp - - - enabled Unsupported # Remove the restrictions. ➜ codex git:(gt/restrict-mcps) ✗ defaults delete com.openai.codex requirements_toml_base64 # Observe both MCPs are enabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - enabled Unsupported hello_world hello-world-mcp - - - enabled Unsupported # A new requirements that updates the command to one that does not match. ➜ codex git:(gt/restrict-mcps) ✗ cat ~/requirements.toml [mcp_server_allowlist.hello_world] command = "hello-world-mcp-v2" # Use those requirements. ➜ codex git:(gt/restrict-mcps) ✗ defaults write com.openai.codex requirements_toml_base64 "$(base64 -i /Users/gt/requirements.toml)" # Observe both MCPs are disabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.75s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - disabled Unsupported hello_world hello-world-mcp - - - disabled Unsupported ```
2026-01-13 19:45:00 +00:00
normalizer: Option<Arc<ConstraintNormalizer<T>>>,
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
}
impl<T: Send + Sync> Constrained<T> {
pub fn new(
initial_value: T,
validator: impl Fn(&T) -> ConstraintResult<()> + Send + Sync + 'static,
) -> ConstraintResult<Self> {
let validator: Arc<ConstraintValidator<T>> = Arc::new(validator);
validator(&initial_value)?;
Ok(Self {
value: initial_value,
validator,
Restrict MCP servers from `requirements.toml` (#9101) Enterprises want to restrict the MCP servers their users can use. Admins can now specify an allowlist of MCPs in `requirements.toml`. The MCP servers are matched on both Name and Transport (local path or HTTP URL) -- both must match to allow the MCP server. This prevents circumventing the allowlist by renaming MCP servers in user config. (It is still possible to replace the local path e.g. rewrite say `/usr/local/github-mcp` with a nefarious MCP. We could allow hash pinning in the future, but that would break updates. I also think this represents a broader, out-of-scope problem.) We introduce a new field to Constrained: "normalizer". In general, it is a fn(T) -> T and applies when `Constrained<T>.set()` is called. In this particular case, it disables MCP servers which do not match the allowlist. An alternative solution would remove this and instead throw a ConstraintError. That would stop Codex launching if any MCP server was configured which didn't match. I think this is bad. We currently reuse the enabled flag on MCP servers to disable them, but don't propagate any information about why they are disabled. I'd like to add that in a follow up PR, possibly by switching out enabled with an enum. In action: ``` # MCP server config has two MCPs. We are going to allowlist one of them. ➜ codex git:(gt/restrict-mcps) ✗ cat ~/.codex/config.toml | grep mcp_servers -A1 [mcp_servers.hello_world] command = "hello-world-mcp" -- [mcp_servers.docs] command = "docs-mcp" # Restrict the MCPs to the hello_world MCP. ➜ codex git:(gt/restrict-mcps) ✗ defaults read com.openai.codex requirements_toml_base64 | base64 -d [mcp_server_allowlist.hello_world] command = "hello-world-mcp" # List the MCPs, observe hello_world is enabled and docs is disabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - disabled Unsupported hello_world hello-world-mcp - - - enabled Unsupported # Remove the restrictions. ➜ codex git:(gt/restrict-mcps) ✗ defaults delete com.openai.codex requirements_toml_base64 # Observe both MCPs are enabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - enabled Unsupported hello_world hello-world-mcp - - - enabled Unsupported # A new requirements that updates the command to one that does not match. ➜ codex git:(gt/restrict-mcps) ✗ cat ~/requirements.toml [mcp_server_allowlist.hello_world] command = "hello-world-mcp-v2" # Use those requirements. ➜ codex git:(gt/restrict-mcps) ✗ defaults write com.openai.codex requirements_toml_base64 "$(base64 -i /Users/gt/requirements.toml)" # Observe both MCPs are disabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.75s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - disabled Unsupported hello_world hello-world-mcp - - - disabled Unsupported ```
2026-01-13 19:45:00 +00:00
normalizer: None,
})
}
/// normalized creates a `Constrained` value with a normalizer function and a validator that allows any value.
pub fn normalized(
initial_value: T,
normalizer: impl Fn(T) -> T + Send + Sync + 'static,
) -> ConstraintResult<Self> {
let validator: Arc<ConstraintValidator<T>> = Arc::new(|_| Ok(()));
let normalizer: Arc<ConstraintNormalizer<T>> = Arc::new(normalizer);
let normalized = normalizer(initial_value);
validator(&normalized)?;
Ok(Self {
value: normalized,
validator,
normalizer: Some(normalizer),
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
})
}
pub fn allow_any(initial_value: T) -> Self {
Self {
value: initial_value,
validator: Arc::new(|_| Ok(())),
Restrict MCP servers from `requirements.toml` (#9101) Enterprises want to restrict the MCP servers their users can use. Admins can now specify an allowlist of MCPs in `requirements.toml`. The MCP servers are matched on both Name and Transport (local path or HTTP URL) -- both must match to allow the MCP server. This prevents circumventing the allowlist by renaming MCP servers in user config. (It is still possible to replace the local path e.g. rewrite say `/usr/local/github-mcp` with a nefarious MCP. We could allow hash pinning in the future, but that would break updates. I also think this represents a broader, out-of-scope problem.) We introduce a new field to Constrained: "normalizer". In general, it is a fn(T) -> T and applies when `Constrained<T>.set()` is called. In this particular case, it disables MCP servers which do not match the allowlist. An alternative solution would remove this and instead throw a ConstraintError. That would stop Codex launching if any MCP server was configured which didn't match. I think this is bad. We currently reuse the enabled flag on MCP servers to disable them, but don't propagate any information about why they are disabled. I'd like to add that in a follow up PR, possibly by switching out enabled with an enum. In action: ``` # MCP server config has two MCPs. We are going to allowlist one of them. ➜ codex git:(gt/restrict-mcps) ✗ cat ~/.codex/config.toml | grep mcp_servers -A1 [mcp_servers.hello_world] command = "hello-world-mcp" -- [mcp_servers.docs] command = "docs-mcp" # Restrict the MCPs to the hello_world MCP. ➜ codex git:(gt/restrict-mcps) ✗ defaults read com.openai.codex requirements_toml_base64 | base64 -d [mcp_server_allowlist.hello_world] command = "hello-world-mcp" # List the MCPs, observe hello_world is enabled and docs is disabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - disabled Unsupported hello_world hello-world-mcp - - - enabled Unsupported # Remove the restrictions. ➜ codex git:(gt/restrict-mcps) ✗ defaults delete com.openai.codex requirements_toml_base64 # Observe both MCPs are enabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - enabled Unsupported hello_world hello-world-mcp - - - enabled Unsupported # A new requirements that updates the command to one that does not match. ➜ codex git:(gt/restrict-mcps) ✗ cat ~/requirements.toml [mcp_server_allowlist.hello_world] command = "hello-world-mcp-v2" # Use those requirements. ➜ codex git:(gt/restrict-mcps) ✗ defaults write com.openai.codex requirements_toml_base64 "$(base64 -i /Users/gt/requirements.toml)" # Observe both MCPs are disabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.75s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - disabled Unsupported hello_world hello-world-mcp - - - disabled Unsupported ```
2026-01-13 19:45:00 +00:00
normalizer: None,
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
}
}
pub fn allow_only(only_value: T) -> Self
where
T: Clone + fmt::Debug + PartialEq + 'static,
{
let allowed_value = only_value.clone();
Self {
value: only_value,
validator: Arc::new(move |candidate| {
if candidate == &allowed_value {
Ok(())
} else {
Err(ConstraintError::InvalidValue {
field_name: "<unknown>",
candidate: format!("{candidate:?}"),
allowed: format!("[{allowed_value:?}]"),
requirement_source: RequirementSource::Unknown,
})
}
}),
normalizer: None,
}
}
feat: migrate to new constraint-based loading strategy (#8251) 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`. How I would review this PR: - start with `codex-rs/app-server-protocol/src/protocol/v2.rs` and note the new variants added to the `ConfigLayerSource` enum: `LegacyManagedConfigTomlFromFile` and `LegacyManagedConfigTomlFromMdm` - note that `ConfigLayerSource` now has a `precedence()` method and implements `PartialOrd` - `codex-rs/core/src/config_loader/layer_io.rs` is responsible for loading "admin" preferences from `/etc/codex/managed_config.toml` and MDM. Because `/etc/codex/managed_config.toml` is now deprecated in favor of `/etc/codex/requirements.toml` and `/etc/codex/config.toml`, we now include some extra information on the `LoadedConfigLayers` returned in `layer_io.rs`. - `codex-rs/core/src/config_loader/mod.rs` has major changes to `load_config_layers_state()`, which is what produces `ConfigLayerStack`. The docstring has the new specification and describes the various layers that will be loaded and the precedence order. - It uses the information from `LoaderOverrides` "twice," both in the spirit of legacy support: - We use one instances to derive an instance of `ConfigRequirements`. Currently, the only field in `managed_config.toml` that contributes to `ConfigRequirements` is `approval_policy`. This PR introduces `Constrained::allow_only()` to support this. - We use a clone of `LoaderOverrides` to derive `ConfigLayerSource::LegacyManagedConfigTomlFromFile` and `ConfigLayerSource::LegacyManagedConfigTomlFromMdm` layers, as appropriate. As before, this ends up being a "best effort" at enterprise controls, but is enforcement is not guaranteed like it is for `ConfigRequirements`. - Now we only create a "user" layer if `$CODEX_HOME/config.toml` exists. (Previously, a user layer was always created for `ConfigLayerStack`.) - Similarly, we only add a "session flags" layer if there are CLI overrides. - `config_loader/state.rs` contains the updated implementation for `ConfigLayerStack`. Note the public API is largely the same as before, but the implementation is quite different. We leverage the fact that `ConfigLayerSource` is now `PartialOrd` to ensure layers are in the correct order. - A `Config` constructed via `ConfigBuilder.build()` will use `load_config_layers_state()` to create the `ConfigLayerStack` and use the associated `ConfigRequirements` when constructing the `Config` object. - That said, a `Config` constructed via `Config::load_from_base_config_with_overrides()` does _not_ yet use `ConfigBuilder`, so it creates a `ConfigRequirements::default()` instead of loading a proper `ConfigRequirements`. I will fix this in a subsequent PR. Then the following files are mostly test changes: ``` codex-rs/app-server/tests/suite/v2/config_rpc.rs codex-rs/core/src/config/service.rs codex-rs/core/src/config_loader/tests.rs ``` Again, because we do not always include "user" and "session flags" layers when the contents are empty, `ConfigLayerStack` sometimes has fewer layers than before (and the precedence order changed slightly), which is the main reason integration tests changed.
2025-12-18 10:06:05 -08:00
/// Allow any value of T, using T's Default as the initial value.
pub fn allow_any_from_default() -> Self
where
T: Default,
{
Self::allow_any(T::default())
}
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
pub fn get(&self) -> &T {
&self.value
}
pub fn value(&self) -> T
where
T: Copy,
{
self.value
}
pub fn can_set(&self, candidate: &T) -> ConstraintResult<()> {
(self.validator)(candidate)
}
pub fn set(&mut self, value: T) -> ConstraintResult<()> {
Restrict MCP servers from `requirements.toml` (#9101) Enterprises want to restrict the MCP servers their users can use. Admins can now specify an allowlist of MCPs in `requirements.toml`. The MCP servers are matched on both Name and Transport (local path or HTTP URL) -- both must match to allow the MCP server. This prevents circumventing the allowlist by renaming MCP servers in user config. (It is still possible to replace the local path e.g. rewrite say `/usr/local/github-mcp` with a nefarious MCP. We could allow hash pinning in the future, but that would break updates. I also think this represents a broader, out-of-scope problem.) We introduce a new field to Constrained: "normalizer". In general, it is a fn(T) -> T and applies when `Constrained<T>.set()` is called. In this particular case, it disables MCP servers which do not match the allowlist. An alternative solution would remove this and instead throw a ConstraintError. That would stop Codex launching if any MCP server was configured which didn't match. I think this is bad. We currently reuse the enabled flag on MCP servers to disable them, but don't propagate any information about why they are disabled. I'd like to add that in a follow up PR, possibly by switching out enabled with an enum. In action: ``` # MCP server config has two MCPs. We are going to allowlist one of them. ➜ codex git:(gt/restrict-mcps) ✗ cat ~/.codex/config.toml | grep mcp_servers -A1 [mcp_servers.hello_world] command = "hello-world-mcp" -- [mcp_servers.docs] command = "docs-mcp" # Restrict the MCPs to the hello_world MCP. ➜ codex git:(gt/restrict-mcps) ✗ defaults read com.openai.codex requirements_toml_base64 | base64 -d [mcp_server_allowlist.hello_world] command = "hello-world-mcp" # List the MCPs, observe hello_world is enabled and docs is disabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - disabled Unsupported hello_world hello-world-mcp - - - enabled Unsupported # Remove the restrictions. ➜ codex git:(gt/restrict-mcps) ✗ defaults delete com.openai.codex requirements_toml_base64 # Observe both MCPs are enabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - enabled Unsupported hello_world hello-world-mcp - - - enabled Unsupported # A new requirements that updates the command to one that does not match. ➜ codex git:(gt/restrict-mcps) ✗ cat ~/requirements.toml [mcp_server_allowlist.hello_world] command = "hello-world-mcp-v2" # Use those requirements. ➜ codex git:(gt/restrict-mcps) ✗ defaults write com.openai.codex requirements_toml_base64 "$(base64 -i /Users/gt/requirements.toml)" # Observe both MCPs are disabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.75s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - disabled Unsupported hello_world hello-world-mcp - - - disabled Unsupported ```
2026-01-13 19:45:00 +00:00
let value = if let Some(normalizer) = &self.normalizer {
normalizer(value)
} else {
value
};
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
(self.validator)(&value)?;
self.value = value;
Ok(())
}
}
impl<T> std::ops::Deref for Constrained<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T: fmt::Debug> fmt::Debug for Constrained<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Constrained")
.field("value", &self.value)
.finish()
}
}
impl<T: PartialEq> PartialEq for Constrained<T> {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
fn invalid_value(candidate: impl Into<String>, allowed: impl Into<String>) -> ConstraintError {
ConstraintError::InvalidValue {
field_name: "<unknown>",
candidate: candidate.into(),
allowed: allowed.into(),
requirement_source: RequirementSource::Unknown,
}
}
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
#[test]
fn constrained_allow_any_accepts_any_value() {
let mut constrained = Constrained::allow_any(5);
constrained.set(-10).expect("allow any accepts all values");
assert_eq!(constrained.value(), -10);
}
feat: migrate to new constraint-based loading strategy (#8251) 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`. How I would review this PR: - start with `codex-rs/app-server-protocol/src/protocol/v2.rs` and note the new variants added to the `ConfigLayerSource` enum: `LegacyManagedConfigTomlFromFile` and `LegacyManagedConfigTomlFromMdm` - note that `ConfigLayerSource` now has a `precedence()` method and implements `PartialOrd` - `codex-rs/core/src/config_loader/layer_io.rs` is responsible for loading "admin" preferences from `/etc/codex/managed_config.toml` and MDM. Because `/etc/codex/managed_config.toml` is now deprecated in favor of `/etc/codex/requirements.toml` and `/etc/codex/config.toml`, we now include some extra information on the `LoadedConfigLayers` returned in `layer_io.rs`. - `codex-rs/core/src/config_loader/mod.rs` has major changes to `load_config_layers_state()`, which is what produces `ConfigLayerStack`. The docstring has the new specification and describes the various layers that will be loaded and the precedence order. - It uses the information from `LoaderOverrides` "twice," both in the spirit of legacy support: - We use one instances to derive an instance of `ConfigRequirements`. Currently, the only field in `managed_config.toml` that contributes to `ConfigRequirements` is `approval_policy`. This PR introduces `Constrained::allow_only()` to support this. - We use a clone of `LoaderOverrides` to derive `ConfigLayerSource::LegacyManagedConfigTomlFromFile` and `ConfigLayerSource::LegacyManagedConfigTomlFromMdm` layers, as appropriate. As before, this ends up being a "best effort" at enterprise controls, but is enforcement is not guaranteed like it is for `ConfigRequirements`. - Now we only create a "user" layer if `$CODEX_HOME/config.toml` exists. (Previously, a user layer was always created for `ConfigLayerStack`.) - Similarly, we only add a "session flags" layer if there are CLI overrides. - `config_loader/state.rs` contains the updated implementation for `ConfigLayerStack`. Note the public API is largely the same as before, but the implementation is quite different. We leverage the fact that `ConfigLayerSource` is now `PartialOrd` to ensure layers are in the correct order. - A `Config` constructed via `ConfigBuilder.build()` will use `load_config_layers_state()` to create the `ConfigLayerStack` and use the associated `ConfigRequirements` when constructing the `Config` object. - That said, a `Config` constructed via `Config::load_from_base_config_with_overrides()` does _not_ yet use `ConfigBuilder`, so it creates a `ConfigRequirements::default()` instead of loading a proper `ConfigRequirements`. I will fix this in a subsequent PR. Then the following files are mostly test changes: ``` codex-rs/app-server/tests/suite/v2/config_rpc.rs codex-rs/core/src/config/service.rs codex-rs/core/src/config_loader/tests.rs ``` Again, because we do not always include "user" and "session flags" layers when the contents are empty, `ConfigLayerStack` sometimes has fewer layers than before (and the precedence order changed slightly), which is the main reason integration tests changed.
2025-12-18 10:06:05 -08:00
#[test]
fn constrained_allow_any_default_uses_default_value() {
let constrained = Constrained::<i32>::allow_any_from_default();
assert_eq!(constrained.value(), 0);
}
Restrict MCP servers from `requirements.toml` (#9101) Enterprises want to restrict the MCP servers their users can use. Admins can now specify an allowlist of MCPs in `requirements.toml`. The MCP servers are matched on both Name and Transport (local path or HTTP URL) -- both must match to allow the MCP server. This prevents circumventing the allowlist by renaming MCP servers in user config. (It is still possible to replace the local path e.g. rewrite say `/usr/local/github-mcp` with a nefarious MCP. We could allow hash pinning in the future, but that would break updates. I also think this represents a broader, out-of-scope problem.) We introduce a new field to Constrained: "normalizer". In general, it is a fn(T) -> T and applies when `Constrained<T>.set()` is called. In this particular case, it disables MCP servers which do not match the allowlist. An alternative solution would remove this and instead throw a ConstraintError. That would stop Codex launching if any MCP server was configured which didn't match. I think this is bad. We currently reuse the enabled flag on MCP servers to disable them, but don't propagate any information about why they are disabled. I'd like to add that in a follow up PR, possibly by switching out enabled with an enum. In action: ``` # MCP server config has two MCPs. We are going to allowlist one of them. ➜ codex git:(gt/restrict-mcps) ✗ cat ~/.codex/config.toml | grep mcp_servers -A1 [mcp_servers.hello_world] command = "hello-world-mcp" -- [mcp_servers.docs] command = "docs-mcp" # Restrict the MCPs to the hello_world MCP. ➜ codex git:(gt/restrict-mcps) ✗ defaults read com.openai.codex requirements_toml_base64 | base64 -d [mcp_server_allowlist.hello_world] command = "hello-world-mcp" # List the MCPs, observe hello_world is enabled and docs is disabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - disabled Unsupported hello_world hello-world-mcp - - - enabled Unsupported # Remove the restrictions. ➜ codex git:(gt/restrict-mcps) ✗ defaults delete com.openai.codex requirements_toml_base64 # Observe both MCPs are enabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - enabled Unsupported hello_world hello-world-mcp - - - enabled Unsupported # A new requirements that updates the command to one that does not match. ➜ codex git:(gt/restrict-mcps) ✗ cat ~/requirements.toml [mcp_server_allowlist.hello_world] command = "hello-world-mcp-v2" # Use those requirements. ➜ codex git:(gt/restrict-mcps) ✗ defaults write com.openai.codex requirements_toml_base64 "$(base64 -i /Users/gt/requirements.toml)" # Observe both MCPs are disabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.75s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - disabled Unsupported hello_world hello-world-mcp - - - disabled Unsupported ```
2026-01-13 19:45:00 +00:00
#[test]
fn constrained_allow_only_rejects_different_values() {
let mut constrained = Constrained::allow_only(5);
constrained
.set(5)
.expect("allowed value should be accepted");
let err = constrained
.set(6)
.expect_err("different value should be rejected");
assert_eq!(err, invalid_value("6", "[5]"));
assert_eq!(constrained.value(), 5);
}
#[test]
Restrict MCP servers from `requirements.toml` (#9101) Enterprises want to restrict the MCP servers their users can use. Admins can now specify an allowlist of MCPs in `requirements.toml`. The MCP servers are matched on both Name and Transport (local path or HTTP URL) -- both must match to allow the MCP server. This prevents circumventing the allowlist by renaming MCP servers in user config. (It is still possible to replace the local path e.g. rewrite say `/usr/local/github-mcp` with a nefarious MCP. We could allow hash pinning in the future, but that would break updates. I also think this represents a broader, out-of-scope problem.) We introduce a new field to Constrained: "normalizer". In general, it is a fn(T) -> T and applies when `Constrained<T>.set()` is called. In this particular case, it disables MCP servers which do not match the allowlist. An alternative solution would remove this and instead throw a ConstraintError. That would stop Codex launching if any MCP server was configured which didn't match. I think this is bad. We currently reuse the enabled flag on MCP servers to disable them, but don't propagate any information about why they are disabled. I'd like to add that in a follow up PR, possibly by switching out enabled with an enum. In action: ``` # MCP server config has two MCPs. We are going to allowlist one of them. ➜ codex git:(gt/restrict-mcps) ✗ cat ~/.codex/config.toml | grep mcp_servers -A1 [mcp_servers.hello_world] command = "hello-world-mcp" -- [mcp_servers.docs] command = "docs-mcp" # Restrict the MCPs to the hello_world MCP. ➜ codex git:(gt/restrict-mcps) ✗ defaults read com.openai.codex requirements_toml_base64 | base64 -d [mcp_server_allowlist.hello_world] command = "hello-world-mcp" # List the MCPs, observe hello_world is enabled and docs is disabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - disabled Unsupported hello_world hello-world-mcp - - - enabled Unsupported # Remove the restrictions. ➜ codex git:(gt/restrict-mcps) ✗ defaults delete com.openai.codex requirements_toml_base64 # Observe both MCPs are enabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - enabled Unsupported hello_world hello-world-mcp - - - enabled Unsupported # A new requirements that updates the command to one that does not match. ➜ codex git:(gt/restrict-mcps) ✗ cat ~/requirements.toml [mcp_server_allowlist.hello_world] command = "hello-world-mcp-v2" # Use those requirements. ➜ codex git:(gt/restrict-mcps) ✗ defaults write com.openai.codex requirements_toml_base64 "$(base64 -i /Users/gt/requirements.toml)" # Observe both MCPs are disabled. ➜ codex git:(gt/restrict-mcps) ✗ just codex mcp list cargo run --bin codex -- "$@" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.75s Running `target/debug/codex mcp list` Name Command Args Env Cwd Status Auth docs docs-mcp - - - disabled Unsupported hello_world hello-world-mcp - - - disabled Unsupported ```
2026-01-13 19:45:00 +00:00
fn constrained_normalizer_applies_on_init_and_set() -> anyhow::Result<()> {
let mut constrained = Constrained::normalized(-1, |value| value.max(0))?;
assert_eq!(constrained.value(), 0);
constrained.set(-5)?;
assert_eq!(constrained.value(), 0);
constrained.set(10)?;
assert_eq!(constrained.value(), 10);
Ok(())
}
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
#[test]
fn constrained_new_rejects_invalid_initial_value() {
let result = Constrained::new(0, |value| {
if *value > 0 {
Ok(())
} else {
Err(invalid_value(value.to_string(), "positive values"))
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
}
});
assert_eq!(result, Err(invalid_value("0", "positive values")));
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
}
#[test]
fn constrained_set_rejects_invalid_value_and_leaves_previous() {
let mut constrained = Constrained::new(1, |value| {
if *value > 0 {
Ok(())
} else {
Err(invalid_value(value.to_string(), "positive values"))
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
}
})
.expect("initial value should be accepted");
let err = constrained
.set(-5)
.expect_err("negative values should be rejected");
assert_eq!(err, invalid_value("-5", "positive values"));
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
assert_eq!(constrained.value(), 1);
}
#[test]
fn constrained_can_set_allows_probe_without_setting() {
let constrained = Constrained::new(1, |value| {
if *value > 0 {
Ok(())
} else {
Err(invalid_value(value.to_string(), "positive values"))
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
}
})
.expect("initial value should be accepted");
constrained
.can_set(&2)
.expect("can_set should accept positive value");
let err = constrained
.can_set(&-1)
.expect_err("can_set should reject negative value");
assert_eq!(err, invalid_value("-1", "positive values"));
feat: Constrain values for approval_policy (#7778) Constrain `approval_policy` through new `admin_policy` config. This PR will: 1. Add a `admin_policy` section to config, with a single field (for now) `allowed_approval_policies`. This list constrains the set of user-settable `approval_policy`s. 2. Introduce a new `Constrained<T>` type, which combines a current value and a validator function. The validator function ensures disallowed values are not set. 3. Change the type of `approval_policy` on `Config` and `SessionConfiguration` from `AskForApproval` to `Constrained<AskForApproval>`. The validator function is set by the values passed into `allowed_approval_policies`. 4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When set, it disables selection of the value and indicates as such in the menu. This also makes it unselectable with arrow keys or numbers. This is used in the `/approvals` menu. Follow ups are: 1. Do the same thing to `sandbox_policy`. 2. Propagate the allowed set of values through app-server for the extension (though already this should prevent app-server from setting this values, it's just that we want to disable UI elements that are unsettable). Happy to split this PR up if you prefer, into the logical numbered areas above. Especially if there are parts we want to gavel on separately (e.g. admin_policy). Disabled full access: <img width="1680" height="380" alt="image" src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0" /> Disabled `--yolo` on startup: <img width="749" height="76" alt="image" src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb" /> CODEX-4087
2025-12-17 16:19:27 +00:00
assert_eq!(constrained.value(), 1);
}
}