Reject comma-separated forced workspace config

codex-rs/config/src/config_toml.rs: add a custom forced_chatgpt_workspace_id deserializer that keeps single-string and list forms but rejects comma-separated strings with guidance to use a TOML list, plus focused parser tests.

codex-rs/core/config.schema.json: refresh the generated config schema description for the workspace allowlist shape.
This commit is contained in:
rreichel3-oai
2026-05-11 19:07:37 -04:00
parent d5f71d397e
commit 43c04f0e8b
2 changed files with 75 additions and 3 deletions
+74 -2
View File
@@ -56,6 +56,7 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::de::Error as SerdeError;
const RESERVED_MODEL_PROVIDER_IDS: [&str; 4] = [
AMAZON_BEDROCK_PROVIDER_ID,
@@ -86,8 +87,8 @@ const fn default_hide_agent_reasoning() -> Option<bool> {
Some(false)
}
/// Backward-compatible shape for workspace restrictions in config.toml.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)]
/// Backward-compatible shape for ChatGPT workspace login restrictions in config.toml.
#[derive(Serialize, Debug, Clone, PartialEq, JsonSchema)]
#[serde(untagged)]
pub enum ForcedChatgptWorkspaceIds {
Single(String),
@@ -103,6 +104,30 @@ impl ForcedChatgptWorkspaceIds {
}
}
impl<'de> Deserialize<'de> for ForcedChatgptWorkspaceIds {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Repr {
Single(String),
Multiple(Vec<String>),
}
match Repr::deserialize(deserializer)? {
Repr::Single(value) if value.contains(',') => Err(D::Error::custom(
"forced_chatgpt_workspace_id must be a single workspace ID string or a TOML list \
of strings; comma-separated strings are not supported. Use \
`forced_chatgpt_workspace_id = [\"workspace-a\", \"workspace-b\"]` instead.",
)),
Repr::Single(value) => Ok(Self::Single(value)),
Repr::Multiple(values) => Ok(Self::Multiple(values)),
}
}
}
/// Base config deserialized from ~/.codex/config.toml.
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
#[schemars(deny_unknown_fields)]
@@ -980,3 +1005,50 @@ pub fn validate_oss_provider(provider: &str) -> std::io::Result<()> {
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn forced_chatgpt_workspace_id_accepts_single_string() {
let config: ConfigToml = toml::from_str(r#"forced_chatgpt_workspace_id = "workspace-a""#)
.expect("single workspace id should deserialize");
assert_eq!(
config
.forced_chatgpt_workspace_id
.expect("workspace id should be set")
.into_vec(),
vec!["workspace-a".to_string()]
);
}
#[test]
fn forced_chatgpt_workspace_id_accepts_string_list() {
let config: ConfigToml =
toml::from_str(r#"forced_chatgpt_workspace_id = ["workspace-a", "workspace-b"]"#)
.expect("workspace id list should deserialize");
assert_eq!(
config
.forced_chatgpt_workspace_id
.expect("workspace ids should be set")
.into_vec(),
vec!["workspace-a".to_string(), "workspace-b".to_string()]
);
}
#[test]
fn forced_chatgpt_workspace_id_rejects_comma_separated_string() {
let err = toml::from_str::<ConfigToml>(
r#"forced_chatgpt_workspace_id = "workspace-a,workspace-b""#,
)
.expect_err("comma-separated string should be rejected");
let message = err.to_string();
assert!(message.contains("TOML list of strings"));
assert!(message.contains("comma-separated strings are not supported"));
}
}
+1 -1
View File
@@ -870,7 +870,7 @@
"type": "array"
}
],
"description": "Backward-compatible shape for workspace restrictions in config.toml."
"description": "Backward-compatible shape for ChatGPT workspace login restrictions in config.toml."
},
"ForcedLoginMethod": {
"enum": [