Refactor cockpit to use DockerTmuxController pattern
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>
This commit is contained in:
171
lib/request_handler.py
Executable file
171
lib/request_handler.py
Executable file
@@ -0,0 +1,171 @@
|
||||
#!/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))
|
||||
Reference in New Issue
Block a user