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:
@@ -21,7 +21,6 @@ Usage:
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
@@ -112,9 +111,9 @@ class EventRouter:
|
||||
"""Return True if the envelope matches the EventTriggerConfig."""
|
||||
if envelope.type != cfg.event_type:
|
||||
return False
|
||||
# All filter key/value pairs must be present in envelope.payload.
|
||||
# All filter key/value pairs must be present in envelope.attributes.
|
||||
for key, value in cfg.filters.items():
|
||||
if envelope.payload.get(key) != value:
|
||||
if envelope.attributes.get(key) != value:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -122,15 +121,15 @@ class EventRouter:
|
||||
async def _dispatch(self, activity_id: str, envelope: EventEnvelope) -> None:
|
||||
"""Start RunActivityWorkflow for one matched activity.
|
||||
|
||||
Workflow ID is deterministic: activity-{activity_id}:{event_id}
|
||||
Workflow ID is deterministic: activity-{activity_id}:{id}
|
||||
REJECT_DUPLICATE prevents double-processing if the message is redelivered
|
||||
before ack reaches NATS.
|
||||
"""
|
||||
workflow_id = f"activity-{activity_id}:{envelope.event_id}"
|
||||
workflow_id = f"activity-{activity_id}:{envelope.id}"
|
||||
try:
|
||||
await self._temporal.start_workflow(
|
||||
"RunActivityWorkflow",
|
||||
args=[activity_id, envelope.event_id, envelope.occurred_at.isoformat()],
|
||||
args=[activity_id, envelope.id, envelope.timestamp.isoformat()],
|
||||
id=workflow_id,
|
||||
task_queue=_ORCHESTRATOR_TASK_QUEUE,
|
||||
id_conflict_policy=WorkflowIDConflictPolicy.FAIL,
|
||||
@@ -138,18 +137,17 @@ class EventRouter:
|
||||
logger.info(
|
||||
"started workflow %r for event %r (activity %s)",
|
||||
workflow_id,
|
||||
envelope.event_id,
|
||||
envelope.id,
|
||||
activity_id,
|
||||
)
|
||||
except WorkflowAlreadyStartedError:
|
||||
# Duplicate delivery — workflow already running or completed; safe to skip.
|
||||
logger.debug("duplicate event %r for activity %s — skipped", envelope.event_id, activity_id)
|
||||
logger.debug("duplicate event %r for activity %s — skipped", envelope.id, activity_id)
|
||||
|
||||
async def _handle_message(self, msg: Any) -> None:
|
||||
"""Decode a NATS message, match it against routing rules, and dispatch."""
|
||||
try:
|
||||
raw = json.loads(msg.data.decode())
|
||||
envelope = EventEnvelope.model_validate(raw)
|
||||
envelope = EventEnvelope.from_nats_message(msg)
|
||||
except Exception:
|
||||
logger.warning("failed to parse event envelope from NATS message — nacking")
|
||||
await msg.nak()
|
||||
@@ -160,7 +158,7 @@ class EventRouter:
|
||||
matched = [aid for aid, cfg in event_defs if self._matches(envelope, cfg)]
|
||||
|
||||
if not matched:
|
||||
logger.debug("event %r type=%r matched no definitions", envelope.event_id, envelope.type)
|
||||
logger.debug("event %r type=%r matched no definitions", envelope.id, envelope.type)
|
||||
await msg.ack()
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user