Search is the one capability that fits the profile holding none: every other capability either reads this installation or changes it, and chat's whole contract is that it can do neither. The assistant deliberately does not get it — its contract is that answers come from this install's own documents, and a web result there is an answer that looks sourced and is not. A chat question about this machine hands off to the assistant before the search would run, so it never reaches the internet even with the box ticked. Off by default, and not because it is dangerous: searching sends the operator's question outside the house, which is theirs to decide. Asked for per turn as well as enabled in conf. Provider-agnostic, as asked — searxng, brave, tavily. Only whichever is configured here can be verified; all three read every field by name, so a shape that changes underneath yields no results rather than wrong ones. The explain fixtures asserted chat holds no capabilities at all, which is exactly the guarantee worth keeping. caps=only: now states the set rather than its emptiness, so the check still fails the day something else is granted there.
237 lines
13 KiB
PHP
237 lines
13 KiB
PHP
<?php
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
// PURPOSE
|
|
// The one definition of what an AI profile is. Every consumer — the endpoint, the worker, the
|
|
// shared chat include and every page that renders it — reads it from here instead of
|
|
// restating it.
|
|
//
|
|
// WHY THIS EXISTS
|
|
// A profile used to be defined in five places: history depth in api/ai.php, capabilities in
|
|
// include/ai.php, label/hint/depth again in the chat's JavaScript, a prompt branch in the
|
|
// worker, and a label map in pages/scheduler.php. They had already drifted — the JavaScript
|
|
// knew three profiles where PHP knew four, so the shared chat could not offer troubleshoot at
|
|
// all and the Scheduler dock hand-rolled its own labels to compensate. The include carried a
|
|
// comment telling the next person not to let the two tables diverge, which is a comment doing
|
|
// a data structure's job.
|
|
//
|
|
// WHAT LIVES HERE, AND WHAT DELIBERATELY DOES NOT
|
|
// Here: anything more than one file needs to agree on — the set of profiles, their labels and
|
|
// hints, history depth, capabilities, and whether a profile is offered as a button.
|
|
//
|
|
// Not here: the system prompts. They are long, delicate, and have exactly one consumer, so
|
|
// moving them would be churn against the most sensitive text in the subsystem for no reduction
|
|
// in duplication. Tools/ai_chat_worker.php still owns them; it just keys off ids validated
|
|
// here rather than an if-chain that invents its own vocabulary.
|
|
//
|
|
// CAPABILITIES ARE PER PROFILE, NOT PER CAPABILITY
|
|
// The old table was inverted — capability => [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.
|
|
//
|
|
// It exists because the alternative already failed. 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,
|
|
'caps' => ['retrieve', 'health', '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', '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',
|
|
'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 '<script>window.VvAiProfiles = '
|
|
. json_encode(vv_ai_profiles_client(), JSON_UNESCAPED_SLASHES) . ";</script>\n";
|
|
}
|