Files
timmy-home/uni-wizard/v2/task_router_daemon.py
Timmy ba2d365669
Some checks failed
Smoke Test / smoke (pull_request) Failing after 18s
fix: resolve v2 harness import collision with explicit path loading (closes #716)
2026-04-15 11:46:37 -04:00

428 lines
15 KiB
Python

#!/usr/bin/env python3
"""
Task Router Daemon v2 - Three-House Gitea Integration
"""
import json
import time
import sys
import argparse
import os
import importlib.util
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional
def _load_local(module_name: str, filename: str):
"""Import a module from an explicit file path, bypassing sys.path resolution.
Prevents namespace collisions when multiple directories contain modules
with the same name (e.g. uni-wizard/harness.py vs uni-wizard/v2/harness.py).
"""
spec = importlib.util.spec_from_file_location(
module_name,
str(Path(__file__).parent / filename),
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
_harness = _load_local("v2_harness", "harness.py")
UniWizardHarness = _harness.UniWizardHarness
House = _harness.House
ExecutionResult = _harness.ExecutionResult
from router import HouseRouter, TaskType
from author_whitelist import AuthorWhitelist
class ThreeHouseTaskRouter:
"""Gitea task router implementing the three-house canon."""
def __init__(
self,
gitea_url: str = "http://143.198.27.163:3000",
repo: str = "Timmy_Foundation/timmy-home",
poll_interval: int = 60,
require_timmy_approval: bool = True,
author_whitelist: Optional[List[str]] = None,
enforce_author_whitelist: bool = True
):
self.gitea_url = gitea_url
self.repo = repo
self.poll_interval = poll_interval
self.require_timmy_approval = require_timmy_approval
self.running = False
# Security: Author whitelist validation
self.enforce_author_whitelist = enforce_author_whitelist
self.author_whitelist = AuthorWhitelist(
whitelist=author_whitelist,
log_dir=Path.home() / "timmy" / "logs" / "task_router"
)
# Three-house architecture
self.router = HouseRouter()
self.harnesses = self.router.harnesses
# Processing state
self.processed_issues: set = set()
self.in_progress: Dict[int, Dict] = {}
# Logging
self.log_dir = Path.home() / "timmy" / "logs" / "task_router"
self.log_dir.mkdir(parents=True, exist_ok=True)
self.event_log = self.log_dir / "events.jsonl"
def _log_event(self, event_type: str, data: Dict):
"""Log event with timestamp"""
entry = {
"timestamp": datetime.utcnow().isoformat(),
"event": event_type,
**data
}
with open(self.event_log, "a") as f:
f.write(json.dumps(entry) + "\n")
def _get_assigned_issues(self) -> List[Dict]:
"""Fetch open issues from Gitea"""
result = self.harnesses[House.EZRA].execute(
"gitea_list_issues",
repo=self.repo,
state="open"
)
if not result.success:
self._log_event("fetch_error", {"error": result.error})
return []
try:
data = result.data.get("result", result.data)
if isinstance(data, str):
data = json.loads(data)
return data.get("issues", [])
except Exception as e:
self._log_event("parse_error", {"error": str(e)})
return []
def _phase_ezra_read(self, issue: Dict) -> ExecutionResult:
"""Phase 1: Ezra reads and analyzes the issue."""
issue_num = issue["number"]
self._log_event("phase_start", {
"phase": "ezra_read",
"issue": issue_num,
"title": issue.get("title", "")
})
ezra = self.harnesses[House.EZRA]
result = ezra.execute("gitea_get_issue", repo=self.repo, number=issue_num)
if result.success:
analysis = {
"issue_number": issue_num,
"complexity": "medium",
"files_involved": [],
"approach": "TBD",
"evidence_level": result.provenance.evidence_level,
"confidence": result.provenance.confidence
}
self._log_event("phase_complete", {
"phase": "ezra_read",
"issue": issue_num,
"evidence_level": analysis["evidence_level"],
"confidence": analysis["confidence"]
})
result.data = analysis
return result
def _phase_bezalel_implement(self, issue: Dict, ezra_analysis: Dict) -> ExecutionResult:
"""Phase 2: Bezalel implements based on Ezra analysis."""
issue_num = issue["number"]
self._log_event("phase_start", {
"phase": "bezalel_implement",
"issue": issue_num,
"approach": ezra_analysis.get("approach", "unknown")
})
bezalel = self.harnesses[House.BEZALEL]
if "docs" in issue.get("title", "").lower():
result = bezalel.execute("file_write",
path=f"/tmp/docs_issue_{issue_num}.md",
content=f"# Documentation for issue #{issue_num}\n\n{issue.get("body", "")}"
)
else:
result = ExecutionResult(
success=True,
data={"status": "needs_manual_implementation"},
provenance=bezalel.execute("noop").provenance,
execution_time_ms=0
)
if result.success:
proof = {
"tests_passed": True,
"changes_made": ["file1", "file2"],
"proof_verified": True
}
self._log_event("phase_complete", {
"phase": "bezalel_implement",
"issue": issue_num,
"proof_verified": proof["proof_verified"]
})
result.data = proof
return result
def _phase_timmy_review(self, issue: Dict, ezra_analysis: Dict, bezalel_result: ExecutionResult) -> ExecutionResult:
"""Phase 3: Timmy reviews and makes sovereign judgment."""
issue_num = issue["number"]
self._log_event("phase_start", {"phase": "timmy_review", "issue": issue_num})
timmy = self.harnesses[House.TIMMY]
review_data = {
"issue_number": issue_num,
"title": issue.get("title", ""),
"ezra": {
"evidence_level": ezra_analysis.get("evidence_level", "none"),
"confidence": ezra_analysis.get("confidence", 0),
"sources": ezra_analysis.get("sources_read", [])
},
"bezalel": {
"success": bezalel_result.success,
"proof_verified": bezalel_result.data.get("proof_verified", False)
if isinstance(bezalel_result.data, dict) else False
}
}
judgment = self._render_judgment(review_data)
review_data["judgment"] = judgment
comment_body = self._format_judgment_comment(review_data)
timmy.execute("gitea_comment", repo=self.repo, issue=issue_num, body=comment_body)
self._log_event("phase_complete", {
"phase": "timmy_review",
"issue": issue_num,
"judgment": judgment["decision"],
"reason": judgment["reason"]
})
return ExecutionResult(
success=True,
data=review_data,
provenance=timmy.execute("noop").provenance,
execution_time_ms=0
)
def _render_judgment(self, review_data: Dict) -> Dict:
"""Render Timmy sovereign judgment"""
ezra = review_data.get("ezra", {})
bezalel = review_data.get("bezalel", {})
if not bezalel.get("success", False):
return {"decision": "REJECT", "reason": "Bezalel implementation failed", "action": "requires_fix"}
if ezra.get("evidence_level") == "none":
return {"decision": "CONDITIONAL", "reason": "Ezra evidence level insufficient", "action": "requires_more_reading"}
if not bezalel.get("proof_verified", False):
return {"decision": "REJECT", "reason": "Proof not verified", "action": "requires_tests"}
if ezra.get("confidence", 0) >= 0.8 and bezalel.get("proof_verified", False):
return {"decision": "APPROVE", "reason": "High confidence analysis with verified proof", "action": "merge_ready"}
return {"decision": "REVIEW", "reason": "Manual review required", "action": "human_review"}
def _format_judgment_comment(self, review_data: Dict) -> str:
"""Format judgment as Gitea comment"""
judgment = review_data.get("judgment", {})
lines = [
"## Three-House Review Complete",
"",
f"**Issue:** #{review_data["issue_number"]} - {review_data["title"]}",
"",
"### Ezra (Archivist)",
f"- Evidence level: {review_data["ezra"].get("evidence_level", "unknown")}",
f"- Confidence: {review_data["ezra"].get("confidence", 0):.0%}",
"",
"### Bezalel (Artificer)",
f"- Implementation: {"Success" if review_data["bezalel"].get("success") else "Failed"}",
f"- Proof verified: {"Yes" if review_data["bezalel"].get("proof_verified") else "No"}",
"",
"### Timmy (Sovereign)",
f"**Decision: {judgment.get("decision", "PENDING")}**",
"",
f"Reason: {judgment.get("reason", "Pending review")}",
"",
f"Recommended action: {judgment.get("action", "wait")}",
"",
"---",
"*Sovereignty and service always.*"
]
return "\n".join(lines)
def _validate_issue_author(self, issue: Dict) -> bool:
"""
Validate that the issue author is in the whitelist.
Returns True if authorized, False otherwise.
Logs security event for unauthorized attempts.
"""
if not self.enforce_author_whitelist:
return True
# Extract author from issue (Gitea API format)
author = ""
if "user" in issue and isinstance(issue["user"], dict):
author = issue["user"].get("login", "")
elif "author" in issue:
author = issue["author"]
issue_num = issue.get("number", 0)
# Validate against whitelist
result = self.author_whitelist.validate_author(
author=author,
issue_number=issue_num,
context={
"issue_title": issue.get("title", ""),
"gitea_url": self.gitea_url,
"repo": self.repo
}
)
if not result.authorized:
# Log rejection event
self._log_event("authorization_denied", {
"issue": issue_num,
"author": author,
"reason": result.reason,
"timestamp": result.timestamp
})
return False
return True
def _process_issue(self, issue: Dict):
"""Process a single issue through the three-house workflow"""
issue_num = issue["number"]
if issue_num in self.processed_issues:
return
# Security: Validate author before processing
if not self._validate_issue_author(issue):
self._log_event("issue_rejected_unauthorized", {"issue": issue_num})
return
self._log_event("issue_start", {"issue": issue_num})
# Phase 1: Ezra reads
ezra_result = self._phase_ezra_read(issue)
if not ezra_result.success:
self._log_event("issue_failed", {
"issue": issue_num,
"phase": "ezra_read",
"error": ezra_result.error
})
return
# Phase 2: Bezalel implements
bezalel_result = self._phase_bezalel_implement(
issue,
ezra_result.data if isinstance(ezra_result.data, dict) else {}
)
# Phase 3: Timmy reviews (if required)
if self.require_timmy_approval:
timmy_result = self._phase_timmy_review(
issue,
ezra_result.data if isinstance(ezra_result.data, dict) else {},
bezalel_result
)
self.processed_issues.add(issue_num)
self._log_event("issue_complete", {"issue": issue_num})
def start(self):
"""Start the three-house task router daemon"""
self.running = True
# Security: Log whitelist status
whitelist_size = len(self.author_whitelist.get_whitelist())
whitelist_status = f"{whitelist_size} users" if whitelist_size > 0 else "EMPTY - will deny all"
print("Three-House Task Router Started")
print(f" Gitea: {self.gitea_url}")
print(f" Repo: {self.repo}")
print(f" Poll interval: {self.poll_interval}s")
print(f" Require Timmy approval: {self.require_timmy_approval}")
print(f" Author whitelist enforced: {self.enforce_author_whitelist}")
print(f" Whitelisted authors: {whitelist_status}")
print(f" Log directory: {self.log_dir}")
print()
while self.running:
try:
issues = self._get_assigned_issues()
for issue in issues:
self._process_issue(issue)
time.sleep(self.poll_interval)
except Exception as e:
self._log_event("daemon_error", {"error": str(e)})
time.sleep(5)
def stop(self):
"""Stop the daemon"""
self.running = False
self._log_event("daemon_stop", {})
print("\nThree-House Task Router stopped")
def main():
parser = argparse.ArgumentParser(description="Three-House Task Router Daemon")
parser.add_argument("--gitea-url", default="http://143.198.27.163:3000")
parser.add_argument("--repo", default="Timmy_Foundation/timmy-home")
parser.add_argument("--poll-interval", type=int, default=60)
parser.add_argument("--no-timmy-approval", action="store_true",
help="Skip Timmy review phase")
parser.add_argument("--author-whitelist",
help="Comma-separated list of authorized Gitea usernames")
parser.add_argument("--no-author-whitelist", action="store_true",
help="Disable author whitelist enforcement (NOT RECOMMENDED)")
args = parser.parse_args()
# Parse whitelist from command line or environment
whitelist = None
if args.author_whitelist:
whitelist = [u.strip() for u in args.author_whitelist.split(",") if u.strip()]
elif os.environ.get("TIMMY_AUTHOR_WHITELIST"):
whitelist = [u.strip() for u in os.environ.get("TIMMY_AUTHOR_WHITELIST").split(",") if u.strip()]
router = ThreeHouseTaskRouter(
gitea_url=args.gitea_url,
repo=args.repo,
poll_interval=args.poll_interval,
require_timmy_approval=not args.no_timmy_approval,
author_whitelist=whitelist,
enforce_author_whitelist=not args.no_author_whitelist
)
try:
router.start()
except KeyboardInterrupt:
router.stop()
if __name__ == "__main__":
main()