openvibers/openvibe.network Commits
Frame: /api/frame/code/search for the composer's GitHub tool (#878)
Changes
nnew file mode 100644@@@ -0,0 +1,60 @@++'use strict';++/**++ * GET /api/frame/code/search?q=&limit= — repositories for the composer's GitHub tool (Shared 4.225.0), from++ * OpenVibe.Exchange's public code search, through the Frame: every site's connect-src reaches openvibe.network and++ * not openvibe.exchange, and Exchange's search sends no CORS header.++ *++ * Only what a card needs leaves here: the repository's owner/name, its GitHub address, a short description, its++ * language, stars, licence, the owner's picture (GitHub's avatar host only) and when it was last pushed. Answers are++ * kept a minute per query (public data), at most `max` queries; a slow or failing Exchange is a 502 that says so.++ */++++const NAME = /^[A-Za-z0-9_.-]{1,100}\/[A-Za-z0-9_.-]{1,100}$/;++++function createFrameCodeSearch({ exchangeUrl = 'http://127.0.0.1:5150', fetchImpl = globalThis.fetch, now = Date.now, ttlMs = 60000, max = 200, timeoutMs = 4000 } = {}) {++ const base = String(exchangeUrl || '').replace(/\/+$/, '');++ const cache = new Map();++ const clip = (v, n) => String(v == null ? '' : v).replace(/\s+/g, ' ').trim().slice(0, n);++ const httpsOn = (u, host) => { try { const x = new URL(String(u)); return x.protocol === 'https:' && (!host || x.hostname === host) ? x.href : null; } catch { return null; } };++++ function shape(it) {++ if (!it || !NAME.test(String(it.full_name || ''))) return null;++ const url = httpsOn(it.url, 'github.com');++ if (!url) return null;++ const out = { full_name: it.full_name, url };++ const d = clip(it.description, 300); if (d) out.description = d;++ const l = clip(it.language, 40); if (l) out.language = l;++ if (Number.isFinite(Number(it.stars))) out.stars = Math.max(0, Math.round(Number(it.stars)));++ const lic = clip(it.license, 60); if (lic) out.license = lic;++ const av = httpsOn(it.owner_avatar_url, 'avatars.githubusercontent.com'); if (av) out.owner_avatar_url = av;++ if (it.pushed && Number.isFinite(Date.parse(it.pushed))) out.pushed = new Date(Date.parse(it.pushed)).toISOString();++ return out;++ }++++ async function handle(req, res) {++ res.set({ 'Access-Control-Allow-Origin': '*', 'Cross-Origin-Resource-Policy': 'cross-origin', 'Content-Type': 'application/json; charset=utf-8' });++ const q = clip(req.query && req.query.q, 120);++ const limit = Math.min(Math.max(parseInt(req.query && req.query.limit, 10) || 8, 1), 10);++ if (q.length < 2) return res.set('Cache-Control', 'public, max-age=300').json({ items: [] });++ const key = `${q.toLowerCase()}|${limit}`;++ const hit = cache.get(key);++ if (hit && now() - hit.at < ttlMs) return res.set('Cache-Control', 'public, max-age=60').json({ items: hit.items, source: 'exchange' });++ const ctl = new AbortController();++ const timer = setTimeout(() => ctl.abort(), timeoutMs);++ try {++ const r = await fetchImpl(`${base}/api/v1/code/search?q=${encodeURIComponent(q)}&limit=${limit}`, { headers: { accept: 'application/json' }, redirect: 'error', signal: ctl.signal });++ if (!r.ok) throw new Error(`exchange ${r.status}`);++ const j = await r.json();++ const items = (Array.isArray(j && j.items) ? j.items : []).map(shape).filter(Boolean).slice(0, limit);++ cache.set(key, { at: now(), items });++ if (cache.size > max) cache.delete(cache.keys().next().value);++ return res.set('Cache-Control', 'public, max-age=60').json({ items, source: 'exchange' });++ } catch {++ return res.status(502).set('Cache-Control', 'no-store').json({ error: 'Code search is unavailable right now', code: 'code.unavailable' });++ } finally { clearTimeout(timer); }++ }++++ return { handle, shape };++}++++module.exports = { createFrameCodeSearch };@@@ -34,6 +34,7 @@ const { SITES, LISTED, LISTED_SITES, siteForHost, GROUPS, groupOf, PRODUCTS, MERconst { createMenuCards } = require('./menu-cards');const { createFrameChoices } = require('./choices');const { createFrameAsk } = require('./ask');++const { createFrameCodeSearch } = require('./code-search');const { read: readCommands } = require('./commands');const { createFrameMentions } = require('./mentions');const { createFrameRuns } = require('./runs');@@@ -178,6 +179,8 @@ async function createFrameService(db, config, analytics, { privateKey = null, isexchangeUrl: (services.exchange && services.exchange.internalUrl) || 'http://127.0.0.1:5150',reportUrl: (services.report && services.report.internalUrl) || 'http://127.0.0.1:5080',});++ // The composer's GitHub tool (Shared 4.225.0): Exchange's public code search, through the Frame.++ const codeSearch = createFrameCodeSearch({ exchangeUrl: (services.exchange && services.exchange.internalUrl) || 'http://127.0.0.1:5150', now });const ask = createFrameAsk({aiUrl, now, requireAuth, sheet: choices,searchUrl: (services.search && services.search.internalUrl) || 'http://127.0.0.1:4710',@@@ -520,6 +523,7 @@ async function createFrameService(db, config, analytics, { privateKey = null, isupdated: new Date().toISOString(),};}++ router.get('/code/search', (req, res) => codeSearch.handle(req, res));router.get('/pulse', async (req, res) => {if (!pulse || Date.now() - pulseAt > 30_000) {pulseInflight = pulseInflight || readPulse().then((p) => { pulse = p; pulseAt = Date.now(); }).catch(() => {}).finally(() => { pulseInflight = null; });nnew file mode 100644@@@ -0,0 +1,50 @@++'use strict';++// GET /api/frame/code/search: the composer's GitHub tool reads Exchange's public code search through the Frame.++const assert = require('assert');++const express = require('express');++const { createFrameCodeSearch } = require('../server/frame/code-search');++++(async () => {++ const asked = [];++ let down = false;++ const exchange = async (url) => {++ asked.push(url);++ if (down) return { ok: false, status: 503, json: async () => ({}) };++ return { ok: true, status: 200, json: async () => ({ items: [++ { full_name: 'ada/goal-bar', url: 'https://github.com/ada/goal-bar', description: ' A goal bar\n for streams ', language: 'JavaScript', stars: 42, license: 'MIT', owner_avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4', pushed: '2026-11-10T10:00:00Z', homepage: 'https://x.example' },++ { full_name: 'evil/x', url: 'javascript:alert(1)' },++ { full_name: 'not a name', url: 'https://github.com/a/b' },++ { full_name: 'bo/tracker', url: 'https://github.com/bo/tracker', owner_avatar_url: 'https://evil.example/p.png', stars: 'many' },++ ] }) };++ };++ let t = 1000;++ const cs = createFrameCodeSearch({ exchangeUrl: 'http://exchange.local/', fetchImpl: exchange, now: () => t });++ const app = express();++ app.get('/api/frame/code/search', (req, res) => cs.handle(req, res));++ const server = await new Promise((ok) => { const s = app.listen(0, '127.0.0.1', () => ok(s)); });++ const base = `http://127.0.0.1:${server.address().port}/api/frame/code/search`;++ try {++ let r = await fetch(`${base}?q=goal%20bar&limit=50`);++ assert.strictEqual(r.status, 200);++ assert.strictEqual(r.headers.get('access-control-allow-origin'), '*', 'every site may read it');++ const body = await r.json();++ assert.deepStrictEqual(body.items, [++ { full_name: 'ada/goal-bar', url: 'https://github.com/ada/goal-bar', description: 'A goal bar for streams', language: 'JavaScript', stars: 42, license: 'MIT', owner_avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4', pushed: '2026-11-10T10:00:00.000Z' },++ { full_name: 'bo/tracker', url: 'https://github.com/bo/tracker' },++ ], 'only what a card needs; a script address, a bad name and a picture from elsewhere are left out');++ assert.strictEqual(asked[0], 'http://exchange.local/api/v1/code/search?q=goal%20bar&limit=10', 'at most ten');++ await fetch(`${base}?q=Goal%20Bar&limit=50`);++ assert.strictEqual(asked.length, 1, 'the same query within a minute is answered from the cache');++ t += 61000;++ await fetch(`${base}?q=goal%20bar&limit=50`);++ assert.strictEqual(asked.length, 2, 'and asked again after it');++ r = await fetch(`${base}?q=a`);++ assert.deepStrictEqual(await r.json(), { items: [] }, 'one letter asks nothing');++ assert.strictEqual(asked.length, 2);++ down = true;++ r = await fetch(`${base}?q=something%20else`);++ assert.strictEqual(r.status, 502);++ assert.strictEqual((await r.json()).code, 'code.unavailable');++ console.log('frame code search: through the Frame, only what a card needs, cached a minute, says when Exchange is down');++ } finally { server.close(); }++})().catch((e) => { console.error(e); process.exit(1); });@@@ -119,6 +119,7 @@ const REVIEWED = {'server/composer/routes.js fetch': [2, 'mention search: the operator-configured OpenVibe.Git internal URL (config.services.git) with a fixed /api/v1/repos path and the query percent-encoded; no caller-chosen host, 2.5 s timeout, and no token or body is sent with it'],'server/frame/today.js fetch': [1, 'Report internal URL from operator config, fixed /api/v1/stats?days=30 path, five-second timeout; no request-selected URL'],'server/frame/choices.js fetch': [1, 'the composer\'s Model sheet: AI (OV_AI_INTERNAL_URL), Exchange (OV_EXCHANGE_INTERNAL_URL / config.services.exchange) and Report (OV_REPORT_INTERNAL_URL) internal URLs from operator config, fixed /api/v1/choices, /api/v1/models and /api/v1/ai/leaderboard paths, the task percent-encoded and a checked person Bearer only; no request value names a host, redirects refused, 3-8 s timeouts'],++ 'server/frame/code-search.js fetch': [1, 'The composer\'s GitHub tool: only the operator-configured internal URL of Exchange (services.exchange.internalUrl), the fixed /api/v1/code/search path with the query encoded and the limit clamped to 10, no credentials, redirects refused, a 4 s timeout; never a request-chosen host'],'server/frame/runs.js fetch': [1, 'Frame Runs: only the operator-configured internal URLs of Chat, Codes and Actor (svcUrl), fixed /api/vibe and /api/v1 paths with encoded ids from a checked ref, the person\'s own Bearer, redirects refused, 4-15 s timeouts; never a request-chosen host'],'server/frame/ask.js fetch': [2, 'the composer\'s Ask: Search (OV_SEARCH_INTERNAL_URL) fixed /api/v1/query path with checked scopes and the person\'s Bearer, and AI (OV_AI_INTERNAL_URL) fixed /api/v1/runs?stream=1 path with the person\'s Bearer; both operator-configured internal URLs, no request value names a host, redirects refused, 30-150 s timeouts'],'server/admin/analytics-routes.js fetch': [2, 'staff analytics: the services\' internal URLs from the registry (owner/admin set), and Insights\' Prometheus (PROMETHEUS_URL, loopback by default) with fixed queries; never a request value'],
new file mode 100644
new file mode 100644
@@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
+'use strict';
+/**
+ * GET /api/frame/code/search?q=&limit= — repositories for the composer's GitHub tool (Shared 4.225.0), from
+ * OpenVibe.Exchange's public code search, through the Frame: every site's connect-src reaches openvibe.network and
+ * not openvibe.exchange, and Exchange's search sends no CORS header.
+ *
+ * Only what a card needs leaves here: the repository's owner/name, its GitHub address, a short description, its
+ * language, stars, licence, the owner's picture (GitHub's avatar host only) and when it was last pushed. Answers are
+ * kept a minute per query (public data), at most `max` queries; a slow or failing Exchange is a 502 that says so.
+ */
+
+const NAME = /^[A-Za-z0-9_.-]{1,100}\/[A-Za-z0-9_.-]{1,100}$/;
+
+function createFrameCodeSearch({ exchangeUrl = 'http://127.0.0.1:5150', fetchImpl = globalThis.fetch, now = Date.now, ttlMs = 60000, max = 200, timeoutMs = 4000 } = {}) {
+ const base = String(exchangeUrl || '').replace(/\/+$/, '');
+ const cache = new Map();
+ const clip = (v, n) => String(v == null ? '' : v).replace(/\s+/g, ' ').trim().slice(0, n);
+ const httpsOn = (u, host) => { try { const x = new URL(String(u)); return x.protocol === 'https:' && (!host || x.hostname === host) ? x.href : null; } catch { return null; } };
+
+ function shape(it) {
+ if (!it || !NAME.test(String(it.full_name || ''))) return null;
+ const url = httpsOn(it.url, 'github.com');
+ if (!url) return null;
+ const out = { full_name: it.full_name, url };
+ const d = clip(it.description, 300); if (d) out.description = d;
+ const l = clip(it.language, 40); if (l) out.language = l;
+ if (Number.isFinite(Number(it.stars))) out.stars = Math.max(0, Math.round(Number(it.stars)));
+ const lic = clip(it.license, 60); if (lic) out.license = lic;
+ const av = httpsOn(it.owner_avatar_url, 'avatars.githubusercontent.com'); if (av) out.owner_avatar_url = av;
+ if (it.pushed && Number.isFinite(Date.parse(it.pushed))) out.pushed = new Date(Date.parse(it.pushed)).toISOString();
+ return out;
+ }
+
+ async function handle(req, res) {
+ res.set({ 'Access-Control-Allow-Origin': '*', 'Cross-Origin-Resource-Policy': 'cross-origin', 'Content-Type': 'application/json; charset=utf-8' });
+ const q = clip(req.query && req.query.q, 120);
+ const limit = Math.min(Math.max(parseInt(req.query && req.query.limit, 10) || 8, 1), 10);
+ if (q.length < 2) return res.set('Cache-Control', 'public, max-age=300').json({ items: [] });
+ const key = `${q.toLowerCase()}|${limit}`;
+ const hit = cache.get(key);
+ if (hit && now() - hit.at < ttlMs) return res.set('Cache-Control', 'public, max-age=60').json({ items: hit.items, source: 'exchange' });
+ const ctl = new AbortController();
+ const timer = setTimeout(() => ctl.abort(), timeoutMs);
+ try {
+ const r = await fetchImpl(`${base}/api/v1/code/search?q=${encodeURIComponent(q)}&limit=${limit}`, { headers: { accept: 'application/json' }, redirect: 'error', signal: ctl.signal });
+ if (!r.ok) throw new Error(`exchange ${r.status}`);
+ const j = await r.json();
+ const items = (Array.isArray(j && j.items) ? j.items : []).map(shape).filter(Boolean).slice(0, limit);
+ cache.set(key, { at: now(), items });
+ if (cache.size > max) cache.delete(cache.keys().next().value);
+ return res.set('Cache-Control', 'public, max-age=60').json({ items, source: 'exchange' });
+ } catch {
+ return res.status(502).set('Cache-Control', 'no-store').json({ error: 'Code search is unavailable right now', code: 'code.unavailable' });
+ } finally { clearTimeout(timer); }
+ }
+
+ return { handle, shape };
+}
+
+module.exports = { createFrameCodeSearch };
@@ -34,6 +34,7 @@ const { SITES, LISTED, LISTED_SITES, siteForHost, GROUPS, groupOf, PRODUCTS, MER
@@ -34,6 +34,7 @@ const { SITES, LISTED, LISTED_SITES, siteForHost, GROUPS, groupOf, PRODUCTS, MER
const { createMenuCards } = require('./menu-cards');
const { createMenuCards } = require('./menu-cards');
const { createFrameChoices } = require('./choices');
const { createFrameChoices } = require('./choices');
const { createFrameAsk } = require('./ask');
const { createFrameAsk } = require('./ask');
+const { createFrameCodeSearch } = require('./code-search');
const { read: readCommands } = require('./commands');
const { read: readCommands } = require('./commands');
const { createFrameMentions } = require('./mentions');
const { createFrameMentions } = require('./mentions');
const { createFrameRuns } = require('./runs');
const { createFrameRuns } = require('./runs');
@@ -178,6 +179,8 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
@@ -178,6 +179,8 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
exchangeUrl: (services.exchange && services.exchange.internalUrl) || 'http://127.0.0.1:5150',
exchangeUrl: (services.exchange && services.exchange.internalUrl) || 'http://127.0.0.1:5150',
reportUrl: (services.report && services.report.internalUrl) || 'http://127.0.0.1:5080',
reportUrl: (services.report && services.report.internalUrl) || 'http://127.0.0.1:5080',
});
});
+ // The composer's GitHub tool (Shared 4.225.0): Exchange's public code search, through the Frame.
+ const codeSearch = createFrameCodeSearch({ exchangeUrl: (services.exchange && services.exchange.internalUrl) || 'http://127.0.0.1:5150', now });
const ask = createFrameAsk({
const ask = createFrameAsk({
aiUrl, now, requireAuth, sheet: choices,
aiUrl, now, requireAuth, sheet: choices,
searchUrl: (services.search && services.search.internalUrl) || 'http://127.0.0.1:4710',
searchUrl: (services.search && services.search.internalUrl) || 'http://127.0.0.1:4710',
@@ -520,6 +523,7 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
@@ -520,6 +523,7 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
updated: new Date().toISOString(),
updated: new Date().toISOString(),
};
};
}
}
+ router.get('/code/search', (req, res) => codeSearch.handle(req, res));
router.get('/pulse', async (req, res) => {
router.get('/pulse', async (req, res) => {
if (!pulse || Date.now() - pulseAt > 30_000) {
if (!pulse || Date.now() - pulseAt > 30_000) {
pulseInflight = pulseInflight || readPulse().then((p) => { pulse = p; pulseAt = Date.now(); }).catch(() => {}).finally(() => { pulseInflight = null; });
pulseInflight = pulseInflight || readPulse().then((p) => { pulse = p; pulseAt = Date.now(); }).catch(() => {}).finally(() => { pulseInflight = null; });
new file mode 100644
new file mode 100644
@@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
+'use strict';
+// GET /api/frame/code/search: the composer's GitHub tool reads Exchange's public code search through the Frame.
+const assert = require('assert');
+const express = require('express');
+const { createFrameCodeSearch } = require('../server/frame/code-search');
+
+(async () => {
+ const asked = [];
+ let down = false;
+ const exchange = async (url) => {
+ asked.push(url);
+ if (down) return { ok: false, status: 503, json: async () => ({}) };
+ return { ok: true, status: 200, json: async () => ({ items: [
+ { full_name: 'ada/goal-bar', url: 'https://github.com/ada/goal-bar', description: ' A goal bar\n for streams ', language: 'JavaScript', stars: 42, license: 'MIT', owner_avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4', pushed: '2026-11-10T10:00:00Z', homepage: 'https://x.example' },
+ { full_name: 'evil/x', url: 'javascript:alert(1)' },
+ { full_name: 'not a name', url: 'https://github.com/a/b' },
+ { full_name: 'bo/tracker', url: 'https://github.com/bo/tracker', owner_avatar_url: 'https://evil.example/p.png', stars: 'many' },
+ ] }) };
+ };
+ let t = 1000;
+ const cs = createFrameCodeSearch({ exchangeUrl: 'http://exchange.local/', fetchImpl: exchange, now: () => t });
+ const app = express();
+ app.get('/api/frame/code/search', (req, res) => cs.handle(req, res));
+ const server = await new Promise((ok) => { const s = app.listen(0, '127.0.0.1', () => ok(s)); });
+ const base = `http://127.0.0.1:${server.address().port}/api/frame/code/search`;
+ try {
+ let r = await fetch(`${base}?q=goal%20bar&limit=50`);
+ assert.strictEqual(r.status, 200);
+ assert.strictEqual(r.headers.get('access-control-allow-origin'), '*', 'every site may read it');
+ const body = await r.json();
+ assert.deepStrictEqual(body.items, [
+ { full_name: 'ada/goal-bar', url: 'https://github.com/ada/goal-bar', description: 'A goal bar for streams', language: 'JavaScript', stars: 42, license: 'MIT', owner_avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4', pushed: '2026-11-10T10:00:00.000Z' },
+ { full_name: 'bo/tracker', url: 'https://github.com/bo/tracker' },
+ ], 'only what a card needs; a script address, a bad name and a picture from elsewhere are left out');
+ assert.strictEqual(asked[0], 'http://exchange.local/api/v1/code/search?q=goal%20bar&limit=10', 'at most ten');
+ await fetch(`${base}?q=Goal%20Bar&limit=50`);
+ assert.strictEqual(asked.length, 1, 'the same query within a minute is answered from the cache');
+ t += 61000;
+ await fetch(`${base}?q=goal%20bar&limit=50`);
+ assert.strictEqual(asked.length, 2, 'and asked again after it');
+ r = await fetch(`${base}?q=a`);
+ assert.deepStrictEqual(await r.json(), { items: [] }, 'one letter asks nothing');
+ assert.strictEqual(asked.length, 2);
+ down = true;
+ r = await fetch(`${base}?q=something%20else`);
+ assert.strictEqual(r.status, 502);
+ assert.strictEqual((await r.json()).code, 'code.unavailable');
+ console.log('frame code search: through the Frame, only what a card needs, cached a minute, says when Exchange is down');
+ } finally { server.close(); }
+})().catch((e) => { console.error(e); process.exit(1); });
@@ -119,6 +119,7 @@ const REVIEWED = {
@@ -119,6 +119,7 @@ const REVIEWED = {
'server/composer/routes.js fetch': [2, 'mention search: the operator-configured OpenVibe.Git internal URL (config.services.git) with a fixed /api/v1/repos path and the query percent-encoded; no caller-chosen host, 2.5 s timeout, and no token or body is sent with it'],
'server/composer/routes.js fetch': [2, 'mention search: the operator-configured OpenVibe.Git internal URL (config.services.git) with a fixed /api/v1/repos path and the query percent-encoded; no caller-chosen host, 2.5 s timeout, and no token or body is sent with it'],
'server/frame/today.js fetch': [1, 'Report internal URL from operator config, fixed /api/v1/stats?days=30 path, five-second timeout; no request-selected URL'],
'server/frame/today.js fetch': [1, 'Report internal URL from operator config, fixed /api/v1/stats?days=30 path, five-second timeout; no request-selected URL'],
'server/frame/choices.js fetch': [1, 'the composer\'s Model sheet: AI (OV_AI_INTERNAL_URL), Exchange (OV_EXCHANGE_INTERNAL_URL / config.services.exchange) and Report (OV_REPORT_INTERNAL_URL) internal URLs from operator config, fixed /api/v1/choices, /api/v1/models and /api/v1/ai/leaderboard paths, the task percent-encoded and a checked person Bearer only; no request value names a host, redirects refused, 3-8 s timeouts'],
'server/frame/choices.js fetch': [1, 'the composer\'s Model sheet: AI (OV_AI_INTERNAL_URL), Exchange (OV_EXCHANGE_INTERNAL_URL / config.services.exchange) and Report (OV_REPORT_INTERNAL_URL) internal URLs from operator config, fixed /api/v1/choices, /api/v1/models and /api/v1/ai/leaderboard paths, the task percent-encoded and a checked person Bearer only; no request value names a host, redirects refused, 3-8 s timeouts'],
+ 'server/frame/code-search.js fetch': [1, 'The composer\'s GitHub tool: only the operator-configured internal URL of Exchange (services.exchange.internalUrl), the fixed /api/v1/code/search path with the query encoded and the limit clamped to 10, no credentials, redirects refused, a 4 s timeout; never a request-chosen host'],
'server/frame/runs.js fetch': [1, 'Frame Runs: only the operator-configured internal URLs of Chat, Codes and Actor (svcUrl), fixed /api/vibe and /api/v1 paths with encoded ids from a checked ref, the person\'s own Bearer, redirects refused, 4-15 s timeouts; never a request-chosen host'],
'server/frame/runs.js fetch': [1, 'Frame Runs: only the operator-configured internal URLs of Chat, Codes and Actor (svcUrl), fixed /api/vibe and /api/v1 paths with encoded ids from a checked ref, the person\'s own Bearer, redirects refused, 4-15 s timeouts; never a request-chosen host'],
'server/frame/ask.js fetch': [2, 'the composer\'s Ask: Search (OV_SEARCH_INTERNAL_URL) fixed /api/v1/query path with checked scopes and the person\'s Bearer, and AI (OV_AI_INTERNAL_URL) fixed /api/v1/runs?stream=1 path with the person\'s Bearer; both operator-configured internal URLs, no request value names a host, redirects refused, 30-150 s timeouts'],
'server/frame/ask.js fetch': [2, 'the composer\'s Ask: Search (OV_SEARCH_INTERNAL_URL) fixed /api/v1/query path with checked scopes and the person\'s Bearer, and AI (OV_AI_INTERNAL_URL) fixed /api/v1/runs?stream=1 path with the person\'s Bearer; both operator-configured internal URLs, no request value names a host, redirects refused, 30-150 s timeouts'],
'server/admin/analytics-routes.js fetch': [2, 'staff analytics: the services\' internal URLs from the registry (owner/admin set), and Insights\' Prometheus (PROMETHEUS_URL, loopback by default) with fixed queries; never a request value'],
'server/admin/analytics-routes.js fetch': [2, 'staff analytics: the services\' internal URLs from the registry (owner/admin set), and Insights\' Prometheus (PROMETHEUS_URL, loopback by default) with fixed queries; never a request value'],