Skip to content

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

Draft
wpfleger96 wants to merge 1 commit intomainfrom
wpfleger/config-schema
Draft

feat: add JSON Schema for config.yaml with typed config accessors#8630
wpfleger96 wants to merge 1 commit intomainfrom
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 user-facing config keys with compile-time enforcement
  • Migrates ~50 raw get_param("STRING_KEY") call sites across 35 files to typed accessors (config.get_openai_host(), config.get_goose_max_turns(), etc.)

Motivation

Goose is the only major AI coding CLI without a machine-readable config schema. Codex, Gemini CLI, and Claude Code all publish JSON Schemas for IDE autocomplete and 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

Three enforcement layers prevent config key drift:

  1. 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.

  2. Typed accessors replace string literals -- ~90 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.

  3. Test-based lint -- a #[test] scans all .rs source files for get_param("...")/set_param("...") calls with literal string arguments, asserting they only appear in allowlisted files. New untyped access fails the test suite.

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, allowlisted in lint 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/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 struct with ~120 fields for JSON Schema generation. Adds ALL_KEYS const array + has_key() const fn for compile-time validation. Two tests: struct↔array consistency check, and source-scanning lint for untyped config access.

crates/goose/src/config/base.rs
Adds const assert! to both forms of the config_value! macro. Adds ~70 new config_value! invocations covering all Category A config keys (providers, core settings, CLI, security, observability, tunnel).

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 (1430 lines). 121 properties with correct types, discriminated union for extension configs.

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 (18 files)
Mechanical migration: config.get_param("KEY")config.get_key() for all provider config keys.

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 mechanical 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 typed accessor.

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

.github/workflows/ci.yml
Added "Check Config Schema is Up-to-Date" step to the schema-check CI job.

Reproduction steps

  1. Run just check-config-schema -- should pass (schema is up-to-date)
  2. Run cargo test -p goose --lib all_keys_matches -- validates ALL_KEYS matches struct fields
  3. Run cargo test -p goose --lib no_untyped_config_access -- validates no literal-string get_param calls outside allowlist
  4. Add config_value!(FAKE_KEY, String) to base.rs without adding it to GooseConfigSchema -- should fail to compile
  5. Add config.get_param("GOOSE_MODEL") to any non-allowlisted file -- no_untyped_config_access test should catch it
  6. Open config.yaml in VS Code with YAML extension and point $schema at the raw GitHub URL of config.schema.json -- should get autocomplete for all 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
Goose has no machine-readable schema for config.yaml and config keys
are accessed via untyped string literals scattered across the codebase.
This adds a JSON Schema for IDE autocomplete/validation and makes
GooseConfigSchema the authoritative registry of user-facing config
keys, with compile-time enforcement linking it to typed accessors.

Three enforcement layers prevent config key drift:
- const assert in config_value! validates keys exist in the schema
- Typed accessors replace raw get_param("KEY") string literals
- Test-based lint catches new untyped access outside allowlisted files
@wpfleger96 wpfleger96 force-pushed the wpfleger/config-schema branch from ba853db to 5a4bb00 Compare April 20, 2026 22:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant