Gateway Config Migration

Upgrade legacy bot.yaml and platforms: configs to the canonical schema

python
from praisonaiagents import Agent

agent = Agent(name="migration-agent", instructions="Migrate gateway configuration to the new format.")
agent.start("Migrate my gateway.yaml from the old schema to the new one.")

PraisonAI auto-migrates legacy bot.yaml and BotOS platforms: configs to the canonical GatewayConfigSchema at load time — on both praisonai bot serve and praisonai gateway start — and praisonai doctor reports migration opportunities so you can persist them.

bash
praisonai doctor --only gateway_config_migration

The user runs praisonai doctor; migration reports legacy bot.yaml or BotOS configs and normalises them to the gateway schema at load time.

Diagram

bot.yaml
platform + token

🧩 Canonical Schema

gateway.yaml
agents + channels

BotOS config
agent + platforms

✅ Channels dict
auto-normalized

🔀 Used by both
bot serve and gateway start

How It Works

Diagram
Gateway Config MigrationAgentUserGateway Config MigrationAgentUserRequestProcess requestResult Agent–>>User: Response

Quick Start

  1. Detect legacy format
    bash
    praisonai doctor --only gateway_config_migration

    If your config is already canonical, you will see PASS — Config uses current format.

  2. Review WARN output

    Example when migration is available:

    ⚠ Gateway Config Migration [MEDIUM]
      Config can be migrated: 2 change(s)
      • Single-bot format can be migrated to multi-channel format
      • telegram: allowed_users string → list migration available
  3. Persist canonical YAML

    Rewrite your config to the canonical channels: form (see migration table below). At runtime, legacy shapes already load — persisting is optional but recommended for clarity.


Migration Table

Single-bot → multi-channel

Before (legacy bot.yaml):

yaml
platform: telegram
token: ${TELEGRAM_BOT_TOKEN}
agent:
  name: assistant
  instructions: "Help users"

After (canonical):

yaml
agent:
  name: assistant
  instructions: "Help users"
channels:
  telegram:
    platform: telegram
    token: ${TELEGRAM_BOT_TOKEN}

BotOS platforms: → channels:

Before:

yaml
agent:
  name: assistant
platforms:
  telegram:
    token: ${TELEGRAM_BOT_TOKEN}
  discord:
    token: ${DISCORD_BOT_TOKEN}

After:

yaml
agent:
  name: assistant
channels:
  telegram:
    token: ${TELEGRAM_BOT_TOKEN}
  discord:
    token: ${DISCORD_BOT_TOKEN}

String allowed_users → list

Before:

yaml
channels:
  telegram:
    token: ${TELEGRAM_BOT_TOKEN}
    allowed_users: "123456,789012"

After:

yaml
channels:
  telegram:
    token: ${TELEGRAM_BOT_TOKEN}
    allowed_users:
      - "123456"
      - "789012"

Config version stamp

Gateway config now carries a canonical, integer config_version stamp. The runtime story is unchanged — the load-time shim still auto-migrates legacy shapes on every load — but the upgrade story is new: both praisonai gateway doctor --fix and praisonai gateway start apply the same declarative rule set, move an out-of-date config forward, and stamp the new version. start runs the gate before binding (PR #3884); doctor --fix runs it on demand.

Import the public surface from praisonaiagents.gateway:

python
from praisonaiagents.gateway import (
    GATEWAY_CONFIG_VERSION,
    ConfigVersionError,
    is_config_current,
    migrate_config_with_doctor,
)
SymbolKindWhat it is
GATEWAY_CONFIG_VERSIONint constantCurrent canonical version (starts at 1).
ConfigVersionErrorValueError subclassRaised when the stamp is a version newer than this build supports, or is malformed (bool / str / float).
LegacyConfigRuledataclassdetect, fix, reason — one declarative migration step.
GATEWAY_CONFIG_RULESList[LegacyConfigRule]Ordered rule list applied by doctor --fix.
is_config_current(raw)boolTrue iff raw["config_version"] == GATEWAY_CONFIG_VERSION. Raises ConfigVersionError on malformed stamps.
migrate_config_with_doctor(raw)Tuple[Dict, List[str]]Returns (migrated_copy, applied_reasons). Copies input, applies rules, stamps config_version. Idempotent. Raises ConfigVersionError on newer/malformed stamps.

Rules currently in scope

GATEWAY_CONFIG_RULES holds two ordered migration steps.

Rule nameBefore → AfterNote
allowed_users_csv_to_listallowed_users: "alice,bob" → ["alice", "bob"] (per channel)Empty string → [].
group_policy_defaultchannel missing group_policy → group_policy: "mention_only"Sets the secure default.

Doctor-driven migration

praisonai gateway doctor inspects gateway.yaml and reports version drift; --fix applies the rules and stamps the file atomically.

Two entry points now converge on the same version check and forward-migration path — doctor --fix and gateway start.

Diagram

current

out of date

newer / malformed

🔧 doctor –fix

📄 Load config

🚀 gateway start

is_config_current?

✅ nothing to do

apply GATEWAY_CONFIG_RULES

🛑 ConfigVersionError
refuse — never downgrade

stamp config_version

atomic rewrite
tmpfile + fsync + os.replace

Start-time migration (new — PR #3884)

praisonai gateway start runs the same is_config_current check and migrate_config_with_doctor forward-migration before binding. If the stamped version is behind, start applies the same declarative rule set that doctor --fix uses. If the stamp is newer than this build supports (or malformed — bool / str / float), start refuses (ConfigVersionError, exit 78) rather than downgrading a config a newer build wrote. See Gateway CLI › Version validation.

Detection (no flag)

When the stamp is missing / stale, or any rule’s detect fires, doctor prints:

config: out of date (config_version unstamped -> 1); run 'gateway doctor --fix'

A newer or malformed stamp prints the error instead — doctor does not attempt to migrate:

config: config_version 2 is newer than supported (1)

--fix

--fix loads gateway.yaml, calls migrate_config_with_doctor, and rewrites the file atomically (tmpfile in the same dir + fsync + os.replace), so an interrupted --fix can never leave the file truncated. It prints one config: <reason> line per applied rule plus the version bump:

bash
praisonai gateway doctor --config gateway.yaml --fix
config: migrating allowed_users (string) -> list [rule: allowed_users_csv_to_list]
config: setting group_policy secure default 'mention_only' [rule: group_policy_default]
config: config_version unstamped -> 1

--fix --dry-run

Preview without writing — every line is prefixed with would:

bash
praisonai gateway doctor --config gateway.yaml --fix --dry-run
config: would migrate allowed_users (string) -> list [rule: allowed_users_csv_to_list]
config: would stamp config_version unstamped -> 1 (--dry-run)

JSON keys

--json adds these keys (alongside any existing ones):

json
{
  "probes": {},
  "config_version": "out-of-date",
  "config_version_error": "config_version 2 is newer than supported (1)",
  "config_fix": "config: migrating allowed_users (string) -> list [rule: allowed_users_csv_to_list]\nconfig: setting group_policy secure default 'mention_only' [rule: group_policy_default]\nconfig: config_version unstamped -> 1"
}
KeyValuesPresent when
config_version"out-of-date" \"unsupported"Stamp is stale, or newer/malformed.
config_version_errorerror messageStamp is newer/malformed (unsupported).
config_fixmulti-line report--fix applied one or more rules.

At start time

praisonai gateway start runs the same config_version check doctor does — before binding — so start and doctor never disagree about whether a config is current. An out-of-date config is migrated forward in place; a config from a newer build (or with a malformed stamp) refuses to start rather than being downgraded.

Diagram

current

out of date

newer / malformed

praisonai gateway start

config_version current?

✅ bind channels

🔧 migrate in place
+ print reasons

🛑 exit 78
never downgrade

An out-of-date config migrates forward and prints the bump before binding:

bash
$ praisonai gateway start
config: migrated config_version 0 -> 1

A config written by a newer build refuses to start with exit 78:

bash
$ praisonai gateway start
config: config_version 2 is newer than supported (1)
$ echo $?
78

Exit 78 (EX_CONFIG) is the do-not-restart contract the installed daemon units honour — a config from the future loops the daemon otherwise. See Gateway Exit Codes.


Programmatic migration

Call migrate_config_with_doctor directly to migrate a config in memory — it copies the input (no mutation), applies the rules, and stamps the version. A second call returns applied == [] because it is idempotent.

python
from praisonaiagents.gateway import (
    GATEWAY_CONFIG_VERSION,
    ConfigVersionError,
    is_config_current,
    migrate_config_with_doctor,
)

raw = {
    "channels": {
        "telegram": {"token": "...", "allowed_users": "alice,bob"},
    },
}

if not is_config_current(raw):
    migrated, applied = migrate_config_with_doctor(raw)
    # migrated["channels"]["telegram"]["allowed_users"] == ["alice", "bob"]
    # migrated["channels"]["telegram"]["group_policy"] == "mention_only"
    # migrated["config_version"] == GATEWAY_CONFIG_VERSION
    for reason in applied:
        print("applied:", reason)

Behaviour Notes

  • Load-time auto-migration of legacy shapes (channel-less BotOS, platforms:, CSV allowed_users) runs on both praisonai bot serve and praisonai gateway start — before PR #3019, only bot serve migrated.
  • Start-time config_version gate (new — PR #3884). praisonai gateway start now also runs the stamped config_version check + forward-migration rules that used to be doctor-only — before binding. This is separate from the load-time legacy-shape shim above: start applies the versioned rule set, not just the load-time auto-migration.
  • BotYamlSchema is an alias of GatewayConfigSchema — existing Python imports keep working.
  • group_policy defaults to mention_only for new channels without an explicit value. Configs that explicitly set respond_all keep that value.
  • observe is a newly-accepted group_policy value (PR #3381). Configs never migrate to observe automatically — set it explicitly. See observe: passive group context.
  • Comma-separated allowed_users strings are auto-converted to lists at load time.
  • access_groups is a new top-level gateway key (brand-new, nothing to migrate). Channels reference it as group:<name> — see Access Groups.
  • agent.model: gpt-4o-mini in a legacy file is not rewritten to auto. Delete the line (or replace it with model: auto) to opt in to provider detection on next load — see Provider auto-detection.
  • All three YAML shapes (platform+token, agents+channels, platforms:) validate against one schema — see Gateway.
  • MultiChannelGatewayConfig and ChannelRouteConfig remain public exports — neither is deprecated. The config_version stamp does not retire them.
  • The --fix auto-repair also mints a strong gateway.auth_token when weak/missing, then re-validates — see Gateway CLI › Auto-repair and Pre-flight credential check.

Load-time validation of the gateway: block

Misspelled or wrong-typed keys under gateway: in gateway.yaml fail loudly at load time with the offending field named — no more silent-drop of settings you thought you’d overridden (#3050).

Every gateway: sub-key is validated field-by-field via GatewayServerSchema (extra="forbid"), mirroring core’s typed praisonaiagents.gateway.config.GatewayConfig. The block itself stays a plain dict — downstream .get(...) access keeps working, so validation is fully backward-compatible.

Diagram

typo / wrong type

valid

📄 gateway.yaml
gateway: block

🔍 GatewayServerSchema
extra=forbid

🛑 rejected at load
field-named error

✅ stored as dict
.get access works

What gets validated

BlockValidated byNotes
gateway:GatewayServerSchema (extra="forbid")Unknown keys rejected; each knob type/range-checked.
gateway.health:HealthMonitorSchema (extra="forbid")Channel health-monitor thresholds.
hooks: entriesHookSchema (extra="allow")Non-empty path, valid action; free-form extras kept.

Before / After

yaml
gateway:
  drain_timout: 30              # misspelled
  reload_drain_timeout: "quick" # wrong type

Before: validates fine; both settings silently ignored.

After: rejected at load with a precise, field-named error:

reload_drain_timeout: Input should be a valid number ...
drain_timout: Extra inputs are not permitted ...

Ranges worth knowing

A few common gateway: knobs carry constraints — a value outside the range fails at load with the field named:

FieldConstraint
port1–65535
timeout knobs (drain_timeout, reload_drain_timeout, per_turn_timeout, heartbeat_interval, …)non-negative
overflow_policyone of reject, queue, shed_oldest
GatewayConfig SDK Reference

Full typed field list for the gateway server block


Strict unknown-key validation (core SDK)

The core config loader now surfaces typos in gateway and channel keys instead of silently dropping them (#5723).

A misspelled key used to be dropped without a trace — the gateway started, reported success, and the affected channel was silently dead:

yaml
# Before #5725: silently dropped — gateway starts, channel is dead.
channels:
  telegram:
    tokenn: "123:ABC"   # typo of 'token'

Default (non-strict) behaviour

Unknown keys are still tolerated for backward compatibility, but each one now logs a prominent WARNING via praisonaiagents.gateway.config so a typo surfaces before it costs you an outage:

WARNING  Ignoring channels.telegram key 'tokenn' which looks like a typo of 'token'; if intentional this is passed through as metadata

Opt-in strict mode

Pass the keyword-only strict=True to turn that warning into a hard, actionable failure:

python
from praisonaiagents.gateway import (
    ConfigValidationError,
    MultiChannelGatewayConfig,
)
import yaml

with open("gateway.yaml") as f:
    raw = yaml.safe_load(f)

try:
    cfg = MultiChannelGatewayConfig.from_dict(raw, strict=True)
except ConfigValidationError as exc:
    print(f"{exc.path}: {exc.keys} (did you mean '{exc.suggestion}'?)")
    raise
Diagram

strict=True

strict=False default

📄 gateway config

🔍 from_dict
strict?

🛑 ConfigValidationError
path-scoped + did-you-mean

⚠ WARNING log
key tolerated

What gets validated

Validation runs at three paths — the dotted path is shown in the error.

PathWhat’s validatedWhat passes through
<root>Top-level sections; allowed: gateway, agents, channels, hooks, config_version.—
gatewayEvery key in the gateway: block against the known set (host, port, auth_token, auth, cors_origins, allowed_origins, max_connections, session_config, heartbeat_interval, reconnect_timeout, per_turn_timeout, ssl_cert, ssl_key, …).Derived read-only keys (ssl_enabled, scope_policy_enabled, executor) stay strict-clean on a GatewayConfig.to_dict() → from_dict() round-trip.
channels.<name>Only near-miss typos of reserved keys (token, app_token, routes, enabled), using a difflib cutoff of 0.8.Every other key is channel-specific metadata (e.g. Telegram’s allowed_updates) and is kept in ChannelRouteConfig.metadata.

Channel-key decision flow

Diagram

yes

no

yes, strict

yes, non-strict

no

channel key

reserved?
token / app_token /
routes / enabled

use as reserved

difflib close match
cutoff 0.8?

🛑 ConfigValidationError

⚠ WARNING log

✅ ChannelRouteConfig.metadata

Example error messages

One per path, all in the form <path>: unknown key(s): <keys> (did you mean '<suggestion>'?):

gateway: unknown key(s): prt (did you mean 'port'?)
<root>: unknown key(s): channelz (did you mean 'channels'?)
channels.telegram: unknown key(s): tokenn (did you mean 'token'?)

ConfigValidationError reference

A ValueError subclass carrying everything an operator needs to fix the config.

AttributeTypeWhat it carries
pathstrDotted path to the offending section ("<root>", "gateway", "channels.telegram").
keysList[str]The offending key(s) as strings.
suggestionOptional[str]Closest known key, or None when no near match exists.
Card

Attributes and raise-sites for the strict validation exception


Best Practices

Run doctor before upgrading

Use praisonai doctor --only gateway_config_migration after pulling a new PraisonAI release to see whether your persisted YAML can be simplified.

Persist canonical YAML when WARN appears

Runtime auto-migration is transparent, but persisting the canonical channels: form makes configs easier to diff and review in PRs.

Keep explicit group_policy values

group_policy defaults to mention_only for new channels only. If your bot should respond to every message, set respond_all explicitly rather than relying on legacy defaults.

Convert allowed_users to lists

Comma-separated strings still load, but list form is clearer and matches the schema validators.


Related