Skip to content

Settings Schema

The settings schema is the structural contract between guest TOML configs, the Rust backend, and the TypeScript frontend. Pydantic models in Python are the single source of truth. JSON Schema is generated from them. Three languages — Python, Rust, TypeScript — must parse settings identically.

Key files:

FileRole
src/capsem/builder/schema.pyPydantic models (canonical schema)
config/settings-schema.jsonGenerated JSON Schema
config/defaults.jsonGenerated defaults from guest TOML configs
crates/capsem-core/tests/settings_spec.rsRust conformance tests
frontend/src/lib/__tests__/settings_spec.test.tsTypeScript conformance tests
tests/test_settings_spec.pyPython schema + conformance tests
tests/settings_spec/golden.jsonGolden fixture (shared by all three)

The settings tree has exactly two node types, discriminated by the kind field:

graph TD
    ROOT["SettingsRoot"]
    ROOT --> G1["GroupNode\nkind=group"]
    ROOT --> G2["GroupNode\nkind=group"]
    G1 --> S1["SettingNode\nkind=setting\nsetting_type=bool"]
    G1 --> S2["SettingNode\nkind=setting\nsetting_type=text"]
    G1 --> S3["SettingNode\nkind=setting\nsetting_type=action"]
    G2 --> G3["GroupNode\nkind=group"]
    G3 --> S4["SettingNode\nkind=setting\nsetting_type=mcp_tool"]

GroupNode (kind="group"): container with children.

FieldTypeRequiredDescription
keystringyesDot-separated path (e.g. ai.anthropic)
namestringyesDisplay name
descriptionstringnoHelp text
enabled_bystringnoKey of a bool setting that gates this group
enabledboolnoEffective enabled state (default true)
collapsedboolyesWhether the UI renders this group collapsed
childrenSettingsNode[]yesNested groups and settings

SettingNode (kind="setting"): everything else — regular settings, actions, and MCP tools. The setting_type field determines which subfields are relevant.

FieldTypeRequiredDescription
keystringyesDot-separated path
namestringyesDisplay name
descriptionstringyesHelp text
setting_typeSettingTypeyesData type (see enum table below)
default_valueanynoDefault from guest config
effective_valueanynoResolved value (corp > user > default)
sourcePolicySourcenoWhere effective value came from
modifiedstringnoISO timestamp of last user change
corp_lockedboolnoWhether corp.toml overrides this
enabled_bystringnoKey of a bool setting that gates this
enabledboolnoEffective enabled state
collapsedboolnoUI collapse state
metadataSettingMetadatanoExtra fields (defaults to empty)
historyHistoryEntry[]noAudit trail of value changes

Actions (check_update, preset_select, rerun_wizard) and MCP tools are SettingNode variants. They use setting_type="action" or setting_type="mcp_tool" with the relevant metadata fields. Consumers check setting_type, not kind.

13 values. The first 11 are data types with stored values. The last two are structural variants.

ValueCategoryDescription
textvalueFree-form string
numbervalueInteger with optional min/max
urlvalueURL string
emailvalueEmail address
apikeyvalueAPI key (masked input, prefix hint)
boolvalueBoolean toggle
filevalue{ path, content } object
kv_mapvalue{ key: value } dictionary
string_listvalueArray of strings
int_listvalueArray of integers
float_listvalueArray of floats
actionstructuralUI button/widget, no stored value
mcp_toolstructuralMCP tool definition

All metadata lives in a single SettingMetadata object. Most fields are optional with sensible defaults. Fields are grouped by purpose.

FieldTypeDefaultDescription
domainsstring[][]Domain patterns for network policy
choicesstring[][]Valid options (drives select widget)
minintnullMinimum value (number types)
maxintnullMaximum value (number types)
rulesdict{}HTTP method permissions per rule
env_varsstring[][]Environment variables injected into guest
collapsedboolfalseDefault collapse state
formatstringnullValue format hint (e.g. domain_list)
docs_urlstringnullLink to external documentation
prefixstringnullExpected value prefix (e.g. sk-ant-)
filetypestringnullFile syntax type (e.g. json)
widgetWidgetnullOverride default UI widget
side_effectSideEffectnullFrontend action on value change
hiddenboolfalseExclude from UI, keep for policy
builtinboolfalseNon-removable (system setting)
maskboolfalseMask display value
validatorstringnullRegex pattern for validation
FieldTypeDefaultDescription
actionActionKindnullAction identifier (check_update, preset_select, rerun_wizard)
FieldTypeDefaultDescription
originMcpToolOriginnullWhere the tool runs (builtin, remote, in_vm)
FieldTypeDefaultDescription
transportMcpTransportnullProtocol (stdio, sse)
commandstringnullExecutable path (stdio transport)
urlstringnullServer URL (sse transport)
argsstring[][]Command arguments
envdict{}Environment variables for the server process
headersdict{}HTTP headers (sse transport)

The schema generation pipeline runs from Pydantic models to two output files:

flowchart LR
    PM["schema.py\nPydantic models"] --> MSJ["model_json_schema()"]
    MSJ --> SCH["config/settings-schema.json"]
    GC["guest/config/*.toml"] --> GD["generate_defaults_json()"]
    GD --> DEF["config/defaults.json"]

just schema regenerates both files:

just schema
# Runs: uv run python scripts/generate_schema.py
# Outputs:
# config/settings-schema.json (JSON Schema from Pydantic)
# config/defaults.json (defaults from guest TOML configs)

The JSON Schema is derived from SettingsRoot.model_json_schema(). It contains $defs for all model types (GroupNode, SettingNode, SettingMetadata, enums) and a properties.settings array at the root.

A golden fixture at tests/settings_spec/golden.json is the contract. Three test suites parse the same fixture and verify identical structure:

flowchart TD
    GOLDEN["tests/settings_spec/golden.json\n(shared fixture)"]
    EXPECTED["tests/settings_spec/expected.json\n(expected counts + fields)"]

    GOLDEN --> PY["Python\ntests/test_settings_spec.py\n73 tests"]
    GOLDEN --> RS["Rust\ncrates/capsem-core/tests/settings_spec.rs\n12 tests"]
    GOLDEN --> TS["TypeScript\nfrontend/.../settings_spec.test.ts\n14 tests"]

    EXPECTED --> PY
    EXPECTED --> RS
    EXPECTED --> TS

    PY --> V["All three agree on:\n- total setting count\n- per-type counts\n- group count\n- setting fields\n- roundtrip serialization"]
    RS --> V
    TS --> V

99 tests total (73 Python, 12 Rust, 14 TypeScript). Every test suite checks:

AssertionVerified by
Golden fixture parsesAll three
Total setting count matches expected.jsonAll three
Per-type counts match expected.jsonAll three
Group count matches expected.jsonAll three
Setting key, name, type, enabled_by matchAll three
Roundtrip serialize/deserializePython, Rust
All 13 setting types presentAll three
Action settings have metadata.actionAll three
MCP tool settings have metadata.originAll three
File settings have { path, content }All three
Hidden/builtin settings existAll three
enabled_by references a valid boolPython, TypeScript

Any schema change requires updating the golden fixture, expected.json, and all three test suites. just test runs all of them.

Two parallel paths connect guest TOML configs to the running application:

flowchart TD
    subgraph "Schema Path (dev time)"
        PM["schema.py\nPydantic models"] --> JSG["model_json_schema()"]
        JSG --> SCHEMA["config/settings-schema.json"]
        SCHEMA --> TESTS["Conformance tests\n(Python + Rust + TypeScript)"]
    end

    subgraph "Data Path (build time)"
        TOML["guest/config/*.toml\n(ai, mcp, security, vm)"] --> GEN["generate_defaults_json()"]
        GEN --> DEF["config/defaults.json"]
        DEF --> RUST["Rust include_str!()\nregistry.rs"]
        RUST --> BOOT["Boot-time config\ninjection"]
    end

    subgraph "Golden Fixture Path (test time)"
        GOLDEN2["tests/settings_spec/golden.json"] --> PY2["Python tests"]
        GOLDEN2 --> RS2["Rust tests"]
        GOLDEN2 --> TS2["TypeScript tests"]
    end

The data path: guest TOML configs are processed by generate_defaults_json() into config/defaults.json. Rust embeds this file at compile time via include_str!() in registry.rs. At boot, the registry resolves settings (corp > user > defaults) and injects the result into the VM.

The schema path: Pydantic models generate JSON Schema for documentation and validation. The conformance tests ensure all three languages agree on parsing.

The original schema had four node types:

Old typeDiscriminant
Groupkind="group"
Leafkind="leaf"
Actionkind="action"
McpServerkind="mcp_server"

This was simplified to two:

New typeDiscriminantCovers
GroupNodekind="group"Containers with children
SettingNodekind="setting"Regular settings, actions, MCP tools

The four-type design forced consumers to match on kind with four arms, even though actions and MCP servers share nearly all fields with regular settings. The two-type design uses setting_type as the discriminant for behavior:

  • Regular settings: setting_type in {text, number, bool, ...} — value fields populated
  • Actions: setting_type="action"metadata.action specifies the action kind
  • MCP tools: setting_type="mcp_tool"metadata.origin specifies where the tool runs

Consumers match on kind (two arms: group vs. setting), then check setting_type when they need type-specific behavior. MCP servers are GroupNodes containing server config settings and MCP tool SettingNodes as children. Tool categories (snapshots, network) are nested sub-groups within the server GroupNode.

The Rust conformance tests use local test-only structs with the two-node schema. The live app’s SettingsNode in capsem-core still uses the old four-variant enum for backward compatibility — migration is tracked separately.