Files

104 lines
4.9 KiB
PHP

<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// The mesh entry point for the AI subsystem. Another node forwards an action here over SSH; this
// runs it through the same vv_ai_dispatch() a browser request on this node would have used, and
// writes the response back as JSON on stdout.
//
// CLI only. It is never served over HTTP and takes nothing from the environment but stdin.
//
// OPERATIONAL MODEL
// stdin {"action":"ask","params":{…},"is_post":true}
// stdout {"_vv_rpc":1,"status":200,"body":{…}}
//
// The wrapper exists so an HTTP status can travel with the body — a 405 raised in the dispatcher
// has to arrive at the calling node as a 405, not as a 200 carrying an error string.
//
// DESIGN PRINCIPLES
// One implementation, two doors.
// This shares every handler with api/ai.php through include/ai_actions.php. A mesh request
// and a browser request cannot diverge in behaviour because there is only one behaviour.
//
// The caller's node id is a label, not a claim.
// params._vv_node says which node asked, so findings and incidents are stored against the
// node they describe. It is not consulted for authorization — possession of the partnership
// SSH key already settled that, at onboard.
//
// OPERATIONAL SAFEGUARDS
// Refuses to run anywhere but the AI owner.
// A mirror that somehow received a forwarded request must not answer it from its own empty
// stores. Exits non-zero so the caller reports a transport failure rather than rendering an
// empty success.
//
// Refuses to run over the web.
// Guarded on PHP_SAPI. Reachable over HTTP this would be an unauthenticated bypass of every
// gate api/ai.php applies, since it takes its whole request from stdin.
//
// The master switch is honoured here too.
// AI_ENABLED false on the owner means the mesh gets the same refusal a local request gets.
// The calling node checks its own switch; this one checks the owner's.
//
// Input is size-capped before it is decoded.
// stdin is an untrusted stream from another process. A malformed or endless payload must
// fail as a bad request, not as an out-of-memory.
//
// RUNTIME MODES
// Not invoked by hand. include/ai_rpc.php opens an SSH session to the owner and runs this
// file with the request as JSON on stdin; the response is JSON on stdout. There are no flags
// and no arguments — the action, the profile and the payload all arrive in the request body.
//
// DEPENDS ON
// include/ai_actions.php vv_ai_dispatch() — the shared handlers
// include/config.php vv_ai_is_owner()
// ═══════════════════════════════════════════════════════════════════════════════════════════════
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit(1);
}
require_once dirname(__DIR__) . '/include/ai_actions.php';
function vv_rpc_out(int $status, array $body): void {
echo json_encode(['_vv_rpc' => 1, 'status' => $status, 'body' => $body],
JSON_UNESCAPED_SLASHES), "\n";
}
// Not this node's job. Exit non-zero: the caller must see a transport failure, not an answer
// assembled from stores that are empty here by design.
if (!vv_ai_is_owner()) {
fwrite(STDERR, "ai_rpc: this node is not the AI owner\n");
exit(2);
}
// 1 MiB. A turn's history is the largest legitimate payload and is capped far below this by the
// per-message truncation in the dispatcher; anything larger is not a request this serves.
$raw = stream_get_contents(STDIN, 1024 * 1024);
$req = json_decode((string)$raw, true);
if (!is_array($req)) {
vv_rpc_out(400, ['ok' => false, 'error' => 'ai_rpc: unreadable request']);
exit(0);
}
$action = trim((string)($req['action'] ?? ''));
$params = is_array($req['params'] ?? null) ? $req['params'] : [];
$isPost = (bool)($req['is_post'] ?? false);
if ($action === '') {
vv_rpc_out(400, ['ok' => false, 'error' => 'ai_rpc: no action']);
exit(0);
}
// The owner's own switch. The calling node already checked its own; this is the other half, and
// it is what makes turning AI off here take it off the whole mesh.
if (!vv_ai_enabled()) {
vv_rpc_out(200, ['ok' => false, 'error' => 'AI_ENABLED is false on the AI owner — AI features are off']);
exit(0);
}
vv_ai_log(sprintf('rpc action=%s from=%s', $action, (string)($params['_vv_node'] ?? '?')));
$httpStatus = 200;
$body = vv_ai_dispatch($action, $params, $isPost, $httpStatus);
vv_rpc_out($httpStatus, $body);