Skip to content

feat: add JSON Schema for config.yaml with typed config accessors#8630

Closed
wpfleger96 wants to merge 1 commit into
mainfrom
wpfleger/config-schema
Closed

feat: add JSON Schema for config.yaml with typed config accessors#8630
wpfleger96 wants to merge 1 commit into
mainfrom
wpfleger/config-schema

Conversation

@wpfleger96
Copy link
Copy Markdown
Collaborator

@wpfleger96 wpfleger96 commented Apr 17, 2026

Summary

  • Adds a generated JSON Schema (crates/goose/config.schema.json) for config.yaml, enabling IDE autocomplete and programmatic validation
  • Makes GooseConfigSchema the authoritative registry of 122 user-facing config fields with compile-time enforcement
  • Migrates raw get_param("STRING_KEY") call sites across Goose to typed accessors (e.g. config.get_openai_host(), config.get_goose_max_turns(), etc.), including current-main keys like OPENAI_BASE_URL

Motivation

Goose did not have a machine-readable schema for config.yaml, while other AI coding CLIs such as Codex and Claude Code support schema-backed configuration validation. Beyond the schema gap, config keys were accessed via untyped string literals scattered across the codebase -- adding a new key required no schema update, and drift between the schema and runtime access was undetectable.

How it works

Schema and typed accessors work together to prevent config key drift:

  • Compile-time assertion -- the config_value! macro now includes const assert!(GooseConfigSchema::has_key(stringify!($key))). Adding a config_value! for a key not registered in the schema struct is a compile error.
  • Typed accessors replace string literals -- config_value! invocations generate get_*/set_* methods on Config. Callers use config.get_openai_host() instead of config.get_param("OPENAI_HOST"). Same return type (Result<T, ConfigError>), same IO path, just type-safe at the call site.

Key taxonomy (what gets typed vs what stays dynamic):

Category Treatment Examples
A: User-facing config Schema + config_value! typed accessor GOOSE_MODEL, OPENAI_HOST, GOOSE_CLI_THEME
B: Internal persisted state Dedicated module helpers, stays on raw get_param extensions, gateway_configs, experiments
C: Dynamic runtime keys Raw get_param where key names come from runtime data ACP server wire input, OAuth credential storage
D: Constant-key aliases Migrated to typed accessor, constant deleted TELEMETRY_ENABLED_KEY, GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY

What does NOT change:

  • get_param and set_param remain public (needed by Category B/C callers)
  • Read-from-disk-on-every-call behavior stays (hot reload)
  • Env var overlay stays (env always wins over YAML)
  • Secret storage stays string-based (separate concern)
File changes

crates/goose/src/config/schema.rs
New file. Defines GooseConfigSchema for JSON Schema generation. Adds ALL_KEYS and has_key() for compile-time validation, plus tests for struct-to-key consistency.

crates/goose/src/config/base.rs
Adds const assert! to both forms of the config_value! macro. Adds typed config accessors covering user-facing config keys across providers, core settings, CLI, security, observability, and tunnel settings.

crates/goose/src/bin/generate_config_schema.rs
New binary. Generates config.schema.json from GooseConfigSchema via schemars::schema_for!.

crates/goose/config.schema.json
Generated schema output for config.yaml, including 122 properties and typed extension config definitions.

crates/goose/src/config/goose_mode.rs, crates/goose/src/slash_commands.rs, crates/goose/src/agents/extension.rs, crates/goose/src/config/extensions.rs
Added #[derive(JsonSchema)] to existing types so the schema references real types rather than duplicating definitions.

crates/goose/src/providers/*.rs
Migrates provider config access from raw string-key reads to typed accessors while preserving current-main behavior, including OPENAI_BASE_URL.

crates/goose/src/agents/*.rs, crates/goose/src/context_mgmt/mod.rs, crates/goose/src/security/*.rs, crates/goose/src/posthog.rs, crates/goose/src/hints/load_hints.rs, crates/goose/src/model.rs, crates/goose/src/otel/otlp.rs
Same typed-accessor migration for core Goose settings, security, observability, and telemetry keys.

crates/goose-cli/src/session/*.rs, crates/goose-cli/src/commands/configure.rs, crates/goose-cli/src/recipes/*.rs
Migrated CLI config access. Deleted Category D constants (TELEMETRY_ENABLED_KEY, GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY).

crates/goose-server/src/tunnel/mod.rs
Migrated tunnel_auto_start to a typed accessor.

Justfile
Added generate-config-schema and check-config-schema recipes. Added config schema check to check-everything.

.github/workflows/ci.yml
Added a config schema freshness check to the existing schema-check path.

Reproduction steps

  • Run just check-config-schema -- should pass when the schema is up to date
  • Run cargo test -p goose --lib all_keys_matches -- validates ALL_KEYS matches struct fields
  • Add config_value!(FAKE_KEY, String) to base.rs without adding it to GooseConfigSchema -- should fail to compile
  • Open config.yaml in VS Code with the YAML extension and point $schema at the raw GitHub URL of config.schema.json -- should get autocomplete for config keys

@wpfleger96 wpfleger96 force-pushed the wpfleger/config-schema branch from e7aff91 to ba853db Compare April 20, 2026 21:47
@wpfleger96 wpfleger96 changed the title add JSON Schema for config.yaml with CI drift check feat: add JSON Schema for config.yaml with typed config accessors Apr 20, 2026
@wpfleger96 wpfleger96 force-pushed the wpfleger/config-schema branch from ba853db to 5a4bb00 Compare April 20, 2026 22:54
@wpfleger96 wpfleger96 force-pushed the wpfleger/config-schema branch from 5a4bb00 to f064808 Compare May 6, 2026 17:09
@wpfleger96 wpfleger96 marked this pull request as ready for review May 6, 2026 18:51
Goose has no machine-readable schema for config.yaml and config keys were accessed via untyped string literals across the codebase. This adds a JSON Schema for IDE autocomplete and validation, with GooseConfigSchema as the authoritative registry for user-facing config keys.

Typed accessors replace raw string-key reads across user-facing config call sites while compile-time assertions keep accessor declarations tied to schema membership. Raw get_param and set_param remain available for dynamic and internal config storage.

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
@wpfleger96 wpfleger96 force-pushed the wpfleger/config-schema branch from f064808 to e44a0cd Compare May 6, 2026 20:03
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e44a0cdc1a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

#[serde(rename = "GOOSE_DISABLE_SESSION_NAMING")]
pub goose_disable_session_naming: Option<bool>,
#[serde(rename = "GOOSE_DISABLE_KEYRING")]
pub goose_disable_keyring: Option<String>,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept boolean keyring flag in config schema

The new schema constrains GOOSE_DISABLE_KEYRING to string|null, but runtime config parsing still explicitly supports booleans (true/false) for this key via keyring_disabled_value in config/base.rs. As a result, valid existing configs like GOOSE_DISABLE_KEYRING: true are now flagged as schema-invalid in editors/validators even though Goose honors them at runtime, which creates a misleading contract for users and tooling.

Useful? React with 👍 / 👎.

@DOsinga DOsinga added the needs_human label to set when a robot looks at a PR and can't handle it label May 12, 2026
@DOsinga
Copy link
Copy Markdown
Collaborator

DOsinga commented May 12, 2026

sorry for letting this linger so long. I do think we need something to structure this, but I wonder if this is the way. In the ideal world I think we should have something that makes it all truly type safe. I had an experiment

@DOsinga
Copy link
Copy Markdown
Collaborator

DOsinga commented May 12, 2026

Thanks for the PR! We have been thinking about this problem too. Before you invest more time rebasing, take a look at the structured-settings branch: https://github.com/aaif-goose/goose/compare/structured-settings

The key difference is that it defines a single Rust struct that flows through the OpenAPI spec into generated TypeScript types, so the UI gets real type safety too — not just a standalone JSON Schema file. Your approach gives compile-time key-name checking on the Rust side, but the schema it produces is not consumed by anything programmatically.

Would be worth discussing whether we can combine the best of both approaches before moving forward.

@wpfleger96
Copy link
Copy Markdown
Collaborator Author

@DOsinga I checked out the structured-settings branch and yeah, is feels like the typed struct → OpenAPI → TypeScript approach is probably the right call

my motivation here is a bit different though - I have a side project CLI tool that manages my local agent configs via symlinks, and it performs config validation for Claude Code/Codex/Gemini against their respective published standalone JSON schemas that are fetched at test time. I'd love for goose to have its own published config schema that I could use for automated validation too

I'm wondering if there's a clean "best of both" path: one typed config struct that derives both utoipa::ToSchema and schemars::JsonSchema. Same source of truth, two outputs: OpenAPI gets the config types for UI type safety, and a standalone config.schema.json gets generated for downstream consumers

is structured-settings being actively worked on? happy to rebase this PR to align with that approach, or push to structured-settings if you'd prefer

@DOsinga
Copy link
Copy Markdown
Collaborator

DOsinga commented May 13, 2026

so I worked on structured settings and then got distracted and it was out of sync so I closed the PR. I think we can do both of both worlds, if we do structured settings, we should be able to generate the schema you have, also, no? or is that what you are saying.

if you would want to take over structured settings, I'd be very happy!

@wpfleger96
Copy link
Copy Markdown
Collaborator Author

yes exactly that's what I was trying to say! I can definitely take over structured settings

@wpfleger96
Copy link
Copy Markdown
Collaborator Author

closing in favor of #9197

@wpfleger96 wpfleger96 closed this May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs_human label to set when a robot looks at a PR and can't handle it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants