openvibers/openvibe.network Commits
Relay AG-UI runs to Dock cards (#821)
Changes
@@@ -814,8 +814,8 @@ async function createFrameService(db, config, analytics, { privateKey = null, is};}const dockJson = express.json({ limit: '8kb' });-- router.options(['/agent-view', '/agent-view/*', '/agent/*', '/agents/*', '/codes-agents/*', '/tasks/*', '/actor', '/actor/*', '/codes-tasks', '/codes-tasks/*'], (req, res) => {-- res.set({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Authorization, Content-Type', 'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE', 'Access-Control-Max-Age': '600' });++ router.options(['/agent-view', '/agent-view/*', '/agent/*', '/agents/*', '/codes-agents/*', '/tasks/*', '/actor', '/actor/*', '/codes-tasks', '/codes-tasks/*', '/codes-runs/*', '/agent-runs/*'], (req, res) => {++ res.set({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Authorization, Content-Type, Last-Event-ID', 'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE', 'Access-Control-Max-Age': '600' });res.status(204).end();});// OpenVibe.Codes: the person's agent stream, its last still, and the four controls.@@@ -869,6 +869,44 @@ async function createFrameService(db, config, analytics, { privateKey = null, is}));router.post('/codes-tasks', dockJson, dockRelay('codes', 'POST', '/api/v1/tasks'));router.post('/codes-tasks/:id/stop', dockJson, dockRelay('codes', 'POST', (req) => (TASK_ID.test(String(req.params.id || '')) ? `/api/v1/tasks/${req.params.id}/stop` : null)));++ // A task card follows its AG-UI run through Network. Codes authorizes the bearer on both reads; the++ // stream forwards only SSE and its id so a reconnect can resume at the last event.++ const RUN_ID = /^tsk_[0-9A-HJKMNP-TV-Z]{26}(?:~[1-9][0-9]*)?$/;++ router.get('/codes-runs/:id', dockRelay('codes', 'GET', (req) => RUN_ID.test(String(req.params.id || ''))++ ? `/api/v1/runs/${encodeURIComponent(req.params.id)}` : null, { maxBytes: 512 * 1024 }));++ router.get('/agent-runs/actor/:id', dockRelay('actor', 'GET', (req) => RUN_ID.test(String(req.params.id || ''))++ ? `/api/v1/runs/${encodeURIComponent(req.params.id)}` : null, { maxBytes: 512 * 1024 }));++ const runEvents = (service) => async (req, res) => {++ res.set({ 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'private, no-store', 'Vary': 'Authorization' });++ const auth = dockAuth(req);++ const id = String(req.params.id || '');++ if (!auth) return res.status(401).json({ error: 'sign in to follow the run' });++ if (!RUN_ID.test(id)) return res.status(404).json({ error: 'not found' });++ const last = String(req.get('last-event-id') || '0');++ if (!/^\d{1,16}$/.test(last)) return res.status(400).json({ error: 'invalid event id' });++ const abort = new AbortController();++ res.on('close', () => abort.abort());++ try {++ const upstream = await fetch(new URL(`/api/v1/runs/${encodeURIComponent(id)}/events`, svcUrl(service, service === 'codes' ? 'http://127.0.0.1:4900' : 'http://127.0.0.1:4950')).href, {++ headers: { Authorization: auth, Accept: 'text/event-stream', 'Last-Event-ID': last }, redirect: 'manual', signal: abort.signal++ });++ if (!upstream.ok || !String(upstream.headers.get('content-type') || '').startsWith('text/event-stream')) {++ return res.status(upstream.status).json({ error: 'run stream unavailable' });++ }++ res.status(200).set({ 'Content-Type': 'text/event-stream', 'X-Accel-Buffering': 'no' });++ res.flushHeaders();++ for await (const chunk of upstream.body) {++ if (abort.signal.aborted) break;++ if (!res.write(chunk)) await new Promise((resolve) => {++ const done = () => { res.off('drain', done); res.off('close', done); resolve(); };++ res.once('drain', done); res.once('close', done);++ });++ }++ res.end();++ } catch { if (!res.headersSent) res.status(502).json({ error: 'run stream unavailable' }); else res.end(); }++ };++ router.get('/codes-runs/:id/events', runEvents('codes'));++ router.get('/agent-runs/actor/:id/events', runEvents('actor'));// GET /api/frame/suggest?q=&owner=&limit= — the navbar's magnifier (openvibe-shared search.js): OpenVibe.Search's// suggestions, asked through Network because every site's connect-src already lets the navbar reach@@@ -19,7 +19,7 @@ const TASK = 'tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3';req.on('data', (c) => chunks.push(c));req.on('end', () => {const u = new URL(req.url, 'http://x');-- asked.push({ method: req.method, url: req.url, auth: req.headers.authorization || '', cookie: req.headers.cookie || '', body: Buffer.concat(chunks).toString('utf8') });++ asked.push({ method: req.method, url: req.url, auth: req.headers.authorization || '', cookie: req.headers.cookie || '', last: req.headers['last-event-id'] || '', body: Buffer.concat(chunks).toString('utf8') });if (down) { req.socket.destroy(); return; }const json = (status, obj) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };if (u.pathname === '/api/v1/me/tasks' && req.method === 'GET') {@@@ -31,12 +31,17 @@ const TASK = 'tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3';return json(201, { id: TASK, url: `https://openvibe.codes/tasks/${TASK}` });}if (u.pathname === `/api/v1/tasks/${TASK}/stop` && req.method === 'POST') return json(200, { ok: true });++ if (u.pathname === `/api/v1/runs/${TASK}` && req.method === 'GET') return json(200, { id: TASK, title: 'A task', plan: [] });++ if (u.pathname === `/api/v1/runs/${TASK}/events` && req.method === 'GET') {++ res.writeHead(200, { 'Content-Type': 'text/event-stream' });++ return res.end('id: 3\nevent: STEP_STARTED\ndata: {"type":"STEP_STARTED","stepName":"Read files"}\n\n');++ }res.writeHead(404, { 'Content-Type': 'application/json' });return res.end('{"error":"no route"}');});});await new Promise((r) => codes.listen(0, '127.0.0.1', r));-- const svc = await createFrameService(getDb(), { services: { codes: { internalUrl: `http://127.0.0.1:${codes.address().port}` } } }, null);++ const svc = await createFrameService(getDb(), { services: { codes: { internalUrl: `http://127.0.0.1:${codes.address().port}` }, actor: { internalUrl: `http://127.0.0.1:${codes.address().port}` } } }, null);const app = express();app.set('trust proxy', 'loopback');app.use('/api/frame', svc.router);@@@ -95,9 +100,33 @@ const TASK = 'tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3';assert.strictEqual(r.status, 404);assert.strictEqual(asked.length, n, 'a bad id never goes upstream');++ // The card's snapshot and resumable AG-UI stream carry the same owner token.++ ({ r, d } = await get(`/codes-runs/${TASK}`, { Authorization: TOKEN }));++ assert.strictEqual(r.status, 200);++ assert.strictEqual(d.title, 'A task');++ const events = await fetch(`${base}/codes-runs/${TASK}/events`, { headers: { Authorization: TOKEN, 'Last-Event-ID': '2' } });++ assert.strictEqual(events.status, 200);++ assert.match(await events.text(), /id: 3\nevent: STEP_STARTED/);++ hit = asked.find((x) => x.url === `/api/v1/runs/${TASK}/events`);++ assert.strictEqual(hit.auth, TOKEN);++ assert.strictEqual(hit.last, '2');++ assert.strictEqual(hit.cookie, '');++ ({ r } = await get('/codes-runs/nope', { Authorization: TOKEN }));++ assert.strictEqual(r.status, 404);++ ({ r } = await get(`/codes-runs/${TASK}`));++ assert.strictEqual(r.status, 401);++ ({ r, d } = await get(`/agent-runs/actor/${TASK}`, { Authorization: TOKEN }));++ assert.strictEqual(r.status, 200);++ assert.strictEqual(d.id, TASK);++ const actorEvents = await fetch(`${base}/agent-runs/actor/${TASK}/events`, { headers: { Authorization: TOKEN } });++ assert.match(await actorEvents.text(), /STEP_STARTED/);++// Preflights end here.const pre = await fetch(`${base}/codes-tasks`, { method: 'OPTIONS' });assert.strictEqual(pre.status, 204);++ const streamPre = await fetch(`${base}/codes-runs/${TASK}/events`, { method: 'OPTIONS', headers: { Origin: 'https://openvibe.chat', 'Access-Control-Request-Headers': 'authorization,last-event-id' } });++ assert.strictEqual(streamPre.status, 204);++ assert.match(streamPre.headers.get('access-control-allow-headers'), /Last-Event-ID/);// Codes down: a 502 that says so.down = true;@@@ -131,7 +131,7 @@ const REVIEWED = {'server/coins/routes.js fetch': [1, 'Billing internal URL from config (the person\'s Vibes with their own token, 2 s timeout)'],'server/today/receipts.js fetch': [1, 'Mail\'s internal URL from config (OV_MAIL_INTERNAL_URL, loopback 5250): the person\'s own token to the fixed path /api/v1/me/mail/receipts, 2.5 s timeout; never a request value names a host'],'server/wallet/routes.js fetch': [2, 'Billing internal URL from config (OV_BILLING_INTERNAL_URL, loopback 4600) only: the person\'s own token for /api/me/activity and every /api/me read and write, and — only while /api/me/activity is missing — the marked temporary journal fallback, a billing.balance.read service read scoped to the signed-in person\'s own usr_ id; the paths are fixed, a charge id must be a vch_ ULID and is percent-encoded, and no request value ever names a host. The second call site is Network\'s own loopback (config.internalUrl, else 127.0.0.1:<port>): the fixed path POST /internal/subject-assertions, which signs the person\'s own token into a 60-second subject assertion; the person\'s token is in the body, never in the URL, and no request value names a host'],-- 'server/frame/service.js fetch': [7, 'The composer\'s pictures and posts go to OpenVibe.Community\'s fixed paths (POST /api/v1/spaces/status/attachments, POST /api/v1/posts) on its internal URL. Live and AI internal URLs from config (navbar ranking copy); https://<open site host>/privacy, hosts from the site registry only, no redirects, 5 s (legal links); Community internal URL from config for the menu composer relay (POST /api/frame/status, fixed path, the person\'s Bearer only); Billing and Inventory internal URLs from config for the wallet relays (/api/frame/wallet/…, fixed paths, a usr_/itd_ id must match its pattern, the person\'s Bearer only, no redirects, 8 s); Codes and Actor internal URLs from config for the Dock relays (/api/frame/agent-view…, /agent/timeline, /agent/approvals/…, /agent/kill-switch, /tasks/…, /actor…, fixed paths, product and id matched against patterns and percent-encoded, the person\'s Bearer only, no cookie, no redirects, 15 s)'],++ 'server/frame/service.js fetch': [8, 'The composer\'s pictures and posts go to OpenVibe.Community\'s fixed paths (POST /api/v1/spaces/status/attachments, POST /api/v1/posts) on its internal URL. Live and AI internal URLs from config (navbar ranking copy); https://<open site host>/privacy, hosts from the site registry only, no redirects, 5 s (legal links); Community internal URL from config for the menu composer relay (POST /api/frame/status, fixed path, the person\'s Bearer only); Billing and Inventory internal URLs from config for the wallet relays (/api/frame/wallet/…, fixed paths, a usr_/itd_ id must match its pattern, the person\'s Bearer only, no redirects, 8 s); Codes and Actor internal URLs from config for the Dock relays (/api/frame/agent-view…, /agent/timeline, /agent/approvals/…, /agent/kill-switch, /tasks/…, /actor…, fixed paths, product and id matched against patterns and percent-encoded, the person\'s Bearer only, no cookie, no redirects, 15 s); the AG-UI run stream relay uses a fixed /api/v1/runs/:id/events path, a validated task id, the trusted service URL, the person\'s Bearer, and no redirects'],'server/frame/menu-cards.js fetch': [2, 'https://<open site host>/.well-known/openvibe-menu.json, hosts from the site registry only, no redirects, 5 s, 64 KB (menu cards)'],'server/stats/open.js fetch': [1, 'open stats (/stats): fixed first-party loopback URLs only — Live (OV_LIVE_INTERNAL_URL, its public home numbers and service-token analytics totals) and Prometheus (PROMETHEUS_URL, loopback by default) with fixed queries; the request only picks 7, 30 or 90 days'],'server/admin/service-proxies.js fetch': [1, 'the admin panel proxies: Live\'s and Chat\'s internal URLs from config, fixed path prefixes, staff only'],
@@ -814,8 +814,8 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
@@ -814,8 +814,8 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
};
};
}
}
const dockJson = express.json({ limit: '8kb' });
const dockJson = express.json({ limit: '8kb' });
- router.options(['/agent-view', '/agent-view/*', '/agent/*', '/agents/*', '/codes-agents/*', '/tasks/*', '/actor', '/actor/*', '/codes-tasks', '/codes-tasks/*'], (req, res) => {
- res.set({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Authorization, Content-Type', 'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE', 'Access-Control-Max-Age': '600' });
+ router.options(['/agent-view', '/agent-view/*', '/agent/*', '/agents/*', '/codes-agents/*', '/tasks/*', '/actor', '/actor/*', '/codes-tasks', '/codes-tasks/*', '/codes-runs/*', '/agent-runs/*'], (req, res) => {
+ res.set({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Authorization, Content-Type, Last-Event-ID', 'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE', 'Access-Control-Max-Age': '600' });
res.status(204).end();
res.status(204).end();
});
});
// OpenVibe.Codes: the person's agent stream, its last still, and the four controls.
// OpenVibe.Codes: the person's agent stream, its last still, and the four controls.
@@ -869,6 +869,44 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
@@ -869,6 +869,44 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
}));
}));
router.post('/codes-tasks', dockJson, dockRelay('codes', 'POST', '/api/v1/tasks'));
router.post('/codes-tasks', dockJson, dockRelay('codes', 'POST', '/api/v1/tasks'));
router.post('/codes-tasks/:id/stop', dockJson, dockRelay('codes', 'POST', (req) => (TASK_ID.test(String(req.params.id || '')) ? `/api/v1/tasks/${req.params.id}/stop` : null)));
router.post('/codes-tasks/:id/stop', dockJson, dockRelay('codes', 'POST', (req) => (TASK_ID.test(String(req.params.id || '')) ? `/api/v1/tasks/${req.params.id}/stop` : null)));
+ // A task card follows its AG-UI run through Network. Codes authorizes the bearer on both reads; the
+ // stream forwards only SSE and its id so a reconnect can resume at the last event.
+ const RUN_ID = /^tsk_[0-9A-HJKMNP-TV-Z]{26}(?:~[1-9][0-9]*)?$/;
+ router.get('/codes-runs/:id', dockRelay('codes', 'GET', (req) => RUN_ID.test(String(req.params.id || ''))
+ ? `/api/v1/runs/${encodeURIComponent(req.params.id)}` : null, { maxBytes: 512 * 1024 }));
+ router.get('/agent-runs/actor/:id', dockRelay('actor', 'GET', (req) => RUN_ID.test(String(req.params.id || ''))
+ ? `/api/v1/runs/${encodeURIComponent(req.params.id)}` : null, { maxBytes: 512 * 1024 }));
+ const runEvents = (service) => async (req, res) => {
+ res.set({ 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'private, no-store', 'Vary': 'Authorization' });
+ const auth = dockAuth(req);
+ const id = String(req.params.id || '');
+ if (!auth) return res.status(401).json({ error: 'sign in to follow the run' });
+ if (!RUN_ID.test(id)) return res.status(404).json({ error: 'not found' });
+ const last = String(req.get('last-event-id') || '0');
+ if (!/^\d{1,16}$/.test(last)) return res.status(400).json({ error: 'invalid event id' });
+ const abort = new AbortController();
+ res.on('close', () => abort.abort());
+ try {
+ const upstream = await fetch(new URL(`/api/v1/runs/${encodeURIComponent(id)}/events`, svcUrl(service, service === 'codes' ? 'http://127.0.0.1:4900' : 'http://127.0.0.1:4950')).href, {
+ headers: { Authorization: auth, Accept: 'text/event-stream', 'Last-Event-ID': last }, redirect: 'manual', signal: abort.signal
+ });
+ if (!upstream.ok || !String(upstream.headers.get('content-type') || '').startsWith('text/event-stream')) {
+ return res.status(upstream.status).json({ error: 'run stream unavailable' });
+ }
+ res.status(200).set({ 'Content-Type': 'text/event-stream', 'X-Accel-Buffering': 'no' });
+ res.flushHeaders();
+ for await (const chunk of upstream.body) {
+ if (abort.signal.aborted) break;
+ if (!res.write(chunk)) await new Promise((resolve) => {
+ const done = () => { res.off('drain', done); res.off('close', done); resolve(); };
+ res.once('drain', done); res.once('close', done);
+ });
+ }
+ res.end();
+ } catch { if (!res.headersSent) res.status(502).json({ error: 'run stream unavailable' }); else res.end(); }
+ };
+ router.get('/codes-runs/:id/events', runEvents('codes'));
+ router.get('/agent-runs/actor/:id/events', runEvents('actor'));
// GET /api/frame/suggest?q=&owner=&limit= — the navbar's magnifier (openvibe-shared search.js): OpenVibe.Search's
// GET /api/frame/suggest?q=&owner=&limit= — the navbar's magnifier (openvibe-shared search.js): OpenVibe.Search's
// suggestions, asked through Network because every site's connect-src already lets the navbar reach
// suggestions, asked through Network because every site's connect-src already lets the navbar reach
@@ -19,7 +19,7 @@ const TASK = 'tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3';
@@ -19,7 +19,7 @@ const TASK = 'tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3';
req.on('data', (c) => chunks.push(c));
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
req.on('end', () => {
const u = new URL(req.url, 'http://x');
const u = new URL(req.url, 'http://x');
- asked.push({ method: req.method, url: req.url, auth: req.headers.authorization || '', cookie: req.headers.cookie || '', body: Buffer.concat(chunks).toString('utf8') });
+ asked.push({ method: req.method, url: req.url, auth: req.headers.authorization || '', cookie: req.headers.cookie || '', last: req.headers['last-event-id'] || '', body: Buffer.concat(chunks).toString('utf8') });
if (down) { req.socket.destroy(); return; }
if (down) { req.socket.destroy(); return; }
const json = (status, obj) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };
const json = (status, obj) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };
if (u.pathname === '/api/v1/me/tasks' && req.method === 'GET') {
if (u.pathname === '/api/v1/me/tasks' && req.method === 'GET') {
@@ -31,12 +31,17 @@ const TASK = 'tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3';
@@ -31,12 +31,17 @@ const TASK = 'tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3';
return json(201, { id: TASK, url: `https://openvibe.codes/tasks/${TASK}` });
return json(201, { id: TASK, url: `https://openvibe.codes/tasks/${TASK}` });
}
}
if (u.pathname === `/api/v1/tasks/${TASK}/stop` && req.method === 'POST') return json(200, { ok: true });
if (u.pathname === `/api/v1/tasks/${TASK}/stop` && req.method === 'POST') return json(200, { ok: true });
+ if (u.pathname === `/api/v1/runs/${TASK}` && req.method === 'GET') return json(200, { id: TASK, title: 'A task', plan: [] });
+ if (u.pathname === `/api/v1/runs/${TASK}/events` && req.method === 'GET') {
+ res.writeHead(200, { 'Content-Type': 'text/event-stream' });
+ return res.end('id: 3\nevent: STEP_STARTED\ndata: {"type":"STEP_STARTED","stepName":"Read files"}\n\n');
+ }
res.writeHead(404, { 'Content-Type': 'application/json' });
res.writeHead(404, { 'Content-Type': 'application/json' });
return res.end('{"error":"no route"}');
return res.end('{"error":"no route"}');
});
});
});
});
await new Promise((r) => codes.listen(0, '127.0.0.1', r));
await new Promise((r) => codes.listen(0, '127.0.0.1', r));
- const svc = await createFrameService(getDb(), { services: { codes: { internalUrl: `http://127.0.0.1:${codes.address().port}` } } }, null);
+ const svc = await createFrameService(getDb(), { services: { codes: { internalUrl: `http://127.0.0.1:${codes.address().port}` }, actor: { internalUrl: `http://127.0.0.1:${codes.address().port}` } } }, null);
const app = express();
const app = express();
app.set('trust proxy', 'loopback');
app.set('trust proxy', 'loopback');
app.use('/api/frame', svc.router);
app.use('/api/frame', svc.router);
@@ -95,9 +100,33 @@ const TASK = 'tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3';
@@ -95,9 +100,33 @@ const TASK = 'tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3';
assert.strictEqual(r.status, 404);
assert.strictEqual(r.status, 404);
assert.strictEqual(asked.length, n, 'a bad id never goes upstream');
assert.strictEqual(asked.length, n, 'a bad id never goes upstream');
+ // The card's snapshot and resumable AG-UI stream carry the same owner token.
+ ({ r, d } = await get(`/codes-runs/${TASK}`, { Authorization: TOKEN }));
+ assert.strictEqual(r.status, 200);
+ assert.strictEqual(d.title, 'A task');
+ const events = await fetch(`${base}/codes-runs/${TASK}/events`, { headers: { Authorization: TOKEN, 'Last-Event-ID': '2' } });
+ assert.strictEqual(events.status, 200);
+ assert.match(await events.text(), /id: 3\nevent: STEP_STARTED/);
+ hit = asked.find((x) => x.url === `/api/v1/runs/${TASK}/events`);
+ assert.strictEqual(hit.auth, TOKEN);
+ assert.strictEqual(hit.last, '2');
+ assert.strictEqual(hit.cookie, '');
+ ({ r } = await get('/codes-runs/nope', { Authorization: TOKEN }));
+ assert.strictEqual(r.status, 404);
+ ({ r } = await get(`/codes-runs/${TASK}`));
+ assert.strictEqual(r.status, 401);
+ ({ r, d } = await get(`/agent-runs/actor/${TASK}`, { Authorization: TOKEN }));
+ assert.strictEqual(r.status, 200);
+ assert.strictEqual(d.id, TASK);
+ const actorEvents = await fetch(`${base}/agent-runs/actor/${TASK}/events`, { headers: { Authorization: TOKEN } });
+ assert.match(await actorEvents.text(), /STEP_STARTED/);
+
// Preflights end here.
// Preflights end here.
const pre = await fetch(`${base}/codes-tasks`, { method: 'OPTIONS' });
const pre = await fetch(`${base}/codes-tasks`, { method: 'OPTIONS' });
assert.strictEqual(pre.status, 204);
assert.strictEqual(pre.status, 204);
+ const streamPre = await fetch(`${base}/codes-runs/${TASK}/events`, { method: 'OPTIONS', headers: { Origin: 'https://openvibe.chat', 'Access-Control-Request-Headers': 'authorization,last-event-id' } });
+ assert.strictEqual(streamPre.status, 204);
+ assert.match(streamPre.headers.get('access-control-allow-headers'), /Last-Event-ID/);
// Codes down: a 502 that says so.
// Codes down: a 502 that says so.
down = true;
down = true;
@@ -131,7 +131,7 @@ const REVIEWED = {
@@ -131,7 +131,7 @@ const REVIEWED = {
'server/coins/routes.js fetch': [1, 'Billing internal URL from config (the person\'s Vibes with their own token, 2 s timeout)'],
'server/coins/routes.js fetch': [1, 'Billing internal URL from config (the person\'s Vibes with their own token, 2 s timeout)'],
'server/today/receipts.js fetch': [1, 'Mail\'s internal URL from config (OV_MAIL_INTERNAL_URL, loopback 5250): the person\'s own token to the fixed path /api/v1/me/mail/receipts, 2.5 s timeout; never a request value names a host'],
'server/today/receipts.js fetch': [1, 'Mail\'s internal URL from config (OV_MAIL_INTERNAL_URL, loopback 5250): the person\'s own token to the fixed path /api/v1/me/mail/receipts, 2.5 s timeout; never a request value names a host'],
'server/wallet/routes.js fetch': [2, 'Billing internal URL from config (OV_BILLING_INTERNAL_URL, loopback 4600) only: the person\'s own token for /api/me/activity and every /api/me read and write, and — only while /api/me/activity is missing — the marked temporary journal fallback, a billing.balance.read service read scoped to the signed-in person\'s own usr_ id; the paths are fixed, a charge id must be a vch_ ULID and is percent-encoded, and no request value ever names a host. The second call site is Network\'s own loopback (config.internalUrl, else 127.0.0.1:<port>): the fixed path POST /internal/subject-assertions, which signs the person\'s own token into a 60-second subject assertion; the person\'s token is in the body, never in the URL, and no request value names a host'],
'server/wallet/routes.js fetch': [2, 'Billing internal URL from config (OV_BILLING_INTERNAL_URL, loopback 4600) only: the person\'s own token for /api/me/activity and every /api/me read and write, and — only while /api/me/activity is missing — the marked temporary journal fallback, a billing.balance.read service read scoped to the signed-in person\'s own usr_ id; the paths are fixed, a charge id must be a vch_ ULID and is percent-encoded, and no request value ever names a host. The second call site is Network\'s own loopback (config.internalUrl, else 127.0.0.1:<port>): the fixed path POST /internal/subject-assertions, which signs the person\'s own token into a 60-second subject assertion; the person\'s token is in the body, never in the URL, and no request value names a host'],
- 'server/frame/service.js fetch': [7, 'The composer\'s pictures and posts go to OpenVibe.Community\'s fixed paths (POST /api/v1/spaces/status/attachments, POST /api/v1/posts) on its internal URL. Live and AI internal URLs from config (navbar ranking copy); https://<open site host>/privacy, hosts from the site registry only, no redirects, 5 s (legal links); Community internal URL from config for the menu composer relay (POST /api/frame/status, fixed path, the person\'s Bearer only); Billing and Inventory internal URLs from config for the wallet relays (/api/frame/wallet/…, fixed paths, a usr_/itd_ id must match its pattern, the person\'s Bearer only, no redirects, 8 s); Codes and Actor internal URLs from config for the Dock relays (/api/frame/agent-view…, /agent/timeline, /agent/approvals/…, /agent/kill-switch, /tasks/…, /actor…, fixed paths, product and id matched against patterns and percent-encoded, the person\'s Bearer only, no cookie, no redirects, 15 s)'],
+ 'server/frame/service.js fetch': [8, 'The composer\'s pictures and posts go to OpenVibe.Community\'s fixed paths (POST /api/v1/spaces/status/attachments, POST /api/v1/posts) on its internal URL. Live and AI internal URLs from config (navbar ranking copy); https://<open site host>/privacy, hosts from the site registry only, no redirects, 5 s (legal links); Community internal URL from config for the menu composer relay (POST /api/frame/status, fixed path, the person\'s Bearer only); Billing and Inventory internal URLs from config for the wallet relays (/api/frame/wallet/…, fixed paths, a usr_/itd_ id must match its pattern, the person\'s Bearer only, no redirects, 8 s); Codes and Actor internal URLs from config for the Dock relays (/api/frame/agent-view…, /agent/timeline, /agent/approvals/…, /agent/kill-switch, /tasks/…, /actor…, fixed paths, product and id matched against patterns and percent-encoded, the person\'s Bearer only, no cookie, no redirects, 15 s); the AG-UI run stream relay uses a fixed /api/v1/runs/:id/events path, a validated task id, the trusted service URL, the person\'s Bearer, and no redirects'],
'server/frame/menu-cards.js fetch': [2, 'https://<open site host>/.well-known/openvibe-menu.json, hosts from the site registry only, no redirects, 5 s, 64 KB (menu cards)'],
'server/frame/menu-cards.js fetch': [2, 'https://<open site host>/.well-known/openvibe-menu.json, hosts from the site registry only, no redirects, 5 s, 64 KB (menu cards)'],
'server/stats/open.js fetch': [1, 'open stats (/stats): fixed first-party loopback URLs only — Live (OV_LIVE_INTERNAL_URL, its public home numbers and service-token analytics totals) and Prometheus (PROMETHEUS_URL, loopback by default) with fixed queries; the request only picks 7, 30 or 90 days'],
'server/stats/open.js fetch': [1, 'open stats (/stats): fixed first-party loopback URLs only — Live (OV_LIVE_INTERNAL_URL, its public home numbers and service-token analytics totals) and Prometheus (PROMETHEUS_URL, loopback by default) with fixed queries; the request only picks 7, 30 or 90 days'],
'server/admin/service-proxies.js fetch': [1, 'the admin panel proxies: Live\'s and Chat\'s internal URLs from config, fixed path prefixes, staff only'],
'server/admin/service-proxies.js fetch': [1, 'the admin panel proxies: Live\'s and Chat\'s internal URLs from config, fixed path prefixes, staff only'],