Gateway & Control Plane

Unified gateway and control plane architecture for PraisonAI integrations

Agents can connect through a unified gateway providing single-entry access to multi-agent coordination, tools, and streaming events.

Diagram

Gateway Architecture

๐Ÿค– Agent

๐ŸŒ Gateway

โš™๏ธ Services

๐Ÿ“Š Results

Quick Start

  1. Simple Agent Gateway

    Deploy an agent through the unified gateway:

    python
    from praisonaiagents import Agent
    
    # Gateway enables unified access
    agent = Agent(
        name="Gateway Agent",
        instructions="Connect through unified gateway",
        gateway=True
    )
    
    agent.start("Process through gateway")
  2. Multi-Service Gateway

    Configure agents with full gateway capabilities:

    python
    from praisonaiagents import Agent, GatewayConfig
    
    agent = Agent(
        name="Gateway Agent",
        instructions="Multi-service coordination",
        gateway=GatewayConfig(
            unified=True,
            services=["agents", "mcp", "a2a", "a2u"]
        )
    )

How It Works

Agents connect to services through gateway routing:

Diagram
ServicesGatewayAgentServicesGatewayAgentRequestRouteResponseResult
ComponentPurposeAgent Access
GatewaySingle entry pointgateway=True
UnifiedAll services combinedDefault mode
ServicesIndependent scalingService-specific

Hot Reload

Edit gateway.yaml while the gateway is running โ€” file watch (with optional watchdog) and SIGHUP share the same diff-driven reload path, with drain-coordinated channel restarts. Three timing/logging keys (gateway.logging.level, gateway.drain_timeout, gateway.reload_drain_timeout) are hot-applied in place โ€” no channel restart at all โ€” see Hot-Apply (no restart). Rotating gateway.auth_token in the same reload also revokes any live sessions on the old secret โ€” see Gateway Credential Rotation. See Gateway Config Reload.

For proactive/scheduled outbound delivery routing โ€” including friendly alias targets like "family" or "ops" โ€” see Friendly Aliases for Scheduled Delivery.

To declare recurring agent โ†’ channel deliveries directly in gateway.yaml (with matching praisonai gateway schedule add | list | remove CLI), see Gateway Schedules.

Voice notes

Voice messages sent to your gateway bot are transcribed automatically on Telegram, Slack, WhatsApp, Discord, and Signal โ€” no code changes required. Configure the stt: block under each channel to force a language or opt out. See Voice Notes (Speech-to-Text).

The bot can also speak back. Configure an opt-in voice: block under any channel to have the agent’s reply delivered as a native voice note โ€” the outbound mirror of stt:. See Voice Reply (Text-to-Speech).


Authentication

Authentication posture changes automatically based on the bind interface.

Diagram

127.0.0.1

0.0.0.0

๐Ÿ” Interface

๐ŸŸข Permissive
No token required

๐Ÿ”’ Strict
Token required

InterfaceModeAuth Required
Loopback (127.0.0.1, localhost, ::1)PermissiveNo
External (0.0.0.0, LAN IPs, public IPs)StrictYes
Bind-Aware Authentication

Complete authentication security guide

Rotating the shared secret

Change gateway.auth_token in the running gateway.yaml and hot-reload โ€” every already-connected session that authenticated under the old secret is force-closed with WebSocket close code 4001 and reason credentials_rotated. New connections use the new secret. Defaults on; opt out with gateway.revoke_on_secret_rotation: false. See Gateway Credential Rotation for the full behaviour matrix.

The auto-generated per-install HMAC secret (~/.praisonai/gateway/.gateway_secret, mode 0600) is stored and permission-remediated separately from gateway.auth_token. Both the pairing codes and the inline Approve/Deny callback buttons sign with this same secret, so both survive gateway restarts by default โ€” no operator action required. Set PRAISONAI_CALLBACK_SECRET to a shared value only when running the gateway on multiple hosts/replicas. See Bot Pairing โ†’ Secret Management for pairing codes and Interactive Bot Actions for the callback-button flow.

Rejecting weak / placeholder secrets

A gateway bound to an external interface refuses to start if auth_token is a well-known placeholder (change-me, your-token-here, the literal string $(openssl rand -hex 16), etc.). Loopback binds still start but log a warning.

Card

Full denylist, bind-aware behaviour matrix, and how to fix a weak token

Pre-auth edge protections

Two guards protect internet-exposed gateway deployments by default: a per-IP connection budget (preauth_max_connections_per_ip=32) that caps unauthenticated WebSocket slots before auth runs, and a per-connection flood guard (max_unauthorized_frames=10) that closes connections sending too many unauthorized frames. Loopback clients are always exempt. Both are disabled by setting the option to 0.

Gateway Edge Protections

Per-IP connection budget, unauthorized-frame flood guard, and WebSocket close codes 4028 / 4029

Trusted proxies

Behind a reverse proxy or tunnel, set gateway.trusted_proxies so per-IP rate-limits and the operator id key on the real client instead of the proxy peer. The resolver walks X-Forwarded-For only across declared trusted hops and fails closed for anything unattributable. Empty (default) keeps the raw-peer behaviour.

Trusted Proxies

Real-client-IP resolution behind nginx, Cloudflare, or a loopback tunnel


Handshake and Version Negotiation

New WebSocket clients should send a hello frame to negotiate protocol version, capabilities, and policy limits in one round trip. The legacy join โ†’ joined flow remains supported for existing clients.

Gateway Handshake Protocol

Version negotiation, capabilities, and structured connection errors

Handling connect errors

A rejected handshake carries a structured code and next_step, so a client can tell a terminal failure (revoked token, unpaired device, unsupported protocol) from a transient one (rate limit) instead of probing-by-failure. The bundled client classifies each rejection with the shared is_recoverable(code) helper: terminal codes stop the reconnect loop and fire on_reconnect_paused(code, next_step); transient codes back off and retry, honouring any server retry_after_seconds as a lower bound.

python
from praisonai_bot.gateway.client import GatewayClient
from praisonaiagents.gateway import ConnectErrorCode

client = GatewayClient(url="wss://gateway.example.com/ws", agent_id="my-agent")

def on_paused(code: ConnectErrorCode, next_step):
    print(f"Reconnect paused: {code.value} โ€” next: {next_step}")

client.on_reconnect_paused = on_paused
await client.connect()
Gateway Client โ€” Terminal vs Transient Errors

Connect-error classification, on_reconnect_paused, and the recoverable-code table


Configuration Options

Gateway Configuration

Python gateway configuration options

ServiceDefault PortProtocolAgent Access
unified8765HTTP + WSDefault gateway
agents8000HTTP/RESTDirect API calls
mcp8080HTTP/SSETool protocols
a2a8001JSON-RPCAgent communication
a2u8002SSEEvent streams
openai8765HTTP + SSEOpenAI API compatibility

Edge protection fields (available on GatewayConfig and gateway.yaml):

OptionTypeDefaultDescription
preauth_max_connections_per_ipint32Max concurrent unauthenticated WS connections per source IP. 0 disables. Loopback exempt.
max_unauthorized_framesint10Close the connection after N unauthorized frames. 0 disables.
revoke_on_secret_rotationbooltrueForce-close live sessions when auth_token is rotated via config reload. false adopts the new secret for new connections only. See Gateway Credential Rotation.
api.openaiboolfalseWhen on, mounts /v1/chat/completions, /v1/responses (with background/async runs, GET /v1/responses/{id}, POST /v1/responses/{id}/cancel, and client Idempotency-Key support for safe retries), and /v1/models on the same port and auth. See Gateway API Endpoints.
api.mcpboolfalseWhen on, mounts an MCP JSON-RPC endpoint at /mcp on the same port and auth. See Gateway API Endpoints.

Python-first surface (parity with CLI/YAML)

A Python embedder can express the graceful-drain and event-loop-watchdog knobs directly on GatewayConfig โ€” no CLI flag and no YAML key required.

python
from praisonaiagents.gateway import GatewayConfig

config = GatewayConfig(
    drain_timeout=20,        # bounded drain on shutdown (seconds)
    watchdog=True,           # event-loop watchdog on
    watchdog_timeout=30,     # loop-stall budget before self-exit (seconds)
)
FieldTypeDefaultValidation
drain_timeoutOptional[float]None>= 0 โ€” None/0 disables the drain window.
watchdogboolFalseOpt-in event-loop liveness watchdog.
watchdog_timeoutOptional[float]None> 0 when set โ€” leave unset for the default budget.

Precedence is unchanged: **CLI flag > explicit YAML (block or scalar, incl. watchdog: false) > GatewayConfig field > default**. An explicit gateway.watchdog: false in YAML is honoured and is not re-enabled by GatewayConfig(watchdog=True). See Graceful Drain and Event-Loop Watchdog for the per-knob detail.


Gateway YAML Configuration

The gateway.yaml schema accepts three optional top-level blocks alongside channels and agents. All three are optional and ignored when not present.

gateway: block

Global gateway settings โ€” reliability preset, drain timeout, health monitor:

yaml
gateway:
  reliability: production    # "production" | "default" | "off"
  drain_timeout: 15          # seconds to wait for in-flight turns on shutdown

channels:
  local: {}                  # chat from your own terminal โ€” no token
  telegram:
    platform: telegram
    token: ${TELEGRAM_BOT_TOKEN}
    stt:
      enabled: true          # transcribe inbound voice notes (default on)
      echo_transcripts: true  # echo the recognised text back to the user
  discord:
    platform: discord
    token: ${DISCORD_BOT_TOKEN}
    stt: true                # Discord voice notes are transcribed too (default on)

Inbound voice notes on Telegram, Slack, WhatsApp, Discord, and Signal are transcribed automatically โ€” see Voice Notes.

Local terminal channel

Add local: {} alongside your remote channels to chat with the same live agent from your own terminal โ€” no token, no webhook, one shared session across both surfaces. See Local (Terminal) Channel.

The gateway: block validates every knob below. Extra keys are rejected (extra="forbid"), so a typo like drain_timout fails at load with a field-named error instead of silently running with the default.

OptionTypeDefaultNotes
hoststrNonePublic host name.
portintNone1โ€“65535. Rejected outside the range.
bind_hoststrNoneBind interface.
cors_originslist[str]None
allowed_originslist[str]None
auth_tokenstrNoneSee Rotating the shared secret and Weak Secret Guard. Well-known placeholder values refuse the start on an external bind.
authdictNone
auth_scopes`dict[str, list[str] \{scopes, agents}]`NoneEach entry is a plain scope list or {scopes, agents} to confine the token to specific agents. See Confining operators to specific agents.
max_connectionsintNoneโ‰ฅ 0
max_sessions_per_agentintNoneโ‰ฅ 0
session_configdictNone
heartbeat_intervalintNoneโ‰ฅ 0
reconnect_timeoutintNoneโ‰ฅ 0
ssl_certstrNone
ssl_keystrNone
max_buffered_bytesintNoneโ‰ฅ 0
max_queued_framesintNoneโ‰ฅ 0
max_concurrent_runsintNoneโ‰ฅ 0 โ€” admission control.
max_concurrent_runs_per_scopeintNoneโ‰ฅ 0 โ€” per-tenant sub-limit within max_concurrent_runs. See Per-tenant fairness.
queue_depthintNoneโ‰ฅ 0 โ€” admission control.
overflow_policystrNoneExactly one of "reject", "queue", "shed_oldest".
preauth_max_connections_per_ipintNoneโ‰ฅ 0 โ€” see Pre-auth edge protections.
max_unauthorized_framesintNoneโ‰ฅ 0 โ€” same.
trusted_proxieslist[str][]CIDRs/IPs of trusted upstream proxies for real-client-IP resolution โ€” see Trusted Proxies.
drain_timeoutfloatNoneโ‰ฅ 0 seconds.
reload_drain_timeoutfloatNoneโ‰ฅ 0 seconds โ€” drain window applied on config reload.
watchdogbool or dictNoneEnable the event-loop watchdog. Scalar true/false or a block with enabled, liveness_interval, liveness_strikes. See Event-Loop Watchdog.
watchdog_timeoutfloatNone> 0 seconds โ€” loop-stall budget before self-exit. Matches the CLI --watchdog-timeout flag.
reliabilitystrNoneSee Gateway Reliability.
durable_runsboolauto (true when session store is durable)Auto-enabled when a durable session store is present (the default). Explicit false or gateway.reliability: "off" opts back out. Accepts env-substituted string forms. Per-agent durable overrides. See Gateway Durable Runs.
apidictNoneSee the api: block below.
livenessdictNoneLiveness probe settings.
healthdictNoneHealth monitor โ€” see the health: block below.
forensicsdictNoneCrash/shutdown forensics.
hookslistNoneSame shape as top-level hooks: โ€” nest under gateway: for grouping.
notify_on_undeliveredboolfalseOpt in to a last-resort user notice + MESSAGE_UNDELIVERED hook when a reply fails permanently. See Undelivered Notice.
undelivered_templatestrbuilt-in noteOne-line plain-text notice sent when notify_on_undelivered is on.

api: block

Serve OpenAI-compatible and MCP HTTP endpoints from the same gateway process, sharing the live agents and sessions that chat users reach:

yaml
gateway:
  api:
    openai: true    # mounts /v1/chat/completions, /v1/responses (+ background/{id}/cancel), /v1/models
    mcp: true       # mounts /mcp (JSON-RPC)

Both surfaces are opt-in and off by default. See Gateway API Endpoints for the endpoint list, auth, and client examples.

health: block

Nest a health: block under gateway: to configure the worker health monitor. Extra keys are rejected (extra="forbid"):

yaml
gateway:
  health:
    enabled: true
    interval: 60
    stale_after: 90
    stuck_after: 600
    max_restarts_per_hour: 5
    fleet_restarts_per_hour: 40
    failing_channel_fraction: 0.5
    breaker_cooldown_s: 120
FieldTypeDefaultConstraint
enabledbooltrue
intervalfloat300.0> 0
startup_gracefloat60.0โ‰ฅ 0
stale_afterfloat120.0> 0
stuck_afterfloat900.0> 0
max_restarts_per_hourint10โ‰ฅ 0 (per channel)
fleet_restarts_per_hourint40โ‰ฅ 1 โ€” fleet-level breaker
failing_channel_fractionfloat0.50.0 < x โ‰ค 1.0
breaker_cooldown_sfloat120.0โ‰ฅ 0

hooks: block

Register inbound trigger hooks that expose an authenticated POST /hooks/<path> endpoint and run an agent (or wake a session) on each request. Each hook requires a non-empty path::

yaml
hooks:
  - path: gmail
    agent: assistant
    action: agent           # optional; "agent" or "wake"
FieldTypeDefaultRequiredNotes
pathstrโ€”yesPath segment for the inbound trigger (e.g. "gmail").
actionstr"agent"noOnly "agent" or "wake".
agentstrNonenoTarget agent id.
authstrNonenoBearer token for the trigger endpoint.
session_keystrNonenoTemplate for the session id.
idempotency_keystrNonenoTemplate for the dedup key.
deliver_tostrNonenochannel:target delivery spec for the reply.
messagestrNonenoTemplate for the agent message.
enabledbooltrueno
metadatadict{}noFree-form extra settings.
secretstrNonenoHMAC signing secret. Verifies the provider signature over the raw body before any agent runs; rejects (401) a missing/invalid signature โ€” fail-closed.
signature_headerstrNonenoHeader carrying the signature. Auto-defaults to X-Hub-Signature-256 when secret is set.
signature_algostr"sha256"noHMAC digest name.
signature_prefixstrNonenoOptional signature prefix, e.g. sha256=.
signature_schemestr"hmac_hex"noHow the provider signs โ€” "hmac_hex" (default, hex HMAC of the body), "hmac_base64" (base64 HMAC of the body), "hmac_timestamped" (hex HMAC of "{timestamp}.{body}"), or "structured" (Stripe-style t=โ€ฆ,v1=โ€ฆ composite header; any v1 may match). Invalid values raise ValueError.
timestamp_headerstrNonenoHeader carrying the provider timestamp (unix seconds). Only valid with hmac_timestamped or structured; required for hmac_timestamped. For structured, the t= component is read from the signature header when omitted.
signature_tolerance_secondsint300noReplay window for timestamped and structured schemes โ€” a request outside ยฑ this many seconds is rejected before any agent runs. 0 disables the freshness check (not recommended). Negative values raise ValueError.
eventslist[str]NonenoAllow-list of event types. Unmatched deliveries are ack’d (200) with no LLM turn. A string is coerced to a list.
event_headerstrNonenoHeader carrying the event type (e.g. X-GitHub-Event); when omitted the event is read from the payload as a dotted path (default "event").
deliver_onlyboolfalsenoDeliver the rendered message straight through deliver_to with no LLM turn. Requires deliver_to.

See Gateway Inbound Hooks for signature-verification, event-filter, and deliver-only examples.

scheduler: block

Tune the default RunPolicy the gateway applies to every unattended scheduled run. All fields are optional; omitted fields keep the fail-closed defaults.

yaml
scheduler:
  deliver_on_failure: true          # send a failure summary to the job's delivery target
  scan_prompt: true                 # scan the assembled prompt before the turn
  denied_toolsets:                  # tool categories removed from unattended runs
    - cronjob
    - shell
  audit_dir: /var/log/praisonai/scheduler   # durable full-output audit trail
OptionTypeDefaultDescription
deliver_on_failurebooltrueSend a failure summary to the job’s delivery target when the run is blocked or the agent raises.
scan_promptbooltrueRun the injection scan over the assembled prompt before the turn.
denied_toolsetslist[str]DEFAULT_DENIED_TOOLSETSTool categories removed for the unattended turn. Provide a list to override; omit to keep defaults.
audit_dirstrnullDirectory for the durable full-output audit trail.

Overrides apply on the next tick without a process restart. See Scheduled Run Policy for the full policy semantics.

mcp_servers: block

Declare downstream MCP servers once; every agent that lists the server name in tools: gets the same pooled client.

yaml
mcp_servers:
  github:
    command: npx
    args: ["-y", "@modelcontextprotocol/server-github"]
  search:
    url: https://mcp.example.com/sse
    headers:
      Authorization: Bearer ${SEARCH_MCP_TOKEN}

agents:
  support:
    instructions: "Answer questions using GitHub and web search."
    tools: [github, search]

See Gateway MCP Servers for hot-reload, pool shutdown, failure modes, and tool-name namespacing.

BotOS.from_config and tool names

BotOS.from_config("botos.yaml") now routes tool names through the standard ToolResolver chain:

  1. Local tools.py in the working directory
  2. Wrapper ToolRegistry
  3. praisonaiagents built-in tools
  4. praisonai-tools package
  5. Installed plugins

This means a YAML bot referencing a praisonai-tools symbol or a local tools.py function just works โ€” no ad-hoc importlib lookup required:

yaml
# botos.yaml
agent:
  name: assistant
  instructions: You are a helpful assistant.
  tools:
    - web_search          # resolved via praisonai-tools
    - my_custom_tool      # resolved from local tools.py
platforms:
  telegram:
    token: ${TELEGRAM_BOT_TOKEN}
python
from praisonai.bots import BotOS

botos = BotOS.from_config("botos.yaml")
botos.run()

Gateway Agent Defaults

Gateway agents loaded from YAML use chat-optimised defaults that differ from the Python SDK.

YAML keyGateway defaultWhy
reflectionfalseChat channels need sub-second replies; self-reflection adds ~8x latency on short prompts
tool_choicenull (auto)Let the LLM decide when to call tools
allow_delegationfalsePrevents cross-agent routing unless explicitly opted in
yaml
agents:
  assistant:
    instructions: "You are a helpful AI assistant."
    model: gpt-4o-mini
    reflection: true   # opt-in: enables self-critique, ~8x slower

Per-Agent Approval Isolation

“Allow always” grants persist across gateway restart and default to being scoped to the approving agent, so one agent’s approval never authorises another. Grants live in a durable SQLite store at ~/.praisonai/state/gateway/approvals.sqlite.

Gateway Scoped Approvals

Durable, agent-scoped allow-always grants, resolver scoping options, and the /api/approval/allow-list endpoint

Unanswered approvals fail closed and say so: the Slack / Telegram / Discord card edits to “โŒ› Timed out โ€” not run”, and a GatewayApprovalBackend you build yourself with notify_url sends a follow-up "timed_out" POST (the auto-wired default has no notify_url, so it sends none). See Timeout feedback on the card and GatewayApprovalBackend webhook payload.

Gateway also supports isolated exec approval managers for multi-tenant environments where agents must not share approval state.

Diagram

Approval Isolation

๐Ÿค– Agent A

๐Ÿ“‹ Manager A

๐Ÿค– Agent B

๐Ÿ“‹ Manager B

โœ… Isolated Approvals A

โœ… Isolated Approvals B

Default vs Per-Agent Managers

Manager TypeUse CaseApproval Sharing
DefaultCLI, single-agent setupsShared process-wide
Per-AgentMulti-tenant gatewaysIsolated per instance
python
from praisonai.gateway.exec_approval import (
    get_default_exec_approval_manager,
    create_exec_approval_manager
)

# Default manager - shared across all agents
default_manager = get_default_exec_approval_manager(ttl=300.0)

# Per-agent manager - isolated approval state
agent1_manager = create_exec_approval_manager(ttl=120.0)
agent2_manager = create_exec_approval_manager(ttl=180.0)

Multi-Agent Gateway with Isolation

python
from praisonaiagents import Agent
from praisonai.gateway.exec_approval import create_exec_approval_manager

class IsolatedGateway:
    def __init__(self):
        self.agents = {}

    def add_agent(self, name, instructions, approval_ttl=300):
        # Each agent gets isolated approval manager
        manager = create_exec_approval_manager(ttl=approval_ttl)

        agent = Agent(
            name=name,
            instructions=instructions,
            exec_approval_manager=manager,
            gateway=True
        )

        self.agents[name] = {"agent": agent, "manager": manager}
        return agent

    def get_pending_approvals(self, agent_name):
        if agent_name in self.agents:
            manager = self.agents[agent_name]["manager"]
            return manager.list_pending()
        return []

# Usage
gateway = IsolatedGateway()

# Tenant A agent
tenant_a = gateway.add_agent(
    "TenantA_Assistant", 
    "Help tenant A users",
    approval_ttl=180  # 3 minutes
)

# Tenant B agent  
tenant_b = gateway.add_agent(
    "TenantB_Assistant",
    "Help tenant B users", 
    approval_ttl=600  # 10 minutes
)

# Isolated approval queues
pending_a = gateway.get_pending_approvals("TenantA_Assistant")
pending_b = gateway.get_pending_approvals("TenantB_Assistant")

Allow-list endpoint

GET/POST/DELETE /api/approval/allow-list manages allow-always grants. POST and DELETE accept an optional agent_id; GET returns a grants view alongside the legacy allow_list.

json
{
  "allow_list": ["read_file"],
  "grants": [
    {"agent_id": "shell-bot", "tool_name": "shell_exec", "arg_signature": null, "created_at": 1720000000.0, "approver": "gateway:human"}
  ]
}

Omitting agent_id on POST/DELETE targets a legacy any-agent grant; supplying it scopes to that agent. See Gateway Scoped Approvals for full curl examples.


Gateway lifecycle predicates

Core SDK exports three pure predicates for gateway lifecycle decisions. Each is side-effect free and testable in isolation; the wrapper owns the actual teardown.

SymbolPurposeDocumentation
ScaleToZeroPolicyQuiesce when idle; wake on next inboundScale to Zero
DrainTimeoutPolicyBounded wait for in-flight turns on shutdownSession Continuity
DrainMarkerPolicy + current_epoch()Port-less, restart-safe external drain signalDrain Trigger
classify_exit_reason + FatalConfigErrorSupervisor-friendly sysexits exit codes (0 / 75 / 78)Gateway Exit Codes
assert_gateway_secret_strong + is_weak_secret + WeakGatewaySecretError + KNOWN_WEAK_SECRETSReject known-weak/placeholder shared secretsWeak Secret Guard
ThreadResult + ThreadStatusTyped outcome of create_thread โ€” open a native thread/topic, never raisesGateway Threads
python
from praisonaiagents.gateway import (
    ScaleToZeroPolicy,
    DrainTimeoutPolicy,
    DrainMarkerPolicy,
    current_epoch,
    is_weak_secret,
    assert_gateway_secret_strong,
    WeakGatewaySecretError,
    KNOWN_WEAK_SECRETS,
    ThreadResult,
    ThreadStatus,
)

Python API โ€” repair and provisioning

The same detect โ†’ repair โ†’ re-validate lifecycle behind praisonai gateway doctor --fix and the interactive praisonai onboard wizard is available as a typed Python API โ€” call it from a management script or notebook instead of shelling out to the CLI.

python
from praisonai_bot import (
    repair_gateway_config,
    provision_gateway_config,
    GatewayRepairResult,
)
FunctionOne-liner
repair_gateway_config(config_path, *, fix=False, dry_run=False)Detect โ†’ repair โ†’ re-validate a config; returns a GatewayRepairResult.
provision_gateway_config(*, platform, token, agents=None, config_path=None)Write bot.yaml + persist the channel token to ~/.praisonai/.env; returns the config Path.

Repair from a management script

python
from praisonai_bot import repair_gateway_config

result = repair_gateway_config("~/.praisonai/bot.yaml", fix=True)

print("token minted:", result.auth_token_minted)
print("version migrated:", result.config_version_migrated)
for line in result.changes:
    print("changed:", line)

# JSON-safe copy for structured logging
import json
print(json.dumps(result.to_dict()))

Provision on first run

python
from praisonai_bot import provision_gateway_config

path = provision_gateway_config(
    platform="telegram",
    token="${TELEGRAM_BOT_TOKEN}",   # persisted to ~/.praisonai/.env
    agents=["assistant"],
)
print(path)   # ~/.praisonai/bot.yaml

provision_gateway_config is fail-closed: an empty token or unsupported platform raises ValueError and writes nothing.


Kanban Dispatcher

Kanban dispatcher (v4.6.x): Worker file descriptors are now released as soon as the subprocess starts (fixes a slow leak under sustained dispatch). release_claim failures are logged with full stack traces instead of being silently swallowed โ€” orphaned claimed tasks now leave a clear log trail for triage. KeyboardInterrupt / SystemExit / asyncio cancellation propagate cleanly through dispatcher shutdown.

Kanban dispatcher (off-loop git): Every git command the dispatcher runs โ€” worktree add, merge --no-ff, commit, status, worktree remove โ€” now runs in a worker thread, off the gateway event loop. A slow git step no longer freezes chats, WebSocket pings or health probes.

  • Serialised per gateway process. A single in-process lock, shared by every dispatcher, lets only one blocking git section run at a time, so two dispatchers in the same process never run git merge concurrently. Running dispatchers in separate processes against one repo? Serialise at the repo level yourself (file lock, dedicated worker host).
  • Shutdown stops the whole worker group. On POSIX, workers start in their own session. Shutdown sends SIGTERM to each worker’s process group, waits up to 5 s for all of them together, then SIGKILLs the group โ€” so shells, dev servers and test runners a worker spawned don’t outlive the gateway. Windows is unchanged.
  • Cancel during worktree prep is clean. If the dispatcher stops while git worktree add is running, it waits for git to finish, removes the unused worktree and kanban/<task_id> branch, closes the run as failed and releases the claim so a retry starts fresh.
  • Every git command the dispatcher runs is now bounded by PRAISONAI_KANBAN_GIT_TIMEOUT (default 300 s). A hung git operation returns returncode=124 with a diagnosis in stderr instead of stalling the event loop. See Gateway Subprocess Guard.

Common Patterns

Single Gateway Deployment

python
from praisonaiagents import Agent

# Simple unified gateway
agent = Agent(
    name="Gateway Agent",
    gateway=True,  # Enables unified gateway
)

# CLI deployment
# praisonai serve unified --port 8765

Multi-Agent Gateway

python
from praisonaiagents import Agent, Task, PraisonAIAgents

agents = [
    Agent(name="Researcher", gateway=True),
    Agent(name="Writer", gateway=True),
]

# Multi-agent coordination through gateway
tasks = [Task(description="Research topic", agent=agents[0])]
crew = PraisonAIAgents(agents=agents, tasks=tasks)

Development Gateway

python
from praisonaiagents import Agent

agent = Agent(
    name="Dev Agent",
    gateway=True,
    debug=True  # Development features
)

# CLI: praisonai serve unified --reload

Best Practices

Start with Unified Gateway

Use praisonai serve unified for simplicity. Agents connect automatically without configuration.

Development vs Production

Enable --reload for development. Use separate services for production scaling.

Service Discovery

All servers expose /__praisonai__/discovery for endpoint discovery. Agents can auto-discover capabilities.

Port Planning

Reserve port 8765 for unified gateway. Use default ports for service-specific deployments.


Exit Codes & Supervisor Integration

praisonai serve gateway uses sysexits-based exit codes so process supervisors can distinguish a transient failure (restart) from a fatal misconfiguration (stop and fix).

Diagram

Exit Code Mapping

clean stop

transient error

bad config

Gateway\nExit

โœ… 0 โ€” OK\nrestart not needed

๐Ÿ”„ 75 โ€” EX_TEMPFAIL\nrestart

๐Ÿ›‘ 78 โ€” EX_CONFIG\ndo NOT restart

CodeConstantMeaningSupervisor action
0GATEWAY_OK_EXIT_CODEClean shutdown / successNo restart needed
75GATEWAY_RESTART_EXIT_CODE (EX_TEMPFAIL)Transient failure โ€” network blip, recoverable upstream errorRestart
78GATEWAY_FATAL_CONFIG_EXIT_CODE (EX_CONFIG)Fatal misconfiguration โ€” fix before restartingDo not restart

What Triggers Fatal (78)

CauseExit code
Missing or malformed gateway.yaml (schema invalid / ValueError)78
Missing or malformed --agents file78
Missing required dependencies78
auth_token is a known-weak/placeholder value on an external bind (see Weak Secret Guard)78
Any FatalConfigError raised explicitly78
Network blip at startup75
Recoverable platform error75

systemd Unit

ini
[Unit]
Description=PraisonAI Gateway
After=network.target

[Service]
ExecStart=/usr/local/bin/praisonai serve gateway
Restart=on-failure
RestartSec=5

# Stop the crash-loop on fatal config โ€” fix the config and start manually
RestartPreventExitStatus=78

[Install]
WantedBy=multi-user.target

Kubernetes Restart Policy

yaml
apiVersion: v1
kind: Pod
spec:
  containers:
    - name: praisonai-gateway
      image: praisonai:latest
      command: ["praisonai", "serve", "gateway"]
  restartPolicy: OnFailure

For Kubernetes, use a wrapper script that maps exit code 78 to success (exit 0) to prevent the pod from restarting on fatal config:

bash
#!/bin/sh
praisonai serve gateway "$@"
code=$?
if [ $code -eq 78 ]; then
  echo "Fatal config error โ€” not restarting (code 78)" >&2
  exit 0
fi
exit $code

In-Place Code Updates and /model

When a running gateway’s code changes on disk (via git pull, pip install -U, or an auto-update on a durable volume), the first-use lazy import triggered by a /model switch can hit a code mismatch and crash with a cryptic ImportError. The code-skew guard refuses with a clear message instead.

How It Works

At startup, the gateway captures a fingerprint of the installed code (git SHA + newest .py mtime). Before each /model switch it compares this to the current on-disk fingerprint:

Diagram

/model switch

yes

no

๐Ÿš€ Gateway Boot

๐Ÿ“ธ Capture boot\nfingerprint

๐Ÿƒ Running

Fingerprints\nmatch?

โœ… Switch model

โš ๏ธ Refuse with\nrestart message

User-Facing Message

When skew is detected, the user sees:

โš ๏ธ Gateway code changed on disk since it started (<boot-short> โ†’ <disk-short>). Restart the gateway to apply updates before switching models.

Where <boot-short> and <disk-short> are 7-character shortened fingerprints (e.g. git SHAs or mtime: suffixes).

Fail-Open

The guard is best-effort and fail-open: if the fingerprint cannot be determined (no git, unreadable directory), the guard returns None and the /model switch proceeds normally. Normal operation is never blocked by the guard itself.

Opt-Out

Disable the guard for a session manager:

python
session_manager.code_skew_guard = False

Threads / Topics

Open a new thread under a chat so a delegated subtask has its own scoped conversation instead of flooding the shared channel.

Diagram

Thread Creation

Yes

No

๐Ÿ‘ฅ Parent Chat

๐Ÿงต create_thread

Supports threads?

๐Ÿ†• New Thread

๐Ÿ“ข Parent Fallback

โœ‰๏ธ send subtask

The running gateway registers an OutboundMessengerProtocol into the per-turn context. Resolve it with get_outbound_messenger(), open a thread, then route the subtask into it โ€” falling back to the parent target when the platform can’t thread.

python
from praisonaiagents.session.context import get_outbound_messenger

async def handoff():
    messenger = get_outbound_messenger()
    res = await messenger.create_thread("telegram:12345", name="research")
    target = f"telegram:12345:{res.thread_id}" if res.ok else "telegram:12345"
    await messenger.send(target, "starting research...")

How It Works

Diagram
AdapterMessengerAgentUserAdapterMessengerAgentUserdelegate subtaskcreate_thread(target, name)open native thread/topicthread_idThreadResult(status=”ok”, thread_id=โ€ฆ)send(“<target>:<thread_id>”, subtask)

ThreadResult

create_thread(target, name) -> ThreadResult never raises โ€” a channel that cannot thread returns a typed unsupported outcome and the caller falls back to the parent target. Exported from praisonaiagents.gateway as ThreadResult and ThreadStatus.

FieldTypeDescription
statusLiteral["ok", "unsupported", "failed", "no_route"]Outcome; drives fallback logic
targetstrResolved parent target the thread was opened under
thread_idstrNew thread/topic id (populated only when status == "ok")
detailOptional[str]Model-readable explanation
.okbool (property)status == "ok"
.as_dict()dictSerializable form for tool return values

ThreadStatus is a closed set of four values:

  • ok โ€” a new thread/topic was opened; thread_id carries its id.
  • unsupported โ€” the channel has no supports_threads capability.
  • failed โ€” the transport rejected the attempt (topics mode off, or missing permission).
  • no_route โ€” the target could not be resolved to a reachable channel.

Fallback Pattern

Compose the routed target from the result โ€” never wrap the call in try/except, it always resolves to a typed outcome:

python
res = await messenger.create_thread(parent, name="subtask")
target = f"{parent}:{res.thread_id}" if res.ok else parent
await messenger.send(target, subtask_output)

Capabilities: supports_threads

Threading is gated on the channel’s PlatformCapabilities.supports_threads, which defaults to False โ€” so unaware adapters degrade to unsupported, safe by default. Adapters that can open native threads must set supports_threads=True.


Related