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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user