Let General Chat search the web, and only General Chat

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.
This commit is contained in:
Gmer4Lfe
2026-08-09 22:42:43 -04:00
parent 47861b0dc3
commit 613634473a
10 changed files with 405 additions and 12 deletions
+30
View File
@@ -464,6 +464,16 @@ vv_ai_profiles_script();
if (!sources || !sources.length) return '';
let h = '<div class="vv-ai-src"><div class="vv-ai-src-h">Sources</div>';
sources.forEach((s, i) => {
// A web result is a page on the internet, not a file in this install: it opens in a tab
// rather than in the source viewer, it leads with its title rather than its URL, and it
// carries no retrieval score because nothing here scored it.
if (s.web && s.url) {
const label = [s.heading, s.url].filter(Boolean).join(' — ');
h += `<div class="vv-ai-src-i" data-web="${esc(s.url)}">`
+ `<span class="vv-ai-src-n">[${i+1}]</span><span>${esc(label)}</span>`
+ `<span class="vv-ai-src-s">web</span></div>`;
return;
}
const label = [s.path, s.section, s.heading].filter(Boolean).join(' ');
h += `<div class="vv-ai-src-i" data-src="${esc(s.path)}">`
+ `<span class="vv-ai-src-n">[${i+1}]</span><span>${esc(label)}</span>`
@@ -511,10 +521,23 @@ vv_ai_profiles_script();
const think = e.target.closest('.vv-ai-think-t');
if (think) { think.nextElementSibling.classList.toggle('open'); return; }
const src = e.target.closest('.vv-ai-src-i');
// Through vvSafeUrl, which is the global that exists precisely so a URL from outside this
// machine cannot become a javascript: href. The server drops anything that is not http(s)
// as well; this is the second of the two, not the only one.
if (src && src.dataset.web) {
const u = vvSafeUrl(src.dataset.web);
if (u) window.open(u, '_blank', 'noopener,noreferrer');
return;
}
if (src && src.dataset.src) { vvAiOpen(src.dataset.src); return; }
const cite = e.target.closest('.vv-ai-cite');
if (cite) {
const s = lastSources[Number(cite.dataset.cite) - 1];
if (s && s.web && s.url) {
const u = vvSafeUrl(s.url);
if (u) window.open(u, '_blank', 'noopener,noreferrer');
return;
}
if (s && s.path) vvAiOpen(s.path);
return;
}
@@ -556,6 +579,7 @@ vv_ai_profiles_script();
// never reaches PHP — no CSRF termination, no fatal, no entry log.
const kindEl = o.kindEl ? document.getElementById(o.kindEl) : null;
const thinkEl = o.thinkEl ? document.getElementById(o.thinkEl) : null;
const webEl = o.webEl ? document.getElementById(o.webEl) : null;
let res;
try {
@@ -571,6 +595,12 @@ vv_ai_profiles_script();
// not — an inline answer that stalls reads as broken.
think: (typeof o.think === 'function' ? o.think(profile)
: thinkEl ? thinkEl.checked : true) ? '1' : '0',
// Only sent when the profile in force actually holds the capability, so ticking the box
// and then switching to the assistant cannot send a question about this machine to a
// search engine. The worker checks the same thing again — this is the courtesy, not
// the control.
web: (PROFILES[profile] && PROFILES[profile].web && webEl && webEl.checked)
? '1' : '0',
});
res = fetch(API, { method: 'POST', headers: POST_HEAD, body });
} catch (e) {