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);