Phase 5: multi-host agents (0.5.0)

- stackpilot-agent: slim token-guarded FastAPI (reuses compose_service) exposing
  stack CRUD/lifecycle/logs + system info; same image, different CMD. agent/
  Dockerfile + compose + .env.example.
- Central proxy: Agent model, agent_service (httpx ping/proxy + live status:
  online/offline/unauthorized + hostname/last_seen), routers/agents.py
  (CRUD + ping + proxied stacks/lifecycle/logs/system).
- Frontend: Settings → Remote hosts (add/check/remove, connectivity dot); Stacks
  grouped by host; remote stack detail with lifecycle, live logs, compose/.env edit.

Verified end-to-end: agent+main on a shared network — register (good/bad token),
list/create/start/logs/delete remote stacks, offline detection (502).

Remote backup destinations (SFTP/S3) deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
menzelj
2026-06-07 21:23:17 +00:00
co-authored by Claude Opus 4.8
parent 8d19b09abd
commit 59037f4287
21 changed files with 1380 additions and 39 deletions
+2 -1
View File
@@ -1,8 +1,9 @@
"""SQLModel table models. Importing this package registers all tables."""
from models.agent import Agent
from models.audit import AuditLog
from models.setting import Setting, Webhook
from models.stack import Stack
from models.template import Template
from models.user import User
__all__ = ["User", "Stack", "AuditLog", "Template", "Setting", "Webhook"]
__all__ = ["User", "Stack", "AuditLog", "Template", "Setting", "Webhook", "Agent"]
+49
View File
@@ -0,0 +1,49 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import Optional
from sqlmodel import Field, SQLModel
def _now() -> datetime:
return datetime.now(timezone.utc)
class Agent(SQLModel, table=True):
"""A remote host running stackpilot-agent."""
id: Optional[int] = Field(default=None, primary_key=True)
name: str
url: str # e.g. http://10.0.0.5:5010
token: str # shared AGENT_TOKEN of that host
status: str = "unknown" # online | offline | unauthorized | unknown
hostname: Optional[str] = None # reported by the agent on ping
last_seen: Optional[datetime] = None
created_at: datetime = Field(default_factory=_now)
# --- API schemas ---
class AgentCreate(SQLModel):
name: str
url: str
token: str
class AgentUpdate(SQLModel):
name: Optional[str] = None
url: Optional[str] = None
token: Optional[str] = None
class AgentRead(SQLModel):
id: int
name: str
url: str
status: str
hostname: Optional[str]
last_seen: Optional[datetime]
created_at: datetime
token_set: bool