mirror of
https://github.com/openai/codex.git
synced 2026-11-05 16:29:31 +00:00
[codex] Support multiple forced ChatGPT workspaces (#18161)
## Summary This change lets `forced_chatgpt_workspace_id` accept multiple workspace IDs instead of a single value. It keeps the existing config key name, adds backward-compatible parsing for a single string in `config.toml`, and normalizes the setting into an allowed workspace list across login enforcement, app-server config surfaces, and local ChatGPT auth helpers. ## Why Workspace-restricted deployments may need to allow more than one ChatGPT workspace without dropping the guardrail entirely. ## Server-side impact Codex's local server and app-server protocol needed changes because they previously assumed a single workspace ID. The local login flow now matches the auth backend interface by sending the allowed workspace list as a single comma-separated `allowed_workspace_id` query parameter. ## Validation This was tested with: - A single workspace config - With multi-workspace configs - With multiple workspaces in the config - The user only being a part of a subset of them All were successful. Automated coverage: - `cargo test -p codex-login` - `cargo test -p codex-app-server-protocol` - `cargo test -p codex-tui local_chatgpt_auth` - `cargo test --locked -p codex-app-server login_account_chatgpt_includes_forced_workspace_allowlist_query_param`
This commit is contained in:
1 parent
32b45a43e2
commit
02a7205250
22 files changed
+673
-117
No files matched your search
@@ -2725,6 +2725,55 @@ async fn runtime_config_resolves_terminal_resize_reflow_defaults_and_overrides()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn forced_chatgpt_workspace_id_empty_values_disable_runtime_restriction()
|
||||
-> std::io::Result<()> {
|
||||
let cases: Vec<(&str, &str, Option<Vec<&str>>)> = vec![
|
||||
("unset", "", None),
|
||||
("empty string", r#"forced_chatgpt_workspace_id = """#, None),
|
||||
(
|
||||
"whitespace string",
|
||||
r#"forced_chatgpt_workspace_id = " ""#,
|
||||
None,
|
||||
),
|
||||
("empty list", r#"forced_chatgpt_workspace_id = []"#, None),
|
||||
(
|
||||
"blank list entries",
|
||||
r#"forced_chatgpt_workspace_id = ["", " "]"#,
|
||||
None,
|
||||
),
|
||||
(
|
||||
"mixed list entries",
|
||||
r#"forced_chatgpt_workspace_id = ["", " 123e4567-e89b-42d3-a456-426614174000 ", "123e4567-e89b-42d3-a456-426614174001"]"#,
|
||||
Some(vec![
|
||||
"123e4567-e89b-42d3-a456-426614174000",
|
||||
"123e4567-e89b-42d3-a456-426614174001",
|
||||
]),
|
||||
),
|
||||
];
|
||||
|
||||
for (name, toml, expected) in cases {
|
||||
let cfg_toml: ConfigToml = toml::from_str(toml)
|
||||
.unwrap_or_else(|err| panic!("{name} should parse forced_chatgpt_workspace_id: {err}"));
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
cfg_toml,
|
||||
ConfigOverrides::default(),
|
||||
tempdir().expect("tempdir").abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let expected = expected.map(|values| {
|
||||
values
|
||||
.into_iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(config.forced_chatgpt_workspace_id, expected, "{name}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_remote_thread_store_endpoint_is_rejected() {
|
||||
let cfg: ConfigToml =
|
||||
|
||||
@@ -752,15 +752,14 @@ pub struct Config {
|
||||
/// instructions inserted into developer messages when realtime becomes
|
||||
/// active.
|
||||
pub experimental_realtime_start_instructions: Option<String>,
|
||||
|
||||
/// Experimental / do not use. When set, app-server fetches thread-scoped
|
||||
/// config from a remote service at this endpoint.
|
||||
pub experimental_thread_config_endpoint: Option<String>,
|
||||
|
||||
/// Experimental / do not use. Selects the thread persistence backend.
|
||||
pub experimental_thread_store: ThreadStoreConfig,
|
||||
/// When set, restricts ChatGPT login to a specific workspace identifier.
|
||||
pub forced_chatgpt_workspace_id: Option<String>,
|
||||
/// When set, restricts ChatGPT login to one or more workspace identifiers.
|
||||
pub forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
|
||||
/// When set, restricts the login mechanism users may use.
|
||||
pub forced_login_method: Option<ForcedLoginMethod>,
|
||||
@@ -891,7 +890,7 @@ impl AuthManagerConfig for Config {
|
||||
self.cli_auth_credentials_store_mode
|
||||
}
|
||||
|
||||
fn forced_chatgpt_workspace_id(&self) -> Option<String> {
|
||||
fn forced_chatgpt_workspace_id(&self) -> Option<Vec<String>> {
|
||||
self.forced_chatgpt_workspace_id.clone()
|
||||
}
|
||||
|
||||
@@ -2849,15 +2848,18 @@ impl Config {
|
||||
let include_apply_patch_tool_flag = features.enabled(Feature::ApplyPatchFreeform);
|
||||
let use_experimental_unified_exec_tool = features.enabled(Feature::UnifiedExec);
|
||||
|
||||
let forced_chatgpt_workspace_id =
|
||||
cfg.forced_chatgpt_workspace_id.as_ref().and_then(|value| {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
});
|
||||
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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user