704 lines
32 KiB
PHP
704 lines
32 KiB
PHP
<?php
|
|
/**
|
|
* ╔══════════════════════════════════════════════════════════════════════╗
|
|
* ║ WEVIA DIRECTOR AGENT v1.0 — Autonomous Project Director ║
|
|
* ║ Replaces Claude sessions for routine ops & monitoring ║
|
|
* ║ Observe → Plan → Act → Verify → Report ║
|
|
* ║ WEVAL Consulting — April 2026 ║
|
|
* ╚══════════════════════════════════════════════════════════════════════╝
|
|
*
|
|
* ARCHITECTURE:
|
|
* Director uses existing agents (devops, ethica, security, monitor)
|
|
* + Master Router for LLM reasoning
|
|
* + Sentinel bridge for S95 ops
|
|
* + Telegram alerts for escalation
|
|
*
|
|
* ENDPOINTS:
|
|
* POST /api/wevia-director.php → Run director cycle
|
|
* GET /api/wevia-director.php?status → Current status
|
|
* GET /api/wevia-director.php?history → Action history
|
|
* GET /api/wevia-director.php?run → Manual trigger
|
|
* GET /api/wevia-director.php?health → Quick health
|
|
*/
|
|
|
|
header("Content-Type: application/json; charset=utf-8");
|
|
// CORS handled by nginx
|
|
// CORS handled by nginx
|
|
if ($_SERVER["REQUEST_METHOD"] === "OPTIONS") { http_response_code(200); exit; }
|
|
|
|
// Load dependencies
|
|
require_once "/opt/wevia-brain/wevia-master-router.php";
|
|
require_once "/opt/wevia-brain/wevia-docker-autofix.php";
|
|
// require_once "/opt/wevia-brain/wevia-agent-loop.php";
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// CONFIGURATION
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
define('DIR_VERSION', '1.0.0');
|
|
define('DIR_LOG_DIR', '/var/log/wevia-director');
|
|
define('DIR_STATE_FILE', '/var/log/wevia-director/state.json');
|
|
define('DIR_HISTORY_FILE', '/var/log/wevia-director/history.jsonl');
|
|
define('DIR_MAX_ACTIONS_PER_CYCLE', 5);
|
|
define('DIR_ESCALATION_THRESHOLD', 'critical');
|
|
define('DIR_TELEGRAM_BOT', '7605775322');
|
|
define('DIR_TELEGRAM_CHAT', '7605775322');
|
|
define('DIR_DRY_RUN', false); // Set true to log without executing
|
|
|
|
@mkdir(DIR_LOG_DIR, 0777, true);
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// PROJECT MEMORY — Condensed from 3 months of Claude sessions
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
define('PROJECT_CONTEXT', <<<'CTX'
|
|
WEVAL CONSULTING INFRASTRUCTURE — Director Knowledge Base
|
|
|
|
SERVERS:
|
|
- S95 (95.216.167.89): WEVADS prod + Arsenal(5890) + Sentinel + PostgreSQL. SSH port 49222.
|
|
- S204 (204.168.152.13): MTA/PMTA + WEVIA + Ethica. PHP 8.5. PRIMARY for IA.
|
|
- S151 (151.80.235.110): OVH tracking + consent.wevup.app.
|
|
- S88: DECOMMISSIONED.
|
|
|
|
CRITICAL RULES:
|
|
- NEVER modify is_installed/pmtahttpd/config on PMTA.
|
|
- NEVER place non-MTA files on S204 except WEVIA/Ethica.
|
|
- Conversions WEVADS = PULL (conversions-collector.php). NEVER configure postbacks.
|
|
- Sentinel API: POST port 5890. Port 80 protected .htaccess.
|
|
- GOLD backup mandatory before any migration/refactor.
|
|
- Zero regression: test ALL pages before AND after changes.
|
|
|
|
ACTIVE BUSINESS:
|
|
- WEVADS: Email marketing platform. 6.65M contacts, 85 offers, 38 crons.
|
|
- Ethica: B2B pharma HCP data. 67,450 HCPs. Campaigns 2027 Algeria/Tunisia/Morocco.
|
|
- WEVIA: AI chatbot. 130 intents, 15 providers, 9 Ollama local.
|
|
- Vistex: SAP pricing partner (dispute active).
|
|
- Huawei Cloud: 4 ECS PMTA servers (billing dispute active).
|
|
|
|
MONITORING PRIORITIES:
|
|
1. Disk space (S204 at 83% — WATCH closely, alert >90%)
|
|
2. Docker containers (16 — all must be UP)
|
|
3. Ollama service (port 11435 — must respond)
|
|
4. PostgreSQL (S95+S204 — both must be healthy)
|
|
5. PMTA servers (4 ECS — check if still billing)
|
|
6. SSL certificates (all subdomains)
|
|
7. Crons (30 www-data crons — none should fail silently)
|
|
8. NonReg tests (target: 153/153)
|
|
9. E2E Playwright (target: 18/18)
|
|
10. Git repos (0 dirty, all pushed)
|
|
|
|
ESCALATION RULES:
|
|
- Disk >90% → CRITICAL → Telegram + auto-cleanup
|
|
- Docker container DOWN → HIGH → auto-restart + Telegram
|
|
- Ollama DOWN → MEDIUM → auto-restart
|
|
- NonReg regression → HIGH → Telegram + block deploys
|
|
- PMTA billing anomaly → CRITICAL → Telegram (do NOT auto-fix)
|
|
- Security breach → CRITICAL → Telegram + isolate
|
|
CTX
|
|
);
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// ROUTING
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
$action = null;
|
|
if (isset($_GET['status'])) $action = 'status';
|
|
elseif (isset($_GET['history'])) $action = 'history';
|
|
elseif (isset($_GET['run'])) { set_time_limit(60); if (!isset($_GET['bg'])) { header('Content-Type: application/json'); $cmd = 'timeout 55 php8.4 ' . __FILE__ . ' run_bg > /dev/null 2>&1 &'; exec($cmd); echo json_encode(['status'=>'launched','duration_ms'=>0,'observations'=>new stdClass(),'plan'=>[],'actions'=>[],'report'=>'Cycle lance en background. Tapez status dans 30s.']); exit; } $action = 'run'; }
|
|
elseif (isset($_GET['health'])) $action = 'health';
|
|
elseif ($_SERVER['REQUEST_METHOD'] === 'POST') $action = 'run';
|
|
|
|
switch ($action) {
|
|
case 'health':
|
|
echo json_encode(['status' => 'alive', 'version' => DIR_VERSION, 'uptime' => dir_uptime()]);
|
|
exit;
|
|
case 'status':
|
|
echo json_encode(dir_getStatus(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
case 'history':
|
|
$n = intval($_GET['n'] ?? 20);
|
|
echo json_encode(dir_getHistory($n), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
case 'run':
|
|
$force = isset($_GET['force']) || isset($_POST['force']);
|
|
echo json_encode(dir_runCycle($force), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
default:
|
|
echo json_encode([
|
|
'director' => 'WEVIA Director Agent v' . DIR_VERSION,
|
|
'endpoints' => ['?health','?status','?history','?run','POST /'],
|
|
'description' => 'Autonomous project director — Observe→Plan→Act→Verify→Report',
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// CORE: DIRECTOR CYCLE
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
function dir_runCycle($force = false) {
|
|
$start = microtime(true);
|
|
$cycle = [
|
|
'timestamp' => date('c'),
|
|
'phase' => 'init',
|
|
'observations' => [],
|
|
'plan' => [],
|
|
'actions' => [],
|
|
'verifications' => [],
|
|
'report' => '',
|
|
'escalations' => [],
|
|
'duration_ms' => 0,
|
|
];
|
|
|
|
// Rate limit: max 1 cycle per 10 minutes unless forced
|
|
if (!$force && dir_lastCycleAge() < 600) {
|
|
return ['skipped' => true, 'reason' => 'Last cycle was ' . dir_lastCycleAge() . 's ago (min 600s)', 'next_in' => 600 - dir_lastCycleAge()];
|
|
}
|
|
|
|
// ── PHASE 1: OBSERVE ──
|
|
$cycle['phase'] = 'observe';
|
|
$observations = dir_observe();
|
|
$cycle['observations'] = $observations;
|
|
|
|
// ── PHASE 2: PLAN ──
|
|
$cycle['phase'] = 'plan';
|
|
$issues = dir_identifyIssues($observations);
|
|
|
|
if (empty($issues)) {
|
|
$cycle['phase'] = 'done';
|
|
$cycle['report'] = 'All systems nominal. No action needed.';
|
|
$cycle['duration_ms'] = round((microtime(true) - $start) * 1000);
|
|
dir_logCycle($cycle);
|
|
return $cycle;
|
|
}
|
|
|
|
// Ask LLM to prioritize and plan
|
|
$plan = dir_planActions($issues, $observations);
|
|
$cycle['plan'] = $plan;
|
|
|
|
// ── PHASE 3: ACT ──
|
|
$cycle['phase'] = 'act';
|
|
$actionsExecuted = 0;
|
|
foreach ($plan as $task) {
|
|
if ($actionsExecuted >= DIR_MAX_ACTIONS_PER_CYCLE) break;
|
|
|
|
// Check severity — escalate if critical and not auto-fixable
|
|
if (($task['severity'] ?? '') === 'critical' && !($task['auto_fixable'] ?? false)) {
|
|
$cycle['escalations'][] = $task;
|
|
dir_escalateTelegram($task);
|
|
continue;
|
|
}
|
|
|
|
if (DIR_DRY_RUN) {
|
|
$cycle['actions'][] = ['task' => $task, 'result' => 'DRY_RUN — would execute', 'status' => 'skipped'];
|
|
continue;
|
|
}
|
|
|
|
$result = dir_executeTask($task);
|
|
$cycle['actions'][] = ['task' => $task, 'result' => $result, 'status' => $result['success'] ? 'ok' : 'failed'];
|
|
$actionsExecuted++;
|
|
}
|
|
|
|
// ── PHASE 4: VERIFY ──
|
|
$cycle['phase'] = 'verify';
|
|
foreach ($cycle['actions'] as &$action) {
|
|
if ($action['status'] === 'ok' && isset($action['task']['verify_cmd'])) {
|
|
$verify = dir_execLocal($action['task']['verify_cmd']);
|
|
$action['verified'] = !empty($verify) && strpos($verify, 'ERROR') === false;
|
|
$cycle['verifications'][] = [
|
|
'task' => $action['task']['name'] ?? 'unknown',
|
|
'verified' => $action['verified'],
|
|
'output' => mb_substr($verify, 0, 500),
|
|
];
|
|
}
|
|
}
|
|
unset($action);
|
|
|
|
// ── PHASE 5: REPORT ──
|
|
$cycle['phase'] = 'done';
|
|
$cycle['duration_ms'] = round((microtime(true) - $start) * 1000);
|
|
$cycle['report'] = dir_generateReport($cycle);
|
|
|
|
// Log cycle
|
|
dir_logCycle($cycle);
|
|
|
|
// Telegram summary if actions were taken
|
|
if (!empty($cycle['actions']) || !empty($cycle['escalations'])) {
|
|
dir_notifyTelegram($cycle);
|
|
}
|
|
|
|
return $cycle;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// OBSERVE: Collect system state
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
function dir_observe() {
|
|
$obs = [];
|
|
|
|
// S204 local checks
|
|
$obs['s204_disk'] = dir_parseDisk(dir_execLocal("df -h / | tail -1"));
|
|
$obs['s204_mem'] = dir_execLocal("free -h | grep Mem | awk '{print \$3\"/\"\$2}'");
|
|
$obs['s204_load'] = dir_execLocal("cat /proc/loadavg | awk '{print \$1,\$2,\$3}'");
|
|
$obs['s204_docker'] = dir_execLocal("docker ps --format '{{.Names}}: {{.Status}}' 2>/dev/null | head -25");
|
|
$obs['s204_docker_count'] = intval(dir_execLocal("docker ps -q 2>/dev/null | wc -l"));
|
|
$obs['s204_ollama'] = dir_execLocal("curl -s --max-time 5 http://127.0.0.1:11435/api/tags 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(len(d.get('models',[])))\" 2>/dev/null") ?: '0';
|
|
$obs['s204_postgres'] = dir_execLocal("pg_isready 2>/dev/null && echo OK || echo DOWN");
|
|
$obs['s204_crons'] = intval(dir_execLocal("crontab -u www-data -l 2>/dev/null | grep -v '^#' | grep -v '^\$' | wc -l"));
|
|
$obs['s204_qdrant'] = dir_execLocal("curl -s --max-time 5 http://127.0.0.1:6333/collections 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); cols=d.get('result',{}).get('collections',[]); print(len(cols))\" 2>/dev/null") ?: '0';
|
|
|
|
// S95 remote checks via SSH
|
|
$obs['s95_alive'] = dir_execS95("echo ALIVE");
|
|
$obs['s95_disk'] = dir_parseDisk(dir_execS95("df -h / | tail -1"));
|
|
$obs['s95_postgres'] = dir_execS95("pg_isready 2>/dev/null && echo OK || echo DOWN");
|
|
$obs['s95_arsenal'] = dir_execLocal("curl -s --max-time 5 -o /dev/null -w '%{http_code}' http://95.216.167.89:5890/ 2>/dev/null") ?: 'DOWN';
|
|
$obs['s95_sentinel'] = dir_execLocal("curl -s --max-time 5 -o /dev/null -w '%{http_code}' http://95.216.167.89:5890/api/sentinel-brain.php 2>/dev/null") ?: 'DOWN';
|
|
$obs['s95_crons'] = intval(dir_execS95("crontab -l 2>/dev/null | grep -v '^#' | grep -v '^\$' | wc -l") ?: '0');
|
|
|
|
// S151 tracking check
|
|
$obs['s151_http'] = dir_execLocal("curl -s --max-time 5 -o /dev/null -w '%{http_code}' http://151.80.235.110/ 2>/dev/null") ?: 'DOWN';
|
|
|
|
// WEVIA health
|
|
$obs['wevia_api'] = dir_execLocal("curl -s --max-time 10 'http://127.0.0.1/api/wevia-master-api.php?health' 2>/dev/null | head -500");
|
|
|
|
// Git status
|
|
$obs['git_brain'] = dir_execLocal("cd /opt/wevia-brain && git status --porcelain 2>/dev/null | wc -l");
|
|
$obs['git_weval'] = dir_execLocal("cd /var/www/weval && git status --porcelain 2>/dev/null | wc -l");
|
|
|
|
// NonReg last result
|
|
$obs['nonreg_last'] = dir_execLocal("tail -1 /opt/weval-nonreg/logs/cron.log 2>/dev/null | tail -c 200");
|
|
|
|
// Errors in logs (last hour)
|
|
$obs['php_errors'] = intval(dir_execLocal("find /var/log/nginx -name '*.log' -newer /tmp/.director-last-check -exec grep -c 'PHP Fatal\\|502\\|503' {} + 2>/dev/null | awk -F: '{s+=\$2}END{print s}'") ?: '0');
|
|
|
|
|
|
// ARCHITECTURE SCANNER DATA (from architecture-scanner.php cron)
|
|
$archIndex = @json_decode(@file_get_contents("/var/www/weval/api/architecture-index.json"), true);
|
|
if ($archIndex) {
|
|
$obs["arch_generated"] = $archIndex["generated"] ?? "unknown";
|
|
$obs["arch_servers"] = count($archIndex["servers"] ?? []);
|
|
$obs["arch_docker"] = count($archIndex["docker"] ?? []);
|
|
$obs["arch_recommendations"] = $archIndex["recommendations"] ?? [];
|
|
$obs["arch_domains"] = count($archIndex["domains"] ?? []);
|
|
}
|
|
// TOPOLOGY DATA (from architecture-autonomous.php cron)
|
|
$topo = @json_decode(@file_get_contents("/var/www/weval/api/architecture-topology.json"), true);
|
|
if ($topo) {
|
|
$obs["topo_nodes"] = count($topo["nodes"] ?? []);
|
|
$obs["topo_edges"] = count($topo["edges"] ?? []);
|
|
$obs["topo_auto_actions"] = $topo["auto_actions"] ?? [];
|
|
$obs["topo_bpmn_processes"] = count($topo["bpmn_processes"] ?? []);
|
|
$obs["topo_soa_active"] = $topo["soa_stats"]["active"] ?? 0;
|
|
}
|
|
|
|
// EXHAUSTIVE URL HEALTH CHECKS (from director-registry.json)
|
|
$registry = @json_decode(@file_get_contents("/opt/wevia-brain/wevia-director-registry.json"), true);
|
|
if ($registry) {
|
|
$obs["registry_domains"] = count($registry["domains"] ?? []);
|
|
$obs["registry_total_assets"] = $registry["total_assets"] ?? [];
|
|
$urlResults = [];
|
|
foreach (($registry["health_checks"]["critical_urls"] ?? []) as $check) {
|
|
$url = $check["url"];
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 8,
|
|
CURLOPT_FOLLOWLOCATION => true, CURLOPT_SSL_VERIFYPEER => false,
|
|
CURLOPT_NOBODY => true]);
|
|
curl_exec($ch);
|
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
$ok = ($code >= 200 && $code < 500);
|
|
$urlResults[] = ["url" => basename(parse_url($url, PHP_URL_PATH)), "code" => $code, "ok" => $ok];
|
|
}
|
|
$obs["url_checks_total"] = count($urlResults);
|
|
$obs["url_checks_ok"] = count(array_filter($urlResults, fn($r) => $r["ok"]));
|
|
$obs["url_checks_failed"] = array_values(array_filter($urlResults, fn($r) => !$r["ok"]));
|
|
// Subdomain checks
|
|
$subResults = [];
|
|
foreach (($registry["health_checks"]["subdomain_checks"] ?? []) as $sub) {
|
|
$ch = curl_init("https://$sub/");
|
|
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5,
|
|
CURLOPT_FOLLOWLOCATION => true, CURLOPT_SSL_VERIFYPEER => false,
|
|
CURLOPT_NOBODY => true]);
|
|
curl_exec($ch);
|
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
$subResults[] = ["sub" => $sub, "code" => $code, "ok" => ($code >= 200 && $code < 500)];
|
|
}
|
|
$obs["subdomain_checks_total"] = count($subResults);
|
|
$obs["subdomain_checks_ok"] = count(array_filter($subResults, fn($r) => $r["ok"]));
|
|
$obs["subdomain_checks_failed"] = array_values(array_filter($subResults, fn($r) => !$r["ok"]));
|
|
}
|
|
// Touch marker for next run
|
|
dir_execLocal("touch /tmp/.director-last-check");
|
|
|
|
return $obs;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// ANALYZE: Identify issues from observations
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
function dir_identifyIssues($obs) {
|
|
$issues = [];
|
|
|
|
// Disk checks
|
|
if (isset($obs['s204_disk']['percent']) && $obs['s204_disk']['percent'] > 90) {
|
|
$issues[] = [
|
|
'name' => 'S204 disk critical',
|
|
'severity' => 'critical',
|
|
'auto_fixable' => true,
|
|
'detail' => "S204 disk at {$obs['s204_disk']['percent']}%",
|
|
'fix_agent' => 'devops',
|
|
'fix_goal' => 'cleanup disk space: docker prune, log rotation, tmp cleanup',
|
|
'fix_cmd' => 'docker system prune -f && find /var/log -name "*.log" -size +100M -exec truncate -s 0 {} \; && find /tmp -type f -mtime +7 -delete',
|
|
'verify_cmd' => "df -h / | tail -1 | awk '{print \$5}'",
|
|
];
|
|
} elseif (isset($obs['s204_disk']['percent']) && $obs['s204_disk']['percent'] > 92) {
|
|
$issues[] = [
|
|
'name' => 'S204 disk warning',
|
|
'severity' => 'medium',
|
|
'auto_fixable' => true,
|
|
'detail' => "S204 disk at {$obs['s204_disk']['percent']}%",
|
|
'fix_cmd' => 'find /var/log -name "*.log" -size +50M -exec truncate -s 0 {} \; && find /tmp -type f -mtime +3 -delete',
|
|
'verify_cmd' => "df -h / | tail -1 | awk '{print \$5}'",
|
|
];
|
|
}
|
|
|
|
// Docker containers
|
|
if ($obs['s204_docker_count'] < 14) {
|
|
$issues[] = [
|
|
'name' => 'Docker containers down',
|
|
'severity' => 'high',
|
|
'auto_fixable' => true,
|
|
'detail' => "Only {$obs['s204_docker_count']}/16 containers running",
|
|
'fix_cmd' => 'docker ps -a --filter status=exited --format {{.Names}} | xargs -r docker start 2>&1',
|
|
'verify_cmd' => 'docker ps -q | wc -l',
|
|
];
|
|
}
|
|
|
|
// Ollama
|
|
if (intval($obs['s204_ollama']) < 3) {
|
|
$issues[] = [
|
|
'name' => 'Ollama models low',
|
|
'severity' => 'medium',
|
|
'auto_fixable' => true,
|
|
'detail' => "Only {$obs['s204_ollama']} Ollama models loaded",
|
|
'fix_cmd' => 'systemctl restart ollama 2>/dev/null || service ollama restart 2>/dev/null; sleep 5; curl -s http://127.0.0.1:11435/api/tags | python3 -c "import sys,json; print(len(json.load(sys.stdin).get(\'models\',[])))"',
|
|
'verify_cmd' => 'curl -s http://127.0.0.1:11435/api/tags | python3 -c "import sys,json; print(len(json.load(sys.stdin).get(\'models\',[])))"',
|
|
];
|
|
}
|
|
|
|
// S95 connectivity
|
|
if (trim($obs['s95_alive'] ?? '') !== 'ALIVE') {
|
|
$issues[] = [
|
|
'name' => 'S95 unreachable',
|
|
'severity' => 'critical',
|
|
'auto_fixable' => false,
|
|
'detail' => 'Cannot SSH to S95',
|
|
];
|
|
}
|
|
|
|
// S95 disk
|
|
if (isset($obs['s95_disk']['percent']) && $obs['s95_disk']['percent'] > 92) {
|
|
$issues[] = [
|
|
'name' => 'S95 disk warning',
|
|
'severity' => 'high',
|
|
'auto_fixable' => false, // Don't auto-fix prod
|
|
'detail' => "S95 disk at {$obs['s95_disk']['percent']}%",
|
|
];
|
|
}
|
|
|
|
// PostgreSQL
|
|
if (strpos($obs['s204_postgres'] ?? '', 'OK') === false) {
|
|
$issues[] = [
|
|
'name' => 'S204 PostgreSQL down',
|
|
'severity' => 'critical',
|
|
'auto_fixable' => true,
|
|
'detail' => 'PostgreSQL not responding on S204',
|
|
'fix_cmd' => 'systemctl restart postgresql 2>/dev/null || pg_ctlcluster 16 main start 2>/dev/null; sleep 3; pg_isready',
|
|
'verify_cmd' => 'pg_isready',
|
|
];
|
|
}
|
|
|
|
// Git dirty
|
|
if (intval($obs['git_brain'] ?? 0) > 0) {
|
|
$issues[] = [
|
|
'name' => 'Git dirty: wevia-brain',
|
|
'severity' => 'low',
|
|
'auto_fixable' => true,
|
|
'detail' => "Uncommitted changes in /opt/wevia-brain",
|
|
'fix_cmd' => 'cd /opt/wevia-brain && git add -A && git commit -m "auto: director agent commit $(date +%Y%m%d-%H%M)" 2>&1 | tail -3',
|
|
'verify_cmd' => 'cd /opt/wevia-brain && git status --porcelain | wc -l',
|
|
];
|
|
}
|
|
|
|
// S151 tracking
|
|
if (($obs['s151_http'] ?? 'DOWN') === "DECOMMISSIONED") {
|
|
$issues[] = [
|
|
'name' => 'S151 tracking down',
|
|
'severity' => 'high',
|
|
'auto_fixable' => false,
|
|
'detail' => "S151 returned HTTP {$obs['s151_http']}",
|
|
];
|
|
}
|
|
|
|
// PHP errors spike
|
|
if (intval($obs['php_errors'] ?? 0) > 500) {
|
|
$issues[] = [
|
|
'name' => 'PHP error spike',
|
|
'severity' => 'high',
|
|
'auto_fixable' => true,
|
|
'detail' => "{$obs['php_errors']} PHP errors in last period",
|
|
'fix_cmd' => 'find /var/log/nginx -name "*.log" -exec truncate -s 0 {} \; && touch /tmp/.director-last-check && echo LOGS_CLEARED',
|
|
];
|
|
}
|
|
|
|
|
|
// ARCHITECTURE RECOMMENDATIONS (from architecture-scanner.php)
|
|
foreach (($obs["arch_recommendations"] ?? []) as $rec) {
|
|
$sev = $rec["severity"] ?? "info";
|
|
if ($sev === "info") continue; // Skip info-level
|
|
$issues[] = [
|
|
"name" => "[ARCH] " . ($rec["title"] ?? "Unknown"),
|
|
"severity" => ($sev === "critical") ? "critical" : (($sev === "warning") ? "medium" : "low"),
|
|
"auto_fixable" => !empty($rec["fix_cmd"]) && ($rec["action"] ?? "") === "auto",
|
|
"detail" => $rec["detail"] ?? "",
|
|
"fix_cmd" => $rec["fix_cmd"] ?? null,
|
|
"verify_cmd" => null,
|
|
"source" => "architecture-scanner",
|
|
];
|
|
}
|
|
|
|
// TOPOLOGY AUTO-ACTIONS (from architecture-autonomous.php)
|
|
foreach (($obs["topo_auto_actions"] ?? []) as $act) {
|
|
$issues[] = [
|
|
"name" => "[TOPO] " . ($act["title"] ?? "Auto-action"),
|
|
"severity" => $act["severity"] ?? "low",
|
|
"auto_fixable" => true,
|
|
"detail" => $act["detail"] ?? "",
|
|
"fix_cmd" => $act["cmd"] ?? null,
|
|
"source" => "architecture-topology",
|
|
];
|
|
}
|
|
|
|
// CRITICAL URL FAILURES
|
|
foreach (($obs["url_checks_failed"] ?? []) as $fail) {
|
|
$issues[] = [
|
|
"name" => "[URL] " . $fail["url"] . " returned " . $fail["code"],
|
|
"severity" => "high",
|
|
"auto_fixable" => false,
|
|
"detail" => "Critical page " . $fail["url"] . " returned HTTP " . $fail["code"],
|
|
"source" => "director-registry",
|
|
];
|
|
}
|
|
// SUBDOMAIN FAILURES
|
|
foreach (($obs["subdomain_checks_failed"] ?? []) as $fail) {
|
|
$issues[] = [
|
|
"name" => "[SUBDOMAIN] " . $fail["sub"] . " returned " . $fail["code"],
|
|
"severity" => "high",
|
|
"auto_fixable" => true,
|
|
"fix_cmd" => "docker restart authentik-server authentik-worker 2>/dev/null; sleep 10; echo AUTHENTIK_RESTARTED",
|
|
"detail" => "Subdomain " . $fail["sub"] . " returned HTTP " . $fail["code"],
|
|
"source" => "director-registry",
|
|
];
|
|
}
|
|
return $issues;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// PLAN: Use LLM to prioritize (or simple rule-based)
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
function dir_planActions($issues, $obs) {
|
|
// Sort by severity
|
|
$severityOrder = ['critical' => 0, 'high' => 1, 'medium' => 2, 'low' => 3];
|
|
usort($issues, function($a, $b) use ($severityOrder) {
|
|
return ($severityOrder[$a['severity'] ?? 'low'] ?? 9) - ($severityOrder[$b['severity'] ?? 'low'] ?? 9);
|
|
});
|
|
return $issues;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// EXECUTE: Run fix commands
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
function dir_executeTask($task) {
|
|
$start = microtime(true);
|
|
|
|
if (!empty($task['fix_agent'])) {
|
|
// Use existing agent
|
|
$result = dir_callAgent($task['fix_agent'], $task['fix_goal'] ?? $task['name']);
|
|
return [
|
|
'success' => ($result['status'] ?? '') !== 'error',
|
|
'output' => mb_substr(json_encode($result), 0, 2000),
|
|
'method' => 'agent:' . $task['fix_agent'],
|
|
'duration_ms' => round((microtime(true) - $start) * 1000),
|
|
];
|
|
}
|
|
|
|
if (!empty($task['fix_cmd'])) {
|
|
// Direct command execution
|
|
$target = $task['target'] ?? 's204';
|
|
$output = ($target === 's95') ? dir_execS95($task['fix_cmd']) : dir_execLocal($task['fix_cmd']);
|
|
return [
|
|
'success' => !empty($output) && strpos($output, 'ERROR') === false && strpos($output, 'FATAL') === false,
|
|
'output' => mb_substr($output, 0, 2000),
|
|
'method' => 'exec:' . $target,
|
|
'duration_ms' => round((microtime(true) - $start) * 1000),
|
|
];
|
|
}
|
|
|
|
return ['success' => false, 'output' => 'No fix_cmd or fix_agent defined', 'method' => 'none'];
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// EXECUTION HELPERS
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
function dir_execLocal($cmd, $timeout = 15) {
|
|
$result = shell_exec("timeout $timeout bash -c " . escapeshellarg($cmd) . " 2>&1");
|
|
return trim($result ?? '');
|
|
}
|
|
|
|
function dir_execS95($cmd, $timeout = 20) {
|
|
$escaped = str_replace("'", "'\\''", $cmd);
|
|
$sshCmd = "timeout $timeout ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -p49222 root@10.1.0.3 '$escaped' 2>&1";
|
|
return trim(shell_exec($sshCmd) ?? '');
|
|
}
|
|
|
|
function dir_callAgent($agent, $goal) {
|
|
$url = "http://127.0.0.1/api/wevia-agent-loop.php?agent=" . urlencode($agent) . "&goal=" . urlencode($goal);
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 60]);
|
|
$resp = curl_exec($ch);
|
|
curl_close($ch);
|
|
return json_decode($resp ?: '{}', true) ?: ['error' => 'Agent call failed'];
|
|
}
|
|
|
|
function dir_parseDisk($line) {
|
|
if (preg_match('/(\d+)%/', $line, $m)) {
|
|
return ['percent' => intval($m[1]), 'raw' => trim($line)];
|
|
}
|
|
return ['percent' => 0, 'raw' => trim($line)];
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// TELEGRAM NOTIFICATIONS
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
function dir_escalateTelegram($task) {
|
|
$msg = "🚨 *WEVIA DIRECTOR — ESCALATION*\n\n"
|
|
. "⚠️ *{$task['name']}*\n"
|
|
. "Severity: `{$task['severity']}`\n"
|
|
. "Detail: {$task['detail']}\n\n"
|
|
. "⚡ _Action requise — non auto-fixable_\n"
|
|
. "🕐 " . date('H:i d/m/Y');
|
|
dir_sendTelegram($msg);
|
|
}
|
|
|
|
function dir_notifyTelegram($cycle) {
|
|
$actionsCount = count($cycle['actions']);
|
|
$escalationsCount = count($cycle['escalations']);
|
|
$status = empty($cycle['escalations']) ? '✅' : '⚠️';
|
|
|
|
$msg = "$status *WEVIA DIRECTOR — Cycle Report*\n\n"
|
|
. "📊 Observations: " . count($cycle['observations']) . "\n"
|
|
. "🔧 Actions: $actionsCount\n"
|
|
. "🚨 Escalations: $escalationsCount\n"
|
|
. "⏱ Duration: {$cycle['duration_ms']}ms\n\n";
|
|
|
|
foreach ($cycle['actions'] as $a) {
|
|
$icon = $a['status'] === 'ok' ? '✅' : '❌';
|
|
$msg .= "$icon {$a['task']['name']}\n";
|
|
}
|
|
foreach ($cycle['escalations'] as $e) {
|
|
$msg .= "🚨 {$e['name']}: {$e['detail']}\n";
|
|
}
|
|
|
|
$msg .= "\n🕐 " . date('H:i d/m/Y');
|
|
dir_sendTelegram($msg);
|
|
}
|
|
|
|
function dir_sendTelegram($msg) {
|
|
// Use existing notify script
|
|
$escaped = str_replace(["'", "\n"], ["'\\''", "\\n"], $msg);
|
|
shell_exec("/opt/wevia-brain/wevia-notify.sh '$escaped' 2>/dev/null &");
|
|
|
|
// Direct API fallback
|
|
$token = trim(shell_exec("cat /opt/wevia-brain/.telegram_token 2>/dev/null") ?? '');
|
|
if (!empty($token)) {
|
|
$url = "https://api.telegram.org/bot$token/sendMessage";
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => http_build_query([
|
|
'chat_id' => DIR_TELEGRAM_CHAT,
|
|
'text' => $msg,
|
|
'parse_mode' => 'Markdown',
|
|
]),
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 10,
|
|
]);
|
|
curl_exec($ch);
|
|
curl_close($ch);
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// REPORTING & STATE
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
function dir_generateReport($cycle) {
|
|
$ok = 0; $fail = 0;
|
|
foreach ($cycle['actions'] as $a) {
|
|
if ($a['status'] === 'ok') $ok++; else $fail++;
|
|
}
|
|
return sprintf(
|
|
"Director cycle completed: %d observations, %d issues found, %d actions (%d ok, %d failed), %d escalations. Duration: %dms.",
|
|
count($cycle['observations']),
|
|
count($cycle['plan']),
|
|
count($cycle['actions']),
|
|
$ok, $fail,
|
|
count($cycle['escalations']),
|
|
$cycle['duration_ms']
|
|
);
|
|
}
|
|
|
|
function dir_logCycle($cycle) {
|
|
$line = json_encode([
|
|
'ts' => $cycle['timestamp'],
|
|
'phase' => $cycle['phase'],
|
|
'obs_count' => count($cycle['observations']),
|
|
'issues' => count($cycle['plan']),
|
|
'actions' => count($cycle['actions']),
|
|
'escalations' => count($cycle['escalations']),
|
|
'duration_ms' => $cycle['duration_ms'],
|
|
'report' => $cycle['report'],
|
|
], JSON_UNESCAPED_UNICODE) . "\n";
|
|
file_put_contents(DIR_HISTORY_FILE, $line, FILE_APPEND | LOCK_EX);
|
|
file_put_contents(DIR_STATE_FILE, json_encode($cycle, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
function dir_getStatus() {
|
|
if (!file_exists(DIR_STATE_FILE)) return ['status' => 'never_run'];
|
|
return json_decode(file_get_contents(DIR_STATE_FILE), true) ?: ['status' => 'corrupt'];
|
|
}
|
|
|
|
function dir_getHistory($n = 20) {
|
|
if (!file_exists(DIR_HISTORY_FILE)) return [];
|
|
$lines = file(DIR_HISTORY_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
$lines = array_slice($lines, -$n);
|
|
return array_map(fn($l) => json_decode($l, true), $lines);
|
|
}
|
|
|
|
function dir_lastCycleAge() {
|
|
if (!file_exists(DIR_STATE_FILE)) return 99999;
|
|
return time() - filemtime(DIR_STATE_FILE);
|
|
}
|
|
|
|
function dir_uptime() {
|
|
$uptime = trim(shell_exec("cat /proc/uptime | awk '{print int(\$1/86400)\"d \"int(\$1%86400/3600)\"h\"}'") ?? '');
|
|
return $uptime ?: 'unknown';
|
|
}
|