Initial commit: Agentic OS troubleshooting platform

Self-hosted, Docker-based agentic troubleshooting platform: FastAPI backend +
LangGraph agent, Next.js UI, tiered LLM routing (local Ollama -> Gemini ->
DeepSeek -> OpenRouter), MCP server manager, encrypted device credentials,
RBAC, audit log, project-memory + Obsidian integrations, and editable
troubleshooting decision rules tuned for the GeneseasX vessel stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-14 22:11:07 +03:00
commit 6185b9b85a
126 changed files with 14565 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
"""SQLAlchemy ORM models for Agentic OS."""
from app.models.audit import AuditLog
from app.models.balance import BalanceSnapshot
from app.models.inventory import Device, Vessel
from app.models.task import ApprovalRequest, Task, TaskEvent
from app.models.user import User
__all__ = [
"User",
"Vessel",
"Device",
"Task",
"TaskEvent",
"ApprovalRequest",
"BalanceSnapshot",
"AuditLog",
]

View File

@@ -0,0 +1,27 @@
"""Audit log of security-relevant actions."""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class AuditLog(Base):
__tablename__ = "audit_logs"
id: Mapped[int] = mapped_column(primary_key=True)
actor_id: Mapped[int | None] = mapped_column(
ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
actor_email: Mapped[str | None] = mapped_column(String(255), nullable=True)
action: Mapped[str] = mapped_column(String(128), index=True)
target_type: Mapped[str | None] = mapped_column(String(64), nullable=True)
target_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
detail: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), index=True
)

View File

@@ -0,0 +1,26 @@
"""Snapshots of API provider balances/usage."""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, Float, String, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class BalanceSnapshot(Base):
__tablename__ = "balance_snapshots"
id: Mapped[int] = mapped_column(primary_key=True)
provider: Mapped[str] = mapped_column(String(64), index=True) # openrouter | deepseek
total_credits: Mapped[float | None] = mapped_column(Float, nullable=True)
total_usage: Mapped[float | None] = mapped_column(Float, nullable=True)
remaining: Mapped[float | None] = mapped_column(Float, nullable=True)
currency: Mapped[str] = mapped_column(String(8), default="USD")
raw: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
error: Mapped[str | None] = mapped_column(String(512), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), index=True
)

View File

@@ -0,0 +1,37 @@
"""Shared enums."""
from __future__ import annotations
import enum
class Role(str, enum.Enum):
admin = "admin"
support = "support"
readonly = "readonly"
class DeviceType(str, enum.Enum):
debian = "debian" # plain Debian/Linux server (ssh-generic-mcp)
geneseasx = "geneseasx" # GeneseasX host
proxmox = "proxmox"
pfsense = "pfsense"
asterisk = "asterisk"
fortigate = "fortigate"
fortiswitch = "fortiswitch"
tplink = "tplink"
other = "other"
class TaskStatus(str, enum.Enum):
queued = "queued"
running = "running"
waiting_approval = "waiting_approval"
succeeded = "succeeded"
failed = "failed"
cancelled = "cancelled"
class ApprovalStatus(str, enum.Enum):
pending = "pending"
approved = "approved"
rejected = "rejected"

View File

@@ -0,0 +1,48 @@
"""Vessels and their onboard devices (inventory with encrypted credentials)."""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, Enum, ForeignKey, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
from app.models.enums import DeviceType
class Vessel(Base):
__tablename__ = "vessels"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(255), index=True)
site: Mapped[str | None] = mapped_column(String(255), nullable=True)
# Public IP / endpoint used to reach devices on this vessel (port forwards)
public_ip: Mapped[str | None] = mapped_column(String(255), nullable=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
devices: Mapped[list["Device"]] = relationship(
back_populates="vessel", cascade="all, delete-orphan"
)
class Device(Base):
__tablename__ = "devices"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(255), index=True)
# Catalog slot key (e.g. proxmox, pfsense, docker_vm)
catalog_key: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
device_type: Mapped[DeviceType] = mapped_column(Enum(DeviceType), default=DeviceType.other)
address: Mapped[str] = mapped_column(String(255))
port: Mapped[int | None] = mapped_column(Integer, nullable=True)
mcp_server: Mapped[str | None] = mapped_column(String(128), nullable=True)
username: Mapped[str | None] = mapped_column(String(255), nullable=True)
secret_enc: Mapped[str | None] = mapped_column(Text, nullable=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
vessel_id: Mapped[int | None] = mapped_column(
ForeignKey("vessels.id", ondelete="CASCADE"), nullable=True, index=True
)
vessel: Mapped[Vessel | None] = relationship(back_populates="devices")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

View File

@@ -0,0 +1,85 @@
"""Troubleshooting tasks, their event stream, and approval requests."""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, Enum, ForeignKey, Index, Integer, String, Text, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
from app.models.enums import ApprovalStatus, TaskStatus
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(255))
issue: Mapped[str] = mapped_column(Text) # description of the problem
details: Mapped[dict | None] = mapped_column(JSONB, nullable=True) # extra structured context
status: Mapped[TaskStatus] = mapped_column(Enum(TaskStatus), default=TaskStatus.queued, index=True)
device_id: Mapped[int | None] = mapped_column(
ForeignKey("devices.id", ondelete="SET NULL"), nullable=True
)
vessel_id: Mapped[int | None] = mapped_column(
ForeignKey("vessels.id", ondelete="SET NULL"), nullable=True, index=True
)
created_by_id: Mapped[int | None] = mapped_column(
ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
# Final structured report (issue, steps, root cause, resolution)
report: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
error: Mapped[str | None] = mapped_column(Text, nullable=True)
# Links to persisted artifacts
memory_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
obsidian_path: Mapped[str | None] = mapped_column(String(512), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
events: Mapped[list["TaskEvent"]] = relationship(
back_populates="task", cascade="all, delete-orphan", order_by="TaskEvent.id"
)
approvals: Mapped[list["ApprovalRequest"]] = relationship(
back_populates="task", cascade="all, delete-orphan"
)
class TaskEvent(Base):
__tablename__ = "task_events"
id: Mapped[int] = mapped_column(primary_key=True)
task_id: Mapped[int] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE"), index=True)
# node / kind: triage, connect, diagnose, reason, propose_fix, report, tool_call, log, error
kind: Mapped[str] = mapped_column(String(64))
message: Mapped[str | None] = mapped_column(Text, nullable=True)
payload: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
task: Mapped[Task] = relationship(back_populates="events")
class ApprovalRequest(Base):
__tablename__ = "approval_requests"
__table_args__ = (
Index("ix_approval_requests_task_status", "task_id", "status"),
)
id: Mapped[int] = mapped_column(primary_key=True)
task_id: Mapped[int] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE"), index=True)
tool_name: Mapped[str] = mapped_column(String(255))
tool_args: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
risk: Mapped[str | None] = mapped_column(Text, nullable=True)
status: Mapped[ApprovalStatus] = mapped_column(Enum(ApprovalStatus), default=ApprovalStatus.pending)
decided_by_id: Mapped[int | None] = mapped_column(
ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
decided_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
task: Mapped[Task] = relationship(back_populates="approvals")

View File

@@ -0,0 +1,22 @@
"""User accounts and roles."""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import Boolean, DateTime, Enum, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
from app.models.enums import Role
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
full_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
hashed_password: Mapped[str] = mapped_column(String(255))
role: Mapped[Role] = mapped_column(Enum(Role), default=Role.readonly)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())