Files
html/api/wevia-pending-loader.php
2026-04-20 13:15:02 +02:00

123 lines
5.2 KiB
PHP

<?php
// ============================================================================
// WEVIA PENDING LOADER - SAFE WRAPPER - Opus WIRE 19-avr
// Reads /var/www/html/api/wired-pending/intent-opus4-*.php
// Matches user message against triggers, executes whitelisted cmd
// Returns SSE answer event. Standalone - no writes to core files.
// Zero ecrasement. Zero suppression. Zero hardcode. Zero regression.
// ============================================================================
function wpl_log($msg) {
@file_put_contents('/tmp/wevia-pending-loader.log', date('c') . " $msg\n", FILE_APPEND);
}
function wpl_is_safe($cmd) {
// Whitelist safe prefixes (mirrors opus5-stub-promoter)
$SAFE = ['echo ', 'curl -sk ', 'curl -s ', 'php8.4 /var/www/html/api/', 'php /var/www/html/api/',
'git log', 'git status', 'cat /var/log/', 'cat /opt/weval-l99/', 'grep ', 'psql ',
'PGPASSWORD=', 'env PGPASSWORD=', 'ls /opt/weval-l99/', 'ls /var/www/html/', 'head '];
$BLOCKED = ['sudo', 'chattr', 'rm -rf', 'rm /', 'dd ', 'mkfs', '> /dev', 'systemctl stop',
'systemctl disable', 'shutdown', 'reboot', 'useradd', 'userdel', 'passwd ',
'/etc/passwd', '/etc/shadow', 'iptables -F', 'kill -9'];
foreach ($BLOCKED as $b) if (stripos($cmd, $b) !== false) return false;
foreach ($SAFE as $p) {
if (stripos(ltrim($cmd), $p) === 0) return true;
if (stripos($cmd, " $p") !== false) return true;
}
return false;
}
function wpl_match_intent($message) {
$message_lower = strtolower(trim($message));
if (strlen($message_lower) < 3) return null;
$stubs = glob('/var/www/html/api/wired-pending/intent-opus4-*.php') ?: [];
$best = null; $best_score = 0;
$exact_matches = [];
foreach ($stubs as $s) {
$info = @include $s;
if (!is_array($info)) continue;
if (empty($info['triggers']) || empty($info['cmd'])) continue;
if (!empty($info['status'])) {
$_skip_statuses = ['PENDING_SECURITY_REVIEW', 'DEPRECATED_HARDCODED_20AVR_OPUS46', 'DEPRECATED', 'DISABLED'];
if (in_array($info['status'], $_skip_statuses, true)) continue;
}
foreach ($info['triggers'] as $trigger) {
$t = strtolower(trim($trigger));
if (strlen($t) < 4) continue;
// Exact match - collect all candidates, do not return early
if ($message_lower === $t) {
$exact_matches[] = $info;
break; // next stub
}
// Full trigger in message
if (strpos($message_lower, $t) !== false) {
// Compound score: trigger length + triggers_count + cmd_length/100 (prefer richer stubs)
$richness = count($info['triggers']) + strlen($info['cmd']) / 100.0;
$score = strlen($t) + $richness;
if ($score > $best_score) { $best = $info; $best_score = $score; }
}
}
}
// Priority on exact match: richest stub wins (most triggers + longest cmd)
if (!empty($exact_matches)) {
usort($exact_matches, function($a, $b) {
$score_a = count($a['triggers']) + strlen($a['cmd']) / 100.0;
$score_b = count($b['triggers']) + strlen($b['cmd']) / 100.0;
return $score_b <=> $score_a;
});
return $exact_matches[0];
}
return $best;
}
function wpl_execute_intent($info) {
$name = $info['name'] ?? 'unknown';
$cmd = $info['cmd'] ?? '';
if (!wpl_is_safe($cmd)) {
return [
'ok' => false,
'name' => $name,
'text' => "Intent '$name' detecte mais commande non whitelistee (securite). Review manuel requis.",
'intent' => 'pending_unsafe'
];
}
$start = microtime(true);
$out = @shell_exec("timeout 20 $cmd 2>&1");
$ms = round((microtime(true) - $start) * 1000);
$text = trim((string)$out);
if (strlen($text) > 1500) $text = substr($text, 0, 1500) . "\n... (tronque)";
if ($text === '') $text = "Intent '$name' execute (cmd sans output).";
wpl_log("MATCH name=$name ms=$ms bytes=" . strlen($text));
return [
'ok' => true,
'name' => $name,
'text' => $text,
'intent' => 'pending_' . $name,
'ms' => $ms
];
}
// Main entry point - supports both standalone API and include
function wevia_pending_loader($message) {
$info = wpl_match_intent($message);
if (!$info) return null;
return wpl_execute_intent($info);
}
// If called directly as API
if (basename($_SERVER['SCRIPT_NAME'] ?? '') === 'wevia-pending-loader.php') {
$input = json_decode(file_get_contents("php://input"), true);
$message = $input['message'] ?? ($_GET['message'] ?? '');
if (!$message) { http_response_code(400); echo json_encode(['error' => 'no message']); exit; }
$r = wevia_pending_loader($message);
if (!$r) { echo json_encode(['match' => false, 'message' => $message]); exit; }
header("Content-Type: text/event-stream");
echo "data: " . json_encode([
'type' => 'answer',
'text' => $r['text'],
'engine' => 'PendingLoader/' . $r['name'],
'intent' => $r['intent']
], JSON_UNESCAPED_UNICODE) . "\n\n";
echo "data: [DONE]\n\n";
}