orchestrata_ultimate.postman.router
Reference › orchestrata_ultimate › postman › router
The routing engine of the broker, SPEC-002 story FR-003 (REQ-002-FR3-001 to -011), also traced to SPEC-001 message routing. RoutingEngine implements IRoutingEngine from interfaces and turns the recipient field of a MessageEnvelope into a list of destination agent ids. delivery is its importer. It is the broker-level counterpart of the kernel's CXPRouter, which routes from the volk registry instead.
get_routing_type classifies the recipient: empty raises RoutingError; *, broadcast or all is broadcast; a comma means multicast; a pattern like U:Cd (letter U, colon, a capital and a lower-case letter) is direct; a name containing ., * or ? is a topic; anything else is direct. resolve first looks in a route cache (the cache key combines sender and recipient; the cache time-to-live is 60 seconds by default), then calls _check_governance, then dispatches to _resolve_direct, _resolve_topic, _resolve_broadcast or _resolve_multicast, caches the answer and returns it. The built-in governance rule is simple: the orchestrator U:Ox may send anywhere, only the orchestrator may broadcast, and topic, multicast and direct routes are allowed; a denial raises RoutingError. Topic resolution unions the static routes with the subscribers of every matching pattern (fnmatch) and raises NoRouteFoundError when there are none. Multicast resolution requires every listed agent to be in the registered-agent set and raises NoRouteFoundError otherwise.
add_route and remove_route manage static routes, subscribe_to_topic and unsubscribe_from_topic manage topic patterns, and invalidate_cache clears cached routes, either all or those matching a pattern.
At a glance
| Source | src/orchestrata_ultimate/postman/router.py |
| Lines of code | 433 |
| Classes | 1 |
| Functions | 0 |
| Imports (internal) | orchestrata_ultimate.core.base.traces, orchestrata_ultimate.postman.constants, orchestrata_ultimate.postman.exceptions, orchestrata_ultimate.postman.interfaces, orchestrata_ultimate.postman.types |
| Imported by | orchestrata_ultimate.postman.delivery |
| Tests that import it | tests/postman/test_cov100_postman.py, tests/postman/test_delivery.py, tests/postman/test_delivery_routing_engine_wiring.py, tests/postman/test_postman_interface_compliance.py, tests/postman/test_postman_signatures.py, tests/postman/test_router.py, tests/test_postman.py |
Class diagram
Dependency diagram
API
Classes
RoutingEngine
Bases: IRoutingEngine · Decorators: traces(spec='002', story='FR-003', reqs=['REQ-002-FR3-001', 'REQ-002-FR3-002', 'REQ-002-FR3-004', 'REQ-002-FR3-005']) · Line 37
Routes messages to recipients based on routing rules.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(cache_ttl: int = ROUTER_CACHE_TTL_SECONDS) -> None |
Initialize routing engine. |
get_routing_type |
get_routing_type(recipient: str) -> str |
Determine routing type from recipient pattern. |
_check_governance |
async _check_governance(sender: str, recipient: str) -> tuple[bool, str] |
Check if sender has authority to send to recipient. |
_resolve_direct |
async _resolve_direct(recipient: str) -> list[str] |
Resolve direct routing to single agent. |
_resolve_topic |
async _resolve_topic(topic: str) -> list[str] |
Resolve topic routing to all subscribers. |
_resolve_broadcast |
async _resolve_broadcast() -> list[str] |
Resolve broadcast routing to all registered agents. |
_resolve_multicast |
async _resolve_multicast(recipient: str) -> list[str] |
Resolve multicast routing to multiple agents. |
_get_cached_route |
async _get_cached_route(cache_key: str) -> list[str] | None |
Get route from cache if not expired. |
_set_cached_route |
async _set_cached_route(cache_key: str, destinations: list[str]) -> None |
Store route in cache with timestamp. |
resolve |
async resolve(envelope: MessageEnvelope) -> list[str] |
Resolve envelope to destination agent IDs. |
add_route |
async add_route(pattern: str, destinations: list[str]) -> None |
Add static route. |
remove_route |
async remove_route(pattern: str) -> bool |
Remove route. |
invalidate_cache |
invalidate_cache(pattern: str | None = None) -> None |
Invalidate route cache. |
subscribe_to_topic |
async subscribe_to_topic(agent_id: str, topic_pattern: str) -> None |
Subscribe an agent to a topic pattern. |
unsubscribe_from_topic |
async unsubscribe_from_topic(agent_id: str, topic_pattern: str) -> bool |
Unsubscribe an agent from a topic pattern. |
Source
"""Routing engine for Postman message broker.
Implements the IRoutingEngine interface with support for:
- Direct routing to specific agents
- Topic-based routing to subscribers
- Broadcast routing to all agents
- Multicast routing to multiple agents
- PAM++ governance integration for authority checks
- Route caching with TTL
"""
import asyncio
import fnmatch
import logging
import re
import time
from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.postman.constants import ROUTER_CACHE_TTL_SECONDS
from orchestrata_ultimate.postman.exceptions import NoRouteFoundError, RoutingError
from orchestrata_ultimate.postman.interfaces import IRoutingEngine
from orchestrata_ultimate.postman.types import MessageEnvelope
logger = logging.getLogger(__name__)
@traces(
spec="002",
story="FR-003",
reqs=[
"REQ-002-FR3-001",
"REQ-002-FR3-002",
"REQ-002-FR3-004",
"REQ-002-FR3-005",
],
)
class RoutingEngine(IRoutingEngine):
"""Routes messages to recipients based on routing rules.
Supports direct, topic-based, broadcast, and multicast routing
with PAM++ governance integration for authority checks.
"""
# Known agent IDs for broadcast routing
REGISTERED_AGENTS = {
"U:Ox", # Orchestrator
"U:PS", # Postman
"U:Cd", # CodeDirector
"U:Ux", # UX
"U:PM", # PM
"U:Ar", # Architect
"U:Qa", # QA
"U:Cx", # Concierge
}
# Routing type constants
ROUTING_DIRECT = "direct"
ROUTING_TOPIC = "topic"
ROUTING_BROADCAST = "broadcast"
ROUTING_MULTICAST = "multicast"
@traces(spec="002", story="FR-003", reqs=["REQ-002-FR3-014"])
def __init__(self, cache_ttl: int = ROUTER_CACHE_TTL_SECONDS) -> None:
"""Initialize routing engine.
Args:
cache_ttl: Time-to-live for cached routes in seconds
"""
self._cache_ttl = cache_ttl
self._route_cache: dict[str, tuple[list[str], float]] = {}
self._cache_lock = asyncio.Lock()
# Static routes: pattern -> list of destinations
self._static_routes: dict[str, list[str]] = {}
self._routes_lock = asyncio.Lock()
# Topic subscriptions: topic_pattern -> set of subscriber agent IDs
self._topic_subscriptions: dict[str, set[str]] = {}
self._subscriptions_lock = asyncio.Lock()
@traces(
spec="002",
story="FR-003",
reqs=[
"REQ-002-FR3-001",
"REQ-002-FR3-002",
"REQ-002-FR3-004",
"REQ-002-FR3-005",
],
)
def get_routing_type(self, recipient: str) -> str:
"""Determine routing type from recipient pattern.
Args:
recipient: Recipient pattern from envelope
Returns:
Routing type constant (ROUTING_DIRECT, ROUTING_TOPIC, etc.)
"""
if not recipient:
raise RoutingError("Recipient cannot be empty")
# Broadcast patterns
if recipient in ("*", "broadcast", "all"):
return self.ROUTING_BROADCAST
# Multicast (comma-separated agent IDs)
if "," in recipient:
return self.ROUTING_MULTICAST
# Direct agent ID (U:XX format)
if re.match(r"^U:[A-Z][a-z]$", recipient):
return self.ROUTING_DIRECT
# Topic pattern (contains dots or wildcards)
if "." in recipient or "*" in recipient or "?" in recipient:
return self.ROUTING_TOPIC
# Default to direct
return self.ROUTING_DIRECT
@traces(spec="001", story="PAM-ENFORCEMENT", reqs=["REQ-001-US-003-011"])
async def _check_governance(self, sender: str, recipient: str) -> tuple[bool, str]:
"""Check if sender has authority to send to recipient.
This method implements basic governance rules and can be extended
to integrate with PolicyEvaluator when available.
Basic rules:
- U:Ox (Orchestrator) can send to anyone
- Agents can send to their known recipients
- Topics are generally allowed
- Broadcast requires orchestrator authority
Args:
sender: Sender agent ID
recipient: Recipient pattern or agent ID
Returns:
Tuple of (authorized, reason)
"""
# Orchestrator has universal authority
if sender == "U:Ox":
return True, "Orchestrator has universal authority"
# Determine routing type
routing_type = self.get_routing_type(recipient)
# Broadcast requires orchestrator authority
if routing_type == self.ROUTING_BROADCAST:
return False, f"Only orchestrator can broadcast, not {sender}"
# Topics are generally allowed
if routing_type == self.ROUTING_TOPIC:
return True, "Topic routing allowed"
# Multicast allowed for any agent
if routing_type == self.ROUTING_MULTICAST:
return True, "Multicast routing allowed"
# Direct routing - allow it, let _resolve_direct handle registration check
if routing_type == self.ROUTING_DIRECT:
return True, "Direct routing allowed"
# Default deny
return False, f"No governance rule matches for {sender} -> {recipient}"
@traces(spec="001", story="FR-003", reqs=["REQ-001-US-002-003"])
@traces(spec="002", story="FR-003", reqs=["REQ-002-FR3-006"])
async def _resolve_direct(self, recipient: str) -> list[str]:
"""Resolve direct routing to single agent.
Args:
recipient: Agent ID
Returns:
List containing single agent ID
Raises:
NoRouteFoundError: If agent is not registered
"""
if recipient not in self.REGISTERED_AGENTS:
raise NoRouteFoundError(f"Agent {recipient} is not registered")
return [recipient]
@traces(spec="002", story="FR-003", reqs=["REQ-002-FR3-007"])
async def _resolve_topic(self, topic: str) -> list[str]:
"""Resolve topic routing to all subscribers.
Args:
topic: Topic pattern
Returns:
List of subscriber agent IDs
Raises:
NoRouteFoundError: If no subscribers found
"""
subscribers: set[str] = set()
async with self._subscriptions_lock:
# Check static routes first
if topic in self._static_routes:
subscribers.update(self._static_routes[topic])
# Match against subscription patterns
for pattern, subs in self._topic_subscriptions.items():
if fnmatch.fnmatch(topic, pattern):
subscribers.update(subs)
if not subscribers:
raise NoRouteFoundError(f"No subscribers for topic {topic}")
return list(subscribers)
@traces(spec="001", story="MESSAGE-ROUTING", reqs=["REQ-001-US-002-005"])
async def _resolve_broadcast(self) -> list[str]:
"""Resolve broadcast routing to all registered agents.
Returns:
List of all registered agent IDs
"""
return list(self.REGISTERED_AGENTS)
@traces(spec="001", story="MESSAGE-ROUTING", reqs=["REQ-001-US-002-004"])
async def _resolve_multicast(self, recipient: str) -> list[str]:
"""Resolve multicast routing to multiple agents.
Args:
recipient: Comma-separated list of agent IDs
Returns:
List of agent IDs
Raises:
NoRouteFoundError: If any agent is not registered
"""
agent_ids = [aid.strip() for aid in recipient.split(",")]
# Validate all agents are registered
for agent_id in agent_ids:
if agent_id not in self.REGISTERED_AGENTS:
raise NoRouteFoundError(
f"Agent {agent_id} in multicast list is not registered"
)
return agent_ids
@traces(spec="002", story="FR-003", reqs=["REQ-002-FR3-011"])
async def _get_cached_route(self, cache_key: str) -> list[str] | None:
"""Get route from cache if not expired.
Args:
cache_key: Cache key for route
Returns:
Cached route list or None if expired/missing
"""
async with self._cache_lock:
if cache_key in self._route_cache:
destinations, timestamp = self._route_cache[cache_key]
if time.time() - timestamp < self._cache_ttl:
return destinations
# Expired - remove from cache
del self._route_cache[cache_key]
return None
@traces(spec="002", story="FR-003", reqs=["REQ-002-FR3-011"])
async def _set_cached_route(self, cache_key: str, destinations: list[str]) -> None:
"""Store route in cache with timestamp.
Args:
cache_key: Cache key for route
destinations: List of destination agent IDs
"""
async with self._cache_lock:
self._route_cache[cache_key] = (destinations, time.time())
@traces(spec="001", story="MESSAGE-ROUTING", reqs=["REQ-001-US-002-001"])
@traces(
spec="002", story="MESSAGE-ROUTING", reqs=["REQ-002-FR3-010", "REQ-002-FR3-011"]
)
async def resolve(self, envelope: MessageEnvelope) -> list[str]:
"""Resolve envelope to destination agent IDs.
Performs governance checks and routes based on recipient pattern.
Args:
envelope: Message envelope to route
Returns:
List of destination agent IDs
Raises:
RoutingError: If governance check fails
NoRouteFoundError: If no route can be found
"""
recipient = envelope.to
sender = envelope.from_
# Check cache first
cache_key = f"{sender}|{recipient}"
cached = await self._get_cached_route(cache_key)
if cached is not None:
return cached
# Governance check
authorized, reason = await self._check_governance(sender, recipient)
if not authorized:
raise RoutingError(
f"Governance denied: {reason} (sender={sender}, recipient={recipient})"
)
# Resolve based on routing type
routing_type = self.get_routing_type(recipient)
if routing_type == self.ROUTING_DIRECT:
destinations = await self._resolve_direct(recipient)
elif routing_type == self.ROUTING_TOPIC:
destinations = await self._resolve_topic(recipient)
elif routing_type == self.ROUTING_BROADCAST:
destinations = await self._resolve_broadcast()
elif routing_type == self.ROUTING_MULTICAST:
destinations = await self._resolve_multicast(recipient)
else:
raise RoutingError(f"Unknown routing type: {routing_type}")
# Cache the resolved route
await self._set_cached_route(cache_key, destinations)
return destinations
@traces(spec="002", story="US-005", reqs=["REQ-002-US-005-007"])
async def add_route(self, pattern: str, destinations: list[str]) -> None:
"""Add static route.
Args:
pattern: Route pattern (topic or agent ID)
destinations: List of destination agent IDs
"""
async with self._routes_lock:
self._static_routes[pattern] = destinations.copy()
# Invalidate cache for this pattern
self.invalidate_cache(pattern)
@traces(spec="002", story="US-005", reqs=["REQ-002-US-005-014"])
async def remove_route(self, pattern: str) -> bool:
"""Remove route.
Args:
pattern: Route pattern to remove
Returns:
True if route was removed, False if not found
"""
async with self._routes_lock:
if pattern in self._static_routes:
del self._static_routes[pattern]
self.invalidate_cache(pattern)
return True
return False
@traces(spec="002", story="FR-003", reqs=["REQ-002-FR3-012"])
def invalidate_cache(self, pattern: str | None = None) -> None:
"""Invalidate route cache.
Args:
pattern: Specific pattern to invalidate, or None for all
"""
if pattern is None:
# Clear entire cache
self._route_cache.clear()
else:
# Remove cache entries matching pattern
# Cache key format is "sender|recipient"
# For topic patterns, we need to match the recipient part
keys_to_remove = []
for key in self._route_cache.keys():
# Extract recipient from cache key (format: "sender|recipient")
parts = key.split("|", 1)
if len(parts) == 2:
recipient = parts[1]
# Check if pattern matches recipient using fnmatch
if fnmatch.fnmatch(recipient, pattern) or pattern in recipient:
keys_to_remove.append(key)
elif pattern in key:
keys_to_remove.append(key)
for key in keys_to_remove:
del self._route_cache[key]
@traces(spec="002", story="US-002", reqs=["REQ-002-US-002-001"])
async def subscribe_to_topic(self, agent_id: str, topic_pattern: str) -> None:
"""Subscribe an agent to a topic pattern.
Args:
agent_id: Agent ID to subscribe
topic_pattern: Topic pattern to subscribe to
"""
async with self._subscriptions_lock:
if topic_pattern not in self._topic_subscriptions:
self._topic_subscriptions[topic_pattern] = set()
self._topic_subscriptions[topic_pattern].add(agent_id)
# Invalidate cache for this topic
self.invalidate_cache(topic_pattern)
@traces(
spec="002",
story="US-002",
reqs=["REQ-002-US-002-002", "REQ-002-FR5-011"],
)
async def unsubscribe_from_topic(self, agent_id: str, topic_pattern: str) -> bool:
"""Unsubscribe an agent from a topic pattern.
Args:
agent_id: Agent ID to unsubscribe
topic_pattern: Topic pattern to unsubscribe from
Returns:
True if unsubscribed, False if not found
"""
async with self._subscriptions_lock:
if topic_pattern in self._topic_subscriptions:
if agent_id in self._topic_subscriptions[topic_pattern]:
self._topic_subscriptions[topic_pattern].remove(agent_id)
# Clean up empty subscription sets
if not self._topic_subscriptions[topic_pattern]:
del self._topic_subscriptions[topic_pattern]
self.invalidate_cache(topic_pattern)
return True
return False