job state // GET ?action=memory_get the operator memory file and its budget // GET ?action=chats stored conversations, newest first, metadata only // GET ?action=chat_get&id= one stored conversation with its transcript // GET ?action=findings [all=1] repair findings, open only unless all=1 // POST action=finding_action id= act=fix|move|ack|dismiss|reopen|cancel [note=…] // POST action=ask question=… [history=] [kind=…] [think=0|1] // POST action=memory_set memory=… replace the memory file // POST action=clear token= discard a finished job // POST action=chat_save [id=] profile=… messages= // POST action=chat_delete id= // // RESPONSE // stats {"ok":true,"stats":{…}} // ask {"ok":true,"token":""} // poll {"ok":true,"job":{"status":"retrieving|generating|done|error",…}} // clear {"ok":true} // chats {"ok":true,"chats":[{id,ts,profile,title,turns}],"max":N} // findings {"ok":true,"repair":{enabled,autofix,last},"findings":[…],"counts":{…}} // finding_action {"ok":true,"action":"fix"} // chat_save {"ok":true,"id":"","title":…} // {"ok":false,"error":…} // // DEPENDS ON // include/ai_actions.php vv_ai_dispatch() — every action, shared with the mesh entry point // include/ai_rpc.php vv_ai_route(), vv_ai_rpc() — where an action runs, and the SSH hop // include/ai.php vv_ai_enabled(), reached through ai_actions.php // ═══════════════════════════════════════════════════════════════════════════════════════════════ // First executable statement, deliberately dependency-free. A request that is rejected by the // CSRF prepend never reaches here and a request that dies inside the include never reaches the // action log below, and those two look identical from outside — which is what made a POST that // the browser demonstrably sent leave no trace anywhere on the server. // Polls are excluded here for the same reason they are excluded from the action log below: one // line per second per open tab buries every line worth reading. if (strpos($_SERVER['REQUEST_URI'] ?? '', 'action=poll') === false) { @file_put_contents('/var/log/varaverk/ai.log', date('Y-m-d H:i:s') . ' ENTER ' . ($_SERVER['REQUEST_METHOD'] ?? '?') . ' ' . ($_SERVER['REQUEST_URI'] ?? '?') . ' ct=' . substr($_SERVER['CONTENT_TYPE'] ?? '-', 0, 40) . ' len=' . ($_SERVER['CONTENT_LENGTH'] ?? '-') . "\n", FILE_APPEND | LOCK_EX); } header('Content-Type: application/json'); header('Cache-Control: no-store, no-cache'); require_once dirname(__DIR__) . '/include/ai_actions.php'; require_once dirname(__DIR__) . '/include/ai_rpc.php'; $isPost = $_SERVER['REQUEST_METHOD'] === 'POST'; $action = trim($isPost ? ($_POST['action'] ?? '') : ($_GET['action'] ?? 'stats')); // Params merged rather than picked by method. The POST-only checks inside the dispatcher are what // enforce the CSRF contract; which superglobal a value arrived in is not a security property, and // merging means a handler that reads one key does not care how the request was shaped. $params = $_POST + $_GET; if ($action !== 'poll') { vv_ai_log(sprintf('%s action=%s from=%s', $_SERVER['REQUEST_METHOD'] ?? '?', $action ?: '(none)', $_SERVER['REMOTE_ADDR'] ?? '?')); } // Master switch, ahead of everything. With AI_ENABLED false the tab is not in the tab list and no // dock is rendered, so nothing in the UI can legitimately reach any action here — including the // cheap reads, which would otherwise still answer with index and token figures for a subsystem the // operator has turned off. Not a 404: the switch is a setting, and the message names the setting. // // Checked before the routing below because it is this node's own switch either way. A mirror with // AI off must not forward to the owner: the operator turned AI off on this box, and honouring that // locally while quietly using someone else's model is not what the switch says it does. if (!vv_ai_enabled()) { echo json_encode(['ok' => false, 'error' => 'AI_ENABLED is false — AI features are off']); exit; } // Routing. vv_ai_route() decides local, remote or refused for this action on this node; the three // outcomes and the reasoning behind each live in include/ai_rpc.php, next to the transport that // carries them, rather than being restated here. $httpStatus = 200; $route = vv_ai_route($action); if ($route === VV_AI_ROUTE_DENY) { http_response_code(404); echo json_encode(['ok' => false, 'error' => 'This AI surface lives on the owner node only']); exit; } $body = $route === VV_AI_ROUTE_REMOTE ? vv_ai_rpc($action, $params, $isPost, $httpStatus) : vv_ai_dispatch($action, $params, $isPost, $httpStatus); if ($httpStatus !== 200) http_response_code($httpStatus); echo json_encode($body);