From eede02a5ef81032e39d5f3c80809e4f8cdf57da6 Mon Sep 17 00:00:00 2001 From: Sayan Sisodiya Date: Thu, 8 Jan 2026 19:10:12 -0800 Subject: [PATCH] deny unknown fields and add docs --- codex-rs/core/src/config/SCHEMA.md | 12 +++++ codex-rs/core/src/config/mod.rs | 8 ++++ codex-rs/core/src/config/profile.rs | 1 + codex-rs/core/src/config/schema.rs | 10 +++- codex-rs/core/src/config/types.rs | 9 ++++ codex-rs/core/src/model_provider_info.rs | 1 + docs/config.schema.json | 61 +++++++++++++++--------- justfile | 4 ++ 8 files changed, 81 insertions(+), 25 deletions(-) create mode 100644 codex-rs/core/src/config/SCHEMA.md diff --git a/codex-rs/core/src/config/SCHEMA.md b/codex-rs/core/src/config/SCHEMA.md new file mode 100644 index 0000000000..848b2b8421 --- /dev/null +++ b/codex-rs/core/src/config/SCHEMA.md @@ -0,0 +1,12 @@ +# Config JSON Schema + +We generate a JSON Schema for `~/.codex/config.toml` from the `ConfigToml` type +and commit it at `docs/config.schema.json` for editor integration. + +When you change any fields included in `ConfigToml` (or nested config types), +regenerate the schema and update the fixture: + +``` +just write-config-schema +cargo test -p codex-core config_schema_matches_fixture +``` diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index b374ab537e..84b352c087 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -684,8 +684,13 @@ pub fn set_default_oss_provider(codex_home: &Path, provider: &str) -> std::io::R } /// Base config deserialized from ~/.codex/config.toml. +// The JSON Schema in `docs/config.schema.json` is generated from this struct. +// If you add, remove, or rename fields, regenerate the schema via +// `just write-config-schema` and update the fixture test. #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] +#[schemars(deny_unknown_fields)] pub struct ConfigToml { + pub test: Option, /// Optional override of model selection. pub model: Option, /// Review model override used by the `/review` feature. @@ -886,6 +891,7 @@ impl From for UserSavedConfig { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] pub struct ProjectConfig { pub trust_level: Option, } @@ -901,6 +907,7 @@ impl ProjectConfig { } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] +#[schemars(deny_unknown_fields)] pub struct ToolsToml { #[serde(default, alias = "web_search_request")] pub web_search: Option, @@ -920,6 +927,7 @@ impl From for Tools { } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] pub struct GhostSnapshotToml { /// Exclude untracked files larger than this many bytes from ghost snapshots. #[serde(alias = "ignore_untracked_files_over_bytes")] diff --git a/codex-rs/core/src/config/profile.rs b/codex-rs/core/src/config/profile.rs index 8d01c425e8..0507c90f20 100644 --- a/codex-rs/core/src/config/profile.rs +++ b/codex-rs/core/src/config/profile.rs @@ -12,6 +12,7 @@ use codex_protocol::openai_models::ReasoningEffort; /// Collection of common configuration options that a user can define as a unit /// in `config.toml`. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[schemars(deny_unknown_fields)] pub struct ConfigProfile { pub model: Option, /// The key in the `model_providers` map identifying the diff --git a/codex-rs/core/src/config/schema.rs b/codex-rs/core/src/config/schema.rs index d89f010be4..0b15722b3f 100644 --- a/codex-rs/core/src/config/schema.rs +++ b/codex-rs/core/src/config/schema.rs @@ -48,7 +48,7 @@ pub(crate) fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema { .properties .insert(feature.key.to_string(), schema_gen.subschema_for::()); } - validation.additional_properties = Some(Box::new(schema_gen.subschema_for::())); + validation.additional_properties = Some(Box::new(Schema::Bool(false))); object.object = Some(Box::new(validation)); Schema::Object(object) @@ -84,6 +84,7 @@ fn mcp_server_schema(schema_gen: &mut SchemaGenerator) -> Schema { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[schemars(deny_unknown_fields)] struct McpServerStdioSchema { command: String, #[serde(default)] @@ -109,6 +110,7 @@ struct McpServerStdioSchema { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[schemars(deny_unknown_fields)] struct McpServerStreamableHttpSchema { url: String, #[serde(default)] @@ -145,9 +147,13 @@ mod tests { let fixture = std::fs::read_to_string(fixture_path).expect("read config schema fixture"); let fixture_value: serde_json::Value = serde_json::from_str(&fixture).expect("parse config schema fixture"); - assert_eq!(fixture_value, schema_value); + assert_eq!( + fixture_value, schema_value, + "Current schema for `config.toml` doesn't match the fixture. Run `just write-config-schema` to overwrite with your changes." + ); } + /// Overwrite the config schema fixture with the current schema. #[test] #[ignore] fn write_config_schema_fixture() { diff --git a/codex-rs/core/src/config/types.rs b/codex-rs/core/src/config/types.rs index 36f140ed92..2a705a75d7 100644 --- a/codex-rs/core/src/config/types.rs +++ b/codex-rs/core/src/config/types.rs @@ -256,6 +256,7 @@ impl UriBasedFileOpener { /// Settings that govern if and what will be written to `~/.codex/history.jsonl`. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] pub struct History { /// If true, history entries will not be written to disk. pub persistence: HistoryPersistence, @@ -279,6 +280,7 @@ pub enum HistoryPersistence { /// Analytics settings loaded from config.toml. Fields are optional so we can apply defaults. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] pub struct AnalyticsConfigToml { /// When `false`, disables analytics across Codex product surfaces in this profile. pub enabled: Option, @@ -302,6 +304,7 @@ pub enum OtelHttpProtocol { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] #[serde(rename_all = "kebab-case")] pub struct OtelTlsConfig { pub ca_certificate: Option, @@ -311,6 +314,7 @@ pub struct OtelTlsConfig { /// Which OTEL exporter to use. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] +#[schemars(deny_unknown_fields)] #[serde(rename_all = "kebab-case")] pub enum OtelExporterKind { None, @@ -334,6 +338,7 @@ pub enum OtelExporterKind { /// OTEL settings loaded from config.toml. Fields are optional so we can apply defaults. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] pub struct OtelConfigToml { /// Log user prompt in traces pub log_user_prompt: Option, @@ -407,6 +412,7 @@ impl Default for ScrollInputMode { /// Collection of settings that are specific to the TUI. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] pub struct Tui { /// Enable desktop notifications from the TUI when the terminal is unfocused. /// Defaults to `true`. @@ -546,6 +552,7 @@ const fn default_true() -> bool { /// (primarily the Codex IDE extension). NOTE: these are different from /// notifications - notices are warnings, NUX screens, acknowledgements, etc. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] pub struct Notice { /// Tracks whether the user has acknowledged the full access warning prompt. pub hide_full_access_warning: Option, @@ -569,6 +576,7 @@ impl Notice { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] pub struct SandboxWorkspaceWrite { #[serde(default)] pub writable_roots: Vec, @@ -609,6 +617,7 @@ pub enum ShellEnvironmentPolicyInherit { /// Policy for building the `env` when spawning a process via either the /// `shell` or `local_shell` tool. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] pub struct ShellEnvironmentPolicyToml { pub inherit: Option, diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index 8f2501dec3..278a99e03b 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -50,6 +50,7 @@ pub enum WireApi { /// Serializable representation of a provider definition. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema)] +#[schemars(deny_unknown_fields)] pub struct ModelProviderInfo { /// Friendly display name. pub name: String, diff --git a/docs/config.schema.json b/docs/config.schema.json index 7dadfd4c9f..ccb21d9041 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -118,9 +118,7 @@ "type": "boolean" } }, - "additionalProperties": { - "type": "boolean" - } + "additionalProperties": false }, "file_opener": { "description": "Optional URI-based file opener. If set, citations to files in the model output will be hyperlinked using the specified URI scheme.", @@ -371,6 +369,7 @@ "type": "boolean" } }, + "additionalProperties": false, "definitions": { "AbsolutePathBuf": { "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", @@ -384,7 +383,8 @@ "description": "When `false`, disables analytics across Codex product surfaces in this profile.", "type": "boolean" } - } + }, + "additionalProperties": false }, "AskForApproval": { "description": "Determines the conditions under which the user is consulted to approve running the command proposed by Codex.", @@ -521,9 +521,7 @@ "type": "boolean" } }, - "additionalProperties": { - "type": "boolean" - } + "additionalProperties": false }, "include_apply_patch_tool": { "type": "boolean" @@ -556,7 +554,8 @@ "tools_web_search": { "type": "boolean" } - } + }, + "additionalProperties": false }, "ForcedLoginMethod": { "type": "string", @@ -582,7 +581,8 @@ "type": "integer", "format": "int64" } - } + }, + "additionalProperties": false }, "History": { "description": "Settings that govern if and what will be written to `~/.codex/history.jsonl`.", @@ -605,7 +605,8 @@ } ] } - } + }, + "additionalProperties": false }, "HistoryPersistence": { "oneOf": [ @@ -626,6 +627,7 @@ ] }, "McpServerStdioSchema": { + "description": "Schema-only representations of MCP server configs.\n\nKeep these in sync with the custom MCP config parsing in `crate::config::types::McpServerConfig`.", "type": "object", "required": [ "command" @@ -693,7 +695,8 @@ "type": "number", "format": "double" } - } + }, + "additionalProperties": false }, "McpServerStreamableHttpSchema": { "type": "object", @@ -756,7 +759,8 @@ "url": { "type": "string" } - } + }, + "additionalProperties": false }, "ModelProviderInfo": { "description": "Serializable representation of a provider definition.", @@ -838,7 +842,8 @@ } ] } - } + }, + "additionalProperties": false }, "Notice": { "description": "Settings for notices we display to users via the tui and app-server clients (primarily the Codex IDE extension). NOTE: these are different from notifications - notices are warnings, NUX screens, acknowledgements, etc.", @@ -872,7 +877,8 @@ "type": "string" } } - } + }, + "additionalProperties": false }, "Notifications": { "anyOf": [ @@ -941,7 +947,8 @@ } ] } - } + }, + "additionalProperties": false }, "OtelExporterKind": { "description": "Which OTEL exporter to use.", @@ -987,7 +994,8 @@ } ] } - } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -1022,7 +1030,8 @@ } ] } - } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -1059,7 +1068,8 @@ "client-private-key": { "$ref": "#/definitions/AbsolutePathBuf" } - } + }, + "additionalProperties": false }, "ProjectConfig": { "type": "object", @@ -1067,7 +1077,8 @@ "trust_level": { "$ref": "#/definitions/TrustLevel" } - } + }, + "additionalProperties": false }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", @@ -1131,7 +1142,8 @@ "$ref": "#/definitions/AbsolutePathBuf" } } - } + }, + "additionalProperties": false }, "ScrollInputMode": { "description": "How TUI2 should interpret mouse scroll events.\n\nTerminals generally encode both mouse wheels and trackpads as the same \"scroll up/down\" mouse button events, without a magnitude. This setting controls whether Codex uses a heuristic to infer wheel vs trackpad per stream, or forces a specific behavior.", @@ -1217,7 +1229,8 @@ "type": "string" } } - } + }, + "additionalProperties": false }, "ToolsToml": { "type": "object", @@ -1231,7 +1244,8 @@ "default": null, "type": "boolean" } - } + }, + "additionalProperties": false }, "TrustLevel": { "description": "Represents the trust level for a project directory. This determines the approval policy and sandbox mode applied.", @@ -1320,7 +1334,8 @@ "default": true, "type": "boolean" } - } + }, + "additionalProperties": false }, "UriBasedFileOpener": { "oneOf": [ diff --git a/justfile b/justfile index bbe18987a8..2d4c60d5a8 100644 --- a/justfile +++ b/justfile @@ -56,3 +56,7 @@ build-for-release: # Run the MCP server mcp-server-run *args: cargo run -p codex-mcp-server -- "$@" + +# Regenerate the json schema for config.toml from the current config types. +write-config-schema: + cargo test -p codex-core write_config_schema_fixture -- --ignored