Gateway Config Migration
Upgrade legacy bot.yaml and platforms: configs to the canonical schema
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.
praisonai doctor --only gateway_config_migrationThe user runs praisonai doctor; migration reports legacy bot.yaml or BotOS configs and normalises them to the gateway schema at load time.
How It Works
Quick Start
- Detect legacy format
bash praisonai doctor --only gateway_config_migrationIf your config is already canonical, you will see PASS — Config uses current format.
- 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 - 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):
platform: telegram
token: ${TELEGRAM_BOT_TOKEN}
agent:
name: assistant
instructions: "Help users"After (canonical):
agent:
name: assistant
instructions: "Help users"
channels:
telegram:
platform: telegram
token: ${TELEGRAM_BOT_TOKEN}BotOS platforms: → channels:
Before:
agent:
name: assistant
platforms:
telegram:
token: ${TELEGRAM_BOT_TOKEN}
discord:
token: ${DISCORD_BOT_TOKEN}After:
agent:
name: assistant
channels:
telegram:
token: ${TELEGRAM_BOT_TOKEN}
discord:
token: ${DISCORD_BOT_TOKEN}String allowed_users → list
Before:
channels:
telegram:
token: ${TELEGRAM_BOT_TOKEN}
allowed_users: "123456,789012"After:
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:
from praisonaiagents.gateway import (
GATEWAY_CONFIG_VERSION,
ConfigVersionError,
is_config_current,
migrate_config_with_doctor,
)| Symbol | Kind | What it is |
|---|---|---|
GATEWAY_CONFIG_VERSION | int constant | Current canonical version (starts at 1). |
ConfigVersionError | ValueError subclass | Raised when the stamp is a version newer than this build supports, or is malformed (bool / str / float). |
LegacyConfigRule | dataclass | detect, fix, reason — one declarative migration step. |
GATEWAY_CONFIG_RULES | List[LegacyConfigRule] | Ordered rule list applied by doctor --fix. |
is_config_current(raw) | bool | True 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 name | Before → After | Note |
|---|---|---|
allowed_users_csv_to_list | allowed_users: "alice,bob" → ["alice", "bob"] (per channel) | Empty string → []. |
group_policy_default | channel 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.
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:
praisonai gateway doctor --config gateway.yaml --fixconfig: 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:
praisonai gateway doctor --config gateway.yaml --fix --dry-runconfig: 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):
{
"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"
}| Key | Values | Present when | |
|---|---|---|---|
config_version | "out-of-date" \ | "unsupported" | Stamp is stale, or newer/malformed. |
config_version_error | error message | Stamp is newer/malformed (unsupported). | |
config_fix | multi-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.
An out-of-date config migrates forward and prints the bump before binding:
$ praisonai gateway start
config: migrated config_version 0 -> 1A config written by a newer build refuses to start with exit 78:
$ praisonai gateway start
config: config_version 2 is newer than supported (1)
$ echo $?
78Exit 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.
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:, CSVallowed_users) runs on bothpraisonai bot serveandpraisonai gateway start— before PR #3019, onlybot servemigrated. - Start-time
config_versiongate (new — PR #3884).praisonai gateway startnow also runs the stampedconfig_versioncheck + forward-migration rules that used to bedoctor-only — before binding. This is separate from the load-time legacy-shape shim above:startapplies the versioned rule set, not just the load-time auto-migration. BotYamlSchemais an alias ofGatewayConfigSchema— existing Python imports keep working.group_policydefaults tomention_onlyfor new channels without an explicit value. Configs that explicitly setrespond_allkeep that value.observeis a newly-acceptedgroup_policyvalue (PR #3381). Configs never migrate toobserveautomatically — set it explicitly. Seeobserve: passive group context.- Comma-separated
allowed_usersstrings are auto-converted to lists at load time. access_groupsis a new top-level gateway key (brand-new, nothing to migrate). Channels reference it asgroup:<name>— see Access Groups.agent.model: gpt-4o-miniin a legacy file is not rewritten toauto. Delete the line (or replace it withmodel: 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. MultiChannelGatewayConfigandChannelRouteConfigremain public exports — neither is deprecated. Theconfig_versionstamp does not retire them.- The
--fixauto-repair also mints a stronggateway.auth_tokenwhen 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.
What gets validated
| Block | Validated by | Notes |
|---|---|---|
gateway: | GatewayServerSchema (extra="forbid") | Unknown keys rejected; each knob type/range-checked. |
gateway.health: | HealthMonitorSchema (extra="forbid") | Channel health-monitor thresholds. |
hooks: entries | HookSchema (extra="allow") | Non-empty path, valid action; free-form extras kept. |
Before / After
gateway:
drain_timout: 30 # misspelled
reload_drain_timeout: "quick" # wrong typeBefore: 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:
| Field | Constraint |
|---|---|
port | 1–65535 |
timeout knobs (drain_timeout, reload_drain_timeout, per_turn_timeout, heartbeat_interval, …) | non-negative |
overflow_policy | one of reject, queue, shed_oldest |
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:
# 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 metadataOpt-in strict mode
Pass the keyword-only strict=True to turn that warning into a hard, actionable failure:
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}'?)")
raiseWhat gets validated
Validation runs at three paths — the dotted path is shown in the error.
| Path | What’s validated | What passes through |
|---|---|---|
<root> | Top-level sections; allowed: gateway, agents, channels, hooks, config_version. | — |
gateway | Every 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
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.
| Attribute | Type | What it carries |
|---|---|---|
path | str | Dotted path to the offending section ("<root>", "gateway", "channels.telegram"). |
keys | List[str] | The offending key(s) as strings. |
suggestion | Optional[str] | Closest known key, or None when no near match exists. |
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.