Initial commit: add .gitignore and README
Some checks failed
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
Tests / lint (push) Has been cancelled
Tests / docker (push) Has been cancelled

This commit is contained in:
defiQUG
2026-02-09 21:51:42 -08:00
commit c052b07662
3146 changed files with 808305 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
"""Governance and safety: guardrails, rate limiting, access control, override, audit, policy, intent alignment."""
from fusionagi.governance.guardrails import Guardrails, PreCheckResult
from fusionagi.governance.rate_limiter import RateLimiter
from fusionagi.governance.access_control import AccessControl
from fusionagi.governance.override import OverrideHooks
from fusionagi.governance.audit_log import AuditLog
from fusionagi.governance.policy_engine import PolicyEngine
from fusionagi.governance.intent_alignment import IntentAlignment
from fusionagi.governance.safety_pipeline import (
SafetyPipeline,
InputModerator,
OutputScanner,
ModerationResult,
OutputScanResult,
)
__all__ = [
"Guardrails",
"PreCheckResult",
"RateLimiter",
"AccessControl",
"OverrideHooks",
"AuditLog",
"PolicyEngine",
"IntentAlignment",
"SafetyPipeline",
"InputModerator",
"OutputScanner",
"ModerationResult",
"OutputScanResult",
]

View File

@@ -0,0 +1,30 @@
"""Tool access control: central policy for which agent may call which tools.
Optional; not wired to Executor or Orchestrator by default. Wire by passing
an AccessControl instance and checking allowed(agent_id, tool_name, task_id)
before tool invocation.
"""
class AccessControl:
"""Policy: (agent_id, tool_name, task_id) -> allowed."""
def __init__(self) -> None:
self._deny: set[tuple[str, str]] = set()
self._task_tools: dict[str, set[str]] = {}
def deny(self, agent_id: str, tool_name: str) -> None:
"""Deny agent from using tool (global)."""
self._deny.add((agent_id, tool_name))
def allow_tools_for_task(self, task_id: str, tool_names: list[str]) -> None:
"""Set allowed tools for a task (empty = all allowed)."""
self._task_tools[task_id] = set(tool_names)
def allowed(self, agent_id: str, tool_name: str, task_id: str | None = None) -> bool:
"""Return True if agent may call tool (optionally for this task)."""
if (agent_id, tool_name) in self._deny:
return False
if task_id and task_id in self._task_tools:
return tool_name in self._task_tools[task_id]
return True

View File

@@ -0,0 +1,29 @@
"""Structured audit log for AGI."""
from typing import Any
from fusionagi.schemas.audit import AuditEntry, AuditEventType
from fusionagi._logger import logger
import uuid
class AuditLog:
def __init__(self, max_entries=100000):
self._entries = []
self._max_entries = max_entries
self._by_task = {}
self._by_type = {}
def append(self, event_type, actor, action="", task_id=None, payload=None, outcome=""):
entry_id = str(uuid.uuid4())
entry = AuditEntry(entry_id=entry_id, event_type=event_type, actor=actor, task_id=task_id, action=action, payload=payload or {}, outcome=outcome)
if len(self._entries) >= self._max_entries:
self._entries.pop(0)
idx = len(self._entries)
self._entries.append(entry)
if entry.task_id:
self._by_task.setdefault(entry.task_id, []).append(idx)
self._by_type.setdefault(entry.event_type.value, []).append(idx)
return entry_id
def get_by_task(self, task_id, limit=100):
indices = self._by_task.get(task_id, [])[-limit:]
return [self._entries[i] for i in indices if i < len(self._entries)]
def get_by_type(self, event_type, limit=100):
indices = self._by_type.get(event_type.value, [])[-limit:]
return [self._entries[i] for i in indices if i < len(self._entries)]

View File

@@ -0,0 +1,71 @@
"""Guardrails: pre/post checks for tool calls (block paths, sanitize inputs)."""
import re
from typing import Any
from pydantic import BaseModel, Field
from fusionagi._logger import logger
class PreCheckResult(BaseModel):
"""Result of a guardrails pre-check: allowed, optional sanitized args, optional error message."""
allowed: bool = Field(..., description="Whether the call is allowed")
sanitized_args: dict[str, Any] | None = Field(default=None, description="Args to use if allowed and sanitized")
error_message: str | None = Field(default=None, description="Reason for denial if not allowed")
class Guardrails:
"""Pre/post checks for tool invocations."""
def __init__(self) -> None:
self._blocked_paths: list[str] = []
self._blocked_patterns: list[re.Pattern[str]] = []
self._custom_checks: list[Any] = []
def block_path_prefix(self, prefix: str) -> None:
"""Block any file path starting with this prefix."""
self._blocked_paths.append(prefix.rstrip("/"))
def block_path_pattern(self, pattern: str) -> None:
"""Block paths matching this regex."""
self._blocked_patterns.append(re.compile(pattern))
def add_check(self, check: Any) -> None:
"""
Add a custom pre-check. Check receives (tool_name, args); must not mutate caller's args.
Returns (allowed, sanitized_args or error_message): (True, dict) or (True, None) or (False, str).
Returned sanitized_args are used for subsequent checks and invocation.
"""
self._custom_checks.append(check)
def pre_check(self, tool_name: str, args: dict[str, Any]) -> PreCheckResult:
"""Run all pre-checks. Returns PreCheckResult (allowed, sanitized_args, error_message)."""
args = dict(args) # Copy to avoid mutating caller's args
for key in ("path", "file_path"):
if key in args and isinstance(args[key], str):
path = args[key]
for prefix in self._blocked_paths:
if path.startswith(prefix) or path.startswith(prefix + "/"):
reason = "Blocked path prefix: " + prefix
logger.info("Guardrails pre_check blocked", extra={"tool_name": tool_name, "reason": reason})
return PreCheckResult(allowed=False, error_message=reason)
for pat in self._blocked_patterns:
if pat.search(path):
reason = "Blocked path pattern"
logger.info("Guardrails pre_check blocked", extra={"tool_name": tool_name, "reason": reason})
return PreCheckResult(allowed=False, error_message=reason)
for check in self._custom_checks:
allowed, result = check(tool_name, args)
if not allowed:
reason = result if isinstance(result, str) else "Check failed"
logger.info("Guardrails pre_check blocked", extra={"tool_name": tool_name, "reason": reason})
return PreCheckResult(allowed=False, error_message=reason)
if isinstance(result, dict):
args = result
return PreCheckResult(allowed=True, sanitized_args=args)
def post_check(self, tool_name: str, result: Any) -> tuple[bool, str]:
"""Optional post-check; return (True, "") or (False, error_message)."""
return True, ""

View File

@@ -0,0 +1,29 @@
"""Intent alignment: what user meant vs what user said for AGI."""
from typing import Any
from fusionagi._logger import logger
class IntentAlignment:
"""
Checks that system interpretation of user goal matches user intent.
Placeholder: returns True; wire to confirmation or paraphrase flow.
"""
def __init__(self) -> None:
self._checks: list[tuple[str, str]] = [] # (interpreted_goal, user_input)
def check(self, interpreted_goal: str, user_input: str, context: dict[str, Any] | None = None) -> tuple[bool, str]:
"""
Returns (aligned, message). If not aligned, message suggests clarification.
"""
if not interpreted_goal or not user_input:
return True, ""
self._checks.append((interpreted_goal, user_input))
logger.debug("IntentAlignment check", extra={"goal": interpreted_goal[:80]})
return True, ""
def suggest_paraphrase(self, goal: str) -> str:
"""Return suggested paraphrase for user to confirm."""
return f"Just to confirm, you want: {goal}"

View File

@@ -0,0 +1,44 @@
"""Human override hooks: events the orchestrator can fire before high-risk steps."""
from typing import Any, Callable
from fusionagi._logger import logger
# Callback: (event_type, payload) -> proceed: bool
OverrideCallback = Callable[[str, dict[str, Any]], bool]
class OverrideHooks:
"""Optional callbacks for human override; no UI, just interface and logging."""
def __init__(self) -> None:
self._hooks: list[OverrideCallback] = []
self._log: list[dict[str, Any]] = []
def register(self, callback: OverrideCallback) -> None:
"""Register a callback; if any returns False, treat as 'do not proceed'."""
self._hooks.append(callback)
def fire(self, event_type: str, payload: dict[str, Any]) -> bool:
"""
Fire event (e.g. task_paused_for_approval). If no hooks, return True (proceed).
If any hook returns False, return False (do not proceed). Log all events.
Exception in a hook implies do not proceed.
"""
entry = {"event": event_type, "payload": payload}
self._log.append(entry)
logger.info("Override fire", extra={"event_type": event_type})
for h in self._hooks:
try:
if not h(event_type, payload):
logger.info("Override hook returned do not proceed", extra={"event_type": event_type})
return False
except Exception:
logger.exception("Override hook raised", extra={"event_type": event_type})
return False
logger.debug("Override fire proceed", extra={"event_type": event_type})
return True
def get_log(self, limit: int = 100) -> list[dict[str, Any]]:
"""Return recent override events (for auditing)."""
return list(self._log[-limit:])

View File

@@ -0,0 +1,73 @@
"""Policy engine: hard constraints independent of LLM for AGI."""
from typing import Any
from fusionagi.schemas.policy import PolicyEffect, PolicyRule
from fusionagi._logger import logger
class PolicyEngine:
"""Evaluates policy rules; higher priority first; first match wins (allow/deny)."""
def __init__(self) -> None:
self._rules: list[PolicyRule] = []
def add_rule(self, rule: PolicyRule) -> None:
self._rules.append(rule)
self._rules.sort(key=lambda r: -r.priority)
logger.debug("PolicyEngine: rule added", extra={"rule_id": rule.rule_id})
def get_rules(self) -> list[PolicyRule]:
"""Return all rules (copy)."""
return list(self._rules)
def get_rule(self, rule_id: str) -> PolicyRule | None:
"""Return rule by id or None."""
for r in self._rules:
if r.rule_id == rule_id:
return r
return None
def update_rule(self, rule_id: str, updates: dict[str, Any]) -> bool:
"""
Update an existing rule by id. Updates can include condition, effect, reason, priority.
Returns True if updated, False if rule_id not found.
"""
for i, r in enumerate(self._rules):
if r.rule_id == rule_id:
allowed = {"condition", "effect", "reason", "priority"}
data = r.model_dump()
for k, v in updates.items():
if k in allowed:
data[k] = v
self._rules[i] = PolicyRule.model_validate(data)
self._rules.sort(key=lambda x: -x.priority)
logger.debug("PolicyEngine: rule updated", extra={"rule_id": rule_id})
return True
return False
def remove_rule(self, rule_id: str) -> bool:
"""Remove a rule by id. Returns True if removed."""
for i, r in enumerate(self._rules):
if r.rule_id == rule_id:
self._rules.pop(i)
logger.debug("PolicyEngine: rule removed", extra={"rule_id": rule_id})
return True
return False
def check(self, action: str, context: dict[str, Any]) -> tuple[bool, str]:
"""
Returns (allowed, reason). Context has e.g. tool_name, domain, data_class, agent_id.
"""
for rule in self._rules:
if self._match(rule.condition, context):
if rule.effect == PolicyEffect.DENY:
return False, rule.reason or "Policy denied"
return True, rule.reason or "Policy allowed"
return True, ""
def _match(self, condition: dict[str, Any], context: dict[str, Any]) -> bool:
for k, v in condition.items():
if context.get(k) != v:
return False
return True

View File

@@ -0,0 +1,38 @@
"""Rate limiting: per agent or per tool; reject or queue if exceeded.
Optional; not wired to Executor or Orchestrator by default. Wire by calling
allow(key) before tool invocation or message routing and checking the result.
"""
import time
from collections import defaultdict
from fusionagi._logger import logger
class RateLimiter:
"""Simple in-memory rate limiter: max N calls per window_seconds per key."""
def __init__(self, max_calls: int = 60, window_seconds: float = 60.0) -> None:
self._max_calls = max_calls
self._window = window_seconds
self._calls: dict[str, list[float]] = defaultdict(list)
def allow(self, key: str) -> tuple[bool, str]:
"""Record a call for key; return (True, "") or (False, reason)."""
now = time.monotonic()
cutoff = now - self._window
self._calls[key] = [t for t in self._calls[key] if t > cutoff]
if len(self._calls[key]) >= self._max_calls:
reason = f"Rate limit exceeded for {key}"
logger.info("Rate limiter rejected", extra={"key": key, "reason": reason})
return False, reason
self._calls[key].append(now)
return True, ""
def reset(self, key: str | None = None) -> None:
"""Reset counts for key or all."""
if key is None:
self._calls.clear()
else:
self._calls.pop(key, None)

View File

@@ -0,0 +1,132 @@
"""Safety pipeline: pre-check (input moderation), post-check (output scan)."""
import re
from dataclasses import dataclass
from typing import Any
from fusionagi.governance.guardrails import Guardrails, PreCheckResult
from fusionagi.schemas.audit import AuditEventType
from fusionagi._logger import logger
@dataclass
class ModerationResult:
"""Result of input moderation."""
allowed: bool
transformed: str | None = None
reason: str | None = None
class InputModerator:
"""Pre-check: block or transform user input before processing."""
def __init__(self) -> None:
self._blocked_patterns: list[re.Pattern[str]] = []
self._blocked_phrases: list[str] = []
def add_blocked_pattern(self, pattern: str) -> None:
"""Add regex pattern to block (e.g. prompt injection attempts)."""
self._blocked_patterns.append(re.compile(pattern, re.I))
def add_blocked_phrase(self, phrase: str) -> None:
"""Add exact phrase to block."""
self._blocked_phrases.append(phrase.lower())
def moderate(self, text: str) -> ModerationResult:
"""Check input; return allowed/denied and optional transformed text."""
if not text or not text.strip():
return ModerationResult(allowed=False, reason="Empty input")
lowered = text.lower()
for phrase in self._blocked_phrases:
if phrase in lowered:
logger.info("Input blocked: blocked phrase", extra={"phrase": phrase[:50]})
return ModerationResult(allowed=False, reason=f"Blocked phrase: {phrase[:30]}...")
for pat in self._blocked_patterns:
if pat.search(text):
logger.info("Input blocked: pattern match", extra={"pattern": pat.pattern[:50]})
return ModerationResult(allowed=False, reason="Input matched blocked pattern")
return ModerationResult(allowed=True)
@dataclass
class OutputScanResult:
"""Result of output (final answer) scan."""
passed: bool
flags: list[str]
sanitized: str | None = None
class OutputScanner:
"""Post-check: scan final answer for policy violations, PII leakage."""
def __init__(self) -> None:
self._pii_patterns: list[tuple[str, re.Pattern[str]]] = [
("ssn", re.compile(r"\b\d{3}-\d{2}-\d{4}\b")),
("credit_card", re.compile(r"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b")),
]
self._blocked_patterns: list[re.Pattern[str]] = []
def add_pii_pattern(self, name: str, pattern: str) -> None:
"""Add PII detection pattern."""
self._pii_patterns.append((name, re.compile(pattern)))
def add_blocked_pattern(self, pattern: str) -> None:
"""Add pattern that fails the output."""
self._blocked_patterns.append(re.compile(pattern, re.I))
def scan(self, text: str) -> OutputScanResult:
"""Scan output; return passed, flags, optional sanitized."""
flags: list[str] = []
for name, pat in self._pii_patterns:
if pat.search(text):
flags.append(f"potential_pii:{name}")
for pat in self._blocked_patterns:
if pat.search(text):
flags.append("blocked_content_detected")
if flags:
return OutputScanResult(passed=False, flags=flags)
return OutputScanResult(passed=True, flags=[])
class SafetyPipeline:
"""Combined pre/post safety checks for Dvādaśa."""
def __init__(
self,
moderator: InputModerator | None = None,
scanner: OutputScanner | None = None,
guardrails: Guardrails | None = None,
audit_log: Any | None = None,
) -> None:
self._moderator = moderator or InputModerator()
self._scanner = scanner or OutputScanner()
self._guardrails = guardrails or Guardrails()
self._audit = audit_log
def pre_check(self, user_input: str) -> ModerationResult:
"""Run input moderation."""
result = self._moderator.moderate(user_input)
if self._audit and not result.allowed:
self._audit.append(
AuditEventType.POLICY_CHECK,
actor="safety_pipeline",
action="input_moderation",
payload={"reason": result.reason},
outcome="denied",
)
return result
def post_check(self, final_answer: str) -> OutputScanResult:
"""Run output scan."""
result = self._scanner.scan(final_answer)
if self._audit and not result.passed:
self._audit.append(
AuditEventType.POLICY_CHECK,
actor="safety_pipeline",
action="output_scan",
payload={"flags": result.flags},
outcome="flagged",
)
return result