Skip to content

Gateway server-config block is an untyped Dict[str, Any] — invalid or misspelled server settings pass validation silently #3050

Description

@MervinPraison

Summary

The gateway/bot config schema validates almost everything a user writes in gateway.yaml — channels, sessions, streaming, delivery, STT, daemon — with friendly, field-by-field errors. But the one block that controls the server itself (gateway:) is modelled as an opaque Optional[Dict[str, Any]]. Every server knob an operator actually tunes for a robust deployment — drain_timeout, reload_drain_timeout, admission ceiling, health-monitor thresholds, host/port, and the inbound hooks — is read ad-hoc deep inside server.py and is never validated. A typo (drain_timout), a wrong type ("10s" instead of 10), or an out-of-place key is accepted at load time and then silently ignored at runtime, so the gateway quietly runs with defaults the operator believes they overrode. This is exactly the class of "it validated fine but didn't do what I wrote" failure that erodes trust in a production gateway and makes the settings undiscoverable (no schema = no autocomplete, no docs surface, no error).

Current behaviour

The canonical schema explicitly keeps the server block permissive and unvalidated:

src/praisonai-bot/praisonai_bot/bots/_config_schema.py

# Gateway server settings (host/port, drain_timeout, admission control,
# etc.) and inbound trigger hooks. These are read by
# ``gateway/server.py::load_gateway_config`` / ``_apply_hooks_from_config``
# rather than modelled field-by-field here; kept permissive so a real
# ``gateway.yaml`` with a top-level ``gateway:``/``hooks:`` block validates
# through this single schema instead of being rejected.
gateway: Optional[Dict[str, Any]] = None
hooks: Optional[List[Dict[str, Any]]] = None

Those settings are then pulled out by string key at runtime, with no schema behind them, so a misspelled or mistyped key simply falls through to the default:

src/praisonai-bot/praisonai_bot/gateway/server.py

reload_drain_cfg = gw_cfg.get("reload_drain_timeout")   # typo => None => default, silently
...
# drain_timeout, admission ceiling, health-monitor thresholds, hooks:
# all read via gw_cfg.get(...) with no validation of names, types, or ranges

By contrast, core already ships a typed dataclass tree for exactly these settings in src/praisonai-agents/praisonaiagents/gateway/config.py (GatewayConfig with host/port/max_connections/drain/liveness/admission fields, LivenessConfig, etc.), but the wrapper's load-time schema does not use or mirror it for the gateway: block — so the strong typing that exists in core never reaches the point where user YAML is validated.

Net effect: rich, friendly validation everywhere except the settings that determine the gateway's operational robustness.

Desired behaviour

The gateway: server block is validated field-by-field at load time, exactly like channels:/session:/streaming: already are:

  • Unknown/misspelled keys produce a clear error (or an explicit warning) that names the offending key, instead of being silently dropped.
  • Wrong types and out-of-range values (e.g. a negative drain_timeout, a string where a number is expected) are rejected with a friendly message.
  • The accepted settings become discoverable — one schema is the single source of truth for what the gateway server accepts, feeding docs and editor autocompletion.
  • The typed shape reuses/mirrors core's existing GatewayConfig so there is one definition of a gateway server setting, not two.

Layer placement

  • Primary layer: wrapper
  • Why not core: core already defines the typed GatewayConfig dataclass; the gap is that load-time validation of user gateway.yaml — a wrapper responsibility (load_gateway_config / GatewayConfigSchema) — does not apply it. The missing work is in the wrapper's schema, not core's types.
  • Why not tools: this is operator configuration of the gateway server, not an agent-callable integration.
  • Why not plugins: it is validation of first-party gateway config, not a cross-cutting lifecycle hook; plugin-declared channel fields already have their own descriptor path and are unaffected.
  • Secondary touch: core (praisonaiagents/gateway/config.py) — ensure GatewayConfig/LivenessConfig cover every server knob server.py reads (reload_drain_timeout, health-monitor thresholds, hooks) so the wrapper schema can derive from a complete core definition rather than re-declaring fields.
  • 3-way surface (CLI + YAML + Python): primarily YAML (validated at load), with the same typed shape available to Python users constructing config programmatically; no new CLI verb required.

Proposed approach

  • Extension point: replace gateway: Optional[Dict[str, Any]] in GatewayConfigSchema with a typed GatewayServerSchema (a Pydantic model mirroring core's GatewayConfig), and validate hooks as a typed HookSchema list. Use model_config = ConfigDict(extra="forbid") (or an explicit warn-on-unknown pass) so misspelled keys are caught, keeping the friendly-error style already used across the schema.

Minimal API sketch:

class GatewayServerSchema(BaseModel):
    model_config = ConfigDict(extra="forbid")  # unknown keys -> clear error
    host: str = "127.0.0.1"
    port: int = Field(8000, ge=1, le=65535)
    drain_timeout: float = Field(10.0, ge=0)
    reload_drain_timeout: Optional[float] = Field(None, ge=0)
    max_concurrent_runs: Optional[int] = Field(None, ge=1)
    health: Optional[HealthMonitorSchema] = None
    # ... derived from praisonaiagents.gateway.config.GatewayConfig

class GatewayConfigSchema(BaseModel):
    gateway: Optional[GatewayServerSchema] = None
    hooks: Optional[List[HookSchema]] = None

Resolution sketch

# gateway.yaml with a typo in a server knob
gateway:
  drain_timout: 30        # <-- misspelled ("timout")
  reload_drain_timeout: "quick"   # <-- wrong type
# Before (today): validates fine, both settings silently ignored,
# gateway drains with the default 10s and the operator never knows.
GatewayConfigSchema(**yaml_load("gateway.yaml"))   # OK — no error

# After (proposed): rejected at load with a precise message
# ValidationError: gateway.drain_timout: unexpected key (did you mean 'drain_timeout'?)
#                  gateway.reload_drain_timeout: value is not a valid float

Severity

High — silent config drop-through on the settings that govern drain, admission, and health is a genuine production-robustness and ease-of-use hazard, and the surrounding schema already sets the bar (every other block is validated). The fix reuses an existing core dataclass and follows an established pattern in the same file.

Validation

  • src/praisonai-bot/praisonai_bot/bots/_config_schema.pygateway: Optional[Dict[str, Any]] with an inline comment stating it is "kept permissive" and the settings are "read ... rather than modelled field-by-field here."
  • src/praisonai-bot/praisonai_bot/gateway/server.py — server knobs (reload_drain_timeout, drain_timeout, admission ceiling, health thresholds, hooks) resolved via gw_cfg.get(...) with no name/type/range validation.
  • src/praisonai-agents/praisonaiagents/gateway/config.pyGatewayConfig/LivenessConfig already model these settings with types, confirming a single typed source could back the wrapper schema; the wrapper simply does not use it for the gateway: block.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingclaudeAuto-trigger Claude analysisdocumentationImprovements or additions to documentation

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions