Skip to content

feat: Config-Runtime Contract Registry (Phase 1) + fix #28046 + fix #28863 - #28995

Open
zccyman wants to merge 1 commit into
NousResearch:mainfrom
atyou2happy:feat/config-runtime-contract
Open

feat: Config-Runtime Contract Registry (Phase 1) + fix #28046 + fix #28863#28995
zccyman wants to merge 1 commit into
NousResearch:mainfrom
atyou2happy:feat/config-runtime-contract

Conversation

@zccyman

@zccyman zccyman commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements Phase 1 of the Typed Config-Runtime Contract — a declarative config binding registry that catches orphan config fields at startup, before users discover them silently.

Along the way, fixes two concrete bugs that motivated this work.

Bug Fixes

Fix #28046: max_tokens silently ignored from custom_providers per-model config

Problem: Only context_length was read from custom_providers[].models.<model>. The max_tokens field was silently ignored, always defaulting to 4096.

Fix: Extract get_custom_provider_model_field() as a generic lookup helper, add get_custom_provider_max_tokens() symmetric to context_length, and read it in agent_init.py after the existing context_length lookup.

Fix #28863: terminal.docker_extra_args silently dropped

Problem: docker_extra_args was declared in DEFAULT_CONFIG but missing from the _terminal_env_map bridge in gateway/run.py, so it was never passed to the terminal tool.

Fix: Add the mapping entry.

Feature: Config-Runtime Contract Registry (Phase 1)

New module: hermes_cli/config_contracts.py

  • CONFIG_BINDINGS — declarative registry of 29 config→runtime bindings (all terminal.* env vars, model.max_tokens, model.context_length, custom_providers wildcards)
  • get_nested() — resolves dotted keys with wildcard support, including list-of-dicts (for custom_providers)
  • validate_config_bindings() — cross-checks loaded config against registry, returns warning list
  • get_binding_report() — human-readable status report for debugging

Gateway integration

At startup, after env var bridging, the gateway runs validate_config_bindings() and logs any warnings. This is purely additive — warnings only, no behavior change, wrapped in try/except so it can never block startup.

How this prevents future bugs

When a developer adds a new terminal.* key to DEFAULT_CONFIG, the contract registry immediately shows it as unregistered. The next person who reviews the code sees the gap before shipping.

Test Coverage

Test file Tests Status
test_config_contracts.py 17 All pass
test_custom_provider_max_tokens.py 10 All pass
test_custom_provider_context_length.py 12 All pass (existing, unchanged)
Total 39 All pass

Backward Compatibility

  • get_custom_provider_context_length() signature unchanged
  • get_custom_provider_max_tokens() is a new function (purely additive)
  • config_contracts.py is a new module (purely additive)
  • Gateway validation is wrapped in try/except (cannot break startup)
  • Zero behavior change to any existing code path

Related

Files Changed

File Change
hermes_cli/config_contracts.py New — Contract registry module (235 lines)
tests/hermes_cli/test_config_contracts.py New — 17 tests
tests/hermes_cli/test_custom_provider_max_tokens.py New — 10 tests
agent/agent_init.py +14 — Read max_tokens from custom_providers
hermes_cli/config.py +66/-25 — Generic helper + max_tokens function
gateway/run.py +14 — docker_extra_args mapping + contract validation

…8046 + fix NousResearch#28863

Adds a declarative config binding registry that maps config.yaml keys to
their runtime consumers. On gateway startup, validates all registered
bindings are active — catching orphan config fields before users discover
them silently.

Changes:
- New module: hermes_cli/config_contracts.py
  - ConfigBinding dataclass + CONFIG_BINDINGS registry (29 bindings)
  - get_nested() resolves dotted/wildcard keys including list-of-dicts
  - validate_config_bindings() cross-checks config vs registry at startup
  - get_binding_report() for /info debugging

- Fix NousResearch#28046: Read max_tokens from custom_providers per-model config
  - Extract get_custom_provider_model_field() as generic lookup helper
  - Add get_custom_provider_max_tokens() symmetric to context_length
  - Read custom_providers max_tokens in agent_init.py
  - 10 regression tests

- Fix NousResearch#28863: Add docker_extra_args to _terminal_env_map
  - The key was in DEFAULT_CONFIG but missing from the env var bridge

- Gateway startup: inject contract validation (warnings only, no behavior change)

- Tests: 17 new (10 max_tokens + 17 contracts = 27 unique, 39 total with
  existing context_length tests)

Implements: NousResearch#28984 (Typed Config-Runtime Contract, Phase 1)
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery area/config Config system, migrations, profiles labels May 20, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Note: The two bug fixes bundled here have existing dedicated PRs:

The Config-Runtime Contract Registry feature implements #28984.

@zccyman

zccyman commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @alt-glitch for the review. Acknowledged on the bundled bug fixes.

My reasoning for keeping them together: #28142 and #28891 are still open (not merged), so the fixes aren't yet available to users. The feature work (config_contracts.py) was motivated by these same bugs, so bundling them here provides a complete solution in one PR.

However, I'm happy to split if you prefer. The options are:

  1. Keep as-is: Feature + both fixes in one PR, coherent scope
  2. Strip bug fixes: Remove Bug: max_tokens not read from custom_providers per-model config, always defaults to 4096 #28046/terminal.docker_extra_args from config.yaml silently dropped — missing in _terminal_env_map bridge #28863 fixes, keep only the feature (config_contracts.py), let fix(agent_init): read max_tokens from custom_providers per-model config #28142/fix(gateway): bridge terminal.docker_extra_args and docker_forward_env to env #28891 handle the fixes separately

Which approach do you prefer? If you want a split, let me know and I'll remove the bug fix commits.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for putting the structural config-contract idea together. I verified the premise against current main, and the feature is still not implemented there, but this PR needs real cleanup before it would be safe to salvage.

Problems

  • hermes_cli/config_contracts.py:138-180 never appends to warnings, so validate_config_bindings() always returns []; the gateway hook at gateway/run.py:664-667 would not catch orphan config fields.
  • hermes_cli/config_contracts.py:183-196 makes strict mode a no-op too: _check_unregistered_keys() computes full_key but never compares or reports it.
  • The PR sets custom-provider max_tokens at agent/agent_init.py:1268, after current main snapshots _session_init_model_config at agent/agent_init.py:1343; sessions created via run_agent.py:515-520 would record stale max_tokens metadata.
  • .dev-workflow/code-graph.db is an unrelated binary artifact.
  • The docker_extra_args bridge hunk is stale: current main already has TERMINAL_CONFIG_ENV_MAP with docker_extra_args at hermes_cli/config.py:5328 and bridge parity tests in tests/tools/test_terminal_config_env_sync.py.

Suggested changes

  • Make the contract validator emit concrete warnings and tighten tests to assert those warnings, not just list shape.
  • Move/update the custom-provider max_tokens resolution so session metadata stays accurate.
  • Remove the binary artifact and the superseded docker bridge hunk.

Automated hermes-sweeper review.

registered_keys = {b.config_key for b in CONFIG_BINDINGS}
_check_unregistered_keys(config, registered_keys, "", warnings)

return warnings

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This function currently never appends to warnings, so gateway startup validation will always be silent even when a config binding is missing. The Phase 1 contract needs at least one concrete check that produces a warning and a test that asserts that warning.

for key, value in config.items():
full_key = f"{prefix}{key}" if not prefix else f"{prefix}.{key}"
if isinstance(value, dict) and key not in ("providers", "custom_providers", "models"):
_check_unregistered_keys(value, registered_keys, full_key, warnings)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

strict=True still cannot report unregistered keys: full_key is computed but never compared to registered_keys or appended to warnings. Either implement the strict check here or remove the strict option/tests until it has behavior.

Comment thread agent/agent_init.py
custom_providers=_custom_providers,
)
if _cp_max_tokens:
agent.max_tokens = int(_cp_max_tokens)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This updates agent.max_tokens after the session init model config has already been snapshotted on current main, so session DB metadata can still record max_tokens=None. Resolve this before the snapshot or update _session_init_model_config["max_tokens"] after setting it.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/feature New feature or request

Projects

None yet

3 participants