Gateway Readiness

Validate gateway config, credentials, shell wiring, and optional inbound turns before going live

Use this checklist before starting a gateway or relying on a Slack/Telegram bot in production.

Turn pre-flight

Your bot’s model credential is now checked at startup — not on the first user message. praisonai gateway start runs one real agent turn (prompt "ping") before serving, so a missing, invalid, or out-of-quota model key fails at start with exit 1 instead of surfacing on the first inbound message.

yaml
gateway:
  preflight:
    verify_turn: true            # default on
    verify_turn_prompt: "ping"
bash
praisonai gateway start                     # runs the real-turn check
praisonai gateway start --no-verify-turn    # skip for offline envs

This differs from the channel/tool probes below: those prove wiring (tokens resolve, tools load) but never exercise a model round-trip. The turn pre-flight sends a real prompt to the agent and waits for a reply.

Diagram
Model providerAgentverify_turn_preflightgateway startOperatorModel providerAgentverify_turn_preflightgateway startOperatoralt[Credential valid][Missing / invalid key or timeout]praisonai gateway startprompt=”ping”chat(“ping”)API callreplytext(ok, “pong”)Turn pre-flight OK — starting401 / 429 / timeouterror(False, detail)Fail — set model key or –no-verify-turn

The turn pre-flight is governed by its own toggle, independent of --preflight / --no-preflight. Running --no-preflight --verify-turn still exercises the model round-trip. It runs against the first configured channel by default.

YAML keys

yaml
gateway:
  preflight:
    verify_turn: true              # default true — run one real agent turn at start
    verify_turn_prompt: "ping"     # default "ping" — cheap, deterministic
    verify_turn_timeout: 60        # default 60.0 seconds — bounds the round-trip
KeyTypeDefaultPurpose
verify_turnbooltrueWhether gateway start runs the real-turn pre-flight.
verify_turn_promptstr"ping"Prompt sent to the agent for the check.
verify_turn_timeoutfloat60.0Bounds the agent round-trip (seconds) so a stalled provider fails fast.

Five-tier diagnostics

Diagram

Tier 5 conversation ops

gateway sessions list/show

bot dlq list –path …

Tier 4 inbound proof

–check-inbound logs

–check-duplicates

Tier 3 runtime

/health /ready /live

Tier 2 credential probe

auth.test per channel

Tier 1 offline

doctor / config

shell wiring

Three-tier checklist

  1. Static config (offline)
    bash
    praisonai doctor
    praisonai doctor bots --file /path/to/bot.yaml

    Validates YAML, env vars, security settings, and shell wiring (gateway_shell_readiness) without network or LLM calls.

  2. Live credential probe
    bash
    praisonai doctor bots --file /path/to/bot.yaml --deep
    # or
    praisonai gateway doctor --config /path/to/bot.yaml

    Probes each channel (auth.test, getMe, …) and surfaces bot identity (e.g. @test).

    gateway doctor also confirms route/binding targets resolve to declared agents — a typo in routes[], routing[], or bindings.agent (including the default slot) fails here with a closest-agent hint. See Fail-Fast Validation.

  3. Full readiness (recommended)
    bash
    praisonai gateway test --config /path/to/bot.yaml

    Combines credential probes + offline shell wiring. Add optional flags:

    bash
    # Shell bot smoke (requires LLM API key)
    praisonai gateway test --config /path/to/bot.yaml --channel slack \
      --turn "run uname -a with execute_command and reply with raw output only"
    
    # Verify running gateway runtime endpoints
    praisonai gateway test --config /path/to/bot.yaml --check-runtime
    
    # Scan for duplicate gateways / shared tokens
    praisonai gateway test --config /path/to/bot.yaml --check-duplicates
    
    # After messaging Slack — prove inbound delivery
    praisonai gateway test --config /path/to/bot.yaml --check-inbound --since 5m
  4. Extended status and sessions
    bash
    praisonai gateway status --deep --config /path/to/bot.yaml
    praisonai gateway status --probe --config /path/to/bot.yaml
    praisonai gateway sessions list --platform slack
    praisonai gateway sessions show U08R1HK9PJS --tail 20
  5. Start and confirm live traffic
    bash
    praisonai gateway start --config /path/to/bot.yaml --preflight
    praisonai gateway status

    After @mentioning the bot in Slack, confirm inbound events in logs:

    bash
    grep "@mention received" ~/.praisonai/logs/bot-stderr.log

    If the turn test passes but Slack is silent, check for duplicate bots, stale sessions, or messages not reaching this gateway process.

Command matrix

GoalCommand
First install, no networkpraisonai doctor + praisonai doctor bots
Config + shell wiring offlinepraisonai doctor bots --file PATH
Live tokenspraisonai gateway doctor --config PATH
Route/binding targets resolvepraisonai gateway doctor --config PATH
Onboarding one-shotpraisonai gateway test --config PATH
Runtime endpointspraisonai gateway test --config PATH --check-runtime
Inbound delivery proofpraisonai gateway test --config PATH --check-inbound --since 5m
Duplicate gateway scanpraisonai gateway test --config PATH --check-duplicates
Session historypraisonai gateway sessions list / show ID
Deep status + DLQ hintpraisonai gateway status --deep --config PATH
Pre-Slack shell smokepraisonai gateway test --config PATH --channel slack --turn "..."
Automated start gatepraisonai gateway start --preflight
Automated start gate (turn)praisonai gateway start (default on)
Skip turn gatepraisonai gateway start --no-verify-turn
Tool resolution start gatepraisonai gateway start --strict-tools
After startpraisonai gateway status + log check

JSON automation

Both commands support `--json` with a single top-level document:

bash
praisonai gateway test --config bot.yaml --json
praisonai gateway doctor --config bot.yaml --json --channel slack --turn "Say OK"

Keys: probes, secrets (optional), shell, runtime (with --check-runtime), running (with --check-running), inbound (with --check-inbound), duplicates (with --check-duplicates), turn (with --turn).

Sub-object schema

Each top-level key maps to a per-channel or per-check sub-object:

json
{
  "probes":     { "<channel>": { "ok": bool, "platform": str, "bot_username": str?, "error": str? } },
  "secrets":    { "<channel>": { "<field>": "available" | "configured-but-unavailable" | "configured" | "missing" } },
  "shell":      { "ok": bool, "message": str, "issues": [str] },
  "runtime":    { "ok": bool, "message": str },
  "running":    { "ok": bool, "message": str },
  "inbound":    { "ok": bool, "message": str, "events": [str] },
  "duplicates": { "ok": bool, "message": str, "duplicates": [str] },
  "turn":       { "channel": str, "ok": bool, "response": str },
  "gateway_auth_token": "weak"?
}
KeyPresent when
probesAlways.
secretsAny channel uses a secret reference or credential field.
shellgateway test only (not gateway doctor).
runtime--check-runtime is passed.
running--check-running is passed.
inbound--check-inbound is passed.
duplicates--check-duplicates is passed.
turn--turn is passed.
gateway_auth_tokenThe gateway’s own auth_token is weak.

/health channels schema

GET /health returns a rich per-channel object under channels[<name>], plus a top-level last_inbound_at.

json
{
  "status": "healthy",
  "uptime": 842.1,
  "channels": {
    "slack": {
      "platform": "slack",
      "running": true,
      "last_activity": 1769500000.0,
      "ok": true,
      "reason": "healthy",
      "probe": { "ok": true },
      "supervision": { "state": "running", "total_recoveries": 0 }
    }
  },
  "last_inbound_at": 1769500000.0
}
FieldTypeMeaning
platformstringChannel platform (slack, telegram, …).
runningboolWhether the channel bot is currently running.
last_activityfloat\nullEpoch seconds of the last inbound activity (falls back to start time).
okboolTrue only when reason == healthy.
reasonstringHealthReason enum value (see below).
probeobject?{ "ok": bool } — present only when a cached probe result exists.
supervisionobject?Present when the channel is supervised: state, last_error, next_retry_at, total_recoveries, manual_pause.
last_inbound_atfloatTop-level. Epoch seconds of the most recent inbound event across all channels — consumed by both CLI and Prometheus tooling.

The reason field comes from evaluate_channel_health(...) and takes one of these values:

reasonWhen
healthyRunning, past startup grace, recent inbound.
not-runningChannel bot is not running.
startup-graceWithin the startup_grace window after boot.
stale-socketIdle with no inbound past stale_after.
busyAn agent run is in flight with recent progress.
stuckBusy but no progress past stuck_after (default 900s).
disconnectedCached probe reports ok=false.
errorThe channel reported an error.

Health liveness config

The health: block under gateway: tunes when a channel flips from healthy to stale-socket.

yaml
gateway:
  host: "0.0.0.0"
  port: 8765
  health:
    startup_grace: 60     # seconds; suppress stale alarms during boot
    stale_after: 120      # seconds without inbound before a channel is stale
    stuck_after: 900      # seconds a busy channel can make no progress
    interval: 300         # supervision poll interval
    max_restarts_per_hour: 10       # per channel
    fleet_restarts_per_hour: 40     # fleet-wide crash-loop breaker (int >= 1)
    failing_channel_fraction: 0.5   # trip when this fraction is failing (0.0 < x <= 1.0)
    breaker_cooldown_s: 120         # hold restarts this long once tripped (float >= 0)
KeyDefaultPurpose
startup_grace60Grace window that suppresses stale-socket alarms right after boot.
stale_after120Age past which an idle channel with no inbound is marked degraded.
stuck_after900Age past which a busy channel making no progress is stuck.
interval300Supervision poll interval.
max_restarts_per_hour10Restart rate limit per channel.
fleet_restarts_per_hour40Aggregate restart-rate window for the fleet-level breaker.
failing_channel_fraction0.5Trip the fleet breaker when this fraction of channels is failing/parked.
breaker_cooldown_s120Hold channel restarts this long once the fleet breaker trips, then re-arm.

Runtime tier (--check-runtime)

--check-runtime (or check_runtime(config)) probes /info, /health, /ready, and /live. /ready requires ready=true and /live requires alive=true in the body; the bearer token is only attached to /info over loopback or HTTPS (never over remote plaintext HTTP).

bash
praisonai gateway test --config bot.yaml --check-runtime --json

Credential probe warnings

A channel’s probes.<channel>.details.warnings[] array surfaces non-fatal setup issues. For Slack, a missing app token appends:

SLACK_APP_TOKEN missing — Socket Mode will not start

This warning fires even when bot.probe() succeeds with SLACK_BOT_TOKEN alone — because inbound Socket Mode still needs SLACK_APP_TOKEN (xapp-…). See Inbound Message Gate → Slack @mentions.


Inbound tier (--check-inbound)

--check-inbound proves live delivery two ways: it scans the gateway log for @mention received: markers and scrapes the messages_inbound_total metric to compute a delta.

How inbound is detected

The CLI parses ~/.praisonai/logs/bot-stderr.log (default_log_path()) for lines containing the literal marker @mention received: within the --since window. parse_inbound_log extracts count, ISO timestamp, and mention text.

Metrics baseline state file

--check-inbound persists the last scraped messages_inbound_total per host/port to:

~/.praisonai/state/inbound_metrics_baseline.json
  • First run seeds the baseline and may return metrics_inbound_delta=null — but still passes if messages_inbound_total > 0.
  • Later runs return the delta within the --since window.
  • Reset by deleting the file.

The inbound result exposes metrics_inbound_total, metrics_inbound_delta, mentions_in_window, last_mention_at, and one of three hint strings:

HintMeaning
No @mention received in the log window…No marker found within --since.
…Message may have hit a different Slack app…No delivery — likely a competing Slack app.
Inbound metrics baseline established from existing traffic…First run with a pre-existing non-zero counter; re-run to confirm.

The --since window accepts Ns/Nm/Nh/Nd or a bare integer (seconds), parsed by parse_since_window.


Duplicates tier (Hermes conflict detection)

--check-duplicates (or check_duplicates(config)) scans for competing gateways and shared tokens. It inspects:

  • macOS LaunchAgents by label: ~/Library/LaunchAgents/ai.praison.bot.plist and ai.hermes.gateway.plist
  • both env files: ~/.praisonai/.env and ~/.hermes/.env
  • Hermes runtime state: ~/.hermes/gateway_state.json
  • the gateway PID lock (GatewayPIDLock)
  • the log tail for the Telegram Conflict: terminated by other getUpdates request string

Since PraisonAI ≥ #4197, the lock also carries a start-time fingerprint so a recycled PID is not mistaken for the original gateway — the “same PID, different process” case is now caught by the lock itself, not just by this scan.

Token values (*_TOKEN) are fingerprinted with SHA-256 (first 12 hex chars) and flagged when the same fingerprint appears across processes.

bash
praisonai gateway test --config bot.yaml --check-duplicates --json

Warnings it can emit:

  • Hermes Slack is connected — events may split if SLACK_APP_TOKEN is shared.
  • Hermes Telegram is connected — stopping ai.hermes.gateway affects Telegram.
  • Telegram getUpdates Conflict detected — another bot may be polling.

Extended status (status --deep / --config)

praisonai gateway status accepts --config, -c PATH. Combining --deep with --config unlocks extra diagnostics.

FlagMeaning
--config, -c PATHGateway config path (unlocks DLQ hints + log tail under --deep).
--deepPer-channel health rows + version-skew warning.
--probeLive credential probe per channel.

What --deep adds:

  • Per-channel rows: running / state / reason / last_activity / probe_ok (from /health).
  • A version-skew warning when the running gateway’s /info.version differs from the installed praisonai-bot package version.

What --deep --config adds on top:

  • The last 20 lines of the gateway log (get_logs(lines=20) from ~/.praisonai/logs/bot-stderr.log).
  • A DLQ (<name>): praisonai bot dlq list --path <sqlite> hint per channel (path from resolve_platform_dlq_path()).

Sessions (stored history)

gateway sessions list supports filtering and JSON output.

FlagMeaning
--platform NAMEFilter by platform (e.g. slack).
--active NOnly sessions updated within the last N seconds.
--jsonEmit JSON.
bash
praisonai gateway sessions list --platform slack --active 300 --json
praisonai gateway sessions show U08R1HK9PJS --tail 20

sessions show <ref> resolves the reference in order: exact session_id → exact filename → substring in filename (first alphabetical match wins). It prints Session: / Agent: header lines, truncates each message body to 200 chars, and always ends with a footer:

Sessions reflect stored history; use `praisonai gateway test --check-inbound` for live delivery.

Programmatic API

Import praisonai_bot.gateway.preflight to build custom health-check endpoints, CI gates, and monitoring integrations without shelling out to the CLI.

python
from praisonai_bot.gateway.preflight import (
    load_channels_mapping,          # (config_path) -> dict
    resolve_gateway_endpoint,       # (config_path) -> (host, port)
    probe_channels,                 # (channels, timeout=15.0) -> dict[str, ProbeResult]
    probe_channels_from_config,     # (config_path, channel_filter=None, timeout=15.0)
    probe_results_to_dict,          # (results) -> JSON-safe dict
    resolve_env_token,              # (value) — handles ${VAR} and {source, id} secret refs
    apply_probe_ca_bundle,          # honours PRAISONAI_SSL_CA_BUNDLE precedence
    run_shell_readiness_check,      # (config_path) -> ShellReadinessResult(ok, message, issues)
    run_turn_test,                  # (config_path, channel_name, prompt, run_timeout=None) -> (ok, message)
    check_gateway_running,          # (config_path, timeout=5.0) -> (ok, message)
    resolve_verify_turn,            # (config_path) -> (enabled: bool, prompt: str)
    verify_turn_preflight,          # (config_path, channel_name=None, prompt="ping", run_timeout=None) -> (ok, detail)
    VERIFY_TURN_TIMEOUT_DEFAULT,    # 60.0
)

resolve_verify_turn and verify_turn_preflight are also re-exported from praisonai_bot.cli.commands.gateway. Gate CI on a successful real agent turn:

python
import asyncio
from praisonai_bot.gateway.preflight import verify_turn_preflight

ok, detail = asyncio.run(verify_turn_preflight("bot.yaml", prompt="ping"))
if not ok:
    raise SystemExit(f"Agent turn pre-flight failed: {detail}")

Tiered check helpers

These helpers back the --check-runtime|--check-inbound|--check-duplicates flags and the session/DLQ commands. All are re-exported from praisonai_bot.cli.commands.gateway for tests and third-party tooling.

python
from praisonai_bot.gateway.preflight import (
    check_runtime,            # (config_path, timeout=5.0) -> RuntimeCheckResult
    check_inbound,            # (config_path, since="10m", log_path=None) -> InboundCheckResult
    check_duplicates,         # (config_path) -> DuplicateCheckResult
    list_gateway_sessions,    # (platform=None, active_seconds=None) -> list[dict]
    show_gateway_session,     # (session_ref, tail=20) -> dict
    resolve_platform_dlq_path,# (platform) -> str
    parse_since_window,       # (since) -> float seconds; accepts Ns/Nm/Nh/Nd or int
    parse_inbound_log,        # (log_path, since_seconds, marker="@mention received:")
)
Result classto_dict() keys
RuntimeProbeResultok, path, status_code, error, body
RuntimeCheckResultok, host, port, info, health, ready, live
InboundCheckResultok, proves, since_seconds, mentions_in_window, last_mention_at, last_mention_text, no_inbound_in_window, metrics_inbound_total, metrics_inbound_delta, hint, log_path
DuplicateServicelabel, installed, running, pid, plist_path, token_fingerprints
DuplicateCheckResultok, services, shared_tokens, hermes_platforms, pid_lock, warnings

Gate CI on runtime readiness:

python
from praisonai_bot.gateway.preflight import check_runtime

result = check_runtime("gateway.yaml").to_dict()
if not result["ok"]:
    raise SystemExit(f"Gateway runtime not ready: {result}")

Healthcheck endpoint

Expose channel readiness as a JSON endpoint — the same probe the CLI runs, embedded in your own service.

python
from fastapi import FastAPI
from praisonai_bot.gateway.preflight import (
    probe_channels_from_config,
    probe_results_to_dict,
)

app = FastAPI()

@app.get("/healthz/channels")
async def channel_health():
    results = await probe_channels_from_config("gateway.yaml")
    payload = probe_results_to_dict(results)
    ok = all(r.ok for r in results.values())
    return {"ok": ok, "probes": payload}

Offline shell + turn gate for CI

Fail a deploy when shell wiring is broken, before any network call.

python
from praisonai_bot.gateway.preflight import run_shell_readiness_check

result = run_shell_readiness_check("gateway.yaml")
if not result.ok:
    raise SystemExit("\n".join(result.issues))
print(result.message)