GaryLeckey's picture
Deploy Aureon OS (deploy/selfhost stack: nginx edge + fusion API x2 + console + HNC organ + redis + postgres; paper mode) (part 3)
1ccf652 verified
Raw History Blame Contribute Delete
16 kB
"""
SkillExecutor β€” runs validated skills in the safe exec sandbox
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Executes skills against:
β€’ A VMControlDispatcher (VM target)
β€’ The local system (local target β€” future; currently the same
dispatcher can be pointed at a real Windows VM via WinRM)
For compound / workflow / role skills, the executor resolves dependencies
via the SkillLibrary and caches compiled function objects to avoid
recompiling on every call.
Safety:
β€’ Uses `primitives.get_safe_globals(dispatcher, thought_bus, executor)`
as the exec globals dict (SAFE_BUILTINS only).
β€’ Re-runs the static safety check before first execution.
β€’ Bounded recursion depth (max 10) to prevent infinite compound chains.
β€’ Per-execution timeout via watchdog thread (soft β€” Python can't hard
cancel arbitrary code, but we record and flag long runs).
"""
from __future__ import annotations
import logging
import threading
import time
import traceback
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional
from aureon.code_architect.skill import Skill, SkillStatus
from aureon.code_architect.skill_library import SkillLibrary
from aureon.code_architect.primitives import get_safe_globals
from aureon.code_architect.validator import SkillValidator
logger = logging.getLogger("aureon.code_architect.executor")
MAX_DEPTH = 10
# ─────────────────────────────────────────────────────────────────────────────
# Result dataclass
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class SkillExecutionResult:
skill_name: str = ""
ok: bool = False
return_value: Any = None
error: Optional[str] = None
traceback: Optional[str] = None
duration_s: float = 0.0
depth: int = 0
timestamp: float = field(default_factory=time.time)
sub_results: List[Dict[str, Any]] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]:
return {
"skill_name": self.skill_name,
"ok": self.ok,
"return_value": self.return_value,
"error": self.error,
"traceback": self.traceback,
"duration_s": round(self.duration_s, 4),
"depth": self.depth,
"timestamp": self.timestamp,
"sub_results_count": len(self.sub_results),
}
# ─────────────────────────────────────────────────────────────────────────────
# SkillExecutor
# ─────────────────────────────────────────────────────────────────────────────
class SkillExecutor:
"""
Runs skills in a safe exec sandbox.
Attributes:
library : the SkillLibrary (source of skills)
dispatcher : the VMControlDispatcher (backends for vm_* primitives)
validator : the SkillValidator (re-runs static safety on first use)
thought_bus: optional ThoughtBus for emit_event primitive
"""
def __init__(
self,
library: Optional[SkillLibrary] = None,
dispatcher: Any = None,
validator: Optional[SkillValidator] = None,
thought_bus: Any = None,
require_static_check: bool = True,
):
self.library = library
self.dispatcher = dispatcher
self.validator = validator or SkillValidator()
self.thought_bus = thought_bus
self.require_static_check = require_static_check
self._compiled_cache: Dict[str, Callable] = {} # skill_name β†’ entry function
self._lock = threading.RLock()
# Metrics
self._total_executions = 0
self._successes = 0
self._failures = 0
# ─────────────────────────────────────────────────────────────────────
# Compilation
# ─────────────────────────────────────────────────────────────────────
def _safe_globals(self) -> Dict[str, Any]:
return get_safe_globals(
dispatcher=self.dispatcher,
thought_bus=self.thought_bus,
executor=self,
)
def _compile(self, skill: Skill) -> Callable:
"""Compile a skill's source into a callable (cached)."""
with self._lock:
cached = self._compiled_cache.get(skill.name)
if cached is not None:
return cached
# Belt & braces: re-run static check before compile
if self.require_static_check:
safe, errors = self.validator.static_check(skill.code)
if not safe:
raise RuntimeError(f"skill {skill.name} failed static safety: {errors[:3]}")
# exec inside a *fresh* globals dict for compilation so function refs
# are properly bound. The execution will use _safe_globals() again.
compile_globals = self._safe_globals()
local_scope: Dict[str, Any] = {}
try:
compiled_code = compile(skill.code, f"<skill:{skill.name}>", "exec")
exec(compiled_code, compile_globals, local_scope)
except Exception as e:
raise RuntimeError(f"skill {skill.name} compile failed: {e}")
entry = skill.entry_function or skill.name
func = local_scope.get(entry)
if not callable(func):
raise RuntimeError(f"skill {skill.name} has no callable entry function '{entry}'")
with self._lock:
self._compiled_cache[skill.name] = func
return func
def invalidate_cache(self, skill_name: Optional[str] = None) -> None:
with self._lock:
if skill_name is None:
self._compiled_cache.clear()
else:
self._compiled_cache.pop(skill_name, None)
def _execution_is_simulated(self, params: Optional[Dict[str, Any]]) -> bool:
"""Return True only when the selected dispatcher session is in-memory."""
if self.dispatcher is None:
return False
try:
session_id = (params or {}).get("session_id")
controller = self.dispatcher.get_session(session_id)
session = getattr(controller, "session", None)
return str(getattr(session, "backend", "")).casefold() == "simulated"
except Exception:
return False
def _blocked_execution(
self,
skill: Skill,
*,
error: str,
depth: int,
started_at: float,
) -> SkillExecutionResult:
self.invalidate_cache(skill.name)
result = SkillExecutionResult(
skill_name=skill.name,
ok=False,
error=error,
depth=depth,
duration_s=time.time() - started_at,
)
self._failures += 1
if self.library:
self.library.record_execution(
skill.name,
success=False,
duration_s=result.duration_s,
error=result.error,
)
return result
# ─────────────────────────────────────────────────────────────────────
# Execution
# ─────────────────────────────────────────────────────────────────────
def execute(
self,
skill: Skill,
params: Optional[Dict[str, Any]] = None,
_depth: int = 0,
) -> SkillExecutionResult:
"""Execute a Skill object."""
if _depth > MAX_DEPTH:
return SkillExecutionResult(
skill_name=skill.name,
ok=False,
error=f"max_recursion_depth_exceeded: {_depth}",
depth=_depth,
)
self._total_executions += 1
start = time.time()
result = SkillExecutionResult(skill_name=skill.name, depth=_depth)
status_value = str(getattr(skill.status, "value", skill.status) or "").casefold()
non_executable_statuses = {
"proposed",
"pending",
"pending_review",
"rejected",
"blocked",
"deprecated",
}
if status_value in non_executable_statuses:
return self._blocked_execution(
skill,
error=f"skill_status_not_executable:{status_value or 'unknown'}",
depth=_depth,
started_at=start,
)
tags = set(skill.tags or [])
if (
"requires_explicit_approval" in tags
and status_value not in {SkillStatus.APPROVED.value, SkillStatus.ACTIVE.value}
):
return self._blocked_execution(
skill,
error="skill_approval_required",
depth=_depth,
started_at=start,
)
if "live_execution_disabled" in tags and not self._execution_is_simulated(params):
return self._blocked_execution(
skill,
error="skill_live_execution_disabled",
depth=_depth,
started_at=start,
)
# Compile (uses cache)
try:
func = self._compile(skill)
except Exception as e:
result.ok = False
result.error = str(e)
result.traceback = traceback.format_exc()
result.duration_s = time.time() - start
self._failures += 1
if self.library:
self.library.record_execution(skill.name, success=False,
duration_s=result.duration_s,
error=result.error)
return result
# We need the globals dict from the compiled function so primitives
# like call_skill/emit_event see the right executor. The function
# was exec'd with our globals, so it already captures them.
# Run the entry function
try:
ret = func(**(params or {}))
result.ok = True
result.return_value = ret
self._successes += 1
except Exception as e:
result.ok = False
result.error = str(e)
result.traceback = traceback.format_exc()
self._failures += 1
result.duration_s = time.time() - start
if self.library:
self.library.record_execution(
name=skill.name,
success=result.ok,
duration_s=result.duration_s,
error=result.error,
)
# Publish
self._publish(skill, result)
return result
def execute_by_name(
self,
skill_name: str,
params: Optional[Dict[str, Any]] = None,
_depth: int = 0,
) -> SkillExecutionResult:
"""Look up a skill by name and execute it."""
if self.library is None:
return SkillExecutionResult(
skill_name=skill_name,
ok=False,
error="no_library",
)
skill = self.library.get(skill_name)
if not skill:
return SkillExecutionResult(
skill_name=skill_name,
ok=False,
error="skill_not_found",
)
return self.execute(skill, params=params, _depth=_depth)
def execute_with_dependencies(
self,
skill_name: str,
params: Optional[Dict[str, Any]] = None,
) -> SkillExecutionResult:
"""
Resolve the dependency tree and execute the top-level skill.
The dependencies themselves are executed lazily via call_skill,
but this method pre-compiles them for speed.
"""
if self.library is None:
return SkillExecutionResult(
skill_name=skill_name,
ok=False,
error="no_library",
)
deps = self.library.resolve_dependencies(skill_name)
# Pre-compile all dependencies so call_skill finds them ready
for dep in deps:
try:
self._compile(dep)
except Exception as e:
logger.warning("Failed to pre-compile dep %s: %s", dep.name, e)
return self.execute_by_name(skill_name, params=params)
# ─────────────────────────────────────────────────────────────────────
# Publishing
# ─────────────────────────────────────────────────────────────────────
def _publish(self, skill: Skill, result: SkillExecutionResult) -> None:
if not self.thought_bus:
return
try:
from aureon.core.aureon_thought_bus import Thought
self.thought_bus.publish(Thought(
source="code_architect.executor",
topic=f"skill.executed.{'ok' if result.ok else 'fail'}",
payload={
"skill_name": skill.name,
"level": skill.level.name,
"ok": result.ok,
"duration_s": result.duration_s,
"depth": result.depth,
"error": result.error,
},
))
except Exception:
pass
# ─────────────────────────────────────────────────────────────────────
# Status
# ─────────────────────────────────────────────────────────────────────
def get_status(self) -> Dict[str, Any]:
return {
"total_executions": self._total_executions,
"successes": self._successes,
"failures": self._failures,
"success_rate": (
self._successes / self._total_executions if self._total_executions > 0 else 0.0
),
"cached_compilations": len(self._compiled_cache),
"dispatcher_wired": self.dispatcher is not None,
}