Restrict the AI tab to HOST1 — the only node with the GPU, Ollama and the index
This commit is contained in:
@@ -74,10 +74,17 @@ unset($_master, $_h1m, $_host1_blank, $_my_hostid, $_conf_missing);
|
|||||||
$tab = $_GET['tab'] ?? 'monitor';
|
$tab = $_GET['tab'] ?? 'monitor';
|
||||||
$validTabs = ['monitor', 'scheduler', 'docker', 'watchdog', 'partnership', 'fallback', 'arrs', 'rsync', 'auth', 'settings'];
|
$validTabs = ['monitor', 'scheduler', 'docker', 'watchdog', 'partnership', 'fallback', 'arrs', 'rsync', 'auth', 'settings'];
|
||||||
|
|
||||||
// The AI tab exists only while AI_ENABLED is true. Appended to $validTabs rather than filtered
|
// The AI tab exists only on HOST1, and only while AI_ENABLED is true. Appended to $validTabs
|
||||||
// out of it, so the check below rejects ?tab=ai server-side as well — omitting the link is
|
// rather than filtered out of it, so the check below rejects ?tab=ai server-side as well —
|
||||||
// presentation, not access control, and api/ai.php refuses `ask` on the same flag independently.
|
// omitting the link is presentation, not access control, and api/ai.php refuses every action
|
||||||
$_vv_ai = strtolower(trim(vv_conf_vars()['AI_ENABLED'] ?? 'false')) === 'true';
|
// on the same two conditions independently.
|
||||||
|
//
|
||||||
|
// The host half is not a preference: HOST1 owns the GPU, the Ollama process and the index, and
|
||||||
|
// include/ai.php only ever reads the *local* {HOST}_OLLAMA_URL — there is no Tailscale resolver
|
||||||
|
// in the PHP layer the way there is in the shell. On any other host the tab could only render
|
||||||
|
// and then fail its own health check.
|
||||||
|
$_vv_ai = vv_is_ai_host()
|
||||||
|
&& strtolower(trim(vv_conf_vars()['AI_ENABLED'] ?? 'false')) === 'true';
|
||||||
if ($_vv_ai) $validTabs[] = 'ai';
|
if ($_vv_ai) $validTabs[] = 'ai';
|
||||||
|
|
||||||
if (!in_array($tab, $validTabs)) $tab = 'monitor';
|
if (!in_array($tab, $validTabs)) $tab = 'monitor';
|
||||||
|
|||||||
@@ -30,9 +30,10 @@
|
|||||||
// its token, so the token is not guessable.
|
// its token, so the token is not guessable.
|
||||||
//
|
//
|
||||||
// OPERATIONAL SAFEGUARDS
|
// OPERATIONAL SAFEGUARDS
|
||||||
// ask is refused when AI is disabled.
|
// Every action is refused off HOST1, and ask is refused when AI is disabled.
|
||||||
// AI_ENABLED gates the whole subsystem; the tab is hidden when it is false, but hiding
|
// AI_ENABLED gates the whole subsystem and vv_is_ai_host() gates the node; the tab is
|
||||||
// a link is not access control and the endpoint is reachable directly.
|
// hidden when either fails, but hiding a link is not access control and the endpoint is
|
||||||
|
// reachable directly. The host check sits ahead of the dispatch and answers 404.
|
||||||
//
|
//
|
||||||
// Every token is validated as hex before it composes a path.
|
// Every token is validated as hex before it composes a path.
|
||||||
// vv_ai_job_path() returns null for anything else, and each caller checks. That pattern
|
// vv_ai_job_path() returns null for anything else, and each caller checks. That pattern
|
||||||
@@ -117,6 +118,16 @@ if ($action !== 'poll') {
|
|||||||
$_SERVER['REMOTE_ADDR'] ?? '?'));
|
$_SERVER['REMOTE_ADDR'] ?? '?'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Host gate, ahead of the dispatch rather than inside each action. Varaverk.page omits the tab
|
||||||
|
// on any host but HOST1, but a hidden link is not access control and this endpoint is reachable
|
||||||
|
// directly. Every action is refused rather than just the expensive ones — there is no such thing
|
||||||
|
// as a read this host is entitled to, since the index and the model are not here.
|
||||||
|
if (!vv_is_ai_host()) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'AI is not available on this host']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// ── stats ─────────────────────────────────────────────────────────────────────
|
// ── stats ─────────────────────────────────────────────────────────────────────
|
||||||
if ($action === 'stats') {
|
if ($action === 'stats') {
|
||||||
echo json_encode(['ok' => true, 'stats' => vv_ai_stats()]);
|
echo json_encode(['ok' => true, 'stats' => vv_ai_stats()]);
|
||||||
|
|||||||
@@ -43,7 +43,8 @@
|
|||||||
// Shared config still resolves; host-specific values are simply absent.
|
// Shared config still resolves; host-specific values are simply absent.
|
||||||
//
|
//
|
||||||
// EXPORTS
|
// EXPORTS
|
||||||
// Identity vv_detect_host(), vv_get_hostname(), vv_is_owner(), vv_known_hosts()
|
// Identity vv_detect_host(), vv_get_hostname(), vv_is_owner(), vv_is_ai_host(),
|
||||||
|
// vv_known_hosts()
|
||||||
// Config vv_conf_vars(), vv_read_conf_raw(), vv_write_conf_raw(), vv_get_conf_files()
|
// Config vv_conf_vars(), vv_read_conf_raw(), vv_write_conf_raw(), vv_get_conf_files()
|
||||||
// Parsing vv_parse_conf_scalar(), vv_parse_kv_db(), vv_format_uptime()
|
// Parsing vv_parse_conf_scalar(), vv_parse_kv_db(), vv_format_uptime()
|
||||||
// Remote vv_resolve_tailscale_ip(), vv_remote_state_cmd(), vv_local_ip()
|
// Remote vv_resolve_tailscale_ip(), vv_remote_state_cmd(), vv_local_ip()
|
||||||
@@ -257,6 +258,16 @@ function vv_is_owner(): bool {
|
|||||||
return vv_detect_host() === 'host1';
|
return vv_detect_host() === 'host1';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The AI subsystem is HOST1-only: it is the node with the GPU, the Ollama process and the
|
||||||
|
// index. Deliberately a separate predicate from vv_is_owner() even though both resolve to
|
||||||
|
// host1 today — one says "auth source of truth", this one says "AI runs here", and the day
|
||||||
|
// either moves, conflating them would move the other by accident.
|
||||||
|
//
|
||||||
|
// Returns false for 'unknown', so a host that cannot identify itself never shows the tab.
|
||||||
|
function vv_is_ai_host(): bool {
|
||||||
|
return vv_detect_host() === 'host1';
|
||||||
|
}
|
||||||
|
|
||||||
function vv_read_conf_raw(string $filename): string {
|
function vv_read_conf_raw(string $filename): string {
|
||||||
$path = CONF_DIR . '/' . $filename;
|
$path = CONF_DIR . '/' . $filename;
|
||||||
return file_exists($path) ? file_get_contents($path) : '';
|
return file_exists($path) ? file_get_contents($path) : '';
|
||||||
|
|||||||
@@ -9,9 +9,11 @@
|
|||||||
// api/ai.php, receives a token, and polls until the job reaches done or error. That keeps
|
// api/ai.php, receives a token, and polls until the job reaches done or error. That keeps
|
||||||
// the api layer on one response convention; see api/ai.php for why SSE was declined.
|
// the api layer on one response convention; see api/ai.php for why SSE was declined.
|
||||||
//
|
//
|
||||||
// The tab is only reachable when AI_ENABLED is true. Varaverk.page omits it from the tab
|
// The tab is only reachable on HOST1, and only when AI_ENABLED is true. Varaverk.page omits
|
||||||
// list and rejects it server-side, and api/ai.php refuses ask independently — hiding a link
|
// it from the tab list and rejects it server-side, and api/ai.php refuses every action on
|
||||||
// is not access control.
|
// the same two conditions independently — hiding a link is not access control. HOST1 is the
|
||||||
|
// node with the GPU, the Ollama process and the index; include/ai.php reads only the local
|
||||||
|
// {HOST}_OLLAMA_URL, so the tab could not function anywhere else regardless.
|
||||||
//
|
//
|
||||||
// DESIGN PRINCIPLES
|
// DESIGN PRINCIPLES
|
||||||
// The banner leads with offload, not with size.
|
// The banner leads with offload, not with size.
|
||||||
|
|||||||
Reference in New Issue
Block a user