orchestrata_ultimate.infra.prolog_bridge
Reference › orchestrata_ultimate › infra › prolog_bridge
The Python interface to SWI-Prolog for governance (Spec 007a). PrologBridge wraps the janus engine from core.prolog_adapter to execute governance rules, manage facts and keep session and request state. It is imported by the gateway server, the policy layers in governance and the agents' own Prolog bridge, and it satisfies GovernancePrologBridgeProtocol from core.protocols.decoupling.
The constructor takes an optional rules directory. load_rules consults one .pl file and load_all_rules loads every file in the directory, returning a count. query returns a QueryResult (success flag, solutions, execution time, error text); query_one returns the first solution and query_bool a success flag; queries are given the governance module prefix by _add_governance_prefix. assert_fact records a PrologFact with a FactScope (core.enums) as session, request or persistent, an optional session id and an optional TTL, and returns a deterministic fact id from _generate_fact_id; retract_fact removes it. start_session/end_session and start_request/end_request manage lifecycles and remove the facts of their scope, cleanup_expired_facts drops facts past their TTL, and get_fact_count reports counts by scope. Strings and keys are cleaned by _sanitize_string and _sanitize_key, and _python_to_prolog converts Python values to Prolog terms. The bridge also imports a Prolog worker from the orchestrator.
At a glance
| Source | src/orchestrata_ultimate/infra/prolog_bridge.py |
| Lines of code | 651 |
| Classes | 3 |
| Functions | 1 |
| Imports (internal) | orchestrata_ultimate.core.base.traces, orchestrata_ultimate.core.enums, orchestrata_ultimate.orchestrator.prolog_worker |
| Imported by | orchestrata_ultimate.agents.prolog_bridge, orchestrata_ultimate.gateway.server, orchestrata_ultimate.governance.policy_layers, orchestrata_ultimate.governance.prolog_client, orchestrata_ultimate.kernel.cli_main, orchestrata_ultimate.kernel.dispatcher, orchestrata_ultimate.kernel.pipeline_kg, orchestrata_ultimate.knowledge_graph.context_pack.relevance, orchestrata_ultimate.knowledge_graph.prolog_bridge, orchestrata_ultimate.orchestrator.engine, orchestrata_ultimate.orchestrator.intent_broker_factory, orchestrata_ultimate.orchestrator.intent_router, orchestrata_ultimate.orchestrator.intent_security, orchestrata_ultimate.orchestrator.intent_validator, orchestrata_ultimate.session.governance, orchestrata_ultimate.session.prolog_facts, orchestrata_ultimate.tools.approval, scripts.check_prolog_boundary, scripts.intent_demo |
| Tests that import it | tests/agents/test_prolog_bridge.py, tests/conftest.py, tests/factories/real.py, tests/gateway/handlers/test_policy_gate.py, tests/gateway/handlers/test_workflow.py, tests/gateway/test_publisher_wiring.py, tests/generated_agents/test_coordinator_generated.py, tests/generated_agents/test_intent_adapter_generated.py, tests/generated_agents/test_pool_generated.py, tests/generated_agents/test_postman_adapter_generated.py, tests/generated_agents/test_prolog_bridge_generated.py, tests/generated_agents/test_runtime_generated.py, tests/generated_agents/test_task_executor_generated.py, tests/generated_orchestrator/test_intent_validator_generated.py, tests/generated_orchestrator/test_prolog_bridge_generated.py, tests/generated_recovery/test_governance_event_emitting_policy_engine.py, tests/generated_recovery/test_governance_policy_engine.py, tests/governance/conftest.py, tests/governance/e2e/test_escalation_enforcement_proof.py, tests/governance/e2e/test_governance_prolog_client_enforcement_proof.py, tests/governance/e2e/test_retro_incidents.py, tests/governance/test_constitutional_real_rules.py, tests/governance/test_constitutional_suite.py, tests/governance/test_dynamic_policy_extended.py, tests/governance/test_error_paths.py … and 36 more |
Class diagram
Dependency diagram
API
Classes
PrologFact (dataclass)
Decorators: dataclass · Line 39
Represents a Prolog fact with metadata.
| Attribute | Type | Default |
|---|---|---|
fact_id |
str |
|
predicate |
str |
|
args |
list[Any] |
|
scope |
FactScope |
|
session_id |
str | None |
None |
ttl_seconds |
int | None |
None |
created_at |
datetime |
field(default_factory=_utc_now) |
| Method | Signature | Summary |
|---|---|---|
is_expired |
is_expired() -> bool |
Check if fact has exceeded TTL. |
QueryResult (dataclass)
Decorators: dataclass · Line 59
Result of a Prolog query.
| Attribute | Type | Default |
|---|---|---|
success |
bool |
|
solutions |
list[dict[str, Any]] |
field(default_factory=list) |
execution_time_ms |
float |
0.0 |
error |
str | None |
None |
| Method | Signature | Summary |
|---|---|---|
results property |
results() -> list[dict[str, Any]] |
Backward-compatible alias for solutions. |
PrologBridge
Line 73
Bridge to SWI-Prolog for governance rule execution.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(rules_dir: Path | None = None) -> None |
Initialize PrologBridge. |
_prolog property |
_prolog() -> Any |
Backward-compatible access to the Prolog engine. |
_prolog |
_prolog(value: Any) -> None |
Allow tests to inject a replacement Prolog engine. |
load_rules |
load_rules(rule_file: Path) -> bool |
Load a Prolog rule file. |
load_all_rules |
load_all_rules() -> int |
Load all .pl files from rules directory. |
_add_governance_prefix |
_add_governance_prefix(query_str: str) -> str |
Add governance module prefix to query if needed. |
query |
query(query_str: str) -> QueryResult |
Execute a Prolog query and return all solutions. |
query_one |
query_one(query_str: str) -> dict[str, Any] | None |
Execute a query and return first solution. |
query_bool |
query_bool(query_str: str) -> bool |
Execute a query and return True if it succeeds. |
assert_fact |
assert_fact(predicate: str, args: list[Any], scope: FactScope, session_id: str | None = None, ttl_seconds: int | None = None) -> str |
Assert a Prolog fact with lifecycle management. |
retract_fact |
retract_fact(fact_id: str) -> bool |
Retract a previously asserted fact. |
_generate_fact_id |
_generate_fact_id(predicate: str, args: list[Any]) -> str |
Generate deterministic fact ID. |
start_session |
start_session(session_id: str, goals: list[str], context: dict[str, Any]) -> None |
Start a new session with goals and context. |
end_session |
end_session(session_id: str) -> int |
End a session and clean up all session-scoped facts. |
start_request |
start_request(session_id: str, tool: str, intent: str) -> None |
Start a new request with tool and intent. |
end_request |
end_request() -> int |
End current request and clean up request-scoped facts. |
cleanup_expired_facts |
cleanup_expired_facts() -> int |
Remove facts that have exceeded their TTL. |
get_fact_count |
get_fact_count() -> dict[str, int] |
Get count of facts by scope. |
_sanitize_string |
_sanitize_string(value: str) -> str |
Sanitize a string for safe use in Prolog. |
_sanitize_key |
_sanitize_key(key: str) -> str |
Sanitize a dictionary key for use in Prolog. |
_python_to_prolog |
_python_to_prolog(value: Any) -> str |
Convert Python value to Prolog term string. |
Functions
| Function | Signature | Summary |
|---|---|---|
_utc_now |
_utc_now() -> datetime |
Get current UTC time with timezone info. |
Source
"""Prolog Bridge for Orchestrata Governance.
Provides a Python interface to SWI-Prolog for executing governance rules,
managing facts, and maintaining session/request state.
Key Features:
- Wraps the janus-swi Prolog engine (via core.prolog_adapter) for Prolog interaction
- Manages fact lifecycle with scoping (session, request, persistent)
- Supports TTL-based fact expiration
- Type conversion between Python and Prolog
- Session and request lifecycle management
- Deterministic fact ID generation
"""
# Spec: 007a - Prolog Governance
import hashlib
import logging
import re
import time
from dataclasses import dataclass, field
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.core.enums import FactScope
# Module-level logger
_logger = logging.getLogger(__name__)
def _utc_now() -> datetime:
"""Get current UTC time with timezone info."""
return datetime.now(UTC)
@dataclass
class PrologFact:
"""Represents a Prolog fact with metadata."""
fact_id: str
predicate: str
args: list[Any]
scope: FactScope
session_id: str | None = None
ttl_seconds: int | None = None
created_at: datetime = field(default_factory=_utc_now)
def is_expired(self) -> bool:
"""Check if fact has exceeded TTL."""
if self.ttl_seconds is None:
return False
elapsed = (_utc_now() - self.created_at).total_seconds()
return elapsed > self.ttl_seconds
@dataclass
class QueryResult:
"""Result of a Prolog query."""
success: bool
solutions: list[dict[str, Any]] = field(default_factory=list)
execution_time_ms: float = 0.0
error: str | None = None
@property
def results(self) -> list[dict[str, Any]]:
"""Backward-compatible alias for solutions."""
return self.solutions
class PrologBridge:
"""Bridge to SWI-Prolog for governance rule execution.
Manages Prolog facts with lifecycle scoping, TTL expiration,
and provides high-level query methods.
"""
def __init__(self, rules_dir: Path | None = None) -> None:
"""Initialize PrologBridge.
Args:
rules_dir: Directory containing .pl rule files
"""
# Lazy import: prolog_worker lives in orchestrator/ (L4) — a
# top-level import would be an L1->L4 upward edge.
from orchestrata_ultimate.orchestrator.prolog_worker import get_prolog_worker
self._worker = get_prolog_worker()
self._rules_dir = rules_dir
self._loaded_files: set[str] = set()
self._facts: dict[str, PrologFact] = {}
self._session_facts: dict[str, set[str]] = {} # session_id -> fact_ids
self._request_facts: set[str] = set() # fact_ids for current request
@property
def _prolog(self) -> Any:
"""Backward-compatible access to the Prolog engine.
Returns a PrologProxy that routes through the worker queue,
or a test-injected override if one was set via the setter.
"""
if hasattr(self, "_prolog_override") and self._prolog_override is not None:
return self._prolog_override
from orchestrata_ultimate.orchestrator.prolog_worker import PrologProxy
return PrologProxy(self._worker)
@_prolog.setter
def _prolog(self, value: Any) -> None:
"""Allow tests to inject a replacement Prolog engine."""
self._prolog_override = value
# ===== Rule Loading =====
def load_rules(self, rule_file: Path) -> bool:
"""Load a Prolog rule file.
Args:
rule_file: Path to .pl file
Returns:
True if loaded successfully, False otherwise
"""
if not rule_file.exists():
return False
abs_path = str(rule_file.absolute())
if abs_path in self._loaded_files:
return True # Already loaded
try:
self._prolog.consult(abs_path)
self._loaded_files.add(abs_path)
return True
except Exception as e:
_logger.error(f"Failed to load Prolog rules from {abs_path}: {e}")
return False
def load_all_rules(self) -> int:
"""Load all .pl files from rules directory.
Returns:
Number of files loaded
"""
if self._rules_dir is None or not self._rules_dir.exists():
return 0
count = 0
for rule_file in self._rules_dir.glob("*.pl"):
if self.load_rules(rule_file):
count += 1
return count
# Governance module predicates that need module prefix
GOVERNANCE_PREDICATES = {
"session_has_goals",
"session_blacklisted",
"session_active",
"session_checklist_complete",
"session_tests_passing",
"session_artifacts_valid",
"tool_blocked",
"intent_action",
"intent_resource",
"intent_constraints",
"volk_active",
"volk_blacklisted",
"volk_priority",
"volk_has_budget",
"volk_exists",
"resource_available",
"issue_severity",
"issue_security_related",
"action_type",
"action_violates_policy",
"action_in_protected_path",
"termination_reason_valid",
"volk_type_allowed",
"volk_type_blacklisted",
"resources_available",
# Also include exported governance predicates for queries
"can_open_session",
"can_close_session",
"session_valid",
"session_requirements_met",
"tool_allowed",
"policy_permits",
"tool_requires_approval",
"escalation_target",
"escalation_required",
"escalation_path_valid",
"can_allocate_resource",
"priority_allocation",
"resource_allocation_fair",
"can_hire_volk",
"can_fire_volk",
"volk_operational",
"audit_required",
"compliance_check_passed",
"compliance_violation",
"evaluate_policy",
"check_constraint",
"protected_path",
# Command classification predicates
"safe_command",
"destructive_command",
"destructive_command_lower",
"command_whitelisted",
"command_blacklisted",
}
# Knowledge module predicates that need module prefix
KNOWLEDGE_PREDICATES = {
"owns",
"depends_on",
"agent_scope_node",
"node_access_override",
}
def _add_governance_prefix(self, query_str: str) -> str:
"""Add governance module prefix to query if needed.
Args:
query_str: Original query string
Returns:
Query with governance: prefix if applicable
"""
# Skip if already has a module prefix
if ":" in query_str.split("(")[0]:
return query_str
# Check if query starts with a governance predicate
for pred in self.GOVERNANCE_PREDICATES:
if query_str.startswith(pred + "("):
return f"governance:{query_str}"
return query_str
# ===== Query Execution =====
def query(self, query_str: str) -> QueryResult:
"""Execute a Prolog query and return all solutions.
Args:
query_str: Prolog query string
Returns:
QueryResult with solutions and metadata
"""
# Auto-add governance module prefix if needed
query_str = self._add_governance_prefix(query_str)
start_time = time.time()
try:
solutions = self._prolog.query(query_str)
execution_time = (time.time() - start_time) * 1000
return QueryResult(
success=len(solutions) > 0,
solutions=solutions,
execution_time_ms=execution_time,
)
except Exception as e:
execution_time = (time.time() - start_time) * 1000
return QueryResult(
success=False,
solutions=[],
execution_time_ms=execution_time,
error=str(e),
)
def query_one(self, query_str: str) -> dict[str, Any] | None:
"""Execute a query and return first solution.
Args:
query_str: Prolog query string
Returns:
First solution dict or None
"""
result = self.query(query_str)
if result.success and len(result.solutions) > 0:
return result.solutions[0]
return None
def query_bool(self, query_str: str) -> bool:
"""Execute a query and return True if it succeeds.
Args:
query_str: Prolog query string
Returns:
True if query has at least one solution
"""
result = self.query(query_str)
return result.success
# ===== Fact Management =====
@traces(spec="001", story="US-004", reqs=["REQ-001-US-004-011"])
def assert_fact(
self,
predicate: str,
args: list[Any],
scope: FactScope,
session_id: str | None = None,
ttl_seconds: int | None = None,
) -> str:
"""Assert a Prolog fact with lifecycle management.
Args:
predicate: Predicate name
args: List of arguments
scope: Fact scope (session, request, persistent)
session_id: Session ID for session-scoped facts
ttl_seconds: Time-to-live in seconds
Returns:
Fact ID for later retraction
"""
fact_id = self._generate_fact_id(predicate, args)
# Convert args to Prolog format
prolog_args = [self._python_to_prolog(arg) for arg in args]
# Add module prefix for governance or knowledge predicates
if predicate in self.GOVERNANCE_PREDICATES:
fact_str = f"governance:{predicate}({', '.join(prolog_args)})"
elif predicate in self.KNOWLEDGE_PREDICATES:
fact_str = f"knowledge:{predicate}({', '.join(prolog_args)})"
else:
fact_str = f"{predicate}({', '.join(prolog_args)})"
# Assert to Prolog via worker queue
try:
self._prolog.assertz(fact_str)
except Exception as e:
# Log the error but continue - fact might already exist
_logger.debug(f"Prolog assertz failed for {fact_str}: {e}")
# Track fact metadata
fact = PrologFact(
fact_id=fact_id,
predicate=predicate,
args=args,
scope=scope,
session_id=session_id,
ttl_seconds=ttl_seconds,
)
self._facts[fact_id] = fact
# Add to scope-specific tracking
if scope == FactScope.SESSION and session_id:
if session_id not in self._session_facts:
self._session_facts[session_id] = set()
self._session_facts[session_id].add(fact_id)
elif scope == FactScope.REQUEST:
self._request_facts.add(fact_id)
return fact_id
@traces(spec="001", story="US-004", reqs=["REQ-001-US-004-011"])
def retract_fact(self, fact_id: str) -> bool:
"""Retract a previously asserted fact.
Args:
fact_id: Fact ID from assert_fact
Returns:
True if retracted, False if not found
"""
if fact_id not in self._facts:
return False
fact = self._facts[fact_id]
# Convert args to Prolog format
prolog_args = [self._python_to_prolog(arg) for arg in fact.args]
# Add module prefix for governance or knowledge predicates
if fact.predicate in self.GOVERNANCE_PREDICATES:
fact_str = f"governance:{fact.predicate}({', '.join(prolog_args)})"
elif fact.predicate in self.KNOWLEDGE_PREDICATES:
fact_str = f"knowledge:{fact.predicate}({', '.join(prolog_args)})"
else:
fact_str = f"{fact.predicate}({', '.join(prolog_args)})"
# Retract from Prolog via worker queue
try:
self._prolog.retract(fact_str)
except Exception as e:
# Log the error but continue - fact might already be retracted
_logger.debug(f"Prolog retract failed for {fact_str}: {e}")
# Remove from tracking
del self._facts[fact_id]
if fact.scope == FactScope.SESSION and fact.session_id:
if fact.session_id in self._session_facts:
self._session_facts[fact.session_id].discard(fact_id)
elif fact.scope == FactScope.REQUEST:
self._request_facts.discard(fact_id)
return True
def _generate_fact_id(self, predicate: str, args: list[Any]) -> str:
"""Generate deterministic fact ID.
Args:
predicate: Predicate name
args: List of arguments
Returns:
Deterministic fact ID
"""
content = f"{predicate}:{str(args)}"
hash_obj = hashlib.sha256(content.encode())
return f"fact-{hash_obj.hexdigest()[:16]}"
# ===== Session Lifecycle =====
@traces(spec="001", story="US-004", reqs=["REQ-001-US-004-011"])
def start_session(
self, session_id: str, goals: list[str], context: dict[str, Any]
) -> None:
"""Start a new session with goals and context.
Args:
session_id: Unique session identifier
goals: List of session goals
context: Session context dictionary
"""
# Assert session_active fact
self.assert_fact(
"session_active", [session_id], FactScope.SESSION, session_id=session_id
)
# Assert goals
for goal in goals:
self.assert_fact(
"session_goal",
[session_id, goal],
FactScope.SESSION,
session_id=session_id,
)
# Assert has_goals if goals present
if goals:
self.assert_fact(
"session_has_goals",
[session_id],
FactScope.SESSION,
session_id=session_id,
)
# Assert context facts
for key, value in context.items():
self.assert_fact(
"session_context",
[session_id, key, value],
FactScope.SESSION,
session_id=session_id,
)
@traces(spec="001", story="US-004", reqs=["REQ-001-US-004-011"])
def end_session(self, session_id: str) -> int:
"""End a session and clean up all session-scoped facts.
Args:
session_id: Session to end
Returns:
Number of facts retracted
"""
if session_id not in self._session_facts:
return 0
fact_ids = list(self._session_facts[session_id])
count = 0
for fact_id in fact_ids:
if self.retract_fact(fact_id):
count += 1
del self._session_facts[session_id]
return count
# ===== Request Lifecycle =====
def start_request(self, session_id: str, tool: str, intent: str) -> None:
"""Start a new request with tool and intent.
Args:
session_id: Current session ID
tool: Tool being executed
intent: Intent classification
"""
self.assert_fact(
"current_request", [session_id, tool, intent], FactScope.REQUEST
)
def end_request(self) -> int:
"""End current request and clean up request-scoped facts.
Returns:
Number of facts retracted
"""
fact_ids = list(self._request_facts)
count = 0
for fact_id in fact_ids:
if self.retract_fact(fact_id):
count += 1
return count
# ===== Fact Cleanup =====
def cleanup_expired_facts(self) -> int:
"""Remove facts that have exceeded their TTL.
Returns:
Number of facts cleaned up
"""
expired = []
for fact_id, fact in self._facts.items():
if fact.is_expired():
expired.append(fact_id)
count = 0
for fact_id in expired:
if self.retract_fact(fact_id):
count += 1
return count
def get_fact_count(self) -> dict[str, int]:
"""Get count of facts by scope.
Returns:
Dict with counts for each scope
"""
counts = {"session": 0, "request": 0, "persistent": 0}
for fact in self._facts.values():
counts[fact.scope.value] += 1
return counts
# ===== Type Conversion =====
def _sanitize_string(self, value: str) -> str:
"""Sanitize a string for safe use in Prolog.
Escapes all potentially dangerous characters to prevent injection.
Args:
value: Raw string value
Returns:
Sanitized string safe for Prolog
"""
# Escape backslashes first (must be done before other escapes)
result = value.replace("\\", "\\\\")
# Escape single quotes (Prolog string delimiter)
result = result.replace("'", "''")
# Escape double quotes
result = result.replace('"', '\\"')
# Escape newlines and carriage returns
result = result.replace("\n", "\\n")
result = result.replace("\r", "\\r")
# Escape tabs
result = result.replace("\t", "\\t")
# Escape backticks (can be special in some Prolog implementations)
result = result.replace("`", "\\`")
# Remove null bytes (security risk)
result = result.replace("\x00", "")
# Limit length to prevent DoS
return result[:4096]
def _sanitize_key(self, key: str) -> str:
"""Sanitize a dictionary key for use in Prolog.
Args:
key: Dictionary key
Returns:
Sanitized key safe for Prolog atoms
"""
# Only allow alphanumeric and underscores in keys
sanitized = re.sub(r"[^\w]", "_", str(key))
# Ensure key starts with lowercase letter (Prolog atom requirement)
if sanitized and not sanitized[0].islower():
sanitized = "k_" + sanitized
return sanitized[:256]
def _python_to_prolog(self, value: Any) -> str:
"""Convert Python value to Prolog term string.
Args:
value: Python value
Returns:
Prolog term string with proper escaping
"""
if isinstance(value, bool):
return "true" if value else "false"
elif isinstance(value, (int, float)):
# Validate numeric values
if not (-1e15 < value < 1e15):
_logger.warning(f"Numeric value {value} out of safe range, clamping")
value = max(-1e15, min(1e15, value))
return str(value)
elif isinstance(value, str):
# Full sanitization for strings
sanitized = self._sanitize_string(value)
return f"'{sanitized}'"
elif isinstance(value, list):
# Limit list size to prevent DoS
if len(value) > 1000:
_logger.warning(
f"List too long ({len(value)} items), truncating to 1000"
)
value = value[:1000]
prolog_items = [self._python_to_prolog(item) for item in value]
return f"[{','.join(prolog_items)}]"
elif isinstance(value, dict):
# Limit dict size to prevent DoS
if len(value) > 100:
_logger.warning(
f"Dict too large ({len(value)} items), truncating to 100"
)
value = dict(list(value.items())[:100])
# Convert dict to list of key-value pairs with sanitized keys
pairs = [
f"'{self._sanitize_key(k)}'-{self._python_to_prolog(v)}"
for k, v in value.items()
]
return f"[{','.join(pairs)}]"
else:
# Convert to string and sanitize
sanitized = self._sanitize_string(str(value))
return f"'{sanitized}'"