[profiles] — which reads well when adding a // capability and badly when answering the question actually asked at runtime, which is // always "what can this profile do". Same content, turned the right way round. // // A profile is a contract plus a set of inputs, and the inputs are the half that has to be // enforced rather than requested. The caps list is that half. // // OPERATIONAL SAFEGUARDS // The capability list is the grant, and it is enforced in one place. // The same permissions used to live as a dozen `$profile === 'varaverk' || $profile === // 'troubleshoot'` conditions spread across the worker, and answering "may chat ever be // shown a log?" meant reading all of them. It could — a gate added for run-outcome // questions granted it by omission, and the chat profile, whose entire value is that it // has NOT been shown this installation, was one phrasing away from being handed a health // sweep and 120 lines of log. Nothing about that was visible at the point of the mistake. // Here it would have been one missing word on one line. // // A capability is permission, not need. // varaverk holds 'health' but only attaches it when the question looks diagnostic; // troubleshoot attaches it always. The gates decide whether an input is warranted, this // decides whether it is allowed, and a gate can never widen the grant. // // chat holding an empty capability list is a guarantee, not an oversight. // Anything added to it stops being general chat and becomes an assistant that sometimes // lies about this installation. // // EXPORTS // vv_ai_profiles() the whole table // vv_ai_profile($id) one entry, or null // vv_ai_profile_ok($id) is this a real profile // vv_ai_profile_turns($id) history depth in turns // vv_ai_profile_can($id,$cap) capability check // vv_ai_profile_caps($id) every capability a profile holds // vv_ai_profile_label($id) display name, falling back to the id // vv_ai_profiles_ui() those offered as buttons, in order // vv_ai_profiles_max_turns() deepest window of any profile // vv_ai_profiles_client() the subset the browser needs, for json_encode // vv_ai_profiles_script() publishes that subset as window.VvAiProfiles, once per page // ═══════════════════════════════════════════════════════════════════════════════════════════════ // turns — conversation depth. Varaverk Assistant spends ~2500 of its 16384 on retrieved // passages so it cannot afford much; troubleshoot carries a whole log tail and can // afford least of all; the two that retrieve nothing can hold a real conversation. // caps — see VV_AI_CAP_MEANING below. // ui — appears in the profile bar. troubleshoot is false because it is entered by opening a // log on the Scheduler tab, not by choosing it: it is a context you are in, and a // button offering it from the dashboard would produce a troubleshooting contract with // nothing to troubleshoot. const VV_AI_PROFILES_DEF = [ 'varaverk' => [ 'label' => 'Varaverk Assistant', 'short' => 'Assistant', 'hint' => 'Answers only from Varaverk\'s own docs, with sources. Says so when they don\'t cover it.', 'turns' => 3, 'ui' => true, 'caps' => ['retrieve', 'kind_filter', 'health', 'run_evidence', 'incidents', 'conf_lookup'], ], 'chat' => [ 'label' => 'General Chat', 'short' => 'Chat', 'hint' => 'Ordinary conversation. Hands anything about this install to the assistant on its own.', 'turns' => 8, 'ui' => true, // The only capability general chat holds, and it holds it *because* chat cannot write. // Search is a read that leaves the house; every other capability in this table either // reads this installation or changes it, and neither belongs on the profile whose // contract is that it has no reach into the machine at all. The Varaverk assistant is // deliberately not given it: its contract is that answers come from this installation's // own documents, and a web result there is an answer that looks sourced and is not. 'caps' => ['web_search'], ], 'code' => [ 'label' => 'Code Sketcher', 'short' => 'Code', 'hint' => 'Drafts short scripts for Custom Scripts. First drafts — test before trusting.', 'turns' => 4, 'ui' => true, 'caps' => ['code_scan'], ], // Offered as a button as well as entered by context. It was context-only on the reasoning // that choosing it from a dashboard produces a troubleshooting contract with nothing to // troubleshoot — true of the inputs, but not of the profile: name a script in the question // and run_evidence resolves the run record and log for it without any scope at all. The // prompt now states which of those it actually received rather than asserting a log, so // picking it with nothing open degrades to an honest "I cannot see one" instead of an // invented reading of a log that was never attached. 'troubleshoot' => [ 'label' => 'Troubleshoot', 'short' => 'Troubleshoot', 'hint' => 'Reasons from evidence — the log you have open, or one you name. Says so when there is none.', 'turns' => 2, 'ui' => true, // system_state is read-only and shared with repair. Both need to know a container is down // or a pool is full to explain anything about this machine rather than about Unraid in // general; neither gets a way to act on it, and only repair can change a setting. 'caps' => ['retrieve', 'health', 'system_state', 'fallback_state', 'run_evidence', 'scoped_log', 'incidents', 'conf_lookup', 'file_bugs'], ], // The only profile that may change a setting, and the only one not offered as a button. // // It is entered by opening a finding, the way troubleshoot was originally entered by opening // a log — a repair conversation with nothing to repair is a contract with no subject. That is // not the only reason though: capabilities are granted per profile, so making this selectable // would put conf_write one click away from any question at all. The narrow grant is the point. // // Reads two records of what has happened before. The phrasebook holds what the operator calls // things and, more usefully, what they have had to correct; closed findings hold what actually // fixed a fault last time. Between them the profile starts a conversation already knowing the // operator's vocabulary and this installation's history, instead of relearning both each time. 'repair' => [ 'label' => 'Repair', 'short' => 'Repair', 'hint' => 'Works through a finding with you, and can apply a fix you approve.', 'turns' => 3, 'ui' => false, 'caps' => ['retrieve', 'health', 'system_state', 'fallback_state', 'run_evidence', 'scoped_log', 'incidents', 'conf_lookup', 'conf_write', 'probe', 'file_findings', 'phrasebook', 'past_fixes'], ], ]; // Documentation only — nothing branches on it. Kept beside the table because a capability name // with no stated meaning is how one ends up granted to a profile that should not have it. const VV_AI_CAP_MEANING = [ 'retrieve' => 'retrieval passages from the documentation index', 'kind_filter' => 'the retrieval kind filter the page exposes', 'health' => 'live health sweep measured at question time — the AI subsystem only', 'system_state' => 'read-only view of the machine: hardware, containers, pools, array, UPS', 'fallback_state' => 'whether a failover would actually work: state, tiers, and whether the partner really has the covered containers', 'run_evidence' => 'run record and log tail for a script named in the question', 'scoped_log' => 'log tail for whatever the operator currently has open', 'incidents' => 'operator-written history of what previously went wrong with this thing', 'conf_lookup' => 'deterministic "where does this conf key actually live" lookup', 'file_bugs' => 'may file a bug report against Varaverk itself', 'code_scan' => 'destructive-operation scan of generated shell', 'conf_write' => 'may change a setting — through the guarded write path, never directly', 'probe' => 'may test a candidate address before anything is written to conf', 'file_findings'=> 'may record, acknowledge and close findings about this installation', 'phrasebook' => 'what the operator calls things, and what they have corrected before', 'past_fixes' => 'findings already closed — what fixed this last time', 'web_search' => 'may search the web — the one capability that sends text off this machine', ]; function vv_ai_profiles(): array { return VV_AI_PROFILES_DEF; } function vv_ai_profile(string $id): ?array { return VV_AI_PROFILES_DEF[$id] ?? null; } function vv_ai_profile_ok(string $id): bool { return isset(VV_AI_PROFILES_DEF[$id]); } // Falls back rather than throwing. An unknown id reaching here is a caller that failed to // validate, and the shallowest window is the safe way to be wrong: it spends the least context. function vv_ai_profile_turns(string $id): int { return (int)(VV_AI_PROFILES_DEF[$id]['turns'] ?? 3); } function vv_ai_profile_caps(string $id): array { return VV_AI_PROFILES_DEF[$id]['caps'] ?? []; } function vv_ai_profile_can(string $id, string $cap): bool { return in_array($cap, VV_AI_PROFILES_DEF[$id]['caps'] ?? [], true); } function vv_ai_profile_label(string $id): string { return VV_AI_PROFILES_DEF[$id]['label'] ?? $id; } // Insertion order is the button order, and varaverk is first because the strict profile is the // one to land on: a misuse there costs "the docs don't cover that" rather than an invented claim // about the system. function vv_ai_profiles_ui(): array { return array_filter(VV_AI_PROFILES_DEF, fn($p) => !empty($p['ui'])); } // The cap api/ai.php applies when storing a conversation, which has to hold the deepest window // any profile could later ask for — a chat saved under one profile can be reopened under another. function vv_ai_profiles_max_turns(): int { return max(array_column(VV_AI_PROFILES_DEF, 'turns')); } // What the browser needs, and nothing more. The prompts are not here to be sent and the server // re-derives depth and capability on every request regardless — this is for drawing buttons and // deciding whether to show the kind filter, not for the client to make policy with. function vv_ai_profiles_client(): array { $out = []; foreach (VV_AI_PROFILES_DEF as $id => $p) { $out[$id] = [ 'label' => $p['label'], 'short' => $p['short'], 'hint' => $p['hint'], 'turns' => $p['turns'], 'ui' => (bool)$p['ui'], 'kind' => in_array('kind_filter', $p['caps'], true), // Whether to offer the search box, on the same terms as the kind filter: derived from // the capability rather than named per profile, so granting or removing it in the // table above is the only edit either side needs. 'web' => in_array('web_search', $p['caps'], true), ]; } return $out; } // Publishes the registry to the browser as window.VvAiProfiles, once per page however many // surfaces ask for it. Emitted as its own tag so any script block that consumes it stays pure // JavaScript and remains syntax-checkable outside PHP. // // The shared chat include calls this, as does any page emitting the registry ahead of it. Before // it existed the Scheduler had its own literal `{ code: 'Code', troubleshoot: 'Troubleshoot' }` // map, which is how a fourth copy of the profile list came to exist in the first place. function vv_ai_profiles_script(): void { static $done = false; if ($done) return; $done = true; echo '\n"; }