Python framework for Squad server operations, inspired by SquadJS and SquadTS, with typed Python APIs and async-first runtime behavior.
- Async RCON client with reconnect and command correlation.
- Parser-backed event bus with typed events for plugin workflows.
- Broad built-in plugin coverage for moderation, seeding, Discord, and data logging.
- Control API with token auth, optional RBAC, signed webhook policy, and audit records.
- Observability endpoints:
/metrics,/api/health,/api/readiness,/api/traces. - One-process multi-server orchestration (
config/servers/*discovery). - RCON broker primitive (
RconBroker) for one upstream socket and many authenticated clients. - Plugin watchdog failure budgets for controlled degradation.
- Dev tooling for migration analysis, plugin scaffolding, and local replay simulation.
- Python 3.12+
- Squad dedicated server with RCON enabled
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"- Copy config examples:
cp config/server.toml.example config/server.toml
cp config/logging.toml.example config/logging.toml- Minimum
config/server.toml:
[rcon]
host = "127.0.0.1"
port = 21114
password = "your_rcon_password"- Run:
python -m squadpySquadPy runtime mode precedence:
- CLI flag:
python -m squadpy --mode <auto|single|multi> - Environment variable:
SQUADPY_RUNTIME_MODE - Config file:
config/runtime.toml - Default:
auto
For DB isolation policy in multi-server mode:
- Environment variable:
SQUADPY_REQUIRE_PER_SERVER_DATABASE - Config file:
config/runtime.toml - Default:
false
Examples:
python -m squadpy --mode single
python -m squadpy --mode multi
SQUADPY_RUNTIME_MODE=single python -m squadpy
SQUADPY_REQUIRE_PER_SERVER_DATABASE=true python -m squadpy --mode multiconfig/runtime.toml example:
mode = "auto" # auto | single | multi
require_per_server_database = falseBehavior summary:
auto: run multi-server only when validconfig/servers/*/server.tomldefinitions are present; otherwise run single-server.single: always runconfig/server.tomlpath and ignore multi-server definitions.multi: require at least oneconfig/servers/*/server.tomldefinition; exits with clear error if none found.require_per_server_database=true: in multi mode, each discovered server must haveconfig/servers/<name>/database.toml; shared fallback is disallowed.
In auto mode, multi-server activates when at least one config/servers/<name>/server.toml exists.
Recommended layout:
config/
logging.toml # optional shared fallback
database.toml # optional shared fallback
plugins/ # canonical plugin examples/defaults
*.toml.example
servers/
_template/
server.toml.example
database.toml.example
ph-server-1/
server.toml # required
database.toml # recommended unique DB per server
logging.toml # optional per-server override
plugins/ # optional per-server overrides only
admin_request.toml
ph-server-2/
server.toml
database.toml
Bootstrap example:
mkdir -p config/servers/ph-server-1 config/servers/ph-server-2
cp config/servers/_template/server.toml.example config/servers/ph-server-1/server.toml
cp config/servers/_template/server.toml.example config/servers/ph-server-2/server.toml
cp config/servers/_template/database.toml.example config/servers/ph-server-1/database.toml
cp config/servers/_template/database.toml.example config/servers/ph-server-2/database.tomlCanonical server template (config/servers/_template/server.toml.example):
[rcon]
host = "10.0.1.21"
port = 21114
password = "replace_with_rcon_password"
[log]
enabled = true
mode = "local"
path = "/srv/squad/SquadGame.log"
[api]
enabled = true
host = "127.0.0.1"
port = 18091
websocket_path = "/ws/control"
auth_token = "replace_with_api_token"Canonical database template (config/servers/_template/database.toml.example):
enabled = true
driver = "sqlite"
path = "data/server-1.db" # use a unique path per serverOperational rules:
- Use distinct RCON endpoints per server context.
- Use distinct log sources/paths per server context.
- Use distinct API ports per server context.
- Prefer distinct per-server DB configs/paths (
config/servers/<name>/database.toml) to avoid data mixing. - If per-server DB config is missing and shared
config/database.tomlexists, SquadPy warns and falls back to shared DB. - Keep canonical plugin examples in
config/plugins/*.toml.example. - Use
config/servers/<name>/plugins/*.tomlonly when a server needs overrides that differ from shared defaults. - By default, plugins without
config/plugins/<plugin>.tomlare disabled; set[plugins].auto_enable_missing_configs = trueinserver.tomlto opt into legacy auto-enable behavior.
RconBroker is available for embedding into local control services:
from squadpy.rcon.client import RconClient
from squadpy.rcon.broker import RconBroker
upstream = RconClient(host="127.0.0.1", port=21114, password="secret")
await upstream.connect()
broker = RconBroker(
upstream,
clients={"tool-a": "token-a", "tool-b": "token-b"},
)
response = await broker.execute(token="token-a", command="ListPlayers")Core files:
| File | Purpose |
|---|---|
config/runtime.toml |
Optional runtime mode and DB isolation selection |
config/server.toml |
Single-server runtime config |
config/logging.toml |
Logging setup |
config/database.toml |
Optional DB setup |
config/plugins/*.toml |
Canonical per-plugin settings/examples |
config/servers/_template/*.example |
Copy-ready multi-server starter templates |
config/servers/<name>/server.toml |
Multi-server per-context config |
config/servers/<name>/database.toml |
Multi-server per-context DB config |
config/servers/<name>/plugins/*.toml |
Optional per-server plugin overrides |
server.toml sections:
[rcon]required.[log]optional local/SFTP log tail.[discord]optional Discord bot connector.[api]optional REST + websocket control API.[plugins]optional plugin activation policy (auto_enable_missing_configs).[plugin_watchdog]optional failure-budget controls (enabled,failure_threshold,failure_window_seconds).
If [api].enabled = true, SquadPy serves:
GET /api/healthGET /api/readinessGET /api/statusGET /api/tracesGET /metrics(Prometheus format)POST /api/commandPOST /api/layers/queryPOST /api/webhook/commandWS /ws/control(path configurable)
Auth and policy:
- API token via
Authorization: Bearer <token>(or?token=query fallback). - Optional RBAC via
[api].rbac_*settings. - Optional signed webhook policy via
[api].webhook_signing_*settings.
Directory: plugins/<plugin_name>/plugin.py.
Moderation and admin:
auto_kick_unassignedtk_warnadmin_requestchat_commandsswitch_commandmax_player_in_squadsquad_name_validatorend_match_vote
Seeding and gameplay automation:
seeding_modeauto_seed_low_playersauto_rejoin_teamteam_randomizerintervalled_broadcastsfog_of_warknife_broadcastheli_crash_broadcast
Discord integrations:
discord_chat_bridgediscord_admin_broadcastdiscord_admin_cam_logsdiscord_killfeeddiscord_teamkilldiscord_round_eventsdiscord_squad_createddiscord_fob_hab_damagediscord_rcon_consoleserver_statusplugin_manager
Data:
db_log
| Script | Purpose |
|---|---|
scripts/migrate_from_squadjs.py |
Report-first SquadJS config analyzer |
scripts/migrate_from_squadts.py |
Report-first SquadTS config analyzer |
scripts/lint_migrated_toml.py |
Lint migrated TOML and emit fixups/severity |
scripts/record_gate_windows.py |
Record daily gate-window evidence for GATE-002/GATE-003/GATE-004 |
scripts/scaffold_plugin.py |
Generate typed plugin skeleton + starter tests/config |
scripts/simulate_local_replay.py |
Replay logs/RCON fixtures through plugins locally |
Create a typed plugin skeleton:
.venv/bin/python scripts/scaffold_plugin.py my_new_pluginThis generates:
plugins/my_new_plugin/plugin.pytests/test_plugin_my_new_plugin.pyconfig/plugins/my_new_plugin.toml.example
from squadpy.events.types import ChatMessage, PlayerConnected
from squadpy.plugin import Plugin, on_event, periodic
class MyPlugin(Plugin):
name = "My Plugin"
description = "Does something useful"
requires = ["rcon"]
@on_event("player_connected")
async def handle_player_connected(self, event: PlayerConnected) -> None:
await self.rcon.warn(event.eos_id, "Welcome")
@on_event("chat_message")
async def handle_chat(self, event: ChatMessage) -> None:
if event.message == "!ping":
await self.rcon.warn(event.eos_id, "pong")
@periodic(seconds=60)
async def periodic_task(self) -> None:
players = await self.rcon.list_players()
self.logger.info("Players online: %d", len(players))Replay fixtures through plugins for fast local loops:
.venv/bin/python scripts/simulate_local_replay.py \
tests/fixtures/simulation/logs/basic_replay.log \
--rcon-fixture tests/fixtures/simulation/rcon/replay.json \
--plugin sim_demo_plugin \
--plugins-dir tests/fixtures/simulation/plugins \
--plugin-config-dir tests/fixtures/simulation/config/pluginsmake setup-dev
make test
make test-unit
make test-contract
make test-integration
make test-integration-discord
make test-integration-rcon
make typecheckFocused test run example:
.venv/bin/python -m pytest tests/test_server.py -vRecord daily parity-gate windows (GATE-002/GATE-003/GATE-004):
.venv/bin/python scripts/record_gate_windows.py --sev1 none --notes "daily check"If running from CI/manual status source without local test execution:
.venv/bin/python scripts/record_gate_windows.py \
--skip-test-runs \
--contract-status pass \
--integration-status pass \
--sev1 none \
--notes "GitHub Actions run #1234"squadpy/
api/ Control API service and HTTP/WS endpoints
rcon/ RCON client, protocol, and broker
log_parser/ Log readers, parser, and pattern matching
events/ Event bus and typed dataclasses
discord/ Discord connector
database/ SQLAlchemy async integration
layers/ Layer catalog services
connectors/ External moderation connector primitives
admin_lists/ Admin list sync engine
simulation/ Local replay harness
multi_server.py Multi-server runtime orchestration
plugin_loader.py Plugin discovery and lifecycle
server.py Main orchestrator
plugins/ Built-in plugins
config/ Runtime and plugin TOML configs
scripts/ Migration/scaffolding/simulation utilities
tests/ Unit and contract coverage
docs/ Plans, runbooks, and parity tracker (`docs/plans/README.md`)
GNU GPL v3.0. See LICENSE.