Based on claude-code-tools TmuxCLIController, this refactor: - Added DockerTmuxController class for robust tmux session management - Implements send_keys() with configurable delay_enter - Implements capture_pane() for output retrieval - Implements wait_for_prompt() for pattern-based completion detection - Implements wait_for_idle() for content-hash-based idle detection - Implements wait_for_shell_prompt() for shell prompt detection Also includes workflow improvements: - Pre-task git snapshot before agent execution - Post-task commit protocol in agent guidelines Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
172 lines
5.5 KiB
Python
Executable File
172 lines
5.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
Luzia Request Handler - Autonomous request approval orchestration
|
||
Implements: /request-approver command
|
||
Luzia's responsibility: monitor and process all pending infrastructure requests
|
||
"""
|
||
|
||
import json
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
from datetime import datetime, timedelta
|
||
|
||
class LuziaRequestApprover:
|
||
"""Luzia's autonomous request approval orchestrator"""
|
||
|
||
def __init__(self):
|
||
self.requests_file = Path("/opt/server-agents/state/pending-requests.json")
|
||
self.log_file = Path("/opt/server-agents/logs/request-approvals.log")
|
||
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
||
|
||
# Auto-approve rules - Luzia's policy
|
||
self.auto_approve = {
|
||
'service_restart': 24, # hours
|
||
'config_change': 24,
|
||
'subdomain_create': 48,
|
||
}
|
||
|
||
# Manual review required
|
||
self.escalate = {
|
||
'support_request': True,
|
||
'service_deploy': True,
|
||
'ssl_certificate': True,
|
||
}
|
||
|
||
def log(self, message):
|
||
"""Log approval action"""
|
||
timestamp = datetime.now().isoformat()
|
||
log_entry = f"[{timestamp}] {message}\n"
|
||
with open(self.log_file, 'a') as f:
|
||
f.write(log_entry)
|
||
print(message)
|
||
|
||
def load_requests(self):
|
||
"""Load pending requests from state"""
|
||
if not self.requests_file.exists():
|
||
return []
|
||
with open(self.requests_file, 'r') as f:
|
||
data = json.load(f)
|
||
return data.get('pending', [])
|
||
|
||
def get_age_hours(self, request):
|
||
"""Calculate request age"""
|
||
ts = request['timestamp']
|
||
if 'Z' in ts or '+' in ts:
|
||
timestamp = datetime.fromisoformat(ts.replace('Z', '+00:00'))
|
||
age = datetime.now(timestamp.tzinfo) - timestamp
|
||
else:
|
||
timestamp = datetime.fromisoformat(ts)
|
||
age = datetime.now() - timestamp
|
||
return age.total_seconds() / 3600
|
||
|
||
def approve_request(self, request_id, reason):
|
||
"""Approve via sarlo-admin MCP"""
|
||
try:
|
||
# Call sarlo-admin to approve
|
||
result = subprocess.run(
|
||
['python3', '-c', f'''
|
||
import sys
|
||
sys.path.insert(0, "/opt/server-agents/mcp-servers/sarlo-admin")
|
||
from server import approve_request
|
||
approve_request("{request_id}", "{reason}")
|
||
'''],
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=10
|
||
)
|
||
return result.returncode == 0
|
||
except Exception as e:
|
||
self.log(f"⚠️ Error approving {request_id}: {e}")
|
||
return False
|
||
|
||
def escalate_request(self, request):
|
||
"""Escalate for manual review"""
|
||
req_id = request['id']
|
||
req_type = request['type']
|
||
user = request.get('user', 'unknown')
|
||
reason = request.get('reason', 'No reason provided')[:100]
|
||
|
||
# Log escalation
|
||
self.log(f"🔶 ESCALATE: {req_id} ({req_type}) from {user}: {reason}")
|
||
|
||
# Send Telegram alert to admin
|
||
try:
|
||
subprocess.run([
|
||
'python3', '-c', f'''
|
||
import sys
|
||
sys.path.insert(0, "/opt/server-agents/mcp-servers/sarlo-admin")
|
||
from server import send_telegram_message
|
||
msg = """🔶 *Request Escalation*
|
||
|
||
ID: {req_id}
|
||
Type: {req_type}
|
||
User: {user}
|
||
Reason: {reason}
|
||
|
||
Review at: /opt/server-agents/state/pending-requests.json"""
|
||
send_telegram_message(msg)
|
||
'''
|
||
], timeout=5)
|
||
except:
|
||
pass
|
||
|
||
def process_requests(self):
|
||
"""Luzia's request processing loop"""
|
||
requests = self.load_requests()
|
||
|
||
if not requests:
|
||
self.log("ℹ️ No pending requests")
|
||
return {'status': 'idle', 'count': 0}
|
||
|
||
auto_approved = []
|
||
escalated = []
|
||
|
||
self.log(f"🔄 Processing {len(requests)} pending request(s)...")
|
||
|
||
for req in requests:
|
||
req_id = req['id']
|
||
req_type = req['type']
|
||
age_hours = self.get_age_hours(req)
|
||
|
||
# Check auto-approve rules
|
||
if req_type in self.auto_approve:
|
||
max_age = self.auto_approve[req_type]
|
||
if age_hours >= max_age:
|
||
reason = f"Auto-approved by luzia (age: {age_hours:.1f}h >= {max_age}h)"
|
||
if self.approve_request(req_id, reason):
|
||
auto_approved.append(req_id)
|
||
self.log(f"✅ APPROVED: {req_id} ({req_type})")
|
||
continue
|
||
|
||
# Check escalate rules
|
||
if req_type in self.escalate or req_type not in self.auto_approve:
|
||
self.escalate_request(req)
|
||
escalated.append(req_id)
|
||
|
||
result = {
|
||
'status': 'processed',
|
||
'auto_approved_count': len(auto_approved),
|
||
'escalated_count': len(escalated),
|
||
'auto_approved': auto_approved,
|
||
'escalated': escalated,
|
||
'total': len(requests)
|
||
}
|
||
|
||
self.log(f"✨ Cycle complete: {len(auto_approved)} approved, {len(escalated)} escalated")
|
||
return result
|
||
|
||
def run_background(self):
|
||
"""Run as luzia's background task"""
|
||
self.log("🚀 Luzia Request Approver started (background)")
|
||
result = self.process_requests()
|
||
return result
|
||
|
||
if __name__ == '__main__':
|
||
approver = LuziaRequestApprover()
|
||
if len(sys.argv) > 1 and sys.argv[1] == '--background':
|
||
result = approver.run_background()
|
||
else:
|
||
result = approver.process_requests()
|
||
print(json.dumps(result, indent=2))
|