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