Expose effective login methods in config requirements (#45495)

## Why

Configuration requirements did not report which login methods the running app server permits after applying managed policy, forced login settings, and workspace restrictions.

## What changed

- Add `allowedLoginMethods` to `configRequirements/read`, using the running authentication manager's effective policy rather than newly read authentication settings.
- Return requirements when login methods are restricted even without managed requirements, while preserving `requirements: null` for the unrestricted default.
- Update protocol schemas and generated TypeScript and Python types. An empty list permits no login method; older servers may omit the field.

## Testing

Add coverage for managed and forced login restrictions, workspace intersections, policy reporting after requirements files change, invalid login methods, and API-only Amazon Bedrock without ChatGPT requests. Extend tests for conflicting authentication requirements and cloud policy precedence.

GitOrigin-RevId: 56c0767a74143e793aac2ac165d0cbe98a09469b
This commit is contained in:
acrognale-oai
2026-09-14 19:11:29 +00:00
committed by copyberry
parent d3812ddbb3
commit a20092a7a2
16 changed files with 355 additions and 31 deletions
@@ -9828,6 +9828,16 @@
"null"
]
},
"allowedLoginMethods": {
"description": "Effective login methods after managed, forced-login, and workspace restrictions. An empty list permits no login method. Older servers may omit this field.",
"items": {
"$ref": "#/definitions/v2/ForcedLoginMethod"
},
"type": [
"array",
"null"
]
},
"allowedPermissionProfiles": {
"additionalProperties": {
"type": "boolean"
@@ -5708,6 +5708,16 @@
"null"
]
},
"allowedLoginMethods": {
"description": "Effective login methods after managed, forced-login, and workspace restrictions. An empty list permits no login method. Older servers may omit this field.",
"items": {
"$ref": "#/definitions/ForcedLoginMethod"
},
"type": [
"array",
"null"
]
},
"allowedPermissionProfiles": {
"additionalProperties": {
"type": "boolean"
@@ -414,6 +414,16 @@
"null"
]
},
"allowedLoginMethods": {
"description": "Effective login methods after managed, forced-login, and workspace restrictions. An empty list permits no login method. Older servers may omit this field.",
"items": {
"$ref": "#/definitions/ForcedLoginMethod"
},
"type": [
"array",
"null"
]
},
"allowedPermissionProfiles": {
"additionalProperties": {
"type": "boolean"
@@ -762,6 +772,13 @@
},
"type": "object"
},
"ForcedLoginMethod": {
"enum": [
"chatgpt",
"api"
],
"type": "string"
},
"InAppBrowserRequirements": {
"properties": {
"allowExternalBrowserSettingsImport": {
@@ -1,6 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ForcedLoginMethod } from "../ForcedLoginMethod";
import type { PathUri } from "../PathUri";
import type { WebSearchMode } from "../WebSearchMode";
import type { JsonValue } from "../serde_json/JsonValue";
@@ -22,4 +23,8 @@ export type ConfigRequirements = {/**
modelProvider: string | null, /**
* Complete required provider definitions, using config.toml field names.
*/
modelProviders: { [key in string]?: JsonValue } | null, cliAuthCredentialsStore: CliAuthCredentialsStoreMode | null, chatgptBaseUrl: string | null, additionalDeveloperInstructions: string | null, allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowBrowserAndComputerUse: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, browserUse: BrowserUseRequirements | null, inAppBrowser: InAppBrowserRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, autoReview: AutoReviewRequirements | null, models: ModelsRequirements | null, sqliteHome: PathUri | null, logDir: PathUri | null, modelCatalogJson: PathUri | null, checkForUpdateOnStartup: boolean | null, allowLoginShell: boolean | null, feedback: FeedbackRequirements | null, windowsSandboxPrivateDesktop: boolean | null};
modelProviders: { [key in string]?: JsonValue } | null, /**
* Effective login methods after managed, forced-login, and workspace restrictions.
* An empty list permits no login method. Older servers may omit this field.
*/
allowedLoginMethods: Array<ForcedLoginMethod> | null, cliAuthCredentialsStore: CliAuthCredentialsStoreMode | null, chatgptBaseUrl: string | null, additionalDeveloperInstructions: string | null, allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowBrowserAndComputerUse: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, browserUse: BrowserUseRequirements | null, inAppBrowser: InAppBrowserRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, autoReview: AutoReviewRequirements | null, models: ModelsRequirements | null, sqliteHome: PathUri | null, logDir: PathUri | null, modelCatalogJson: PathUri | null, checkForUpdateOnStartup: boolean | null, allowLoginShell: boolean | null, feedback: FeedbackRequirements | null, windowsSandboxPrivateDesktop: boolean | null};
@@ -412,6 +412,9 @@ pub struct ConfigRequirements {
pub model_provider: Option<String>,
/// Complete required provider definitions, using config.toml field names.
pub model_providers: Option<HashMap<String, JsonValue>>,
/// Effective login methods after managed, forced-login, and workspace restrictions.
/// An empty list permits no login method. Older servers may omit this field.
pub allowed_login_methods: Option<Vec<ForcedLoginMethod>>,
pub cli_auth_credentials_store: Option<CliAuthCredentialsStoreMode>,
pub chatgpt_base_url: Option<String>,
pub additional_developer_instructions: Option<String>,
@@ -2120,6 +2120,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental()
crate::experimental_api::ExperimentalApi::experimental_reason(&ConfigRequirements {
model_provider: None,
model_providers: None,
allowed_login_methods: None,
application: None,
cli_auth_credentials_store: None,
chatgpt_base_url: None,
@@ -2630,3 +2630,45 @@ exclude = ["AWS_*"]
Ok(())
}
#[tokio::test]
async fn allowed_login_methods_follow_current_forced_workspaces() -> Result<()> {
use codex_protocol::config_types::ForcedLoginMethod;
let tmp = tempdir()?;
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "")?;
std::fs::write(
tmp.path().join("requirements.toml"),
"allowed_chatgpt_workspaces = ['managed']",
)?;
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
Vec::new(),
LoaderOverrides::with_managed_config_path_for_tests(tmp.path().join("managed_config.toml")),
CloudConfigBundleLoader::default(),
);
let config = service.load_latest_config(/*fallback_cwd*/ None).await?;
let auth = codex_login::AuthManager::shared_from_config(
&config, /*enable_codex_api_key_env*/ false,
)
.await?;
for (workspaces, expected) in [
(
Some(vec!["managed".to_string()]),
vec![ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt],
),
(
Some(vec!["other".to_string()]),
vec![ForcedLoginMethod::Api],
),
(Some(Vec::new()), vec![ForcedLoginMethod::Api]),
(
None,
vec![ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt],
),
] {
auth.set_forced_chatgpt_workspace_id(workspaces);
assert_eq!(auth.allowed_login_methods(), expected);
}
Ok(())
}
@@ -55,6 +55,7 @@ use codex_features::canonical_feature_for_key;
use codex_features::feature_for_key;
use codex_model_provider::create_model_provider;
use codex_plugin::PluginId;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::WebSearchMode;
use serde_json::json;
use std::path::PathBuf;
@@ -135,8 +136,11 @@ impl ConfigRequestProcessor {
.config_manager
.read_requirements()
.await
.map_err(map_error)?
.map(map_requirements_toml_to_api);
.map_err(map_error)?;
let requirements = map_requirements_to_api(
requirements,
self.thread_manager.auth_manager().allowed_login_methods(),
);
Ok(ConfigRequirementsReadResponse { requirements })
}
@@ -381,13 +385,23 @@ pub(super) async fn reload_user_config(
}
}
fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigRequirements {
fn map_requirements_to_api(
requirements: Option<ConfigRequirementsToml>,
allowed_login_methods: Vec<ForcedLoginMethod>,
) -> Option<ConfigRequirements> {
let requirements = match requirements {
Some(requirements) => requirements,
None if allowed_login_methods == [ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt] => {
return None;
}
None => ConfigRequirementsToml::default(),
};
let windows_sandbox_private_desktop = requirements
.windows
.as_ref()
.and_then(|windows| windows.sandbox_private_desktop);
ConfigRequirements {
Some(ConfigRequirements {
model_provider: requirements.model_provider,
model_providers: requirements.model_providers.map(|providers| {
providers
@@ -395,6 +409,7 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
.map(|(id, provider)| (id, serde_json::json!(provider)))
.collect()
}),
allowed_login_methods: Some(allowed_login_methods),
application: requirements.application.map(|application| {
codex_app_server_protocol::ApplicationRequirements {
network: application.network.map(|network| {
@@ -522,7 +537,7 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
enabled: feedback.enabled,
}),
windows_sandbox_private_desktop,
}
})
}
fn map_computer_use_requirements_to_api(
@@ -828,7 +843,7 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into<String>) ->
#[cfg(test)]
mod tests {
use super::map_requirements_toml_to_api;
use super::map_requirements_to_api;
use codex_app_server_protocol::AllowDenyRequirement;
use codex_app_server_protocol::AutoReviewRequirements;
use codex_app_server_protocol::BrowserUseAccessApprovalLifetime;
@@ -854,15 +869,26 @@ mod tests {
use codex_config::NewThreadModelDefaultsToml;
use codex_config::WindowsRequirementsToml;
use codex_config::types::FeedbackConfigToml;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::openai_models::ReasoningEffort;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
fn map_test_requirements(
requirements: ConfigRequirementsToml,
) -> codex_app_server_protocol::ConfigRequirements {
map_requirements_to_api(
Some(requirements),
vec![ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt],
)
.expect("requirements")
}
#[test]
fn requirements_api_includes_allow_managed_hooks_only() {
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
let mapped = map_test_requirements(ConfigRequirementsToml {
allow_managed_hooks_only: Some(true),
..ConfigRequirementsToml::default()
});
@@ -873,7 +899,7 @@ mod tests {
#[test]
fn requirements_api_includes_permission_default_and_allowlist() {
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
let mapped = map_test_requirements(ConfigRequirementsToml {
allowed_permission_profiles: Some(BTreeMap::from([
("managed-build".to_string(), false),
("managed-standard".to_string(), true),
@@ -897,7 +923,7 @@ mod tests {
#[test]
fn requirements_api_includes_allow_appshots() {
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
let mapped = map_test_requirements(ConfigRequirementsToml {
allow_appshots: Some(false),
..ConfigRequirementsToml::default()
});
@@ -908,7 +934,7 @@ mod tests {
#[test]
fn requirements_api_includes_allow_remote_control() {
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
let mapped = map_test_requirements(ConfigRequirementsToml {
allow_remote_control: Some(false),
..ConfigRequirementsToml::default()
});
@@ -918,7 +944,7 @@ mod tests {
#[test]
fn requirements_api_includes_model_auto_review_and_new_thread_defaults() {
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
let mapped = map_test_requirements(ConfigRequirementsToml {
auto_review: Some(AutoReviewRequirementsToml {
required_on_models: Some(vec!["gpt-protected".to_string()]),
ignore_rules: Some(vec!["gpt-protected".to_string()]),
@@ -952,7 +978,7 @@ mod tests {
#[test]
fn requirements_api_includes_browser_and_computer_use_requirements() {
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
let mapped = map_test_requirements(ConfigRequirementsToml {
allow_browser_and_computer_use: Some(false),
browser_use: Some(BrowserUseRequirementsToml {
allow_webmcp: Some(true),
@@ -1070,7 +1096,7 @@ mod tests {
#[test]
fn requirements_api_includes_allowed_windows_sandbox_implementations() {
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
let mapped = map_test_requirements(ConfigRequirementsToml {
windows: Some(WindowsRequirementsToml {
allowed_sandbox_implementations: Some(vec![
codex_config::types::WindowsSandboxModeToml::Elevated,
@@ -1100,7 +1126,7 @@ mod tests {
let model_catalog_json =
AbsolutePathBuf::try_from(std::env::temp_dir().join("managed-models.json"))
.expect("managed model catalog path should be absolute");
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
let mapped = map_test_requirements(ConfigRequirementsToml {
sqlite_home: Some(sqlite_home.clone()),
log_dir: Some(log_dir.clone()),
model_catalog_json: Some(model_catalog_json.clone()),
@@ -103,12 +103,24 @@ foo = "bar"
#[test]
fn managed_auth_requirements_fail_closed_for_standalone_app_server() -> Result<()> {
for requirements in [
"allowed_login_methods = []\n",
"allowed_login_methods = [\"chatgpt\"]\nallowed_chatgpt_workspaces = []\n",
for (requirements, config) in [
("allowed_login_methods = []", ""),
(
"allowed_login_methods = ['chatgpt']\nallowed_chatgpt_workspaces = []",
"",
),
(
"allowed_login_methods = ['api']",
"forced_login_method = 'chatgpt'",
),
(
"allowed_login_methods = ['chatgpt']\nallowed_chatgpt_workspaces = ['managed']",
"forced_chatgpt_workspace_id = ['other']",
),
] {
let codex_home = TempDir::new()?;
std::fs::write(codex_home.path().join("requirements.toml"), requirements)?;
std::fs::write(codex_home.path().join("config.toml"), config)?;
let output = Command::new(codex_utils_cargo_bin::cargo_bin("codex-app-server")?)
.env("CODEX_HOME", codex_home.path())
@@ -0,0 +1,183 @@
//! Requirements reads report the authentication policy enforced by the running server.
use anyhow::Result;
use app_test_support::TestAppServer;
use codex_app_server_protocol::Account;
use codex_app_server_protocol::GetAccountParams;
use codex_app_server_protocol::GetAccountResponse;
use codex_app_server_protocol::RequestId;
use codex_protocol::config_types::ForcedLoginMethod;
use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::json;
use std::time::Duration;
use tempfile::TempDir;
use test_case::test_case;
use tokio::time::timeout;
use wiremock::MockServer;
const READ_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 60);
async fn start_server(
config: &str,
requirements: Option<&str>,
) -> Result<(TempDir, TestAppServer)> {
let home = TempDir::new()?;
std::fs::write(home.path().join("config.toml"), config)?;
if let Some(requirements) = requirements {
std::fs::write(home.path().join("requirements.toml"), requirements)?;
}
let server = TestAppServer::builder()
.with_codex_home(home.path())
.build_initialized_with_timeout(READ_TIMEOUT)
.await?;
Ok((home, server))
}
async fn read_requirements(server: &mut TestAppServer) -> Result<Value> {
let id = server.send_config_requirements_read_request().await?;
timeout(READ_TIMEOUT, server.read_response(id)).await?
}
#[test_case(""; "no_requirements")]
#[test_case("forced_chatgpt_workspace_id = []"; "empty_forced_workspaces_are_unrestricted")]
#[test_case("forced_chatgpt_workspace_id = ['managed']"; "forced_workspace_does_not_exclude_api")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_requirements_read_preserves_unrestricted_default(config: &str) -> Result<()> {
let (_home, mut server) = start_server(config, /*requirements*/ None).await?;
assert_eq!(
read_requirements(&mut server).await?,
json!({"requirements": null})
);
Ok(())
}
#[test_case("allow_remote_control = false", "", &[ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt]; "unrestricted_with_other_requirements")]
#[test_case("allowed_login_methods = ['api']", "", &[ForcedLoginMethod::Api]; "managed_api")]
#[test_case("allowed_login_methods = ['chatgpt']", "", &[ForcedLoginMethod::Chatgpt]; "managed_chatgpt")]
#[test_case("allowed_login_methods = ['chatgpt', 'api', 'api']", "", &[ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt]; "both_normalized")]
#[test_case("", "forced_login_method = 'api'", &[ForcedLoginMethod::Api]; "forced_api_without_requirements")]
#[test_case("", "forced_login_method = 'chatgpt'", &[ForcedLoginMethod::Chatgpt]; "forced_chatgpt_without_requirements")]
#[test_case("allowed_login_methods = ['chatgpt', 'api']", "forced_login_method = 'api'", &[ForcedLoginMethod::Api]; "forced_narrows_managed")]
#[test_case("allowed_chatgpt_workspaces = []", "", &[ForcedLoginMethod::Api]; "empty_managed_workspaces")]
#[test_case("allowed_chatgpt_workspaces = ['managed']", "forced_chatgpt_workspace_id = ['other']", &[ForcedLoginMethod::Api]; "disjoint_workspaces")]
#[test_case("allowed_chatgpt_workspaces = ['managed']", "forced_chatgpt_workspace_id = ['other', 'managed']", &[ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt]; "overlapping_workspaces")]
#[test_case("allowed_login_methods = ['api']\nallowed_chatgpt_workspaces = ['managed']", "forced_chatgpt_workspace_id = ['other']", &[ForcedLoginMethod::Api]; "api_only_ignores_workspace_mismatch")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_requirements_read_exposes_effective_login_methods(
requirements: &str,
config: &str,
expected: &[ForcedLoginMethod],
) -> Result<()> {
let (_home, mut server) = start_server(config, Some(requirements)).await?;
let wire = read_requirements(&mut server).await?;
assert_eq!(wire["requirements"]["allowedLoginMethods"], json!(expected));
if !expected.contains(&ForcedLoginMethod::Chatgpt) {
let id = server.send_login_account_chatgpt_request().await?;
let error = timeout(
READ_TIMEOUT,
server.read_stream_until_error_message(RequestId::Integer(id)),
)
.await??;
assert!(error.error.message.contains("disabled"), "{error:?}");
}
if !expected.contains(&ForcedLoginMethod::Api) {
let id = server.send_login_account_api_key_request("sk-test").await?;
let error = timeout(
READ_TIMEOUT,
server.read_stream_until_error_message(RequestId::Integer(id)),
)
.await??;
assert!(error.error.message.contains("disabled"), "{error:?}");
}
Ok(())
}
#[test_case(""; "removed")]
#[test_case("allowed_login_methods = ['chatgpt']"; "changed")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_requirements_read_uses_running_auth_policy(refreshed: &str) -> Result<()> {
let (home, mut server) = start_server("", Some("allowed_login_methods = ['api']")).await?;
std::fs::write(home.path().join("requirements.toml"), refreshed)?;
assert_eq!(
read_requirements(&mut server).await?["requirements"]["allowedLoginMethods"],
json!(["api"])
);
let id = server.send_login_account_chatgpt_request().await?;
let error = timeout(
READ_TIMEOUT,
server.read_stream_until_error_message(RequestId::Integer(id)),
)
.await??;
assert!(error.error.message.contains("disabled"), "{error:?}");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_requirements_read_rejects_invalid_login_method() -> Result<()> {
let (home, mut server) = start_server("", /*requirements*/ None).await?;
std::fs::write(
home.path().join("requirements.toml"),
"allowed_login_methods = ['saml']",
)?;
let id = server.send_config_requirements_read_request().await?;
let error = timeout(
READ_TIMEOUT,
server.read_stream_until_error_message(RequestId::Integer(id)),
)
.await??;
assert!(
error.error.message.contains("allowed_login_methods"),
"{error:?}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_requirements_read_preserves_api_only_bedrock_without_chatgpt_requests() -> Result<()>
{
let backend = MockServer::start().await;
let (_home, mut server) = start_server(
&format!(
r#"
forced_login_method = "api"
chatgpt_base_url = "{}/backend-api"
model_provider = "amazon-bedrock"
[model_providers.amazon-bedrock]
base_url = "https://bedrock.example.com/v1"
[model_providers.amazon-bedrock.auth]
command = "print-token"
"#,
backend.uri()
),
/*requirements*/ None,
)
.await?;
assert_eq!(
read_requirements(&mut server).await?["requirements"]["allowedLoginMethods"],
json!(["api"])
);
let id = server
.send_get_account_request(GetAccountParams {
refresh_token: false,
})
.await?;
let account: GetAccountResponse = timeout(READ_TIMEOUT, server.read_response(id)).await??;
assert_eq!(
account,
GetAccountResponse {
account: Some(Account::AmazonBedrock {
uses_codex_managed_credentials: false
}),
requires_openai_auth: false,
}
);
assert!(
backend
.received_requests()
.await
.expect("recorded requests")
.is_empty()
);
Ok(())
}
@@ -21,6 +21,7 @@ mod config_requirements_browser_use;
#[path = "config_requirements_exec_tests.rs"]
mod config_requirements_exec;
mod config_requirements_in_app_browser;
mod config_requirements_login;
mod config_rpc;
#[cfg(unix)]
#[path = "connection_handling_stdio_tests.rs"]
@@ -65,24 +65,30 @@ fn cloud_auth_requirements_do_not_override_local_or_discard_other_policy() {
cli_auth_credentials_store = "keyring"
chatgpt_base_url = "https://managed.example/backend-api/""#,
);
let cloud = layer(
"req_cloud",
"Cloud policy",
for cloud_auth in [
r#"allowed_login_methods = ["api", "chatgpt"]
allowed_chatgpt_workspaces = ["other"]"#,
r#"allowed_login_methods = ["saml"]
allowed_chatgpt_workspaces = "invalid"
cli_auth_credentials_store = "invalid"
chatgpt_base_url = false
allow_login_shell = false"#,
);
assert_eq!(
compose(vec![local, cloud]).expect("cloud auth cannot invalidate enterprise policy"),
Some(expected_requirements(
r#"allowed_login_methods = ["api"]
chatgpt_base_url = false"#,
] {
let cloud = layer(
"req_cloud",
"Cloud policy",
&format!("{cloud_auth}\nallow_login_shell = false"),
);
assert_eq!(
compose(vec![local.clone(), cloud])
.expect("cloud auth cannot invalidate enterprise policy"),
Some(expected_requirements(
r#"allowed_login_methods = ["api"]
cli_auth_credentials_store = "keyring"
chatgpt_base_url = "https://managed.example/backend-api/"
allow_login_shell = false"#
))
);
))
);
}
}
#[test]
+2 -1
View File
@@ -2691,7 +2691,8 @@ impl AuthManager {
)
}
fn allowed_login_methods(&self) -> Vec<ForcedLoginMethod> {
/// Returns the login methods permitted by the current effective authentication policy.
pub fn allowed_login_methods(&self) -> Vec<ForcedLoginMethod> {
self.managed_auth_policy.allowed_login_methods(
self.forced_login_method,
self.forced_chatgpt_workspace_id().as_deref(),
@@ -11280,6 +11280,13 @@ class ConfigRequirements(BaseModel):
allowed_approval_policies: Annotated[
list[AskForApproval] | None, Field(alias="allowedApprovalPolicies")
] = None
allowed_login_methods: Annotated[
list[ForcedLoginMethod] | None,
Field(
alias="allowedLoginMethods",
description="Effective login methods after managed, forced-login, and workspace restrictions. An empty list permits no login method. Older servers may omit this field.",
),
] = None
allowed_permission_profiles: Annotated[
dict[str, Any] | None, Field(alias="allowedPermissionProfiles")
] = None