generated from coulomb/repo-seed
feat(event-bridge): WP-0003a — domain model, rules module, event type registry
Implements phases 7–8 of the Event Bridge architecture (custodian-WP-0003a). Domain model (T34, T40): - Added RuleDef, InstructionDef, ActionDef to models.py - Updated ActivityDefinition with rules/instructions fields (task_templates deprecated) - Formalized EventEnvelope: id, type, version, timestamp, publisher, attributes - Added from_nats_message() and from_webhook_payload() classmethods Rules module (T35, T36, T37): - src/activity_core/rules/ skeleton with boundary enforcement - evaluate_condition() — sandboxed AST walker, whitelisted nodes only, never exec() - execute_instruction() — LLM task generation with trusted_fields injection guard - tests/rules/test_boundary.py verifies no cross-boundary imports Infrastructure (T38, T39): - Alembic migrations 0004 (task_spawn_log) and 0005 (event_types) - IssueSink ABC + IssueCoreRestSink (REST) + NullSink (testing) - TaskSpawnLog and EventType ORM models Event type registry (T41, T42, T43): - event_type_registry.py: file scanner, parser, DB sync, in-process lookup - ACTIVITY_CURATOR_GATE env var (disabled|required) + approve endpoint - Three org event type definitions: org.repo.registered, org.workstream.completed, org.activity.run.completed All 10 tests pass. Boundary test confirms rules/ isolation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,13 +36,18 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_asyn
|
||||
from temporalio.client import Client
|
||||
|
||||
from activity_core.models import ActivityDefinition, CronTriggerConfig
|
||||
from activity_core.orm import ActivityDefinition as ActivityDefinitionRow
|
||||
from activity_core.orm import ActivityDefinition as ActivityDefinitionRow, EventType as EventTypeRow
|
||||
from activity_core.schedule_manager import delete_schedule, upsert_schedule
|
||||
|
||||
TEMPORAL_HOST = os.environ.get("TEMPORAL_HOST", "localhost:7233")
|
||||
TEMPORAL_NAMESPACE = os.environ.get("TEMPORAL_NAMESPACE", "default")
|
||||
_ORCHESTRATOR_TASK_QUEUE = "orchestrator-tq"
|
||||
|
||||
# T42: Curator gate — controls which event type statuses are accepted by the router.
|
||||
# "disabled" (default): accepts "active" and "pending" types (pending logged as warning).
|
||||
# "required": only "active" types accepted; "pending" events discarded.
|
||||
ACTIVITY_CURATOR_GATE = os.environ.get("ACTIVITY_CURATOR_GATE", "disabled")
|
||||
|
||||
# --- App state ---------------------------------------------------------------
|
||||
|
||||
_session_factory: async_sessionmaker[AsyncSession] | None = None
|
||||
@@ -264,3 +269,28 @@ async def trigger_definition(definition_id: uuid.UUID) -> dict[str, str]:
|
||||
task_queue=_ORCHESTRATOR_TASK_QUEUE,
|
||||
)
|
||||
return {"workflow_id": handle.id, "trigger_key": trigger_key}
|
||||
|
||||
|
||||
# T42: Curator gate — event type approval endpoint
|
||||
|
||||
@app.post("/event-types/{type_id}/approve", status_code=200)
|
||||
async def approve_event_type(type_id: str) -> dict[str, str]:
|
||||
"""Approve a pending event type, setting its status to 'active'.
|
||||
|
||||
Only relevant when ACTIVITY_CURATOR_GATE=required. Requires admin access
|
||||
(same auth as the rest of the API).
|
||||
"""
|
||||
from sqlalchemy import text
|
||||
Session = _get_db()
|
||||
async with Session() as session:
|
||||
row = await session.get(EventTypeRow, type_id)
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail=f"Event type {type_id!r} not found")
|
||||
if row.status == "active":
|
||||
return {"type_id": type_id, "status": "active", "message": "already active"}
|
||||
async with session.begin():
|
||||
await session.execute(
|
||||
text("UPDATE event_types SET status = 'active' WHERE type_id = :tid"),
|
||||
{"tid": type_id},
|
||||
)
|
||||
return {"type_id": type_id, "status": "active", "message": "approved"}
|
||||
|
||||
Reference in New Issue
Block a user