123 lines
5.2 KiB
PHP
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";
|
|
}
|