Files
html/api/wevia-pending-loader.php
opus 12acb77dc4
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
wave(210): PendingLoader bash -c wrapper + Multi-Agent Console + 28 tips unblocked
2026-04-21 15:44:56 +02:00

142 lines
6.3 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) {
// WAVE 203 FINAL: handlers (file_dump.php, scan_file.php etc.) have exit/echo
// and would KILL the loader script. Pre-filter by signature: only files
// whose FIRST 1000 bytes contain "return array" are valid stubs.
$head = @file_get_contents($s, false, null, 0, 1000);
if (!$head || strpos($head, 'return array') === false) continue;
// Now safe to include (guarded by ob_start for any residual echo)
ob_start();
$info = @include $s;
$leaked = ob_get_clean();
unset($leaked);
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'];
// V96: allow DISABLED_* variants (DISABLED_FAKE_*, DISABLED_TEST_*, etc.)
$_status_val = $info['status'] ?? '';
if (in_array($_status_val, $_skip_statuses, true) || strpos($_status_val, 'DISABLED') === 0) 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'
];
}
// WAVE 203: substitute {MSG} placeholder with user message (base64-safe)
if (strpos($cmd, '{MSG}') !== false) {
global $__wpl_current_msg;
$esc = base64_encode((string)($__wpl_current_msg ?? ''));
$cmd = str_replace('{MSG}', $esc, $cmd);
}
$start = microtime(true);
$out = @shell_exec("timeout 20 bash -c " . escapeshellarg($cmd) . " 2>&1"); // WAVE 210 bash -c wrapper for builtins (cd, source, etc.)
$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) {
global $__wpl_current_msg;
$__wpl_current_msg = $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";
}