mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Support packaged defaults in config layering (#37810)
## What changed - Load an optional package-supplied config file as the lowest-precedence configuration layer, so every existing layer can override its values. - Report the layer as `packagedDefaults`, including its source path, through config diagnostics and the app-server protocol. - Return an error when a configured packaged defaults file is missing. ## Testing - Cover precedence across packaged, system, user, and session layers. - Cover the missing-file error path. GitOrigin-RevId: 52406890f4d580eedabc90bf2a9ee3affa36fee1
This commit is contained in:
+26
@@ -8509,6 +8509,32 @@
|
||||
},
|
||||
"ConfigLayerSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Default configuration supplied with the installed Codex package.",
|
||||
"properties": {
|
||||
"file": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Path to the packaged default configuration file."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"packagedDefaults"
|
||||
],
|
||||
"title": "PackagedDefaultsConfigLayerSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"file",
|
||||
"type"
|
||||
],
|
||||
"title": "PackagedDefaultsConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Managed preferences layer delivered by MDM (macOS only).",
|
||||
"properties": {
|
||||
|
||||
+26
@@ -4674,6 +4674,32 @@
|
||||
},
|
||||
"ConfigLayerSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Default configuration supplied with the installed Codex package.",
|
||||
"properties": {
|
||||
"file": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Path to the packaged default configuration file."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"packagedDefaults"
|
||||
],
|
||||
"title": "PackagedDefaultsConfigLayerSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"file",
|
||||
"type"
|
||||
],
|
||||
"title": "PackagedDefaultsConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Managed preferences layer delivered by MDM (macOS only).",
|
||||
"properties": {
|
||||
|
||||
@@ -477,6 +477,32 @@
|
||||
},
|
||||
"ConfigLayerSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Default configuration supplied with the installed Codex package.",
|
||||
"properties": {
|
||||
"file": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Path to the packaged default configuration file."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"packagedDefaults"
|
||||
],
|
||||
"title": "PackagedDefaultsConfigLayerSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"file",
|
||||
"type"
|
||||
],
|
||||
"title": "PackagedDefaultsConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Managed preferences layer delivered by MDM (macOS only).",
|
||||
"properties": {
|
||||
|
||||
@@ -22,6 +22,32 @@
|
||||
},
|
||||
"ConfigLayerSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Default configuration supplied with the installed Codex package.",
|
||||
"properties": {
|
||||
"file": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Path to the packaged default configuration file."
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"packagedDefaults"
|
||||
],
|
||||
"title": "PackagedDefaultsConfigLayerSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"file",
|
||||
"type"
|
||||
],
|
||||
"title": "PackagedDefaultsConfigLayerSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Managed preferences layer delivered by MDM (macOS only).",
|
||||
"properties": {
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -3,7 +3,11 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
|
||||
export type ConfigLayerSource = { "type": "mdm", domain: string, key: string, } | { "type": "system",
|
||||
export type ConfigLayerSource = { "type": "packagedDefaults",
|
||||
/**
|
||||
* Path to the packaged default configuration file.
|
||||
*/
|
||||
file: AbsolutePathBuf, } | { "type": "mdm", domain: string, key: string, } | { "type": "system",
|
||||
/**
|
||||
* This is the path to the system config.toml file, though it is not
|
||||
* guaranteed to exist.
|
||||
|
||||
@@ -27,6 +27,14 @@ use std::path::PathBuf;
|
||||
#[ts(tag = "type")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum ConfigLayerSource {
|
||||
/// Default configuration supplied with the installed Codex package.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
PackagedDefaults {
|
||||
/// Path to the packaged default configuration file.
|
||||
file: AbsolutePathBuf,
|
||||
},
|
||||
|
||||
/// Managed preferences layer delivered by MDM (macOS only).
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
@@ -102,6 +110,7 @@ impl ConfigLayerSource {
|
||||
/// from a layer with a lower precedence.
|
||||
pub fn precedence(&self) -> i16 {
|
||||
match self {
|
||||
ConfigLayerSource::PackagedDefaults { .. } => -10,
|
||||
ConfigLayerSource::Mdm { .. } => 0,
|
||||
ConfigLayerSource::System { .. } => 10,
|
||||
ConfigLayerSource::EnterpriseManaged { .. } => 15,
|
||||
|
||||
@@ -272,7 +272,7 @@ Example with notification opt-out:
|
||||
- `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result.
|
||||
- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`.
|
||||
- `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id.
|
||||
- `config/read` — fetch the runtime-effective config after resolving config layering and managed requirements, including opaque `desktop` values stored in `config.toml`.
|
||||
- `config/read` — fetch the runtime-effective config after resolving config layering and managed requirements, including opaque `desktop` values stored in `config.toml`. When configured, the `packagedDefaults` layer has the lowest precedence.
|
||||
- `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome`, optional `cwds`, and an optional `migrationSource` selector. Omitted, `null`, or unrecognized migration-source values retain the default behavior. The deprecated optional `source` field remains accepted for compatibility but does not select the migration source. Each detected item includes `cwd` (`null` for home), and multi-item migrations may additionally include structured `details` with plugin ids, skill names, memory, session metadata, or other artifact names. The response also includes connector candidates inferred from detected source sessions, with a normalized display `name`, the number of detected sessions that used the connector, and the source metadata field used for detection.
|
||||
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any `details` returned by detect. Pass the same optional `migrationSource` used for detection so the server reads from the matching source; omitted, `null`, or unrecognized values retain the default behavior. The optional `source` identifies the product that initiated the import, while the optional opaque `providerId` attributes analytics to the provider selected by that product without affecting migration-source selection. The response acknowledges the synchronous import phase with an `importId`. Expected migration failures are reported as per-item failures rather than JSON-RPC errors, so the server still returns that `importId` and emits `externalAgentConfig/import/completed` with the same ID once all synchronous and background work finishes. The completion notification contains type-level `itemTypeResults` with successes and failures, including raw failure messages for the client to report separately.
|
||||
- `externalAgentConfig/import/readHistories` — read completed import histories and connector candidates detected from successfully imported session histories. Successful session entries include the original imported title when one was available. Connector candidates include a normalized display `name`, the number of imported sessions that used the connector, and the source metadata field used for detection.
|
||||
|
||||
@@ -13,6 +13,9 @@ use codex_config::ConfigLayerSource;
|
||||
/// function instead of a `From` implementation.
|
||||
pub(crate) fn config_layer_source_to_api(source: ConfigLayerSource) -> ApiConfigLayerSource {
|
||||
match source {
|
||||
ConfigLayerSource::PackagedDefaults { file } => {
|
||||
ApiConfigLayerSource::PackagedDefaults { file }
|
||||
}
|
||||
ConfigLayerSource::Mdm { domain, key } => ApiConfigLayerSource::Mdm { domain, key },
|
||||
ConfigLayerSource::System { file } => ApiConfigLayerSource::System { file },
|
||||
ConfigLayerSource::EnterpriseManaged { id, name } => {
|
||||
|
||||
@@ -781,6 +781,9 @@ fn value_at_semantic_path<'a>(root: &'a TomlValue, segments: &[String]) -> Optio
|
||||
|
||||
fn override_message(layer: &ConfigLayerSource) -> String {
|
||||
match layer {
|
||||
ConfigLayerSource::PackagedDefaults { file } => {
|
||||
format!("Overridden by packaged defaults: {}", file.display())
|
||||
}
|
||||
ConfigLayerSource::Mdm { domain, key: _ } => {
|
||||
format!("Overridden by managed policy (MDM): {domain}")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ use serde_json::Value as JsonValue;
|
||||
/// Provenance for one layer in the effective Codex configuration.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ConfigLayerSource {
|
||||
/// Default configuration supplied with the installed Codex package.
|
||||
PackagedDefaults { file: AbsolutePathBuf },
|
||||
/// Managed preferences delivered by MDM.
|
||||
Mdm { domain: String, key: String },
|
||||
/// Host-wide configuration loaded from a file.
|
||||
@@ -30,6 +32,7 @@ impl ConfigLayerSource {
|
||||
/// from a layer with a lower precedence.
|
||||
pub fn precedence(&self) -> i16 {
|
||||
match self {
|
||||
ConfigLayerSource::PackagedDefaults { .. } => -10,
|
||||
ConfigLayerSource::Mdm { .. } => 0,
|
||||
ConfigLayerSource::System { .. } => 10,
|
||||
ConfigLayerSource::EnterpriseManaged { .. } => 15,
|
||||
@@ -74,6 +77,9 @@ pub struct ConfigLayer {
|
||||
|
||||
pub fn format_config_layer_source(source: &ConfigLayerSource, config_toml_file: &str) -> String {
|
||||
match source {
|
||||
ConfigLayerSource::PackagedDefaults { file } => {
|
||||
format!("packaged defaults ({})", file.as_path().display())
|
||||
}
|
||||
ConfigLayerSource::Mdm { domain, key } => {
|
||||
format!("MDM ({domain}:{key})")
|
||||
}
|
||||
|
||||
@@ -244,6 +244,7 @@ where
|
||||
|
||||
fn config_path_for_layer(layer: &ConfigLayerEntry, config_toml_file: &str) -> Option<PathBuf> {
|
||||
match &layer.name {
|
||||
ConfigLayerSource::PackagedDefaults { file } => Some(file.to_path_buf()),
|
||||
ConfigLayerSource::System { file } => Some(file.to_path_buf()),
|
||||
ConfigLayerSource::User { file, .. } => Some(file.to_path_buf()),
|
||||
ConfigLayerSource::Project { dot_codex_folder } => {
|
||||
|
||||
@@ -101,6 +101,7 @@ async fn first_layer_config_error_from_entries(layers: &[ConfigLayerEntry]) -> O
|
||||
///
|
||||
/// Configuration is built up from multiple layers in the following order:
|
||||
///
|
||||
/// - package: optional default configuration supplied with the Codex package
|
||||
/// - admin: managed preferences (*)
|
||||
/// - system `/etc/codex/config.toml` (Unix) or
|
||||
/// `%ProgramData%\OpenAI\Codex\config.toml` (Windows)
|
||||
@@ -134,6 +135,36 @@ pub async fn load_config_layers_state(
|
||||
strict_config,
|
||||
cloud_config_bundle,
|
||||
} = options.into();
|
||||
let packaged_defaults_layer = if let Some(file) = &overrides.packaged_defaults_path {
|
||||
let config = layer_io::read_config_from_path(
|
||||
fs,
|
||||
file,
|
||||
/*log_missing_as_info*/ false,
|
||||
strict_config,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("packaged defaults config file {} not found", file.display()),
|
||||
)
|
||||
})?;
|
||||
let base_dir = file.as_path().parent().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"packaged defaults config file {} has no parent directory",
|
||||
file.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
Some(ConfigLayerEntry::new(
|
||||
ConfigLayerSource::PackagedDefaults { file: file.clone() },
|
||||
resolve_relative_paths_in_config_toml(config, base_dir)?,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let active_user_profile = overrides.user_config_profile.clone();
|
||||
let ignore_managed_requirements = overrides.ignore_managed_requirements;
|
||||
let ignore_user_config = overrides.ignore_user_config;
|
||||
@@ -218,6 +249,7 @@ pub async fn load_config_layers_state(
|
||||
.map_err(io::Error::other)?;
|
||||
|
||||
let mut layers = Vec::<ConfigLayerEntry>::new();
|
||||
layers.extend(packaged_defaults_layer);
|
||||
|
||||
let cli_overrides_layer = if cli_overrides.is_empty() {
|
||||
None
|
||||
|
||||
@@ -115,6 +115,112 @@ impl ExecutorFileSystem for TestFileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn packaged_defaults_have_lower_precedence_than_existing_config_layers() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let packaged_defaults_path =
|
||||
AbsolutePathBuf::resolve_path_against_base("packaged-defaults.toml", tmp.path());
|
||||
let system_config_path = tmp.path().join("system.toml");
|
||||
let user_config_path = tmp.path().join(CONFIG_TOML_FILE);
|
||||
|
||||
std::fs::write(
|
||||
packaged_defaults_path.as_path(),
|
||||
r#"
|
||||
model = "packaged-model"
|
||||
model_provider = "packaged-provider"
|
||||
model_context_window = 120000
|
||||
"#,
|
||||
)
|
||||
.expect("write packaged defaults");
|
||||
std::fs::write(
|
||||
&system_config_path,
|
||||
r#"
|
||||
model = "system-model"
|
||||
model_provider = "system-provider"
|
||||
"#,
|
||||
)
|
||||
.expect("write system config");
|
||||
std::fs::write(&user_config_path, r#"model = "user-model""#).expect("write user config");
|
||||
|
||||
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
|
||||
overrides.packaged_defaults_path = Some(packaged_defaults_path.clone());
|
||||
overrides.system_config_path = Some(system_config_path.clone());
|
||||
|
||||
let stack = load_config_layers_state(
|
||||
&TestFileSystem,
|
||||
tmp.path(),
|
||||
/*cwd*/ None,
|
||||
&[(
|
||||
"model".to_string(),
|
||||
TomlValue::String("session-model".to_string()),
|
||||
)],
|
||||
overrides,
|
||||
&crate::NoopThreadConfigLoader,
|
||||
)
|
||||
.await
|
||||
.expect("load config layers");
|
||||
|
||||
assert_eq!(
|
||||
stack
|
||||
.all_layers_low_to_high()
|
||||
.map(|layer| layer.name.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
ConfigLayerSource::PackagedDefaults {
|
||||
file: packaged_defaults_path,
|
||||
},
|
||||
ConfigLayerSource::System {
|
||||
file: AbsolutePathBuf::from_absolute_path(system_config_path)
|
||||
.expect("absolute system config path"),
|
||||
},
|
||||
ConfigLayerSource::User {
|
||||
file: AbsolutePathBuf::from_absolute_path(user_config_path)
|
||||
.expect("absolute user config path"),
|
||||
profile: None,
|
||||
},
|
||||
ConfigLayerSource::SessionFlags,
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
stack.effective_config(),
|
||||
toml::toml! {
|
||||
model = "session-model"
|
||||
model_provider = "system-provider"
|
||||
model_context_window = 120000
|
||||
}
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_packaged_defaults_file_returns_an_error() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let packaged_defaults_path =
|
||||
AbsolutePathBuf::resolve_path_against_base("packaged-defaults.toml", tmp.path());
|
||||
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
|
||||
overrides.packaged_defaults_path = Some(packaged_defaults_path.clone());
|
||||
|
||||
let err = load_config_layers_state(
|
||||
&TestFileSystem,
|
||||
tmp.path(),
|
||||
/*cwd*/ None,
|
||||
&[],
|
||||
overrides,
|
||||
&crate::NoopThreadConfigLoader,
|
||||
)
|
||||
.await
|
||||
.expect_err("an explicitly configured packaged defaults file must exist");
|
||||
|
||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
format!(
|
||||
"packaged defaults config file {} not found",
|
||||
packaged_defaults_path.display()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn profile_v2_rejects_matching_legacy_profile_in_base_user_config() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
|
||||
@@ -41,6 +41,8 @@ impl From<LoaderOverrides> for ConfigLoadOptions {
|
||||
/// LoaderOverrides overrides managed configuration inputs (primarily for tests).
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct LoaderOverrides {
|
||||
/// Optional configuration file supplied with the installed Codex package.
|
||||
pub packaged_defaults_path: Option<AbsolutePathBuf>,
|
||||
pub user_config_path: Option<AbsolutePathBuf>,
|
||||
pub user_config_profile: Option<ProfileV2Name>,
|
||||
pub managed_config_path: Option<PathBuf>,
|
||||
@@ -64,6 +66,7 @@ impl LoaderOverrides {
|
||||
pub fn without_managed_config_for_tests() -> Self {
|
||||
let base = std::env::temp_dir().join("codex-config-tests");
|
||||
Self {
|
||||
packaged_defaults_path: None,
|
||||
user_config_path: None,
|
||||
user_config_profile: None,
|
||||
managed_config_path: Some(base.join("managed_config.toml")),
|
||||
@@ -212,6 +215,7 @@ impl ConfigLayerEntry {
|
||||
// Get the `.codex/` folder associated with this config layer, if any.
|
||||
pub fn config_folder(&self) -> Option<AbsolutePathBuf> {
|
||||
match &self.name {
|
||||
ConfigLayerSource::PackagedDefaults { .. } => None,
|
||||
ConfigLayerSource::Mdm { .. } => None,
|
||||
ConfigLayerSource::System { file } => file.parent(),
|
||||
ConfigLayerSource::EnterpriseManaged { .. } => None,
|
||||
|
||||
@@ -117,7 +117,8 @@ fn roots_from_layer_stack(
|
||||
SkillScope::Admin,
|
||||
));
|
||||
}
|
||||
ConfigLayerSource::Mdm { .. }
|
||||
ConfigLayerSource::PackagedDefaults { .. }
|
||||
| ConfigLayerSource::Mdm { .. }
|
||||
| ConfigLayerSource::EnterpriseManaged { .. }
|
||||
| ConfigLayerSource::SessionFlags
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. }
|
||||
|
||||
@@ -375,7 +375,8 @@ fn load_toml_hooks_from_layer(
|
||||
|
||||
fn config_toml_source_path(layer: &ConfigLayerEntry) -> AbsolutePathBuf {
|
||||
match &layer.name {
|
||||
ConfigLayerSource::System { file }
|
||||
ConfigLayerSource::PackagedDefaults { file }
|
||||
| ConfigLayerSource::System { file }
|
||||
| ConfigLayerSource::User { file, .. }
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => file.clone(),
|
||||
ConfigLayerSource::Project { dot_codex_folder } => layer
|
||||
@@ -716,6 +717,7 @@ fn hook_trusted_hash(is_managed: bool, state: Option<&HookStateToml>) -> Option<
|
||||
|
||||
fn hook_metadata_for_config_layer_source(source: &ConfigLayerSource) -> (HookSource, bool) {
|
||||
match source {
|
||||
ConfigLayerSource::PackagedDefaults { .. } => (HookSource::Unknown, false),
|
||||
ConfigLayerSource::System { .. } => (HookSource::System, true),
|
||||
ConfigLayerSource::User { .. } => (HookSource::User, false),
|
||||
ConfigLayerSource::Project { .. } => (HookSource::Project, false),
|
||||
|
||||
@@ -404,7 +404,8 @@ fn render_non_file_layer_details(layer: &ConfigLayerEntry) -> Vec<Line<'static>>
|
||||
ConfigLayerSource::Mdm { .. }
|
||||
| ConfigLayerSource::EnterpriseManaged { .. }
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => render_non_file_layer_value(layer),
|
||||
ConfigLayerSource::System { .. }
|
||||
ConfigLayerSource::PackagedDefaults { .. }
|
||||
| ConfigLayerSource::System { .. }
|
||||
| ConfigLayerSource::User { .. }
|
||||
| ConfigLayerSource::Project { .. }
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => Vec::new(),
|
||||
@@ -467,7 +468,8 @@ fn non_file_layer_value_label(source: &ConfigLayerSource) -> &'static str {
|
||||
"MDM value"
|
||||
}
|
||||
ConfigLayerSource::EnterpriseManaged { .. } => "Enterprise-managed config value",
|
||||
ConfigLayerSource::SessionFlags
|
||||
ConfigLayerSource::PackagedDefaults { .. }
|
||||
| ConfigLayerSource::SessionFlags
|
||||
| ConfigLayerSource::System { .. }
|
||||
| ConfigLayerSource::User { .. }
|
||||
| ConfigLayerSource::Project { .. }
|
||||
|
||||
Reference in New Issue
Block a user