API Clients + structured JSON results: app-level tokens for Synap/WSIT integration

- 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
This commit is contained in:
Eric Jungbauer
2026-04-20 17:54:32 +00:00
parent f01553c511
commit 043aa18f3f
8 changed files with 983 additions and 111 deletions
+44 -1
View File
@@ -31,6 +31,7 @@ class AgentCatalog(Base):
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")
@@ -61,9 +62,11 @@ class Run(Base):
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="")
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")
@@ -109,3 +112,43 @@ class LLMProvider(Base):
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))