chore: rename project Roundtable → Steward

Renames the Python package directory, CLI command, env var prefix,
docker-compose service/container/image, Postgres role/db, and all
visible branding. Marketing form is "Fabled Steward".

Clean break from the previous rebrand: drops the fabledscryer→roundtable
import shim in __init__.py and the FABLEDSCRYER_* env var fallback in
config.py and migrations/env.py. Env vars are now STEWARD_* only.

Heads-up for existing deployments:
- Postgres user/db renamed fabledscryer → steward in docker-compose.yml.
  Existing volumes need the role/db renamed inside Postgres, or override
  POSTGRES_USER/POSTGRES_DB to keep the old names.
- Host-agent systemd unit is now steward-agent.service. Existing agents
  keep running under the old name; reinstall to switch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 16:20:14 -04:00
parent e118543b2e
commit 88ab5b917e
148 changed files with 570 additions and 619 deletions
+36 -36
View File
@@ -2,13 +2,13 @@
**Date:** 2026-04-14
**Status:** Approved, ready for implementation planning
**Scope:** Roundtable plugin for remote host resource monitoring (CPU, memory, storage, load, uptime) via a lightweight Python push agent.
**Scope:** Steward plugin for remote host resource monitoring (CPU, memory, storage, load, uptime) via a lightweight Python push agent.
---
## Goal
Give Roundtable a fleet-glance view of remote Linux hosts — current resource usage, history, and alert integration — without requiring agentless SSH/Ansible round-trips, a new toolchain, or SNMP gymnastics on every target.
Give Steward a fleet-glance view of remote Linux hosts — current resource usage, history, and alert integration — without requiring agentless SSH/Ansible round-trips, a new toolchain, or SNMP gymnastics on every target.
## Non-goals
@@ -24,7 +24,7 @@ Give Roundtable a fleet-glance view of remote Linux hosts — current resource u
```
┌──────────────┐ POST /plugins/host_agent/ingest ┌────────────────────────┐
│ Agent │ ──────────────────────────────────────────→ │ Roundtable
│ Agent │ ──────────────────────────────────────────→ │ Steward
│ (Python) │ Authorization: Bearer <per-host-token> │ host_agent plugin │
│ on target │ JSON body: metrics snapshot │ │
│ host │ │ routes.py (ingest + │
@@ -48,8 +48,8 @@ Give Roundtable a fleet-glance view of remote Linux hosts — current resource u
- **Agent** is a single Python file. No external dependencies beyond Python 3.8+ stdlib.
- **Plugin** is self-contained under `plugins/host_agent/`. Writes to the core `PluginMetric` time-series bus (the designed cross-plugin integration point) and its own private `host_agent_registrations` table. Writes to the core `Host` model for identity, but adds no new columns to it.
- **Auth** is per-host bearer tokens, minted on "Add host" in the plugin's settings page.
- **Install** is a one-line `curl | sh` command rendered per-host by Roundtable with the token already baked in.
- **Failure** is handled at the agent (in-memory ring buffer + exponential backoff) so Roundtable outages don't lose brief-window data.
- **Install** is a one-line `curl | sh` command rendered per-host by Steward with the token already baked in.
- **Failure** is handled at the agent (in-memory ring buffer + exponential backoff) so Steward outages don't lose brief-window data.
---
@@ -58,10 +58,10 @@ Give Roundtable a fleet-glance view of remote Linux hosts — current resource u
### File layout on the target host
```
/usr/local/lib/roundtable-agent/agent.py # the script, target ~300 lines
/etc/roundtable-agent.conf # key=value config, 0640 root:roundtable-agent
/etc/systemd/system/roundtable-agent.service # unit file
# dedicated system user: roundtable-agent
/usr/local/lib/steward-agent/agent.py # the script, target ~300 lines
/etc/steward-agent.conf # key=value config, 0640 root:steward-agent
/etc/systemd/system/steward-agent.service # unit file
# dedicated system user: steward-agent
```
### Config file format
@@ -69,7 +69,7 @@ Give Roundtable a fleet-glance view of remote Linux hosts — current resource u
Flat `key = value`, parsed by a ~20-line homegrown parser (no TOML/YAML dependency):
```
url = https://roundtable.home.lan
url = https://steward.home.lan
token = a1b2c3d4...
interval_seconds = 30
hostname = myhost # optional; defaults to uname -n
@@ -111,7 +111,7 @@ To stderr only (systemd captures to journal). No file logging, no log rotation.
### Identity
The agent's self-reported hostname in the payload is advisory. The real identity is the bearer token — Roundtable looks up the `Host` row via `HostAgentRegistration.token_hash`, not via hostname. Changing a host's hostname does not break its identity, and two hosts accidentally sharing a hostname can't collide.
The agent's self-reported hostname in the payload is advisory. The real identity is the bearer token — Steward looks up the `Host` row via `HostAgentRegistration.token_hash`, not via hostname. Changing a host's hostname does not break its identity, and two hosts accidentally sharing a hostname can't collide.
### Failure behavior
@@ -194,7 +194,7 @@ One `ScheduledTask` running every 60 seconds that flags `HostAgentRegistration`
### `METRIC_CATALOG` registration
Add to `roundtable/alerts/routes.py`:
Add to `steward/alerts/routes.py`:
```python
"host_agent": ["cpu_pct", "mem_used_pct", "mem_available_bytes",
@@ -202,7 +202,7 @@ Add to `roundtable/alerts/routes.py`:
"load_5m", "load_15m", "uptime_secs"],
```
This is the one edit outside the plugin directory, matching the pattern every other plugin already follows. Not ideal from a pure-plugin-independence standpoint but unavoidable until Roundtable core grows a plugin-registered catalog API — deferred as future core work.
This is the one edit outside the plugin directory, matching the pattern every other plugin already follows. Not ideal from a pure-plugin-independence standpoint but unavoidable until Steward core grows a plugin-registered catalog API — deferred as future core work.
---
@@ -212,7 +212,7 @@ This is the one edit outside the plugin directory, matching the pattern every ot
```http
POST /plugins/host_agent/ingest HTTP/1.1
Host: roundtable.home.lan
Host: steward.home.lan
Authorization: Bearer a1b2c3d4e5f6...
Content-Type: application/json
```
@@ -311,7 +311,7 @@ Agent treats anything non-2xx as failure → ring buffer. Agent treats 401 speci
### The one-liner (what the UI shows)
```
curl -sSL 'https://roundtable.home.lan/plugins/host_agent/install.sh?token=a1b2c3...' | sudo sh
curl -sSL 'https://steward.home.lan/plugins/host_agent/install.sh?token=a1b2c3...' | sudo sh
```
The UI also offers a "review script before running" link that opens a modal showing the two-step form:
@@ -326,19 +326,19 @@ sudo sh install.sh
```sh
#!/bin/sh
# Roundtable host agent installer
# Steward host agent installer
# Generated for: {{ host_name }} ({{ host_address }})
# Roundtable URL: {{ url }}
# Steward URL: {{ url }}
set -e
ROUNDTABLE_URL="{{ url }}"
STEWARD_URL="{{ url }}"
AGENT_TOKEN="{{ token }}"
AGENT_VERSION="{{ agent_version }}"
AGENT_USER="roundtable-agent"
AGENT_DIR="/usr/local/lib/roundtable-agent"
CONF_FILE="/etc/roundtable-agent.conf"
UNIT_FILE="/etc/systemd/system/roundtable-agent.service"
AGENT_USER="steward-agent"
AGENT_DIR="/usr/local/lib/steward-agent"
CONF_FILE="/etc/steward-agent.conf"
UNIT_FILE="/etc/systemd/system/steward-agent.service"
# ── preflight ────────────────────────────────────────────────────────────────
[ "$(id -u)" = "0" ] || { echo "must run as root (use sudo)"; exit 1; }
@@ -347,7 +347,7 @@ command -v python3 >/dev/null 2>&1 || { echo "python3 not found — install py
# Handle --uninstall
if [ "${1:-}" = "--uninstall" ]; then
systemctl disable --now roundtable-agent.service 2>/dev/null || true
systemctl disable --now steward-agent.service 2>/dev/null || true
rm -f "$UNIT_FILE" "$CONF_FILE"
rm -rf "$AGENT_DIR"
systemctl daemon-reload
@@ -363,13 +363,13 @@ fi
# ── drop the agent file ──────────────────────────────────────────────────────
mkdir -p "$AGENT_DIR"
curl -sSL "${ROUNDTABLE_URL}/plugins/host_agent/agent.py" -o "$AGENT_DIR/agent.py"
curl -sSL "${STEWARD_URL}/plugins/host_agent/agent.py" -o "$AGENT_DIR/agent.py"
chmod 0755 "$AGENT_DIR/agent.py"
chown root:root "$AGENT_DIR/agent.py"
# ── write config ─────────────────────────────────────────────────────────────
cat > "$CONF_FILE" <<EOF
url = $ROUNDTABLE_URL
url = $STEWARD_URL
token = $AGENT_TOKEN
interval_seconds = 30
EOF
@@ -379,7 +379,7 @@ chmod 0640 "$CONF_FILE"
# ── write systemd unit ───────────────────────────────────────────────────────
cat > "$UNIT_FILE" <<EOF
[Unit]
Description=Roundtable host agent
Description=Steward host agent
After=network-online.target
Wants=network-online.target
@@ -400,17 +400,17 @@ WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now roundtable-agent.service
systemctl enable --now steward-agent.service
echo
echo "Roundtable host agent $AGENT_VERSION installed and running."
echo "Check status: systemctl status roundtable-agent"
echo "Logs: journalctl -u roundtable-agent -f"
echo "Steward host agent $AGENT_VERSION installed and running."
echo "Check status: systemctl status steward-agent"
echo "Logs: journalctl -u steward-agent -f"
```
### Design points
- **Agent binary served by Roundtable itself.** `GET /plugins/host_agent/agent.py` serves the script bundled with the plugin, so version drift between install-script and agent is impossible — re-running the installer picks up whatever agent the currently-running Roundtable ships.
- **Agent binary served by Steward itself.** `GET /plugins/host_agent/agent.py` serves the script bundled with the plugin, so version drift between install-script and agent is impossible — re-running the installer picks up whatever agent the currently-running Steward ships.
- **Systemd hardening is cheap and correct.** `NoNewPrivileges`, `ProtectSystem=strict`, `ProtectHome`, `PrivateTmp`, `ReadOnlyPaths=/proc /sys`. The agent only needs read access to `/proc`, `/sys`, and mount points; denying everything else narrows blast radius.
- **Uninstall is a first-class flag**, not a separate script. Same one-liner with `--uninstall` appended.
- **Fail-fast on preflight.** Missing systemd or python3 → clear error, exit. No half-installed agent.
@@ -421,7 +421,7 @@ echo "Logs: journalctl -u roundtable-agent -f"
## UI surfaces
### Dashboard widgets (`roundtable/core/widgets.py`)
### Dashboard widgets (`steward/core/widgets.py`)
- **`host_resources`** — table widget. One row per monitored host: name, CPU %, mem %, disk % (worst mount), load 1m, "last seen Xs ago" with red/yellow/green coloring. Fleet glance.
- **`host_resource_history`** — chart widget for one host. CPU / mem / disk over a selectable time range (1h, 6h, 24h, 7d). Same pattern as every other history widget — Chart.js.
@@ -447,9 +447,9 @@ List of registered hosts with their enable flags, an "Add host" button that open
| Failure | Who handles it | Response |
|---|---|---|
| Agent can't reach Roundtable | Agent | Push to ring buffer, exponential backoff (30→60→120→300s cap), log WARN. Resets on success. |
| Roundtable rejects with 401 | Agent | Log ERROR with remediation hint, continue retry loop. Admin fixes via UI + conf edit; no restart needed. |
| Roundtable rejects with 400 | Agent | Log ERROR, drop the sample (don't re-buffer), continue. Indicates agent/server version skew. |
| Agent can't reach Steward | Agent | Push to ring buffer, exponential backoff (30→60→120→300s cap), log WARN. Resets on success. |
| Steward rejects with 401 | Agent | Log ERROR with remediation hint, continue retry loop. Admin fixes via UI + conf edit; no restart needed. |
| Steward rejects with 400 | Agent | Log ERROR, drop the sample (don't re-buffer), continue. Indicates agent/server version skew. |
| Ring buffer full | Agent | Drop oldest, keep newest. DEBUG log (expected during outages). |
| Config file missing or malformed | Agent | Log ERROR, exit non-zero. Systemd restarts after 10s. Repeated restarts are visible in journal. |
| `/proc/stat` read fails between samples | Agent | Skip CPU for this cycle, still POST the rest. Partial samples allowed. |
@@ -528,7 +528,7 @@ These are not blockers; they are feature boundaries inherent to the design.
6. Settings page: list, add-host flow, rotate-token, delete.
7. Dashboard widgets: table + history chart. Register in `core/widgets.py`.
8. Per-host detail page.
9. `METRIC_CATALOG` entry in `roundtable/alerts/routes.py`.
9. `METRIC_CATALOG` entry in `steward/alerts/routes.py`.
10. Scheduler: stale-agent marker.
11. Tests at every stage; final integration test to tie it together.
+52 -52
View File
@@ -2,7 +2,7 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Ship a `host_agent` Roundtable plugin plus a stdlib-only Python push agent that reports CPU/memory/storage/load/uptime from remote Linux hosts to Roundtable.
**Goal:** Ship a `host_agent` Steward plugin plus a stdlib-only Python push agent that reports CPU/memory/storage/load/uptime from remote Linux hosts to Steward.
**Architecture:** Plugin lives under `plugins/host_agent/` (mirrors `plugins/http/`). It owns a private `host_agent_registrations` table, writes time-series data into the core `PluginMetric` bus, serves its own agent source at `GET /plugins/host_agent/agent.py`, and renders a per-host install script at `GET /plugins/host_agent/install.sh`. The agent is a single ~300-line Python script with a 30s collect→POST loop, in-memory ring buffer, exponential backoff, and systemd unit installed by a curl one-liner.
@@ -14,7 +14,7 @@
## Execution note — path 1 (no DB-backed tests)
Mid-execution discovery: the Roundtable test harness runs `create_app(testing=True)`, which mocks `db_sessionmaker` and skips all migrations. There is no existing DB-backed test fixture in this codebase, and no existing plugin ships with tests. The first execution attempt tried to invent a SQLite-backed override fixture, which cascaded into modifying a production migration (`0007_dashboard_ownership.py`) to remove an inline FK — unacceptable. That commit was reverted.
Mid-execution discovery: the Steward test harness runs `create_app(testing=True)`, which mocks `db_sessionmaker` and skips all migrations. There is no existing DB-backed test fixture in this codebase, and no existing plugin ships with tests. The first execution attempt tried to invent a SQLite-backed override fixture, which cascaded into modifying a production migration (`0007_dashboard_ownership.py`) to remove an inline FK — unacceptable. That commit was reverted.
**Revised test strategy:** test the agent exhaustively (everything that lives in `plugins/host_agent/agent.py` — pure Python, stdlib only, trivially unit-testable). Test the server-side metric-expansion function as a pure function that takes a sample dict + host name and returns a list of `(metric_name, resource_name, value)` tuples. Everything else — ingest route, install route, settings routes, widgets, detail page, scheduler — is verified manually against the dev server.
@@ -43,7 +43,7 @@ Where code blocks in tasks below reference DB-backed tests, treat them as guidan
- `plugins/host_agent/scheduler.py` — stale-agent marker task.
- `plugins/host_agent/agent.py` — the Python agent script, served to targets.
- `plugins/host_agent/migrations/__init__.py`
- `plugins/host_agent/migrations/env.py` — alembic env (copy of http plugin's, but imports `roundtable.models.base`).
- `plugins/host_agent/migrations/env.py` — alembic env (copy of http plugin's, but imports `steward.models.base`).
- `plugins/host_agent/migrations/versions/__init__.py`
- `plugins/host_agent/migrations/versions/host_agent_001_initial.py` — creates `host_agent_registrations`.
- `plugins/host_agent/templates/install.sh.j2` — Jinja install script template.
@@ -68,8 +68,8 @@ Where code blocks in tasks below reference DB-backed tests, treat them as guidan
**Modified files (core, small edits):**
- `roundtable/alerts/routes.py` — add `"host_agent"` entry to `METRIC_CATALOG`.
- `roundtable/core/widgets.py` — add `host_resources` and `host_resource_history` entries.
- `steward/alerts/routes.py` — add `"host_agent"` entry to `METRIC_CATALOG`.
- `steward/core/widgets.py` — add `host_resources` and `host_resource_history` entries.
- `docs/plugins/index.yaml.example` — add catalog entry.
---
@@ -102,7 +102,7 @@ pytestmark = pytest.mark.asyncio
async def test_host_agent_migration_creates_table(app):
# The app fixture runs all plugin migrations on startup.
from roundtable.core.db import get_engine
from steward.core.db import get_engine
engine = get_engine()
async with engine.connect() as conn:
def _check(sync_conn):
@@ -127,11 +127,11 @@ Expected: FAIL (plugin not loaded, table missing).
name: host_agent
version: "1.0.0"
description: "Remote Linux host resource monitoring via a lightweight Python push agent (CPU, memory, storage, load, uptime)"
author: "Roundtable"
author: "Steward"
license: "MIT"
min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/host_agent"
repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/host_agent"
tags:
- host
- monitoring
@@ -180,7 +180,7 @@ from __future__ import annotations
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, String, DateTime, ForeignKey
from roundtable.models.base import Base
from steward.models.base import Base
def _uuid() -> str:
@@ -269,8 +269,8 @@ from alembic import context
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
from roundtable.models.base import Base
import roundtable.models # noqa: F401
from steward.models.base import Base
import steward.models # noqa: F401
from plugins.host_agent.models import HostAgentRegistration # noqa: F401
config = context.config
@@ -282,14 +282,14 @@ target_metadata = Base.metadata
def _get_url() -> str:
import yaml
cfg_path = os.environ.get("ROUNDTABLE_CONFIG", "config.yaml")
cfg_path = os.environ.get("STEWARD_CONFIG", "config.yaml")
try:
with open(cfg_path) as f:
cfg = yaml.safe_load(f) or {}
url = cfg.get("database", {}).get("url", "")
except FileNotFoundError:
url = ""
return os.environ.get("ROUNDTABLE_DATABASE__URL", url)
return os.environ.get("STEWARD_DATABASE__URL", url)
def run_migrations_offline() -> None:
@@ -413,12 +413,12 @@ from plugins.host_agent.agent import read_config, ConfigError
def test_parses_flat_key_value(tmp_path):
p = tmp_path / "agent.conf"
p.write_text(
"url = https://roundtable.example\n"
"url = https://steward.example\n"
"token = abc123\n"
"interval_seconds = 45\n"
)
cfg = read_config(str(p))
assert cfg["url"] == "https://roundtable.example"
assert cfg["url"] == "https://steward.example"
assert cfg["token"] == "abc123"
assert cfg["interval_seconds"] == 45
@@ -466,7 +466,7 @@ Expected: FAIL (`read_config` does not exist).
```python
# plugins/host_agent/agent.py
"""Roundtable host agent — pushes resource metrics to a Roundtable instance.
"""Steward host agent — pushes resource metrics to a Steward instance.
Python 3.8+ stdlib only. Target ~300 lines. Served to targets at
GET /plugins/host_agent/agent.py.
@@ -1060,7 +1060,7 @@ def post_payload(url: str, token: str, payload: dict) -> tuple[bool, int | None]
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
"User-Agent": f"roundtable-host-agent/{AGENT_VERSION}",
"User-Agent": f"steward-host-agent/{AGENT_VERSION}",
},
method="POST",
)
@@ -1112,7 +1112,7 @@ def main_loop(conf_path: str) -> int:
buffer = RingBuffer(maxlen=20)
backoff = 0
_log("INFO", f"roundtable-host-agent {AGENT_VERSION} starting "
_log("INFO", f"steward-host-agent {AGENT_VERSION} starting "
f"(url={cfg['url']}, interval={cfg['interval_seconds']}s)")
while not _shutdown_requested:
@@ -1171,7 +1171,7 @@ def main_loop(conf_path: str) -> int:
if __name__ == "__main__":
conf = os.environ.get("ROUNDTABLE_AGENT_CONFIG", "/etc/roundtable-agent.conf")
conf = os.environ.get("STEWARD_AGENT_CONFIG", "/etc/steward-agent.conf")
sys.exit(main_loop(conf))
```
@@ -1209,8 +1209,8 @@ git commit -m "feat(host_agent): agent POST, backoff, and main loop"
import hashlib
import pytest_asyncio
from roundtable.models.hosts import Host
from roundtable.core.db import get_session
from steward.models.hosts import Host
from steward.core.db import get_session
from plugins.host_agent.models import HostAgentRegistration
@@ -1233,7 +1233,7 @@ async def registered_host(app):
return {"host": host, "registration": reg, "token": raw_token}
```
> **Note:** Verify `Host.__init__` accepts `name=` and `address=` kwargs before relying on this. If the real Host model requires more fields (e.g., `probe_type`), pass them here. Read `roundtable/models/hosts.py` first to confirm.
> **Note:** Verify `Host.__init__` accepts `name=` and `address=` kwargs before relying on this. If the real Host model requires more fields (e.g., `probe_type`), pass them here. Read `steward/models/hosts.py` first to confirm.
- [ ] **Step 2: Write failing ingest test**
@@ -1244,8 +1244,8 @@ from datetime import datetime, timezone
import pytest
from sqlalchemy import select
from roundtable.models.metrics import PluginMetric
from roundtable.core.db import get_session
from steward.models.metrics import PluginMetric
from steward.core.db import get_session
from plugins.host_agent.models import HostAgentRegistration
pytestmark = pytest.mark.asyncio
@@ -1352,9 +1352,9 @@ from typing import Any
from quart import Blueprint, jsonify, request
from sqlalchemy import select
from roundtable.core.db import get_session
from roundtable.models.hosts import Host
from roundtable.models.metrics import PluginMetric
from steward.core.db import get_session
from steward.models.hosts import Host
from steward.models.metrics import PluginMetric
from .models import HostAgentRegistration
host_agent_bp = Blueprint("host_agent", __name__, template_folder="templates")
@@ -1628,7 +1628,7 @@ async def test_install_sh_renders_with_token(client, registered_host):
assert resp.status_code == 200
assert resp.content_type.startswith("text/plain")
text = (await resp.get_data()).decode()
assert "roundtable-agent" in text
assert "steward-agent" in text
assert registered_host["token"] in text
assert "systemctl enable --now" in text
assert "NoNewPrivileges=yes" in text
@@ -1758,11 +1758,11 @@ git commit -m "feat(host_agent): install.sh and agent.py serving routes"
- Create: `plugins/host_agent/templates/settings_list.html`
- Create: `tests/plugins/host_agent/test_settings_routes.py`
Auth note: the existing Roundtable admin decorator pattern needs to be used here. Read `roundtable/settings/routes.py` (or similar) to find the decorator name — likely something like `@require_admin` or a session check. Use whatever the rest of the codebase uses. **Do not invent a new auth mechanism.**
Auth note: the existing Steward admin decorator pattern needs to be used here. Read `steward/settings/routes.py` (or similar) to find the decorator name — likely something like `@require_admin` or a session check. Use whatever the rest of the codebase uses. **Do not invent a new auth mechanism.**
- [ ] **Step 1: Find the admin auth decorator**
Run: `grep -rn "require_admin\|@admin_required\|def admin" roundtable/settings/ roundtable/core/auth.py 2>/dev/null | head -20`
Run: `grep -rn "require_admin\|@admin_required\|def admin" steward/settings/ steward/core/auth.py 2>/dev/null | head -20`
Note the decorator name and import path for use in Step 3.
- [ ] **Step 2: Write failing test**
@@ -1772,8 +1772,8 @@ Note the decorator name and import path for use in Step 3.
import pytest
from sqlalchemy import select
from roundtable.core.db import get_session
from roundtable.models.hosts import Host
from steward.core.db import get_session
from steward.models.hosts import Host
from plugins.host_agent.models import HostAgentRegistration
pytestmark = pytest.mark.asyncio
@@ -1839,7 +1839,7 @@ from quart import redirect, url_for
# TODO: replace _require_admin with the project-wide decorator found in Step 1.
# Placeholder below mirrors the shape; swap for real admin auth.
from roundtable.core.auth import require_admin # adjust import to actual path
from steward.core.auth import require_admin # adjust import to actual path
def _new_token_pair() -> tuple[str, str]:
@@ -1938,7 +1938,7 @@ async def settings_list():
```html
{% extends "base.html" %}
{% block title %}Host Agent — Roundtable{% endblock %}
{% block title %}Host Agent — Steward{% endblock %}
{% block content %}
<h1 class="page-title">Host Agent — Registered Hosts</h1>
@@ -2003,7 +2003,7 @@ async def settings_list():
- [ ] **Step 6: Run tests — iterate on auth decorator if needed**
Run: `pytest tests/plugins/host_agent/test_settings_routes.py -v`
Expected: PASS. If admin auth fails, the actual import path from Step 1 is wrong — fix the `from roundtable.core.auth import require_admin` line.
Expected: PASS. If admin auth fails, the actual import path from Step 1 is wrong — fix the `from steward.core.auth import require_admin` line.
- [ ] **Step 7: Commit**
@@ -2020,8 +2020,8 @@ git commit -m "feat(host_agent): plugin settings page — add, rotate, delete"
- Modify: `plugins/host_agent/routes.py` — add `/widget` and `/widget/history` partials.
- Create: `plugins/host_agent/templates/widget_table.html`
- Create: `plugins/host_agent/templates/widget_history.html`
- Modify: `roundtable/core/widgets.py`
- Modify: `roundtable/alerts/routes.py`
- Modify: `steward/core/widgets.py`
- Modify: `steward/alerts/routes.py`
- [ ] **Step 1: Add widget partial routes to `plugins/host_agent/routes.py`**
@@ -2165,7 +2165,7 @@ async def widget_history():
</div>
```
- [ ] **Step 4: Register widgets in `roundtable/core/widgets.py`**
- [ ] **Step 4: Register widgets in `steward/core/widgets.py`**
Add to `WIDGET_REGISTRY` (alphabetically by existing convention, or at the end — check the file first):
@@ -2206,7 +2206,7 @@ Add to `WIDGET_REGISTRY` (alphabetically by existing convention, or at the end
- [ ] **Step 5: Register `host_agent` in `METRIC_CATALOG`**
Read `roundtable/alerts/routes.py`. Locate the `METRIC_CATALOG` dict. Add:
Read `steward/alerts/routes.py`. Locate the `METRIC_CATALOG` dict. Add:
```python
"host_agent": [
@@ -2244,7 +2244,7 @@ Expected: PASS.
- [ ] **Step 7: Commit**
```bash
git add plugins/host_agent/routes.py plugins/host_agent/templates/widget_table.html plugins/host_agent/templates/widget_history.html roundtable/core/widgets.py roundtable/alerts/routes.py tests/plugins/host_agent/test_ingest_route.py
git add plugins/host_agent/routes.py plugins/host_agent/templates/widget_table.html plugins/host_agent/templates/widget_history.html steward/core/widgets.py steward/alerts/routes.py tests/plugins/host_agent/test_ingest_route.py
git commit -m "feat(host_agent): dashboard widgets and alert metric catalog entry"
```
@@ -2409,8 +2409,8 @@ from datetime import datetime, timedelta, timezone
import pytest
from sqlalchemy import select
from roundtable.core.db import get_session
from roundtable.models.hosts import Host
from steward.core.db import get_session
from steward.models.hosts import Host
from plugins.host_agent.models import HostAgentRegistration
from plugins.host_agent.scheduler import find_stale_registrations
@@ -2459,8 +2459,8 @@ from datetime import datetime, timedelta, timezone
from sqlalchemy import select
from roundtable.core.db import get_session
from roundtable.models.hosts import Host
from steward.core.db import get_session
from steward.models.hosts import Host
from .models import HostAgentRegistration
@@ -2531,8 +2531,8 @@ from unittest.mock import patch
import pytest
from sqlalchemy import select
from roundtable.core.db import get_session
from roundtable.models.metrics import PluginMetric
from steward.core.db import get_session
from steward.models.metrics import PluginMetric
from plugins.host_agent import agent as a
pytestmark = pytest.mark.asyncio
@@ -2609,12 +2609,12 @@ Append to the `plugins:` list in `docs/plugins/index.yaml.example`:
- name: host_agent
version: "1.0.0"
description: "Remote Linux host resource monitoring via a lightweight Python push agent"
author: "Roundtable"
author: "Steward"
license: "MIT"
min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/host_agent"
download_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/host_agent-v1.0.0/host_agent.zip"
repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/host_agent"
download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/host_agent-v1.0.0/host_agent.zip"
checksum_sha256: ""
tags:
- host
@@ -2643,7 +2643,7 @@ Use `fable_update_task` to set status=done on task 252 ("Implement host_agent pl
- [ ] **Step 2: Add a Fable note summarizing what shipped**
One-paragraph `fable_create_note` attached to Roundtable project (id 6) with:
One-paragraph `fable_create_note` attached to Steward project (id 6) with:
- Link to spec: `docs/plugins/host-agent-design.md`
- Link to plan: `docs/plugins/host-agent-plan.md`
- Note any deviations from the spec discovered during implementation (e.g., auth decorator name, fixture adjustments).
+25 -25
View File
@@ -1,10 +1,10 @@
# roundtable-plugins / index.yaml
# steward-plugins / index.yaml
#
# This file is the catalog index for the roundtable plugin repository.
# This file is the catalog index for the steward plugin repository.
# It is fetched by the app's Settings → Plugins → Plugin Catalog UI.
#
# Roundtable reads this file from:
# https://git.fabledsword.com/bvandeusen/Roundtable-plugins/raw/branch/main/index.yaml
# Steward reads this file from:
# https://git.fabledsword.com/bvandeusen/Steward-plugins/raw/branch/main/index.yaml
#
# After adding or updating a plugin entry, commit and push — the change is
# live immediately for anyone whose app fetches the catalog (cache TTL: 5 min).
@@ -25,7 +25,7 @@
#
# Download URL conventions:
# Gitea release assets (recommended):
# https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/traefik-v1.0.0/traefik.zip
# https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/traefik-v1.0.0/traefik.zip
# Upload the zip as a release attachment in Gitea; paste the URL here.
# Source archive tarballs work too but release assets are preferred (smaller, plugin-only).
@@ -37,12 +37,12 @@ plugins:
- name: http
version: "1.0.0"
description: "Synthetic HTTP endpoint monitoring — status code, response time, content match, TLS expiry"
author: "Roundtable"
author: "Steward"
license: "MIT"
min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/http"
download_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/http-v1.0.0/http.zip"
repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/http"
download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/http-v1.0.0/http.zip"
checksum_sha256: ""
tags:
- monitoring
@@ -53,12 +53,12 @@ plugins:
- name: docker
version: "1.0.0"
description: "Docker container status, resource usage, and restart tracking via Docker socket"
author: "Roundtable"
author: "Steward"
license: "MIT"
min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/docker"
download_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/docker-v1.0.0/docker.zip"
repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/docker"
download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/docker-v1.0.0/docker.zip"
checksum_sha256: ""
tags:
- containers
@@ -68,12 +68,12 @@ plugins:
- name: traefik
version: "1.0.0"
description: "Traefik reverse proxy metrics and access log integration"
author: "Roundtable"
author: "Steward"
license: "MIT"
min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/traefik"
download_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/traefik-v1.0.0/traefik.zip"
repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/traefik"
download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/traefik-v1.0.0/traefik.zip"
checksum_sha256: "" # fill in after running: sha256sum traefik.zip
tags:
- proxy
@@ -83,12 +83,12 @@ plugins:
- name: unifi
version: "1.0.0"
description: "UniFi Network controller integration — WAN health, devices, clients, DPI"
author: "Roundtable"
author: "Steward"
license: "MIT"
min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/unifi"
download_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/unifi-v1.0.0/unifi.zip"
repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/unifi"
download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/unifi-v1.0.0/unifi.zip"
checksum_sha256: ""
tags:
- network
@@ -98,12 +98,12 @@ plugins:
- name: host_agent
version: "1.0.0"
description: "Remote Linux host resource monitoring via a lightweight Python push agent"
author: "Roundtable"
author: "Steward"
license: "MIT"
min_app_version: "0.1.0"
repository_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/src/branch/main/host_agent"
download_url: "https://git.fabledsword.com/bvandeusen/Roundtable-plugins/releases/download/host_agent-v1.0.0/host_agent.zip"
repository_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins"
homepage: "https://git.fabledsword.com/bvandeusen/Steward-plugins/src/branch/main/host_agent"
download_url: "https://git.fabledsword.com/bvandeusen/Steward-plugins/releases/download/host_agent-v1.0.0/host_agent.zip"
checksum_sha256: ""
tags:
- host
+5 -5
View File
@@ -1,12 +1,12 @@
# Plugin System Overview
Plugins extend Roundtable with new data sources, UI pages, scheduled tasks, and dashboard widgets. Only enabled plugins are imported — disabled or unlisted plugins have zero runtime overhead.
Plugins extend Steward with new data sources, UI pages, scheduled tasks, and dashboard widgets. Only enabled plugins are imported — disabled or unlisted plugins have zero runtime overhead.
---
## How Plugins Are Loaded
Plugin loading happens in step 9 of `create_app()`, after all core blueprints and tasks are registered. The entrypoint is `load_plugins(app)` in `roundtable/core/plugin_manager.py`.
Plugin loading happens in step 9 of `create_app()`, after all core blueprints and tasks are registered. The entrypoint is `load_plugins(app)` in `steward/core/plugin_manager.py`.
For each plugin listed as `enabled: true` in the `PLUGINS` config:
@@ -57,7 +57,7 @@ description: "Does a thing"
# Optional
author: "Your Name"
min_app_version: "0.1.0" # Minimum Roundtable version required
min_app_version: "0.1.0" # Minimum Steward version required
# Default config — merged with user overrides at runtime
# Access at runtime via: app.config["PLUGINS"]["myplugin"]["my_setting"]
@@ -90,7 +90,7 @@ def setup(app):
Returns a list of `ScheduledTask` objects. Return `[]` if the plugin has no background tasks. Called after `setup()`, so any app references set in `setup()` are available.
```python
from roundtable.core.scheduler import ScheduledTask
from steward.core.scheduler import ScheduledTask
def get_scheduled_tasks():
app = _app
@@ -154,7 +154,7 @@ A plugin can contribute a dashboard widget by:
1. Adding a `GET /widget` route to its blueprint that returns an HTMX HTML fragment
2. The dashboard template polling that endpoint with HTMX
The dashboard (`roundtable/templates/dashboard/index.html`) currently includes the Traefik widget conditionally based on `traefik_enabled`. To add a new plugin widget, the dashboard route and template both need to be updated to detect and render the new widget. See the Traefik widget implementation as a reference.
The dashboard (`steward/templates/dashboard/index.html`) currently includes the Traefik widget conditionally based on `traefik_enabled`. To add a new plugin widget, the dashboard route and template both need to be updated to detect and render the new widget. See the Traefik widget implementation as a reference.
---
+16 -16
View File
@@ -46,7 +46,7 @@ config:
## Step 3: Define Models (if needed)
If your plugin stores data, define SQLAlchemy models using the shared `Base` from `roundtable.models.base`.
If your plugin stores data, define SQLAlchemy models using the shared `Base` from `steward.models.base`.
```python
# plugins/myplugin/models.py
@@ -55,7 +55,7 @@ import uuid
from datetime import datetime
from sqlalchemy import String, Float, DateTime
from sqlalchemy.orm import Mapped, mapped_column
from roundtable.models.base import Base
from steward.models.base import Base
class MyPluginMetric(Base):
@@ -79,7 +79,7 @@ Generate the initial migration:
# From the project root
alembic --config alembic.ini revision \
--autogenerate \
--head=roundtable@head \
--head=steward@head \
--branch-label=myplugin \
-m "myplugin initial"
```
@@ -106,8 +106,8 @@ Keep task logic in a separate file so `__init__.py` stays clean.
# plugins/myplugin/scheduler.py
from __future__ import annotations
import logging
from roundtable.core.scheduler import ScheduledTask
from roundtable.core.alerts import record_metric
from steward.core.scheduler import ScheduledTask
from steward.core.alerts import record_metric
logger = logging.getLogger(__name__)
@@ -174,8 +174,8 @@ async def _fetch_value(url: str) -> float:
```python
# plugins/myplugin/routes.py
from quart import Blueprint, current_app, render_template
from roundtable.auth.middleware import require_role
from roundtable.models.users import UserRole
from steward.auth.middleware import require_role
from steward.models.users import UserRole
from .models import MyPluginMetric
myplugin_bp = Blueprint("myplugin", __name__, template_folder="templates")
@@ -215,7 +215,7 @@ Templates live in `plugins/myplugin/templates/myplugin/` (the extra nesting avoi
```html
{# plugins/myplugin/templates/myplugin/index.html #}
{% extends "base.html" %}
{% block title %}My Plugin — Roundtable{% endblock %}
{% block title %}My Plugin — Steward{% endblock %}
{% block content %}
<div class="page-title">My Plugin</div>
{% for row in rows %}
@@ -283,7 +283,7 @@ On next startup, the plugin will be loaded, its migrations applied, and its blue
`record_metric()` is how plugins feed data into the alert pipeline. Any metric you write here can be the target of an alert rule created in the UI.
```python
from roundtable.core.alerts import record_metric
from steward.core.alerts import record_metric
# Must be inside an active transaction
async with session.begin():
@@ -302,11 +302,11 @@ async with session.begin():
## Auth in Routes
Use the `@require_role` decorator from `roundtable.auth.middleware`:
Use the `@require_role` decorator from `steward.auth.middleware`:
```python
from roundtable.auth.middleware import require_role
from roundtable.models.users import UserRole
from steward.auth.middleware import require_role
from steward.models.users import UserRole
@myplugin_bp.get("/admin-only")
@require_role(UserRole.admin)
@@ -325,13 +325,13 @@ Role hierarchy: `admin > operator > viewer`. Requiring `viewer` grants access to
## Publishing to the Catalog
The official plugin catalog is hosted at `https://git.fabledsword.com/bvandeusen/Roundtable-plugins`. Anyone can submit a plugin by opening a pull request — first-party and third-party plugins are treated identically by the catalog system.
The official plugin catalog is hosted at `https://git.fabledsword.com/bvandeusen/Steward-plugins`. Anyone can submit a plugin by opening a pull request — first-party and third-party plugins are treated identically by the catalog system.
### Repo layout
```
roundtable-plugins/
├── index.yaml ← catalog index — the only file Roundtable fetches
steward-plugins/
├── index.yaml ← catalog index — the only file Steward fetches
├── myplugin/
│ ├── plugin.yaml
│ ├── __init__.py
@@ -381,7 +381,7 @@ myplugin.zip
Generate the zip and its checksum:
```bash
cd roundtable-plugins
cd steward-plugins
zip -r myplugin.zip myplugin/
sha256sum myplugin.zip # paste this into index.yaml checksum_sha256
```