Files
html/api/wevia-agent-exec.php

353 lines
13 KiB
PHP

<?php
// API: /api/wevia-agent-exec.php v3
// Wave 313 - WEVIA Master Agent + CONSENSUS mode
// Doctrine: Avant exec, WEVIA demande avis N IA souveraines, vote majoritaire
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
ob_implicit_flush(true);
@ob_end_flush();
function sse($event, $data) {
echo "event: $event\n";
echo "data: " . json_encode($data) . "\n\n";
@ob_flush(); @flush();
}
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['task'])) {
sse('error', ['error' => 'Missing task']);
exit;
}
$task = $input['task'];
$max_steps = (int)($input['max_steps'] ?? 5);
$dry_run = !empty($input['dry_run']);
$consensus = !empty($input['consensus']);
$consensus_models = $input['consensus_models'] ?? ['cerebras-think', 'groq', 'gemini', 'mistral', 'cloudflare-ai'];
sse('start', ['task' => $task, 'max_steps' => $max_steps, 'dry_run' => $dry_run, 'consensus' => $consensus]);
// === SAFETY GUARDRAILS ===
function isSafeCmd($cmd) {
$blocklist = [
'/rm\s+-rf?\s+\/(?!tmp|var\/log|var\/www\/html\/proofs)/i',
'/dd\s+if=.*of=\/dev\/(sda|nvme|hda)/i',
'/mkfs\./i', '/:(){.*};:/i', '/curl.*\|\s*bash/i', '/wget.*\|\s*sh/i',
'/chattr\s+-i\s+\/etc/i', '/userdel\s+root/i', '/passwd\s+root/i',
'/shutdown|reboot|halt|poweroff/i', '/iptables\s+-F/i',
'/systemctl\s+(stop|disable)\s+(nginx|php|cron|sshd)/i'
];
foreach ($blocklist as $pattern) if (preg_match($pattern, $cmd)) return false;
return true;
}
function llmCall($system, $user, $model = 'cerebras-think', $max_tokens = 1500, $timeout = 25) {
$payload = [
'model' => $model,
'messages' => [
['role' => 'system', 'content' => $system],
['role' => 'user', 'content' => $user]
],
'max_tokens' => $max_tokens,
'temperature' => 0.2
];
$ch = curl_init('http://127.0.0.1:4000/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => $timeout
]);
$resp = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = @json_decode($resp, true);
if ($http !== 200) return ['error' => "LLM HTTP $http", 'raw' => substr($resp, 0, 200)];
return [
'content' => $data['choices'][0]['message']['content'] ?? '',
'model' => $data['model'] ?? null,
'provider' => $data['provider'] ?? null
];
}
function execCmd($cmd, $timeout = 25) {
if (!isSafeCmd($cmd)) {
return ['ok' => false, 'error' => 'BLOCKED by safety guardrails', 'cmd' => $cmd];
}
$start = microtime(true);
$output = shell_exec("timeout $timeout bash -c " . escapeshellarg($cmd) . " 2>&1");
$duration_ms = round((microtime(true) - $start) * 1000);
return [
'ok' => true,
'cmd' => $cmd,
'output' => substr((string)$output, 0, 4000),
'duration_ms' => $duration_ms
];
}
function parsePlanJson($text) {
$text = preg_replace('/^```(?:json)?\s*/m', '', $text);
$text = preg_replace('/\s*```$/m', '', trim($text));
$data = @json_decode($text, true);
if (!$data || !isset($data['commands'])) {
$first = strpos($text, '{');
$last = strrpos($text, '}');
if ($first !== false && $last !== false && $last > $first) {
$data = @json_decode(substr($text, $first, $last - $first + 1), true);
}
}
if (!$data || !isset($data['commands'])) {
$unesc = stripslashes($text);
$first = strpos($unesc, '{');
$last = strrpos($unesc, '}');
if ($first !== false && $last !== false) {
$data = @json_decode(substr($unesc, $first, $last - $first + 1), true);
}
}
return $data;
}
// =========================================================
// STEP 1 — INITIAL PLAN by WEVIA Master (Cerebras-think)
// =========================================================
sse('thinking', ['step' => 'plan', 'msg' => 'WEVIA Master génère plan initial...']);
$systemPlan = "Tu es WEVIA Master, agent autonome S204 (weval-consulting.com).
Capacités: bash, php, python3, curl, git, sudo NOPASSWD www-data.
Réponds UNIQUEMENT en JSON strict:
{\"plan\":[\"étape 1\",...],\"commands\":[\"cmd bash 1\",...],\"risk\":\"low|medium|high\",\"rationale\":\"pourquoi cette approche\"}
Commands courtes, lisibles, exécutables. Pas de rm -rf /, format, fork bombs, curl|bash.
Si dangereux: {\"plan\":[\"REFUSED: raison\"],\"commands\":[],\"risk\":\"high\"}.";
$plan = llmCall($systemPlan, "TÂCHE: $task\n\nPropose plan JSON.", 'cerebras-think', 1500);
if (!empty($plan['error'])) {
sse('error', ['msg' => 'Plan LLM error: ' . $plan['error']]);
exit;
}
$planData = parsePlanJson($plan['content']);
if (!$planData) {
sse('error', ['msg' => 'Plan parse failed', 'raw' => substr($plan['content'], 0, 400)]);
exit;
}
sse('plan', [
'plan' => $planData['plan'] ?? [],
'commands' => $planData['commands'] ?? [],
'risk' => $planData['risk'] ?? 'unknown',
'rationale' => $planData['rationale'] ?? null,
'model' => $plan['model'],
'provider' => $plan['provider']
]);
// =========================================================
// doctrine_178_autoconsensus: force consensus si plan risky
$dangerousKeywords = ["restart", "reboot", "systemctl", "service ", "apt ", "upgrade", "kill ", "rm -rf", "DROP ", "TRUNCATE", "mysql ", "nginx "];
$planText = strtolower(json_encode($planData));
$riskyDetected = [];
foreach ($dangerousKeywords as $kw) {
if (strpos($planText, strtolower($kw)) !== false) $riskyDetected[] = $kw;
}
if (!empty($riskyDetected) && !$consensus) {
$consensus = true;
sse("doctrine_178", ["msg" => "Keywords dangereux detectes - consensus force", "keywords" => $riskyDetected]);
}
// STEP 1.5 — CONSENSUS MODE: ask N IA for opinion
// =========================================================
$consensusResult = null;
if ($consensus) {
sse('thinking', ['step' => 'consensus', 'msg' => 'Demande avis aux ' . count($consensus_models) . ' IA souveraines...']);
$opinionSystem = "Tu es un expert sysadmin/devops consultant. Tu dois donner un avis sur un plan d'exécution proposé par un autre agent.
Réponds UNIQUEMENT en JSON strict:
{\"vote\":\"approve|reject|modify\",\"confidence\":0-10,\"concerns\":[\"souci 1\",\"souci 2\"],\"suggested_changes\":[\"modif cmd 1\",...] ou [],\"rationale\":\"explication courte\"}";
$opinionUser = "TÂCHE INITIALE:\n$task\n\nPLAN PROPOSÉ par WEVIA Master:\n" . json_encode($planData, JSON_PRETTY_PRINT) . "\n\nDonne ton avis structuré JSON.";
$opinions = [];
$approve = 0; $reject = 0; $modify = 0;
$allConcerns = []; $allSuggestions = [];
foreach ($consensus_models as $modelId) {
sse('consensus_query', ['model' => $modelId, 'msg' => "Interroge $modelId..."]);
$opn = llmCall($opinionSystem, $opinionUser, $modelId, 800, 15);
if (!empty($opn['error'])) {
$opinions[] = [
'model' => $modelId,
'provider' => null,
'vote' => 'error',
'error' => $opn['error']
];
sse('consensus_response', end($opinions));
continue;
}
$parsed = parsePlanJson($opn['content']);
if (!$parsed || !isset($parsed['vote'])) {
$opinions[] = [
'model' => $modelId,
'provider' => $opn['provider'],
'vote' => 'unparseable',
'raw' => substr($opn['content'], 0, 200)
];
sse('consensus_response', end($opinions));
continue;
}
$vote = strtolower(trim($parsed['vote']));
if ($vote === 'approve') $approve++;
elseif ($vote === 'reject') $reject++;
elseif ($vote === 'modify') $modify++;
$entry = [
'model' => $modelId,
'provider' => $opn['provider'],
'vote' => $vote,
'confidence' => $parsed['confidence'] ?? null,
'concerns' => $parsed['concerns'] ?? [],
'suggested_changes' => $parsed['suggested_changes'] ?? [],
'rationale' => substr($parsed['rationale'] ?? '', 0, 300)
];
$opinions[] = $entry;
foreach ($entry['concerns'] as $c) $allConcerns[] = $c;
foreach ($entry['suggested_changes'] as $s) $allSuggestions[] = $s;
sse('consensus_response', $entry);
}
// Vote tally
$totalVotes = $approve + $reject + $modify;
$decision = 'unknown';
if ($totalVotes === 0) {
$decision = 'no_quorum';
} elseif ($reject > $approve && $reject > $modify) {
$decision = 'rejected_by_majority';
} elseif ($modify >= $approve && $modify > 0) {
$decision = 'modify_recommended';
} elseif ($approve > 0 && $approve >= $reject) {
$decision = 'approved';
} else {
$decision = 'no_consensus';
}
$consensusResult = [
'opinions' => $opinions,
'tally' => ['approve' => $approve, 'reject' => $reject, 'modify' => $modify, 'total' => $totalVotes],
'decision' => $decision,
'concerns_aggregated' => array_slice(array_unique($allConcerns), 0, 8),
'suggestions_aggregated' => array_slice(array_unique($allSuggestions), 0, 8)
];
sse('consensus_decision', $consensusResult);
// === Honor decision ===
if ($decision === 'rejected_by_majority' || $decision === 'no_quorum') {
sse('aborted', [
'reason' => $decision,
'concerns' => $consensusResult['concerns_aggregated'],
'suggestions' => $consensusResult['suggestions_aggregated']
]);
sse('done', ['task' => $task, 'aborted' => true, 'reason' => $decision]);
exit;
}
// === If MODIFY recommended, ask WEVIA Master to revise plan ===
if ($decision === 'modify_recommended' && !empty($consensusResult['suggestions_aggregated'])) {
sse('thinking', ['step' => 'revise', 'msg' => 'Consensus = modify. WEVIA révise le plan avec les suggestions...']);
$reviseUser = "TÂCHE: $task\n\nPLAN ORIGINAL:\n" . json_encode($planData) . "\n\nRETOURS DES IA CONSULTANTES:\nConcerns: " . json_encode($consensusResult['concerns_aggregated']) . "\nSuggestions: " . json_encode($consensusResult['suggestions_aggregated']) . "\n\nRévise le plan en intégrant les suggestions. Réponds en JSON strict {plan,commands,risk,rationale}.";
$revised = llmCall($systemPlan, $reviseUser, 'cerebras-think', 1500);
$revisedData = parsePlanJson($revised['content'] ?? '');
if ($revisedData && !empty($revisedData['commands'])) {
$planData = $revisedData;
sse('plan_revised', [
'plan' => $planData['plan'] ?? [],
'commands' => $planData['commands'] ?? [],
'risk' => $planData['risk'] ?? 'unknown',
'integrated_suggestions' => count($consensusResult['suggestions_aggregated'])
]);
}
}
}
// =========================================================
// STEP 2 — EXECUTE
// =========================================================
if ($dry_run) {
sse('done', ['msg' => 'DRY RUN - no execution', 'plan' => $planData, 'consensus' => $consensusResult]);
exit;
}
$results = [];
$commands = array_slice($planData['commands'] ?? [], 0, $max_steps);
foreach ($commands as $idx => $cmd) {
sse('exec_start', ['step' => $idx + 1, 'cmd' => $cmd]);
$result = execCmd($cmd, 25);
$results[] = $result;
sse('exec_result', [
'step' => $idx + 1,
'cmd' => $cmd,
'ok' => $result['ok'],
'output' => $result['output'] ?? null,
'error' => $result['error'] ?? null,
'duration_ms' => $result['duration_ms'] ?? null
]);
if (!$result['ok'] || (strpos($result['output'] ?? '', 'error') !== false && strpos($result['output'] ?? '', 'No such') !== false)) {
sse('recovery_thinking', ['msg' => 'Erreur, WEVIA propose fix...']);
$recoverPrompt = "Cmd échouée:\n$cmd\nOutput:\n" . substr($result['output'] ?? $result['error'], 0, 500) . "\n\nPropose UNE cmd bash de fix. UNIQUEMENT la commande.";
$recover = llmCall("Tu es WEVIA. Réponds UNIQUEMENT avec une cmd bash valide.", $recoverPrompt, 'cerebras-fast', 200);
$fixCmd = trim($recover['content'] ?? '');
$fixCmd = preg_replace('/^```(?:bash)?\s*|\s*```$/m', '', $fixCmd);
$fixCmd = trim(explode("\n", $fixCmd)[0]);
if ($fixCmd && strlen($fixCmd) < 500) {
sse('recovery_exec', ['fix_cmd' => $fixCmd]);
$fixResult = execCmd($fixCmd, 15);
sse('recovery_result', $fixResult);
$results[] = $fixResult;
}
}
}
// =========================================================
// STEP 3 — SUMMARIZE
// =========================================================
sse('thinking', ['step' => 'summarize', 'msg' => 'WEVIA résume...']);
$resultsText = '';
foreach ($results as $i => $r) {
$resultsText .= "Step " . ($i+1) . " cmd: " . substr($r['cmd'] ?? '', 0, 200) . "\nOutput: " . substr($r['output'] ?? $r['error'] ?? '', 0, 600) . "\n\n";
}
$summarizeContext = "Tâche: $task\n\nRésultats:\n$resultsText";
if ($consensusResult) {
$summarizeContext .= "\nConsensus: " . $consensusResult['decision'] . " (approve=" . $consensusResult['tally']['approve'] . " reject=" . $consensusResult['tally']['reject'] . " modify=" . $consensusResult['tally']['modify'] . ")";
}
$summarize = llmCall(
"Tu es WEVIA Master. Résume en français pro, concis. Indique succès/échec + 1-2 next actions.",
$summarizeContext,
'cerebras-fast',
600
);
sse('summary', [
'content' => $summarize['content'] ?? '(no summary)',
'model' => $summarize['model'],
'provider' => $summarize['provider']
]);
sse('done', [
'task' => $task,
'steps_executed' => count($results),
'success_count' => count(array_filter($results, fn($r) => $r['ok'] ?? false)),
'consensus_used' => $consensus,
'consensus_decision' => $consensusResult['decision'] ?? null
]);