> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Gateway & Control Plane

> Unified gateway and control plane architecture for PraisonAI integrations

<Note>
  The heavy gateway implementation (`WebSocketGateway`) lives in the **`praisonai_bot.gateway`** package. `praisonaiagents.gateway` holds only protocols and lightweight utilities. `praisonai serve gateway` and `from praisonai.gateway import WebSocketGateway` still work exactly as documented here via re-export shims; for a standalone install see [praisonai-bot Migration](/docs/docs/guides/praisonai-bot-migration).
</Note>

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

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Gateway Architecture"
        Agent[🤖 Agent] --> Gateway[🌐 Gateway]
        Gateway --> Services[⚙️ Services]
        Services --> Output[📊 Results]
    end
    
    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef gateway fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef services fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef output fill:#8B0000,stroke:#7C90A0,color:#fff
    
    class Agent agent
    class Gateway gateway
    class Services services
    class Output output
```

## Quick Start

<Steps>
  <Step title="Simple Agent Gateway">
    Deploy an agent through the unified gateway:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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")
    ```
  </Step>

  <Step title="Multi-Service Gateway">
    Configure agents with full gateway capabilities:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent, GatewayConfig

    agent = Agent(
        name="Gateway Agent",
        instructions="Multi-service coordination",
        gateway=GatewayConfig(
            unified=True,
            services=["agents", "mcp", "a2a", "a2u"]
        )
    )
    ```
  </Step>
</Steps>

***

## How It Works

Agents connect to services through gateway routing:

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Agent
    participant Gateway
    participant Services
    
    Agent->>Gateway: Request
    Gateway->>Services: Route
    Services-->>Gateway: Response
    Gateway-->>Agent: Result
```

| Component    | Purpose               | Agent Access     |
| ------------ | --------------------- | ---------------- |
| **Gateway**  | Single entry point    | `gateway=True`   |
| **Unified**  | All services combined | Default mode     |
| **Services** | Independent scaling   | Service-specific |

<Tip>
  For bot gateways, use the one-line **`reliability="production"`** preset on `BotOS`, gateway YAML, or `praisonai gateway start --reliability production` instead of tuning drain and admission separately. See [Gateway Reliability](/docs/docs/features/gateway-reliability).
</Tip>

## 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)](/docs/docs/features/gateway-hot-reload#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](/docs/docs/features/gateway-credential-rotation). See [Gateway Config Reload](/docs/docs/features/gateway-config-reload).

<Note>
  **Event-loop watchdog (separate from filesystem watching).** The `watchdog` above is the Python package that watches `gateway.yaml` on disk. A different, opt-in **event-loop** watchdog catches a wedged asyncio loop and restarts the process — enable it with `gateway.watchdog.enabled: true` (or `--watchdog`). See [Event-Loop Watchdog](/docs/docs/features/gateway-loop-watchdog#using-it-from-the-gateway).
</Note>

For proactive/scheduled outbound delivery routing — including friendly alias targets like `"family"` or `"ops"` — see [Friendly Aliases for Scheduled Delivery](/docs/docs/features/proactive-delivery#friendly-aliases-for-scheduled-delivery).

<Note>
  Scheduled jobs fire at most once across gateway workers by default. When multiple workers each run a `ScheduleRunner` polling the shared `config.yaml`, the default `ConfigYamlScheduleStore` claims each due job atomically — no duplicate fires without swapping the store. As of PraisonAI #3266 the gateway scheduler tick, the wrapper's schedules bridge, and agent-tool writes all resolve through `praisonaiagents.scheduler.get_default_store()` so a job scheduled by an agent inside a bot conversation actually fires on the same tick loop. See [Multi-process safety](/docs/docs/features/async-scheduler#multi-process-safety-at-most-once).
</Note>

<Note>
  The gateway's scheduled delivery now routes **threaded** sends through the shared resilient `DeliveryRouter` (LRU dedup, rate limiter, dead-target registry) via the same `platform:channel:thread` token — the old `_ThreadBindingBot` workaround is gone. The gateway's dead-target registry is backed by the shared SQLite `dead_targets` table in `~/.praisonai/state/delivery_control.sqlite` (falling back to the node-local JSON sidecar only if the SQLite store can't be constructed). See [Scheduler Delivery → Thread semantics](/docs/docs/features/scheduler-delivery#thread-semantics-per-platform) and [Dead-Target Registry](/docs/docs/features/dead-target-registry).
</Note>

### Voice notes

Voice messages sent to your gateway bot are transcribed automatically on
Telegram, Slack, and WhatsApp — no code changes required. Configure the
`stt:` block under each channel to force a language or opt out. See
[Voice Notes (Speech-to-Text)](/docs/docs/features/gateway-stt).

***

## Authentication

Authentication posture changes automatically based on the bind interface.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Check{🔍 Interface}
    Check -->|127.0.0.1| Local[🟢 Permissive<br/>No token required]
    Check -->|0.0.0.0| External[🔒 Strict<br/>Token required]

    classDef decision fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef permissive fill:#10B981,stroke:#7C90A0,color:#fff
    classDef strict fill:#8B0000,stroke:#7C90A0,color:#fff

    class Check decision
    class Local permissive
    class External strict
```

| Interface                                      | Mode       | Auth Required |
| ---------------------------------------------- | ---------- | ------------- |
| **Loopback** (`127.0.0.1`, `localhost`, `::1`) | Permissive | No            |
| **External** (`0.0.0.0`, LAN IPs, public IPs)  | Strict     | Yes           |

<Card title="Bind-Aware Authentication" icon="shield" href="/docs/docs/features/gateway-bind-aware-auth">
  Complete authentication security guide
</Card>

### 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](/docs/docs/features/gateway-credential-rotation) for the full behaviour matrix.

The auto-generated per-install HMAC secret for pairing codes (`.gateway_secret`) is stored and permission-remediated separately — see [Bot Pairing → Secret Management](/docs/docs/features/bot-pairing#secret-management).

### 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 icon="shield-exclamation" href="/docs/docs/features/gateway-bind-aware-auth#weak-placeholder-secret-guard">
  Full denylist, bind-aware behaviour matrix, and how to fix a weak token
</Card>

### 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`.

<Card title="Gateway Edge Protections" icon="shield-halved" href="/docs/docs/features/gateway-edge-protections">
  Per-IP connection budget, unauthorized-frame flood guard, and WebSocket close codes 4028 / 4029
</Card>

***

## 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.

<Card title="Gateway Handshake Protocol" icon="handshake" href="/docs/docs/features/gateway-handshake-protocol">
  Version negotiation, capabilities, and structured connection errors
</Card>

### 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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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()
```

<Card title="Gateway Client — Terminal vs Transient Errors" icon="plug" href="/docs/docs/features/gateway-client#terminal-vs-transient-connect-errors">
  Connect-error classification, `on_reconnect_paused`, and the recoverable-code table
</Card>

***

## Configuration Options

<Card title="Gateway Configuration" icon="code" href="/docs/docs/sdk/reference/typescript/classes/GatewayConfig">
  Python gateway configuration options
</Card>

| Service     | Default Port | Protocol   | Agent Access             |
| ----------- | ------------ | ---------- | ------------------------ |
| **unified** | 8765         | HTTP + WS  | Default gateway          |
| **agents**  | 8000         | HTTP/REST  | Direct API calls         |
| **mcp**     | 8080         | HTTP/SSE   | Tool protocols           |
| **a2a**     | 8001         | JSON-RPC   | Agent communication      |
| **a2u**     | 8002         | SSE        | Event streams            |
| **openai**  | 8765         | HTTP + SSE | OpenAI API compatibility |

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

| Option                           | Type   | Default | Description                                                                                                                                                                                                      |
| -------------------------------- | ------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `preauth_max_connections_per_ip` | `int`  | `32`    | Max concurrent unauthenticated WS connections per source IP. `0` disables. Loopback exempt.                                                                                                                      |
| `max_unauthorized_frames`        | `int`  | `10`    | Close the connection after N unauthorized frames. `0` disables.                                                                                                                                                  |
| `revoke_on_secret_rotation`      | `bool` | `true`  | Force-close live sessions when `auth_token` is rotated via config reload. `false` adopts the new secret for new connections only. See [Gateway Credential Rotation](/docs/docs/features/gateway-credential-rotation). |
| `api.openai`                     | `bool` | `false` | When on, mounts `/v1/chat/completions`, `/v1/responses`, `/v1/models` on the same port and auth. See [Gateway API Endpoints](/docs/docs/features/gateway-api-endpoints).                                              |
| `api.mcp`                        | `bool` | `false` | When on, mounts an MCP JSON-RPC endpoint at `/mcp` on the same port and auth. See [Gateway API Endpoints](/docs/docs/features/gateway-api-endpoints).                                                                 |

***

## 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.

<Note>
  Since PraisonAI [PR #3079](https://github.com/MervinPraison/PraisonAI/pull/3079), the `gateway:` and `hooks:` blocks are validated field-by-field at load time. Unknown keys (`drain_timout`), wrong types (`reload_drain_timeout: "quick"`), out-of-range values (`port: 99999`), and invalid `overflow_policy` values are rejected with a friendly, field-named error instead of running silently with the default.
</Note>

### `gateway:` block

Global gateway settings — reliability preset, drain timeout, health monitor:

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
gateway:
  reliability: production    # "production" | "default" | "off"
  drain_timeout: 15          # seconds to wait for in-flight turns on shutdown

channels:
  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}
```

Inbound voice notes on Telegram, Slack, and WhatsApp are transcribed automatically — see [Voice Notes](/docs/docs/features/gateway-voice-notes).

<Tip>
  Beyond `${ENV_VAR}`, `token` (and `app_token` / `verify_token`) accept a `{ source, id }` reference to load credentials from a mounted file or secret manager — never plaintext. See [Secret References](/docs/docs/features/gateway-secret-references).
</Tip>

<Tip>
  Discord and Slack channels are auto-discovered on every refresh — your bot can address a channel before anyone messages it. See [Channel Directory](/docs/docs/features/gateway-channel-directory).
</Tip>

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.

| Option                           | Type                   | Default       | Notes                                                                                                                                                                                                                            |
| -------------------------------- | ---------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `host`                           | `str`                  | `None`        | Public host name.                                                                                                                                                                                                                |
| `port`                           | `int`                  | `None`        | 1–65535. Rejected outside the range.                                                                                                                                                                                             |
| `bind_host`                      | `str`                  | `None`        | Bind interface.                                                                                                                                                                                                                  |
| `cors_origins`                   | `list[str]`            | `None`        |                                                                                                                                                                                                                                  |
| `allowed_origins`                | `list[str]`            | `None`        |                                                                                                                                                                                                                                  |
| `auth_token`                     | `str`                  | `None`        | See [Rotating the shared secret](#rotating-the-shared-secret) and [Weak Secret Guard](/docs/docs/features/gateway-bind-aware-auth#weak-placeholder-secret-guard). Well-known placeholder values refuse the start on an external bind. |
| `auth`                           | `dict`                 | `None`        |                                                                                                                                                                                                                                  |
| `auth_scopes`                    | `dict[str, list[str]]` | `None`        |                                                                                                                                                                                                                                  |
| `max_connections`                | `int`                  | `None`        | ≥ 0                                                                                                                                                                                                                              |
| `max_sessions_per_agent`         | `int`                  | `None`        | ≥ 0                                                                                                                                                                                                                              |
| `session_config`                 | `dict`                 | `None`        |                                                                                                                                                                                                                                  |
| `heartbeat_interval`             | `int`                  | `None`        | ≥ 0                                                                                                                                                                                                                              |
| `reconnect_timeout`              | `int`                  | `None`        | ≥ 0                                                                                                                                                                                                                              |
| `ssl_cert`                       | `str`                  | `None`        |                                                                                                                                                                                                                                  |
| `ssl_key`                        | `str`                  | `None`        |                                                                                                                                                                                                                                  |
| `max_buffered_bytes`             | `int`                  | `None`        | ≥ 0                                                                                                                                                                                                                              |
| `max_queued_frames`              | `int`                  | `None`        | ≥ 0                                                                                                                                                                                                                              |
| `max_concurrent_runs`            | `int`                  | `None`        | ≥ 0 — admission control.                                                                                                                                                                                                         |
| `queue_depth`                    | `int`                  | `None`        | ≥ 0 — admission control.                                                                                                                                                                                                         |
| `overflow_policy`                | `str`                  | `None`        | Exactly one of `"reject"`, `"queue"`, `"shed_oldest"`.                                                                                                                                                                           |
| `preauth_max_connections_per_ip` | `int`                  | `None`        | ≥ 0 — see [Pre-auth edge protections](#pre-auth-edge-protections).                                                                                                                                                               |
| `max_unauthorized_frames`        | `int`                  | `None`        | ≥ 0 — same.                                                                                                                                                                                                                      |
| `drain_timeout`                  | `float`                | `None`        | ≥ 0 seconds.                                                                                                                                                                                                                     |
| `reload_drain_timeout`           | `float`                | `None`        | ≥ 0 seconds — drain window applied on config reload.                                                                                                                                                                             |
| `reliability`                    | `str`                  | `None`        | See [Gateway Reliability](/docs/docs/features/gateway-reliability).                                                                                                                                                                   |
| `api`                            | `dict`                 | `None`        | See the [`api:` block](#api-block) below.                                                                                                                                                                                        |
| `liveness`                       | `dict`                 | `None`        | Liveness probe settings.                                                                                                                                                                                                         |
| `health`                         | `dict`                 | `None`        | Health monitor — see the [`health:` block](#health-block) below.                                                                                                                                                                 |
| `forensics`                      | `dict`                 | `None`        | Crash/shutdown forensics.                                                                                                                                                                                                        |
| `hooks`                          | `list`                 | `None`        | Same shape as top-level [`hooks:`](#hooks-block) — nest under `gateway:` for grouping.                                                                                                                                           |
| `notify_on_undelivered`          | `bool`                 | `false`       | Opt in to a last-resort user notice + `MESSAGE_UNDELIVERED` hook when a reply fails permanently. See [Undelivered Notice](/docs/docs/features/gateway-undelivered-notice).                                                            |
| `undelivered_template`           | `str`                  | built-in note | One-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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
gateway:
  api:
    openai: true    # mounts /v1/chat/completions, /v1/responses, /v1/models
    mcp: true       # mounts /mcp (JSON-RPC)
```

Both surfaces are opt-in and off by default. See [Gateway API Endpoints](/docs/docs/features/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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
gateway:
  health:
    enabled: true
    interval: 60
    stale_after: 90
    stuck_after: 600
    max_restarts_per_hour: 5
```

| Field                   | Type    | Default | Constraint |
| ----------------------- | ------- | ------- | ---------- |
| `enabled`               | `bool`  | `true`  |            |
| `interval`              | `float` | `300.0` | `> 0`      |
| `startup_grace`         | `float` | `60.0`  | `≥ 0`      |
| `stale_after`           | `float` | `120.0` | `> 0`      |
| `stuck_after`           | `float` | `900.0` | `> 0`      |
| `max_restarts_per_hour` | `int`   | `10`    | `≥ 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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
hooks:
  - path: gmail
    agent: assistant
    action: agent           # optional; "agent" or "wake"
```

| Field              | Type        | Default    | Required | Notes                                                                                                                                                  |
| ------------------ | ----------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `path`             | `str`       | —          | **yes**  | Path segment for the inbound trigger (e.g. `"gmail"`).                                                                                                 |
| `action`           | `str`       | `"agent"`  | no       | Only `"agent"` or `"wake"`.                                                                                                                            |
| `agent`            | `str`       | `None`     | no       | Target agent id.                                                                                                                                       |
| `auth`             | `str`       | `None`     | no       | Bearer token for the trigger endpoint.                                                                                                                 |
| `session_key`      | `str`       | `None`     | no       | Template for the session id.                                                                                                                           |
| `idempotency_key`  | `str`       | `None`     | no       | Template for the dedup key.                                                                                                                            |
| `deliver_to`       | `str`       | `None`     | no       | `channel:target` delivery spec for the reply.                                                                                                          |
| `message`          | `str`       | `None`     | no       | Template for the agent message.                                                                                                                        |
| `enabled`          | `bool`      | `true`     | no       |                                                                                                                                                        |
| `metadata`         | `dict`      | `{}`       | no       | Free-form extra settings.                                                                                                                              |
| `secret`           | `str`       | `None`     | no       | HMAC signing secret. Verifies the provider signature over the raw body before any agent runs; rejects (401) a missing/invalid signature — fail-closed. |
| `signature_header` | `str`       | `None`     | no       | Header carrying the signature. Auto-defaults to `X-Hub-Signature-256` when `secret` is set.                                                            |
| `signature_algo`   | `str`       | `"sha256"` | no       | HMAC digest name.                                                                                                                                      |
| `signature_prefix` | `str`       | `None`     | no       | Optional signature prefix, e.g. `sha256=`.                                                                                                             |
| `events`           | `list[str]` | `None`     | no       | Allow-list of event types. Unmatched deliveries are ack'd (200) with no LLM turn. A string is coerced to a list.                                       |
| `event_header`     | `str`       | `None`     | no       | Header 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_only`     | `bool`      | `false`    | no       | Deliver the rendered `message` straight through `deliver_to` with no LLM turn. Requires `deliver_to`.                                                  |

See [Gateway Inbound Hooks](/docs/docs/features/gateway-hooks) for signature-verification, event-filter, and deliver-only examples.

### `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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 key           | Gateway default | Why                                                                                       |
| ------------------ | --------------- | ----------------------------------------------------------------------------------------- |
| `reflection`       | `false`         | Chat channels need sub-second replies; self-reflection adds \~8x latency on short prompts |
| `tool_choice`      | `null` (auto)   | Let the LLM decide when to call tools                                                     |
| `allow_delegation` | `false`         | Prevents cross-agent routing unless explicitly opted in                                   |

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
agents:
  assistant:
    instructions: "You are a helpful AI assistant."
    model: gpt-4o-mini
    reflection: true   # opt-in: enables self-critique, ~8x slower
```

<Note>
  Changed in PraisonAI v4.6.26: gateway agents now default to `reflection: false`. Previous versions defaulted to `true`. See [PR #1485](https://github.com/MervinPraison/PraisonAI/pull/1485).
</Note>

***

## 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`.

<Card title="Gateway Scoped Approvals" icon="user-lock" href="/docs/docs/features/gateway-scoped-approvals">
  Durable, agent-scoped allow-always grants, resolver scoping options, and the `/api/approval/allow-list` endpoint
</Card>

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

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Approval Isolation"
        A[🤖 Agent A] --> B[📋 Manager A]
        C[🤖 Agent B] --> D[📋 Manager B]
        B --> E[✅ Isolated Approvals A]
        D --> F[✅ Isolated Approvals B]
    end
    
    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef manager fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef approvals fill:#10B981,stroke:#7C90A0,color:#fff
    
    class A,C agent
    class B,D manager
    class E,F approvals
```

### Default vs Per-Agent Managers

| Manager Type  | Use Case                 | Approval Sharing      |
| ------------- | ------------------------ | --------------------- |
| **Default**   | CLI, single-agent setups | Shared process-wide   |
| **Per-Agent** | Multi-tenant gateways    | Isolated per instance |

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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")
```

<Note>
  The `ttl` parameter controls how long approval requests wait before auto-expiring (default: 5 minutes).
  The `get_exec_approval_manager()` function is preserved as a backward-compatible alias for `get_default_exec_approval_manager()`.
</Note>

### 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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "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](/docs/docs/features/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.

| Symbol                                                                                              | Purpose                                               | Documentation                                                                             |
| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `ScaleToZeroPolicy`                                                                                 | Quiesce when idle; wake on next inbound               | [Scale to Zero](/docs/docs/features/gateway-scale-to-zero)                                     |
| `DrainTimeoutPolicy`                                                                                | Bounded wait for in-flight turns on shutdown          | [Session Continuity](/docs/docs/features/gateway-session-continuity)                           |
| `DrainMarkerPolicy` + `current_epoch()`                                                             | Port-less, restart-safe external drain signal         | [Drain Trigger](/docs/docs/features/gateway-drain-trigger)                                     |
| `classify_exit_reason` + `FatalConfigError`                                                         | Supervisor-friendly sysexits exit codes (0 / 75 / 78) | [Gateway Exit Codes](/docs/docs/features/gateway-exit-codes)                                   |
| `assert_gateway_secret_strong` + `is_weak_secret` + `WeakGatewaySecretError` + `KNOWN_WEAK_SECRETS` | Reject known-weak/placeholder shared secrets          | [Weak Secret Guard](/docs/docs/features/gateway-bind-aware-auth#weak-placeholder-secret-guard) |

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.gateway import (
    ScaleToZeroPolicy,
    DrainTimeoutPolicy,
    DrainMarkerPolicy,
    current_epoch,
    is_weak_secret,
    assert_gateway_secret_strong,
    WeakGatewaySecretError,
    KNOWN_WEAK_SECRETS,
)
```

***

## 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.

***

## Common Patterns

### Single Gateway Deployment

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

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

# CLI: praisonai serve unified --reload
```

***

## Best Practices

<AccordionGroup>
  <Accordion title="Start with Unified Gateway">
    Use `praisonai serve unified` for simplicity. Agents connect automatically without configuration.
  </Accordion>

  <Accordion title="Development vs Production">
    Enable `--reload` for development. Use separate services for production scaling.
  </Accordion>

  <Accordion title="Service Discovery">
    All servers expose `/__praisonai__/discovery` for endpoint discovery. Agents can auto-discover capabilities.
  </Accordion>

  <Accordion title="Port Planning">
    Reserve port 8765 for unified gateway. Use default ports for service-specific deployments.
  </Accordion>
</AccordionGroup>

***

## 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).

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Exit Code Mapping"
        Exit{Gateway\nExit}
        Exit -->|clean stop| Code0["✅ 0 — OK\nrestart not needed"]
        Exit -->|transient error| Code75["🔄 75 — EX_TEMPFAIL\nrestart"]
        Exit -->|bad config| Code78["🛑 78 — EX_CONFIG\ndo NOT restart"]
    end

    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff
    classDef restart fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef fatal fill:#8B0000,stroke:#7C90A0,color:#fff

    class Code0 ok
    class Code75 restart
    class Code78 fatal
```

| Code | Constant                                      | Meaning                                                      | Supervisor action  |
| ---- | --------------------------------------------- | ------------------------------------------------------------ | ------------------ |
| `0`  | `GATEWAY_OK_EXIT_CODE`                        | Clean shutdown / success                                     | No restart needed  |
| `75` | `GATEWAY_RESTART_EXIT_CODE` (EX\_TEMPFAIL)    | Transient failure — network blip, recoverable upstream error | Restart            |
| `78` | `GATEWAY_FATAL_CONFIG_EXIT_CODE` (EX\_CONFIG) | Fatal misconfiguration — fix before restarting               | **Do not restart** |

### What Triggers Fatal (78)

| Cause                                                                                                                                                              | Exit code |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- |
| Missing or malformed `gateway.yaml` (schema invalid / `ValueError`)                                                                                                | 78        |
| Missing or malformed `--agents` file                                                                                                                               | 78        |
| Missing required dependencies                                                                                                                                      | 78        |
| `auth_token` is a known-weak/placeholder value on an external bind (see [Weak Secret Guard](/docs/docs/features/gateway-bind-aware-auth#weak-placeholder-secret-guard)) | 78        |
| Any `FatalConfigError` raised explicitly                                                                                                                           | 78        |
| Network blip at startup                                                                                                                                            | 75        |
| Recoverable platform error                                                                                                                                         | 75        |

### systemd Unit

```ini theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
[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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
#!/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:

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Boot[🚀 Gateway Boot] --> Capture[📸 Capture boot\nfingerprint]
    Capture --> Running[🏃 Running]
    Running -->|/model switch| Compare{Fingerprints\nmatch?}
    Compare -->|yes| Switch[✅ Switch model]
    Compare -->|no| Warn[⚠️ Refuse with\nrestart message]

    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff
    classDef warn fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff

    class Boot,Running agent
    class Capture,Compare process
    class Switch ok
    class Warn warn
```

### 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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
session_manager.code_skew_guard = False
```

***

## Related

<CardGroup cols={2}>
  <Card title="Agents" icon="user" href="/docs/docs/concepts/agents">
    Core agent functionality
  </Card>

  <Card title="MCP Protocol" icon="plug" href="/docs/docs/concepts/mcp">
    Model Context Protocol integration
  </Card>

  <Card title="Gateway CLI" icon="terminal" href="/docs/docs/features/gateway-cli">
    CLI commands for managing the gateway
  </Card>

  <Card title="Gateway API Endpoints" icon="plug" href="/docs/docs/features/gateway-api-endpoints">
    OpenAI-compat and MCP HTTP endpoints on the live gateway
  </Card>

  <Card title="Relay Transport" icon="arrow-left-right" href="/docs/docs/features/relay-transport">
    Out-of-process platform connector relay
  </Card>

  <Card title="Bot Chat Commands" icon="terminal" href="/docs/docs/features/bot-commands">
    Built-in and custom slash commands for gateway bots
  </Card>

  <Card title="Slash Command Menu" icon="list" href="/docs/docs/features/bot-slash-command-menu">
    Native `/` autocomplete for Telegram and Discord bots
  </Card>

  <Card title="Voice Notes" icon="microphone" href="/docs/docs/features/gateway-voice-notes">
    Automatic transcription of inbound voice messages
  </Card>

  <Card title="Run Status Controller" icon="gauge-simple" href="/docs/docs/features/bot-run-status-controller">
    Transport-agnostic run-progress state machine with a stall watchdog
  </Card>
</CardGroup>
