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:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user