openvibers/openvibe.network Commits
Footer copy comes from OpenVibe.AI only: drop the fallback to Live's /internal/ai/site-copy. When AI is unreachable or answers badly the last good copy stays (chrome_cache, across restarts), or the hand-written copy when there has never been one; test/chrome.test.js covers AI down, AI ok, AI down again, restart, and that Live is never called
Changes
@@@ -132,10 +132,11 @@ Public, CORS `*`, `max-age=600`, ETag. What the navbar and footer of every site- Ranking = each service's 7-day page views and visitors plus signed-in cross-site history,refreshed every 30 minutes and stored in `chrome_cache`, so an unreachable service keeps its lastscore. The Network itself counts page views only, at half weight.--- Copy = once a day the AI configured in admin (called through Live's internal-- `POST /internal/ai/site-copy`, budget-gated) may write a blurb per site and pick links **by id**-- from a list we supply. Ids resolve to our URLs server-side; text is length-capped and screened-- (no markup, no URLs, no cost claims). Anything that fails keeps the hand-written copy.++- Copy = once a day OpenVibe.AI's `network.site_copy` workflow (self-signed `svc:network` service++ token, `ns network.*`) may write a blurb per site and pick links **by id** from a list we supply.++ Ids resolve to our URLs server-side; text is length-capped and screened (no markup, no URLs, no++ cost claims). There is no fallback source: when AI fails, the last good copy (kept in++ `chrome_cache`) stays, or the hand-written copy if there has never been one.- Clients: `navbar.js` and `footer.js` share `window.OpenVibeChrome` — a per-host localStorage cache(30 min, refreshed in the background). Navbar options: `networkLinks` (`false` | count, default 4).@@@ -10,9 +10,10 @@// Ranking: page views and visitors from each service's own analytics (last 7 days) plus signed-in// cross-site history, refreshed every 30 minutes and kept in the database so a restart or an// unreachable service never reorders the network at random. Only totals cross the wire.--// Copy: once a day the configured AI (through OpenVibe.Live's internal API) may write a blurb per--// site and pick "discover" links BY ID from a list we supply; ids resolve to our own URLs here, text--// is stripped, length-capped and screened. No AI, or a bad answer → the hand-written copy stays.++// Copy: once a day OpenVibe.AI (network.site_copy) may write a blurb per site and pick "discover"++// links BY ID from a list we supply; ids resolve to our own URLs here, text is stripped, length-capped++// and screened. AI unreachable, failing or answering badly → the last good copy stays (kept in++// chrome_cache across restarts), or the hand-written copy when there has never been one.// ═══════════════════════════════════════════════════════════════const express = require('express');const crypto = require('crypto');@@@ -26,7 +27,7 @@ const BASE_WEIGHT = { live: 6, tools: 5, community: 4, games: 3, media: 2, netwo/*** Footer copy comes from OpenVibe.AI's network.site_copy workflow (roadmap Wave 13, ADR-015), called with a* self-signed Network service token (sub svc:network, aud openvibe.ai, cap ai.run.create, ns network.*).-- * Live's /internal/ai/site-copy stays only as a fallback while AI is unreachable.++ * There is no other source: Live's /internal/ai/site-copy is no longer called.*/function aiSiteCopy({ privateKey, issuer, aiUrl }) {const { serviceAuth } = require('openvibe-contracts');@@@ -126,20 +127,14 @@ function createChromeService(db, config, analytics, { privateKey = null, issuer}async function refreshCopy() {-- if (!fromAi && (!config.internalKey || config.internalKey === 'change-me-in-production')) return;++ if (!fromAi) return;const pool = linkPool();const popular = new Set(popularTools(12).map(t => t.url));const links = [...pool.entries()].filter(([id, l]) => !id.startsWith('tool:') || popular.has(l.url)).map(([id, l]) => ({ id, name: l.name, about: l.about }));const body = { sites: SITES.filter(s => s.status === 'open').map(s => ({ id: s.id, name: 'OpenVibe.' + s.name, what: s.what, popular: s.id === 'tools' ? popularTools(6).map(t => t.name) : [] })), links };let j = null;-- if (fromAi) { try { j = await fromAi(body); } catch { j = null; } }-- if (!j && config.internalKey && config.internalKey !== 'change-me-in-production') {-- try {-- const r = await fetch(`${svcUrl('live', 'http://127.0.0.1:3000')}/internal/ai/site-copy`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Internal-Key': config.internalKey }, body: JSON.stringify(body), signal: AbortSignal.timeout(90_000) });-- j = r.ok ? await r.json() : null;-- } catch { j = null; }-- }-- if (!j || !Array.isArray(j.sites)) return;++ try { j = await fromAi(body); } catch { j = null; }++ if (!j || !Array.isArray(j.sites)) return; // keep the last good copyconst next = {};for (const row of j.sites) {const site = SITES.find(s => s.id === row.id); if (!site) continue;@@@ -1,5 +1,7 @@'use strict';const assert = require('assert');++const crypto = require('crypto');++const http = require('http');const Database = require('better-sqlite3');const { createChromeService, BANNED } = require('../server/chrome/service');const { siteForHost } = require('../server/chrome/sites');@@@ -29,5 +31,49 @@ for (let i = 0; i < 6; i++) ins.run(1, 'tools', 'dns');assert.ok(p.nav.findIndex(n => n.id === 'network') > 0, 'the Network is never first on shared-service traffic alone');assert.ok(BANNED.test('Totally free tools') && BANNED.test('see https://x.y') && BANNED.test('<b>hi</b>') && !BANNED.test('Go live from a browser in seconds.'), 'AI copy screen');assert.equal(p.footer.ai, false);++++ // Footer copy comes from OpenVibe.AI only. Live's /internal/ai/site-copy is never called, and an++ // AI failure keeps the last good copy (or the hand-written one when there has never been one).++ let liveHits = 0; let aiMode = 'ok'; let aiAuth = null;++ const live = http.createServer((req, res) => { liveHits++; res.statusCode = 200; res.setHeader('content-type', 'application/json'); res.end(JSON.stringify({ ok: true, sites: [{ id: 'games', blurb: 'Copy from Live that must never be used here.', picks: [] }] })); });++ const ai = http.createServer((req, res) => {++ aiAuth = req.headers.authorization;++ if (aiMode === 'down') { res.statusCode = 503; return res.end('{}'); }++ res.setHeader('content-type', 'application/json');++ res.end(JSON.stringify({ run: { status: 'succeeded', model: { key: 'test-model' }, output: { sites: [{ id: 'games', blurb: 'Play Scraplandia and the shared pixel canvas in a tab.', picks: ['site:live'] }] } } }));++ });++ await Promise.all([live, ai].map(s => new Promise(r => s.listen(0, '127.0.0.1', r))));++ const { privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, publicKeyEncoding: { type: 'spki', format: 'pem' } });++ const cfg = { internalKey: 'a-real-internal-key', services: { live: { internalUrl: `http://127.0.0.1:${live.address().port}` } } };++ const aiUrl = `http://127.0.0.1:${ai.address().port}`;++ const db2 = new Database(':memory:');++ const copySvc = (d) => createChromeService(d, cfg, null, { privateKey, issuer: 'https://openvibe.network', aiUrl });++++ let s2 = copySvc(db2);++ aiMode = 'down';++ await s2.refreshCopy();++ p = s2.payloadFor('openvibe.games');++ assert.equal(p.footer.ai, false, 'AI down and no earlier copy: the hand-written blurb');++ assert.equal(p.footer.blurb, siteForHost('openvibe.games').what);++ assert.equal(liveHits, 0, 'no fallback to Live when AI fails');++++ aiMode = 'ok';++ await s2.refreshCopy();++ assert.match(aiAuth, /^Bearer ey/, 'AI is called with the self-signed service token');++ p = s2.payloadFor('openvibe.games');++ assert.equal(p.footer.ai, true);++ assert.equal(p.footer.blurb, 'Play Scraplandia and the shared pixel canvas in a tab.');++++ aiMode = 'down';++ await s2.refreshCopy();++ p = s2.payloadFor('openvibe.games');++ assert.equal(p.footer.blurb, 'Play Scraplandia and the shared pixel canvas in a tab.', 'AI failure keeps the last good copy');++ s2 = copySvc(db2);++ assert.equal(s2.payloadFor('openvibe.games').footer.blurb, 'Play Scraplandia and the shared pixel canvas in a tab.', 'and it survives a restart');++++ // Without a signing key there is no AI client, and still no Live call.++ await createChromeService(new Database(':memory:'), cfg, null).refreshCopy();++ assert.equal(liveHits, 0, 'Live is never asked for copy');++ live.close(); ai.close();console.log('chrome service: all checks passed');})().catch(e => { console.error(e); process.exit(1); });
@@ -132,10 +132,11 @@ Public, CORS `*`, `max-age=600`, ETag. What the navbar and footer of every site
@@ -132,10 +132,11 @@ Public, CORS `*`, `max-age=600`, ETag. What the navbar and footer of every site
- Ranking = each service's 7-day page views and visitors plus signed-in cross-site history,
- Ranking = each service's 7-day page views and visitors plus signed-in cross-site history,
refreshed every 30 minutes and stored in `chrome_cache`, so an unreachable service keeps its last
refreshed every 30 minutes and stored in `chrome_cache`, so an unreachable service keeps its last
score. The Network itself counts page views only, at half weight.
score. The Network itself counts page views only, at half weight.
-- Copy = once a day the AI configured in admin (called through Live's internal
- `POST /internal/ai/site-copy`, budget-gated) may write a blurb per site and pick links **by id**
- from a list we supply. Ids resolve to our URLs server-side; text is length-capped and screened
- (no markup, no URLs, no cost claims). Anything that fails keeps the hand-written copy.
+- Copy = once a day OpenVibe.AI's `network.site_copy` workflow (self-signed `svc:network` service
+ token, `ns network.*`) may write a blurb per site and pick links **by id** from a list we supply.
+ Ids resolve to our URLs server-side; text is length-capped and screened (no markup, no URLs, no
+ cost claims). There is no fallback source: when AI fails, the last good copy (kept in
+ `chrome_cache`) stays, or the hand-written copy if there has never been one.
- Clients: `navbar.js` and `footer.js` share `window.OpenVibeChrome` — a per-host localStorage cache
- Clients: `navbar.js` and `footer.js` share `window.OpenVibeChrome` — a per-host localStorage cache
(30 min, refreshed in the background). Navbar options: `networkLinks` (`false` | count, default 4).
(30 min, refreshed in the background). Navbar options: `networkLinks` (`false` | count, default 4).
@@ -10,9 +10,10 @@
@@ -10,9 +10,10 @@
// Ranking: page views and visitors from each service's own analytics (last 7 days) plus signed-in
// Ranking: page views and visitors from each service's own analytics (last 7 days) plus signed-in
// cross-site history, refreshed every 30 minutes and kept in the database so a restart or an
// cross-site history, refreshed every 30 minutes and kept in the database so a restart or an
// unreachable service never reorders the network at random. Only totals cross the wire.
// unreachable service never reorders the network at random. Only totals cross the wire.
-// Copy: once a day the configured AI (through OpenVibe.Live's internal API) may write a blurb per
-// site and pick "discover" links BY ID from a list we supply; ids resolve to our own URLs here, text
-// is stripped, length-capped and screened. No AI, or a bad answer → the hand-written copy stays.
+// Copy: once a day OpenVibe.AI (network.site_copy) may write a blurb per site and pick "discover"
+// links BY ID from a list we supply; ids resolve to our own URLs here, text is stripped, length-capped
+// and screened. AI unreachable, failing or answering badly → the last good copy stays (kept in
+// chrome_cache across restarts), or the hand-written copy when there has never been one.
// ═══════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════
const express = require('express');
const express = require('express');
const crypto = require('crypto');
const crypto = require('crypto');
@@ -26,7 +27,7 @@ const BASE_WEIGHT = { live: 6, tools: 5, community: 4, games: 3, media: 2, netwo
@@ -26,7 +27,7 @@ const BASE_WEIGHT = { live: 6, tools: 5, community: 4, games: 3, media: 2, netwo
/**
/**
* Footer copy comes from OpenVibe.AI's network.site_copy workflow (roadmap Wave 13, ADR-015), called with a
* Footer copy comes from OpenVibe.AI's network.site_copy workflow (roadmap Wave 13, ADR-015), called with a
* self-signed Network service token (sub svc:network, aud openvibe.ai, cap ai.run.create, ns network.*).
* self-signed Network service token (sub svc:network, aud openvibe.ai, cap ai.run.create, ns network.*).
- * Live's /internal/ai/site-copy stays only as a fallback while AI is unreachable.
+ * There is no other source: Live's /internal/ai/site-copy is no longer called.
*/
*/
function aiSiteCopy({ privateKey, issuer, aiUrl }) {
function aiSiteCopy({ privateKey, issuer, aiUrl }) {
const { serviceAuth } = require('openvibe-contracts');
const { serviceAuth } = require('openvibe-contracts');
@@ -126,20 +127,14 @@ function createChromeService(db, config, analytics, { privateKey = null, issuer
@@ -126,20 +127,14 @@ function createChromeService(db, config, analytics, { privateKey = null, issuer
}
}
async function refreshCopy() {
async function refreshCopy() {
- if (!fromAi && (!config.internalKey || config.internalKey === 'change-me-in-production')) return;
+ if (!fromAi) return;
const pool = linkPool();
const pool = linkPool();
const popular = new Set(popularTools(12).map(t => t.url));
const popular = new Set(popularTools(12).map(t => t.url));
const links = [...pool.entries()].filter(([id, l]) => !id.startsWith('tool:') || popular.has(l.url)).map(([id, l]) => ({ id, name: l.name, about: l.about }));
const links = [...pool.entries()].filter(([id, l]) => !id.startsWith('tool:') || popular.has(l.url)).map(([id, l]) => ({ id, name: l.name, about: l.about }));
const body = { sites: SITES.filter(s => s.status === 'open').map(s => ({ id: s.id, name: 'OpenVibe.' + s.name, what: s.what, popular: s.id === 'tools' ? popularTools(6).map(t => t.name) : [] })), links };
const body = { sites: SITES.filter(s => s.status === 'open').map(s => ({ id: s.id, name: 'OpenVibe.' + s.name, what: s.what, popular: s.id === 'tools' ? popularTools(6).map(t => t.name) : [] })), links };
let j = null;
let j = null;
- if (fromAi) { try { j = await fromAi(body); } catch { j = null; } }
- if (!j && config.internalKey && config.internalKey !== 'change-me-in-production') {
- try {
- const r = await fetch(`${svcUrl('live', 'http://127.0.0.1:3000')}/internal/ai/site-copy`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Internal-Key': config.internalKey }, body: JSON.stringify(body), signal: AbortSignal.timeout(90_000) });
- j = r.ok ? await r.json() : null;
- } catch { j = null; }
- }
- if (!j || !Array.isArray(j.sites)) return;
+ try { j = await fromAi(body); } catch { j = null; }
+ if (!j || !Array.isArray(j.sites)) return; // keep the last good copy
const next = {};
const next = {};
for (const row of j.sites) {
for (const row of j.sites) {
const site = SITES.find(s => s.id === row.id); if (!site) continue;
const site = SITES.find(s => s.id === row.id); if (!site) continue;
@@ -1,5 +1,7 @@
@@ -1,5 +1,7 @@
'use strict';
'use strict';
const assert = require('assert');
const assert = require('assert');
+const crypto = require('crypto');
+const http = require('http');
const Database = require('better-sqlite3');
const Database = require('better-sqlite3');
const { createChromeService, BANNED } = require('../server/chrome/service');
const { createChromeService, BANNED } = require('../server/chrome/service');
const { siteForHost } = require('../server/chrome/sites');
const { siteForHost } = require('../server/chrome/sites');
@@ -29,5 +31,49 @@ for (let i = 0; i < 6; i++) ins.run(1, 'tools', 'dns');
@@ -29,5 +31,49 @@ for (let i = 0; i < 6; i++) ins.run(1, 'tools', 'dns');
assert.ok(p.nav.findIndex(n => n.id === 'network') > 0, 'the Network is never first on shared-service traffic alone');
assert.ok(p.nav.findIndex(n => n.id === 'network') > 0, 'the Network is never first on shared-service traffic alone');
assert.ok(BANNED.test('Totally free tools') && BANNED.test('see https://x.y') && BANNED.test('<b>hi</b>') && !BANNED.test('Go live from a browser in seconds.'), 'AI copy screen');
assert.ok(BANNED.test('Totally free tools') && BANNED.test('see https://x.y') && BANNED.test('<b>hi</b>') && !BANNED.test('Go live from a browser in seconds.'), 'AI copy screen');
assert.equal(p.footer.ai, false);
assert.equal(p.footer.ai, false);
+
+ // Footer copy comes from OpenVibe.AI only. Live's /internal/ai/site-copy is never called, and an
+ // AI failure keeps the last good copy (or the hand-written one when there has never been one).
+ let liveHits = 0; let aiMode = 'ok'; let aiAuth = null;
+ const live = http.createServer((req, res) => { liveHits++; res.statusCode = 200; res.setHeader('content-type', 'application/json'); res.end(JSON.stringify({ ok: true, sites: [{ id: 'games', blurb: 'Copy from Live that must never be used here.', picks: [] }] })); });
+ const ai = http.createServer((req, res) => {
+ aiAuth = req.headers.authorization;
+ if (aiMode === 'down') { res.statusCode = 503; return res.end('{}'); }
+ res.setHeader('content-type', 'application/json');
+ res.end(JSON.stringify({ run: { status: 'succeeded', model: { key: 'test-model' }, output: { sites: [{ id: 'games', blurb: 'Play Scraplandia and the shared pixel canvas in a tab.', picks: ['site:live'] }] } } }));
+ });
+ await Promise.all([live, ai].map(s => new Promise(r => s.listen(0, '127.0.0.1', r))));
+ const { privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, publicKeyEncoding: { type: 'spki', format: 'pem' } });
+ const cfg = { internalKey: 'a-real-internal-key', services: { live: { internalUrl: `http://127.0.0.1:${live.address().port}` } } };
+ const aiUrl = `http://127.0.0.1:${ai.address().port}`;
+ const db2 = new Database(':memory:');
+ const copySvc = (d) => createChromeService(d, cfg, null, { privateKey, issuer: 'https://openvibe.network', aiUrl });
+
+ let s2 = copySvc(db2);
+ aiMode = 'down';
+ await s2.refreshCopy();
+ p = s2.payloadFor('openvibe.games');
+ assert.equal(p.footer.ai, false, 'AI down and no earlier copy: the hand-written blurb');
+ assert.equal(p.footer.blurb, siteForHost('openvibe.games').what);
+ assert.equal(liveHits, 0, 'no fallback to Live when AI fails');
+
+ aiMode = 'ok';
+ await s2.refreshCopy();
+ assert.match(aiAuth, /^Bearer ey/, 'AI is called with the self-signed service token');
+ p = s2.payloadFor('openvibe.games');
+ assert.equal(p.footer.ai, true);
+ assert.equal(p.footer.blurb, 'Play Scraplandia and the shared pixel canvas in a tab.');
+
+ aiMode = 'down';
+ await s2.refreshCopy();
+ p = s2.payloadFor('openvibe.games');
+ assert.equal(p.footer.blurb, 'Play Scraplandia and the shared pixel canvas in a tab.', 'AI failure keeps the last good copy');
+ s2 = copySvc(db2);
+ assert.equal(s2.payloadFor('openvibe.games').footer.blurb, 'Play Scraplandia and the shared pixel canvas in a tab.', 'and it survives a restart');
+
+ // Without a signing key there is no AI client, and still no Live call.
+ await createChromeService(new Database(':memory:'), cfg, null).refreshCopy();
+ assert.equal(liveHits, 0, 'Live is never asked for copy');
+ live.close(); ai.close();
console.log('chrome service: all checks passed');
console.log('chrome service: all checks passed');
})().catch(e => { console.error(e); process.exit(1); });
})().catch(e => { console.error(e); process.exit(1); });