mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
Enforce managed authentication during bootstrap
This commit is contained in:
@@ -20,6 +20,7 @@ use crate::ConstraintError;
|
||||
use crate::ManagedHooksRequirementsToml;
|
||||
use crate::mcp_types::AppToolApproval;
|
||||
use crate::permissions_toml::PermissionProfileToml;
|
||||
use crate::types::AuthCredentialsStoreMode;
|
||||
use crate::types::FeedbackConfigToml;
|
||||
use crate::types::WindowsSandboxModeToml;
|
||||
|
||||
@@ -143,6 +144,10 @@ impl<T> std::ops::DerefMut for ConstrainedWithSource<T> {
|
||||
/// normalization.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ConfigRequirements {
|
||||
pub allowed_login_methods: Option<Sourced<BTreeMap<String, bool>>>,
|
||||
pub allowed_chatgpt_workspaces: Option<Sourced<BTreeMap<String, bool>>>,
|
||||
pub cli_auth_credentials_store: Option<Sourced<AuthCredentialsStoreMode>>,
|
||||
pub chatgpt_base_url: Option<Sourced<String>>,
|
||||
pub sqlite_home: Option<Sourced<AbsolutePathBuf>>,
|
||||
pub log_dir: Option<Sourced<AbsolutePathBuf>>,
|
||||
pub model_catalog_json: Option<Sourced<AbsolutePathBuf>>,
|
||||
@@ -176,6 +181,10 @@ pub struct ConfigRequirements {
|
||||
impl Default for ConfigRequirements {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
allowed_login_methods: None,
|
||||
allowed_chatgpt_workspaces: None,
|
||||
cli_auth_credentials_store: None,
|
||||
chatgpt_base_url: None,
|
||||
sqlite_home: None,
|
||||
log_dir: None,
|
||||
model_catalog_json: None,
|
||||
@@ -837,6 +846,10 @@ pub(crate) fn merge_app_requirements_descending(
|
||||
/// Base config deserialized from system `requirements.toml` or MDM.
|
||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq)]
|
||||
pub struct ConfigRequirementsToml {
|
||||
pub allowed_login_methods: Option<BTreeMap<String, bool>>,
|
||||
pub allowed_chatgpt_workspaces: Option<BTreeMap<String, bool>>,
|
||||
pub cli_auth_credentials_store: Option<AuthCredentialsStoreMode>,
|
||||
pub chatgpt_base_url: Option<String>,
|
||||
pub sqlite_home: Option<AbsolutePathBuf>,
|
||||
pub log_dir: Option<AbsolutePathBuf>,
|
||||
pub model_catalog_json: Option<AbsolutePathBuf>,
|
||||
@@ -899,6 +912,10 @@ impl<T> std::ops::Deref for Sourced<T> {
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct ConfigRequirementsWithSources {
|
||||
pub allowed_login_methods: Option<Sourced<BTreeMap<String, bool>>>,
|
||||
pub allowed_chatgpt_workspaces: Option<Sourced<BTreeMap<String, bool>>>,
|
||||
pub cli_auth_credentials_store: Option<Sourced<AuthCredentialsStoreMode>>,
|
||||
pub chatgpt_base_url: Option<Sourced<String>>,
|
||||
pub sqlite_home: Option<Sourced<AbsolutePathBuf>>,
|
||||
pub log_dir: Option<Sourced<AbsolutePathBuf>>,
|
||||
pub model_catalog_json: Option<Sourced<AbsolutePathBuf>>,
|
||||
@@ -947,6 +964,10 @@ impl ConfigRequirementsWithSources {
|
||||
// Destructure without `..` so adding fields to `ConfigRequirementsToml`
|
||||
// forces this merge logic to be updated.
|
||||
let ConfigRequirementsToml {
|
||||
allowed_login_methods: _,
|
||||
allowed_chatgpt_workspaces: _,
|
||||
cli_auth_credentials_store: _,
|
||||
chatgpt_base_url: _,
|
||||
sqlite_home: _,
|
||||
log_dir: _,
|
||||
model_catalog_json: _,
|
||||
@@ -990,6 +1011,10 @@ impl ConfigRequirementsWithSources {
|
||||
other,
|
||||
source,
|
||||
{
|
||||
allowed_login_methods,
|
||||
allowed_chatgpt_workspaces,
|
||||
cli_auth_credentials_store,
|
||||
chatgpt_base_url,
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
@@ -1030,6 +1055,10 @@ impl ConfigRequirementsWithSources {
|
||||
|
||||
pub fn into_toml(self) -> ConfigRequirementsToml {
|
||||
let ConfigRequirementsWithSources {
|
||||
allowed_login_methods,
|
||||
allowed_chatgpt_workspaces,
|
||||
cli_auth_credentials_store,
|
||||
chatgpt_base_url,
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
@@ -1059,6 +1088,10 @@ impl ConfigRequirementsWithSources {
|
||||
guardian_policy_config,
|
||||
} = self;
|
||||
ConfigRequirementsToml {
|
||||
allowed_login_methods: allowed_login_methods.map(|sourced| sourced.value),
|
||||
allowed_chatgpt_workspaces: allowed_chatgpt_workspaces.map(|sourced| sourced.value),
|
||||
cli_auth_credentials_store: cli_auth_credentials_store.map(|sourced| sourced.value),
|
||||
chatgpt_base_url: chatgpt_base_url.map(|sourced| sourced.value),
|
||||
sqlite_home: sqlite_home.map(|sourced| sourced.value),
|
||||
log_dir: log_dir.map(|sourced| sourced.value),
|
||||
model_catalog_json: model_catalog_json.map(|sourced| sourced.value),
|
||||
@@ -1155,7 +1188,11 @@ impl ConfigRequirementsToml {
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.sqlite_home.is_none()
|
||||
self.allowed_login_methods.is_none()
|
||||
&& self.allowed_chatgpt_workspaces.is_none()
|
||||
&& self.cli_auth_credentials_store.is_none()
|
||||
&& self.chatgpt_base_url.is_none()
|
||||
&& self.sqlite_home.is_none()
|
||||
&& self.log_dir.is_none()
|
||||
&& self.model_catalog_json.is_none()
|
||||
&& self.check_for_update_on_startup.is_none()
|
||||
@@ -1218,6 +1255,10 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
// config loading and requirements API projection. The normalized
|
||||
// constraints below only need the compiled PermissionProfile envelope.
|
||||
let ConfigRequirementsWithSources {
|
||||
allowed_login_methods,
|
||||
allowed_chatgpt_workspaces,
|
||||
cli_auth_credentials_store,
|
||||
chatgpt_base_url,
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
@@ -1247,6 +1288,37 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
guardian_policy_config,
|
||||
} = toml;
|
||||
|
||||
if let Some(Sourced {
|
||||
value: methods,
|
||||
source,
|
||||
}) = &allowed_login_methods
|
||||
&& let Some(unknown) = methods
|
||||
.keys()
|
||||
.find(|method| method.as_str() != "api" && method.as_str() != "chatgpt")
|
||||
{
|
||||
return Err(ConstraintError::InvalidValue {
|
||||
field_name: "allowed_login_methods",
|
||||
candidate: unknown.clone(),
|
||||
allowed: "[\"api\", \"chatgpt\"]".to_string(),
|
||||
requirement_source: source.clone(),
|
||||
});
|
||||
}
|
||||
if let Some(Sourced {
|
||||
value: workspaces,
|
||||
source,
|
||||
}) = &allowed_chatgpt_workspaces
|
||||
&& let Some(empty) = workspaces
|
||||
.keys()
|
||||
.find(|workspace| workspace.trim().is_empty())
|
||||
{
|
||||
return Err(ConstraintError::InvalidValue {
|
||||
field_name: "allowed_chatgpt_workspaces",
|
||||
candidate: empty.clone(),
|
||||
allowed: "non-empty workspace identifiers".to_string(),
|
||||
requirement_source: source.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let approval_policy = match allowed_approval_policies {
|
||||
Some(Sourced {
|
||||
value: policies,
|
||||
@@ -1522,6 +1594,10 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
});
|
||||
let guardian_policy_config_source = guardian_policy_config.map(|sourced| sourced.source);
|
||||
Ok(ConfigRequirements {
|
||||
allowed_login_methods,
|
||||
allowed_chatgpt_workspaces,
|
||||
cli_auth_credentials_store,
|
||||
chatgpt_base_url,
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
@@ -1619,6 +1695,10 @@ mod tests {
|
||||
|
||||
fn with_unknown_source(toml: ConfigRequirementsToml) -> ConfigRequirementsWithSources {
|
||||
let ConfigRequirementsToml {
|
||||
allowed_login_methods,
|
||||
allowed_chatgpt_workspaces,
|
||||
cli_auth_credentials_store,
|
||||
chatgpt_base_url,
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
@@ -1649,6 +1729,14 @@ mod tests {
|
||||
guardian_policy_config,
|
||||
} = toml;
|
||||
ConfigRequirementsWithSources {
|
||||
allowed_login_methods: allowed_login_methods
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
allowed_chatgpt_workspaces: allowed_chatgpt_workspaces
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
cli_auth_credentials_store: cli_auth_credentials_store
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
chatgpt_base_url: chatgpt_base_url
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
sqlite_home: sqlite_home.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
log_dir: log_dir.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
model_catalog_json: model_catalog_json
|
||||
@@ -3628,4 +3716,46 @@ command = "python3 /enterprise/hooks/pre.py"
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_rejects_unknown_login_method() -> Result<()> {
|
||||
let source = RequirementSource::Unknown;
|
||||
let mut requirements = ConfigRequirementsWithSources::default();
|
||||
requirements.allowed_login_methods = Some(Sourced::new(
|
||||
BTreeMap::from([("sso".to_string(), true)]),
|
||||
source.clone(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
ConfigRequirements::try_from(requirements),
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "allowed_login_methods",
|
||||
candidate: "sso".to_string(),
|
||||
allowed: "[\"api\", \"chatgpt\"]".to_string(),
|
||||
requirement_source: source,
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_rejects_blank_chatgpt_workspace_id() -> Result<()> {
|
||||
let source = RequirementSource::Unknown;
|
||||
let mut requirements = ConfigRequirementsWithSources::default();
|
||||
requirements.allowed_chatgpt_workspaces = Some(Sourced::new(
|
||||
BTreeMap::from([(" ".to_string(), true)]),
|
||||
source.clone(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
ConfigRequirements::try_from(requirements),
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "allowed_chatgpt_workspaces",
|
||||
candidate: " ".to_string(),
|
||||
allowed: "non-empty workspace identifiers".to_string(),
|
||||
requirement_source: source,
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +173,10 @@ fn populate_merged_regular_fields_with_sources(
|
||||
// Destructure without `..` so every new requirements field must choose
|
||||
// whether it belongs in the regular TOML merge path or in a special merger.
|
||||
let ConfigRequirementsToml {
|
||||
allowed_login_methods,
|
||||
allowed_chatgpt_workspaces,
|
||||
cli_auth_credentials_store,
|
||||
chatgpt_base_url,
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
@@ -203,6 +207,10 @@ fn populate_merged_regular_fields_with_sources(
|
||||
guardian_policy_config,
|
||||
} = requirements;
|
||||
|
||||
set_sourced!(allowed_login_methods, &["allowed_login_methods"]);
|
||||
set_sourced!(allowed_chatgpt_workspaces, &["allowed_chatgpt_workspaces"]);
|
||||
set_sourced!(cli_auth_credentials_store, &["cli_auth_credentials_store"]);
|
||||
set_sourced!(chatgpt_base_url, &["chatgpt_base_url"]);
|
||||
set_sourced!(sqlite_home, &["sqlite_home"]);
|
||||
set_sourced!(log_dir, &["log_dir"]);
|
||||
set_sourced!(model_catalog_json, &["model_catalog_json"]);
|
||||
|
||||
@@ -949,3 +949,45 @@ fn parse_error_names_layer() {
|
||||
assert!(err.to_string().contains("Bad layer (req_bad)"));
|
||||
assert!(err.to_string().contains("allowed_approval_policies"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_config_table_is_composed_and_sourced() {
|
||||
let high_source = RequirementSource::EnterpriseManaged {
|
||||
id: "req_high".to_string(),
|
||||
name: "High".to_string(),
|
||||
};
|
||||
let low_source = RequirementSource::EnterpriseManaged {
|
||||
id: "req_low".to_string(),
|
||||
name: "Low".to_string(),
|
||||
};
|
||||
let composed = compose_requirements_for_hostname(
|
||||
vec![
|
||||
RequirementsLayerEntry::from_toml(
|
||||
low_source.clone(),
|
||||
r#"
|
||||
[allowed_login_methods]
|
||||
chatgpt = true
|
||||
api = true
|
||||
"#,
|
||||
),
|
||||
RequirementsLayerEntry::from_toml(
|
||||
high_source.clone(),
|
||||
r#"
|
||||
[allowed_login_methods]
|
||||
api = false
|
||||
"#,
|
||||
),
|
||||
],
|
||||
/*hostname*/ None,
|
||||
)
|
||||
.expect("compose requirements")
|
||||
.expect("requirements present");
|
||||
|
||||
assert_eq!(
|
||||
composed.allowed_login_methods,
|
||||
Some(Sourced::new(
|
||||
BTreeMap::from([("api".to_string(), false), ("chatgpt".to_string(), true),]),
|
||||
RequirementSource::composite([high_source, low_source]),
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ pub use crate::mcp_types::McpServerTransportConfig;
|
||||
pub use crate::mcp_types::RawMcpServerConfig;
|
||||
pub use codex_protocol::config_types::AltScreenMode;
|
||||
pub use codex_protocol::config_types::ApprovalsReviewer;
|
||||
pub use codex_protocol::config_types::AuthCredentialsStoreMode;
|
||||
use codex_protocol::config_types::EnvironmentVariablePattern;
|
||||
pub use codex_protocol::config_types::ModeKind;
|
||||
pub use codex_protocol::config_types::Personality;
|
||||
@@ -84,21 +85,6 @@ impl fmt::Display for SessionPickerViewMode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine where Codex should store CLI auth credentials.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AuthCredentialsStoreMode {
|
||||
#[default]
|
||||
/// Persist credentials in CODEX_HOME/auth.json.
|
||||
File,
|
||||
/// Persist credentials in the keyring. Fail if unavailable.
|
||||
Keyring,
|
||||
/// Use keyring when available; otherwise, fall back to a file in CODEX_HOME.
|
||||
Auto,
|
||||
/// Store credentials in memory only for the current process.
|
||||
Ephemeral,
|
||||
}
|
||||
|
||||
/// Determine where Codex should store and read MCP credentials.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
|
||||
@@ -281,24 +281,24 @@
|
||||
]
|
||||
},
|
||||
"AuthCredentialsStoreMode": {
|
||||
"description": "Determine where Codex should store CLI auth credentials.",
|
||||
"description": "Determines where Codex stores CLI authentication credentials.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Persist credentials in CODEX_HOME/auth.json.",
|
||||
"description": "Persist credentials in `CODEX_HOME/auth.json`.",
|
||||
"enum": [
|
||||
"file"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Persist credentials in the keyring. Fail if unavailable.",
|
||||
"description": "Persist credentials in the operating system keyring. Fail if unavailable.",
|
||||
"enum": [
|
||||
"keyring"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Use keyring when available; otherwise, fall back to a file in CODEX_HOME.",
|
||||
"description": "Use the operating system keyring when available; otherwise, fall back to `CODEX_HOME/auth.json`.",
|
||||
"enum": [
|
||||
"auto"
|
||||
],
|
||||
@@ -5295,4 +5295,4 @@
|
||||
},
|
||||
"title": "ConfigToml",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8330,6 +8330,10 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
|
||||
let fixture = create_test_fixture()?;
|
||||
|
||||
let requirements_toml = codex_config::ConfigRequirementsToml {
|
||||
allowed_login_methods: None,
|
||||
allowed_chatgpt_workspaces: None,
|
||||
cli_auth_credentials_store: None,
|
||||
chatgpt_base_url: None,
|
||||
sqlite_home: None,
|
||||
log_dir: None,
|
||||
model_catalog_json: None,
|
||||
@@ -10943,3 +10947,90 @@ async fn absent_allow_login_shell_does_not_report_an_override() -> std::io::Resu
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn conflicting_login_method_requirements_fail_closed() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
"forced_login_method = \"api\"\n",
|
||||
)?;
|
||||
let err =
|
||||
load_with_enterprise_requirement(&codex_home, "[allowed_login_methods]\nchatgpt = true\n")
|
||||
.await
|
||||
.expect_err("conflicting login restrictions should fail closed");
|
||||
|
||||
assert_eq!(err.kind(), ErrorKind::InvalidInput);
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("conflicts with `allowed_login_methods`")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_requirements_can_disable_all_chatgpt_workspaces() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let config = load_with_enterprise_requirement(
|
||||
&codex_home,
|
||||
r#"
|
||||
[allowed_chatgpt_workspaces]
|
||||
"A" = false
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(config.forced_chatgpt_workspace_id, Some(Vec::new()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bootstrap_config_applies_local_auth_requirements() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let requirements_path = codex_home.path().join("requirements.toml");
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
cli_auth_credentials_store = "file"
|
||||
chatgpt_base_url = "https://user.example/backend-api"
|
||||
"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
&requirements_path,
|
||||
r#"
|
||||
cli_auth_credentials_store = "keyring"
|
||||
chatgpt_base_url = "https://managed.example/backend-api"
|
||||
|
||||
[features]
|
||||
secret_auth_storage = true
|
||||
"#,
|
||||
)?;
|
||||
let mut loader_overrides = LoaderOverrides::without_managed_config_for_tests();
|
||||
loader_overrides.system_requirements_path = Some(requirements_path);
|
||||
|
||||
let bootstrap_config = load_bootstrap_config_toml_with_layer_stack(
|
||||
codex_home.path(),
|
||||
Some(&codex_home.path().abs()),
|
||||
Vec::new(),
|
||||
ConfigLoadOptions {
|
||||
loader_overrides,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let auth_keyring_backend_kind = resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?;
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
bootstrap_config.config_toml.cli_auth_credentials_store,
|
||||
bootstrap_config.config_toml.chatgpt_base_url,
|
||||
auth_keyring_backend_kind,
|
||||
),
|
||||
(
|
||||
Some(AuthCredentialsStoreMode::Keyring),
|
||||
Some("https://managed.example/backend-api".to_string()),
|
||||
AuthKeyringBackendKind::Secrets,
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1738,6 +1738,26 @@ pub async fn load_config_toml_with_layer_stack(
|
||||
})
|
||||
}
|
||||
|
||||
/// Load the bootstrap config together with its layer stack, applying exact
|
||||
/// auth requirements needed before cloud requirements can be fetched.
|
||||
pub async fn load_bootstrap_config_toml_with_layer_stack(
|
||||
codex_home: &Path,
|
||||
cwd: Option<&AbsolutePathBuf>,
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
options: impl Into<ConfigLoadOptions>,
|
||||
) -> std::io::Result<ConfigTomlLoadResult> {
|
||||
let mut result =
|
||||
load_config_toml_with_layer_stack(codex_home, cwd, cli_overrides, options).await?;
|
||||
let requirements = result.config_layer_stack.requirements_toml();
|
||||
if let Some(required) = requirements.cli_auth_credentials_store {
|
||||
result.config_toml.cli_auth_credentials_store = Some(required);
|
||||
}
|
||||
if let Some(required) = requirements.chatgpt_base_url.as_ref() {
|
||||
result.config_toml.chatgpt_base_url = Some(required.clone());
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn deserialize_config_toml_with_base(
|
||||
root_value: TomlValue,
|
||||
config_base_dir: &Path,
|
||||
@@ -2663,7 +2683,11 @@ impl Config {
|
||||
.startup_warnings()
|
||||
.unwrap_or_default()
|
||||
.to_vec();
|
||||
requirements::apply_to_config(
|
||||
let requirements::AppliedConfigRequirements {
|
||||
cli_auth_credentials_store_mode,
|
||||
forced_login_method,
|
||||
forced_chatgpt_workspace_id,
|
||||
} = requirements::apply_to_config(
|
||||
&mut cfg,
|
||||
config_layer_stack.requirements(),
|
||||
&mut startup_warnings,
|
||||
@@ -2672,6 +2696,10 @@ impl Config {
|
||||
// Destructure every field to ensure ConfigRequirements additions are
|
||||
// either applied above or handled while constructing the final Config.
|
||||
let ConfigRequirements {
|
||||
allowed_login_methods: _,
|
||||
allowed_chatgpt_workspaces: _,
|
||||
cli_auth_credentials_store: _,
|
||||
chatgpt_base_url: _,
|
||||
sqlite_home: _,
|
||||
log_dir: _,
|
||||
model_catalog_json: _,
|
||||
@@ -3274,21 +3302,6 @@ impl Config {
|
||||
|
||||
let use_experimental_unified_exec_tool = features.enabled(Feature::UnifiedExec);
|
||||
|
||||
let forced_chatgpt_workspace_id = cfg
|
||||
.forced_chatgpt_workspace_id
|
||||
.clone()
|
||||
.map(codex_config::config_toml::ForcedChatgptWorkspaceIds::into_vec)
|
||||
.map(|values| {
|
||||
values
|
||||
.into_iter()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.filter(|values| !values.is_empty());
|
||||
|
||||
let forced_login_method = cfg.forced_login_method;
|
||||
|
||||
let model = model.or(cfg.model);
|
||||
let notices = cfg.notice.unwrap_or_default();
|
||||
let service_tier = match service_tier_override {
|
||||
@@ -3547,10 +3560,7 @@ impl Config {
|
||||
include_environment_context,
|
||||
// The config.toml omits "_mode" because it's a config file. However, "_mode"
|
||||
// is important in code to differentiate the mode from the store implementation.
|
||||
cli_auth_credentials_store_mode: resolve_cli_auth_credentials_store_mode(
|
||||
cfg.cli_auth_credentials_store.unwrap_or_default(),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
),
|
||||
cli_auth_credentials_store_mode,
|
||||
mcp_servers,
|
||||
// The config.toml omits "_mode" because it's a config file. However, "_mode"
|
||||
// is important in code to differentiate the mode from the store implementation.
|
||||
|
||||
@@ -3,6 +3,15 @@ use codex_config::RequirementSource;
|
||||
use codex_config::Sourced;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::types::FeedbackConfigToml;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
|
||||
/// Runtime values that cannot be applied by mutating [`ConfigToml`] alone.
|
||||
pub(super) struct AppliedConfigRequirements {
|
||||
pub cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
pub forced_login_method: Option<ForcedLoginMethod>,
|
||||
pub forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Applies managed requirements to regular config before final config construction.
|
||||
///
|
||||
@@ -12,7 +21,7 @@ pub(super) fn apply_to_config(
|
||||
config: &mut ConfigToml,
|
||||
requirements: &ConfigRequirements,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
) -> std::io::Result<()> {
|
||||
) -> std::io::Result<AppliedConfigRequirements> {
|
||||
macro_rules! apply_exact {
|
||||
($field:ident) => {
|
||||
apply_exact_requirement(
|
||||
@@ -24,6 +33,8 @@ pub(super) fn apply_to_config(
|
||||
};
|
||||
}
|
||||
|
||||
apply_exact!(cli_auth_credentials_store);
|
||||
apply_exact!(chatgpt_base_url);
|
||||
apply_exact!(sqlite_home);
|
||||
apply_exact!(log_dir);
|
||||
apply_exact!(model_catalog_json);
|
||||
@@ -46,7 +57,23 @@ pub(super) fn apply_to_config(
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
let (forced_login_method, forced_chatgpt_workspace_id) =
|
||||
resolve_auth_restrictions(config, requirements)?;
|
||||
let configured_auth_credentials_store = config.cli_auth_credentials_store.unwrap_or_default();
|
||||
let cli_auth_credentials_store_mode = if requirements.cli_auth_credentials_store.is_some() {
|
||||
configured_auth_credentials_store
|
||||
} else {
|
||||
super::resolve_cli_auth_credentials_store_mode(
|
||||
configured_auth_credentials_store,
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
)
|
||||
};
|
||||
|
||||
Ok(AppliedConfigRequirements {
|
||||
cli_auth_credentials_store_mode,
|
||||
forced_login_method,
|
||||
forced_chatgpt_workspace_id,
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_exact_requirement<T>(
|
||||
@@ -123,3 +150,74 @@ pub(super) fn push_structured_requirement_override_warning(
|
||||
"Configured values under `{field_name}` are overridden by requirements from {source}."
|
||||
));
|
||||
}
|
||||
|
||||
fn resolve_auth_restrictions(
|
||||
config: &ConfigToml,
|
||||
requirements: &ConfigRequirements,
|
||||
) -> std::io::Result<(Option<ForcedLoginMethod>, Option<Vec<String>>)> {
|
||||
let configured_forced_chatgpt_workspace_id = config
|
||||
.forced_chatgpt_workspace_id
|
||||
.clone()
|
||||
.map(codex_config::config_toml::ForcedChatgptWorkspaceIds::into_vec)
|
||||
.map(|values| {
|
||||
values
|
||||
.into_iter()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.filter(|values| !values.is_empty());
|
||||
let forced_chatgpt_workspace_id = match requirements.allowed_chatgpt_workspaces.as_ref() {
|
||||
Some(requirement) => Some(match configured_forced_chatgpt_workspace_id {
|
||||
Some(configured) => configured
|
||||
.into_iter()
|
||||
.filter(|workspace| requirement.value.get(workspace).copied().unwrap_or(false))
|
||||
.collect(),
|
||||
None => requirement
|
||||
.value
|
||||
.iter()
|
||||
.filter_map(|(workspace, allowed)| allowed.then_some(workspace.clone()))
|
||||
.collect(),
|
||||
}),
|
||||
None => configured_forced_chatgpt_workspace_id,
|
||||
};
|
||||
|
||||
let forced_login_method = match requirements.allowed_login_methods.as_ref() {
|
||||
Some(requirement) => {
|
||||
let chatgpt_allowed = requirement.value.get("chatgpt").copied().unwrap_or(false);
|
||||
let api_allowed = requirement.value.get("api").copied().unwrap_or(false);
|
||||
match config.forced_login_method {
|
||||
Some(ForcedLoginMethod::Chatgpt) if chatgpt_allowed => {
|
||||
Some(ForcedLoginMethod::Chatgpt)
|
||||
}
|
||||
Some(ForcedLoginMethod::Api) if api_allowed => Some(ForcedLoginMethod::Api),
|
||||
Some(configured) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"configured `forced_login_method = \"{configured}\"` conflicts with `allowed_login_methods` from {}",
|
||||
requirement.source
|
||||
),
|
||||
));
|
||||
}
|
||||
None => match (chatgpt_allowed, api_allowed) {
|
||||
(true, true) => None,
|
||||
(true, false) => Some(ForcedLoginMethod::Chatgpt),
|
||||
(false, true) => Some(ForcedLoginMethod::Api),
|
||||
(false, false) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"`allowed_login_methods` from {} does not allow any login method",
|
||||
requirement.source
|
||||
),
|
||||
));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
None => config.forced_login_method,
|
||||
};
|
||||
|
||||
Ok((forced_login_method, forced_chatgpt_workspace_id))
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::ConfigTomlLoadResult;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core::config::load_config_toml_with_layer_stack;
|
||||
use codex_core::config::load_bootstrap_config_toml_with_layer_stack;
|
||||
use codex_core::config::resolve_bootstrap_auth_keyring_backend_kind;
|
||||
use codex_core::config::resolve_oss_provider;
|
||||
use codex_core::config::resolve_profile_v2_config_path;
|
||||
@@ -623,7 +623,7 @@ async fn load_bootstrap_config_or_exit(
|
||||
strict_config: bool,
|
||||
cloud_config_bundle: CloudConfigBundleLoader,
|
||||
) -> ConfigTomlLoadResult {
|
||||
match load_config_toml_with_layer_stack(
|
||||
match load_bootstrap_config_toml_with_layer_stack(
|
||||
codex_home,
|
||||
cwd,
|
||||
cli_kv_overrides,
|
||||
|
||||
@@ -21,6 +21,23 @@ use wildmatch::WildMatchPattern;
|
||||
|
||||
use crate::openai_models::ReasoningEffort;
|
||||
|
||||
/// Determines where Codex stores CLI authentication credentials.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum AuthCredentialsStoreMode {
|
||||
/// Persist credentials in `CODEX_HOME/auth.json`.
|
||||
#[default]
|
||||
File,
|
||||
/// Persist credentials in the operating system keyring. Fail if unavailable.
|
||||
Keyring,
|
||||
/// Use the operating system keyring when available; otherwise, fall back to
|
||||
/// `CODEX_HOME/auth.json`.
|
||||
Auto,
|
||||
/// Store credentials in memory only for the current process.
|
||||
Ephemeral,
|
||||
}
|
||||
|
||||
/// Selects which part of the active context is charged against
|
||||
/// `model_auto_compact_token_limit`.
|
||||
#[derive(
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::legacy_core::config::Config;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use crate::legacy_core::config::ConfigOverrides;
|
||||
use crate::legacy_core::config::ConfigTomlLoadResult;
|
||||
use crate::legacy_core::config::load_config_toml_with_layer_stack;
|
||||
use crate::legacy_core::config::load_bootstrap_config_toml_with_layer_stack;
|
||||
use crate::legacy_core::config::resolve_bootstrap_auth_keyring_backend_kind;
|
||||
use crate::legacy_core::config::resolve_oss_provider;
|
||||
use crate::legacy_core::config::resolve_profile_v2_config_path;
|
||||
@@ -1938,7 +1938,7 @@ async fn load_bootstrap_config_or_exit(
|
||||
strict_config: bool,
|
||||
cloud_config_bundle: CloudConfigBundleLoader,
|
||||
) -> ConfigTomlLoadResult {
|
||||
match load_config_toml_with_layer_stack(
|
||||
match load_bootstrap_config_toml_with_layer_stack(
|
||||
codex_home,
|
||||
cwd,
|
||||
cli_kv_overrides,
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::Cli;
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use crate::legacy_core::config::ConfigOverrides;
|
||||
use crate::legacy_core::config::load_config_toml_with_layer_stack;
|
||||
use crate::legacy_core::config::load_bootstrap_config_toml_with_layer_stack;
|
||||
use crate::legacy_core::config::resolve_bootstrap_auth_keyring_backend_kind;
|
||||
use crate::legacy_core::config::resolve_oss_provider;
|
||||
use crate::legacy_core::config::resolve_profile_v2_config_path;
|
||||
@@ -312,7 +312,7 @@ async fn start_app_server_for_archive_command(
|
||||
loader_overrides.user_config_profile = Some(profile_v2.clone());
|
||||
}
|
||||
|
||||
let bootstrap_config = load_config_toml_with_layer_stack(
|
||||
let bootstrap_config = load_bootstrap_config_toml_with_layer_stack(
|
||||
codex_home.as_path(),
|
||||
config_cwd.as_ref(),
|
||||
cli_kv_overrides.clone(),
|
||||
|
||||
Reference in New Issue
Block a user