043aa18f3f
- New api_clients + api_client_scopes tables; tokens scoped per-instance
- Admin UI tab at /admin for token create/rotate/revoke/delete with one-time reveal
- Dual-auth dependency (user session OR Bearer app token) on trigger + runs endpoints
- /api/instances/{id}/trigger pre-creates a run and returns run_id + cached last_result instantly
- New GET /api/runs/{id} for polling
- Generic trigger path for sub-agent instances (weather, calendar, etc.)
- runs.result column for structured JSON alongside markdown output
- agent_catalog.result_schema describes each agent's result shape
- Weather, daily-briefing, project-monitor retrofitted to emit structured results
- log_run: env INSTANCE_ID/RUN_ID only used when target matches, so nested sub-agents don't clobber parent runs
- Wiki docs: API Clients & Token Scoping + Calling Agents From Your Apps
155 lines
6.7 KiB
Python
155 lines
6.7 KiB
Python
from sqlalchemy import Column, String, Text, DateTime, Integer, Boolean, ForeignKey, JSON
|
|
from sqlalchemy.orm import relationship
|
|
from datetime import datetime, timezone
|
|
from database import Base
|
|
|
|
|
|
class User(Base):
|
|
__tablename__ = "users"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
username = Column(String, unique=True, nullable=False)
|
|
email = Column(String, unique=True, nullable=True)
|
|
password_hash = Column(String, nullable=False)
|
|
display_name = Column(String, default="")
|
|
role = Column(String, default="user") # admin or user
|
|
llm_config = Column(JSON, default=dict) # user's own LLM provider config
|
|
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
|
|
|
instances = relationship("AgentInstance", back_populates="user")
|
|
|
|
|
|
class AgentCatalog(Base):
|
|
__tablename__ = "agent_catalog"
|
|
|
|
id = Column(String, primary_key=True)
|
|
name = Column(String, nullable=False)
|
|
description = Column(Text, default="")
|
|
category = Column(String, default="utility") # data, briefing, utility
|
|
config_schema = Column(JSON, default=dict)
|
|
default_config = Column(JSON, default=dict)
|
|
supports_schedule = Column(Boolean, default=True)
|
|
is_sub_agent = Column(Boolean, default=False)
|
|
requires_llm = Column(Boolean, default=False)
|
|
result_schema = Column(JSON, default=dict) # shape of the agent's structured result
|
|
|
|
instances = relationship("AgentInstance", back_populates="catalog_entry")
|
|
|
|
|
|
class AgentInstance(Base):
|
|
__tablename__ = "agent_instances"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
catalog_id = Column(String, ForeignKey("agent_catalog.id"), nullable=False)
|
|
name = Column(String, nullable=False)
|
|
config = Column(JSON, default=dict)
|
|
schedule = Column(String, default="manual")
|
|
status = Column(String, default="active")
|
|
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
|
|
|
user = relationship("User", back_populates="instances")
|
|
catalog_entry = relationship("AgentCatalog", back_populates="instances")
|
|
runs = relationship("Run", back_populates="instance", order_by="Run.started_at.desc()")
|
|
|
|
|
|
class Run(Base):
|
|
__tablename__ = "runs"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
instance_id = Column(Integer, ForeignKey("agent_instances.id"), nullable=False)
|
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
started_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
|
finished_at = Column(DateTime, nullable=True)
|
|
status = Column(String, default="running")
|
|
output = Column(Text, default="") # markdown rendering (for wiki)
|
|
result = Column(JSON, nullable=True) # structured data for API consumers
|
|
error = Column(Text, default="")
|
|
metadata_ = Column("metadata", JSON, default=dict)
|
|
triggered_by = Column(String, default="") # "user:eric", "api_client:synap", "cron"
|
|
|
|
instance = relationship("AgentInstance", back_populates="runs")
|
|
|
|
|
|
class Bridge(Base):
|
|
__tablename__ = "bridges"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
api_key = Column(String, nullable=False) # Auth token for bridge requests
|
|
bridge_url = Column(String, default="") # http://ip:port
|
|
hostname = Column(String, default="") # e.g. "Jungbauers-MBP"
|
|
platform = Column(String, default="macos") # macos, ios (future)
|
|
capabilities = Column(JSON, default=list) # ["notes", "reading-list"]
|
|
status = Column(String, default="offline") # online, offline
|
|
last_heartbeat = Column(DateTime, nullable=True)
|
|
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
|
|
|
user = relationship("User")
|
|
|
|
|
|
class RouteLog(Base):
|
|
__tablename__ = "route_log"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
request_text = Column(Text, nullable=False)
|
|
recommended_agent = Column(String, default="")
|
|
action = Column(String, default="")
|
|
reasoning = Column(Text, default="")
|
|
outcome = Column(String, default="pending") # pending, accepted, rejected, success, failed
|
|
metadata_ = Column("metadata", JSON, default=dict)
|
|
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
|
|
|
|
|
class LLMProvider(Base):
|
|
__tablename__ = "llm_providers"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
name = Column(String, nullable=False)
|
|
provider_type = Column(String, default="anthropic") # anthropic, openai, litellm, ollama
|
|
api_url = Column(String, default="")
|
|
api_key = Column(String, default="")
|
|
default_model = Column(String, default="")
|
|
is_default = Column(Boolean, default=False)
|
|
|
|
|
|
class APIClient(Base):
|
|
"""App-level API client. Each external app (Synap, WSIT, etc.) gets one.
|
|
Scoped to specific agent instances — see APIClientScope."""
|
|
__tablename__ = "api_clients"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
name = Column(String, nullable=False, unique=True) # "Synap", "WSIT"
|
|
token_hash = Column(String, nullable=False, unique=True) # SHA-256 of the plaintext token
|
|
token_prefix = Column(String, default="") # first 8 chars of token, shown in admin UI
|
|
description = Column(Text, default="")
|
|
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
|
last_used_at = Column(DateTime, nullable=True)
|
|
revoked_at = Column(DateTime, nullable=True)
|
|
|
|
scopes = relationship("APIClientScope", back_populates="client", cascade="all, delete-orphan")
|
|
|
|
|
|
class APIClientScope(Base):
|
|
"""Join table: which instances can an API client trigger/read?"""
|
|
__tablename__ = "api_client_scopes"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
api_client_id = Column(Integer, ForeignKey("api_clients.id", ondelete="CASCADE"), nullable=False)
|
|
instance_id = Column(Integer, ForeignKey("agent_instances.id", ondelete="CASCADE"), nullable=False)
|
|
|
|
client = relationship("APIClient", back_populates="scopes")
|
|
|
|
|
|
class APIClientCall(Base):
|
|
"""Audit log for every authenticated API call by an API client."""
|
|
__tablename__ = "api_client_calls"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
api_client_id = Column(Integer, ForeignKey("api_clients.id", ondelete="CASCADE"), nullable=False)
|
|
instance_id = Column(Integer, nullable=True) # may be null for non-instance endpoints
|
|
endpoint = Column(String, default="") # e.g. "POST /api/instances/2/trigger"
|
|
status_code = Column(Integer, default=0)
|
|
called_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|