Files
wevia-brain/wevia-director-agent.php
2026-04-15 02:30:29 +02:00

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';
}