openvibers/openvibe.network Commits
Exchange joins the AI neighbourhood, and may run ai.complete (#588)
Changes
@@@ -37,18 +37,18 @@ const fullName = (s) => (/^Open[A-Z]/.test(String(s && s.name)) ? s.name : `Open// each. A site missing here lands in the last group, so a new manifest is never left out of the menu.// The six neighbourhoods of the 2026-10-28 plan (§3.1): each label is two words, so it never collides with a site's own// name, and they follow what a person came to do. Rooms not open yet (Cam, Tours, Surf, Fun, Meme, Casino, Today, Report,--// Vision, Website, Design, Domains, open.shoes, Exchange) are named here so they land in their neighbourhood the day++// Vision, Website, Design, Domains, open.shoes) are named here so they land in their neighbourhood the day// their manifest opens. The building blocks stay last, "For developers" (the tail of Make & build).const GROUPS = [{ id: 'streams', name: 'Streams & places', sites: ['live', 'media-hub', 'stream', 'pics', 'download', 'cam', 'tours'] },{ id: 'games', name: 'Games & laughs', sites: ['games', 'surf', 'fun', 'meme', 'casino', 'quest'] },{ id: 'answers', name: 'News & answers', sites: ['today', 'report', 'news', 'search', 'wiki', 'blog', 'reviews', 'vision'] },{ id: 'make', name: 'Make & build', sites: ['website', 'connect', 'design', 'domains', 'codes', 'space', 'tools', 'host', 'bot'] },-- { id: 'shop', name: 'Shop & swap', sites: ['deals', 'coupons', 'shoes', 'exchange', 'rent', 'homes', 'work', 'food', 'trade'] },++ { id: 'shop', name: 'Shop & swap', sites: ['deals', 'coupons', 'shoes', 'rent', 'homes', 'work', 'food', 'trade'] },{ id: 'people', name: 'People & you', sites: ['chat', 'community', 'tips', 'vip', 'help', 'network', 'inventory'] },// Actor is the engine behind every agent (Codes' Auto, Vibe, the Dock call it): a developer service, not a second// place to chat with an agent (plan L39, 2026-10-31).-- { id: 'build', name: 'For developers', sites: ['services', 'ai', 'actor', 'bench', 'run', 'zone', 'events', 'media', 'watch', 'play'] },++ { id: 'build', name: 'For developers', sites: ['services', 'ai', 'exchange', 'actor', 'bench', 'run', 'zone', 'events', 'media', 'watch', 'play'] },];// What each site is, in a few words, for the menus (the launcher's rows are two to a line on a phone: about 22// characters fit). The manifests' taglines stay the sites' own sentences everywhere else; a site missing here shows@@@ -62,7 +62,7 @@ const MENU_LINE = {deals: 'Deals people share', coupons: 'Codes that work', reviews: 'Honest reviews', work: 'Find a job', rent: 'Places to rent',homes: 'Homes for sale', food: 'Food near you', help: 'Help nearby', trade: 'Watch the markets', watch: 'Know when it changes',search: 'Search everything', host: 'Compare hosting', report: 'Your numbers, honest', vision: 'Ask a picture',-- exchange: 'Apps, skills, compute',++ exchange: 'AI models, one account',network: 'Your account', inventory: 'Your items',services: 'APIs, SDK and keys', ai: 'AI router, your keys', bench: 'Which AI is best', run: 'Run jobs anywhere', zone: 'S3 storage', events: 'Event streams', media: 'Media API', play: 'Game services',};@@@ -693,8 +693,11 @@ const DEFAULT_GRANTS = [['vision', 'network.notifications.push', SELF_AUDIENCE, ['vision']],// OpenVibe.Exchange (contracts 0.235.0): its public listings in Search through the owner API (owner exchange), and// its account-event subscriptions; its export/deletion pair is above. v1 writes no grant and runs no sandbox code.++ // The 2026-11-06 remake makes Exchange the network's AI front: chat and Try it run ai.complete on a person's++ // behalf (on_behalf_of), and the catalog/usage reads are public.['exchange', 'search.document.write', 'openvibe.search', []],['exchange', 'events.subscription.manage', 'openvibe.events', []],++ ['exchange', 'ai.run.create', 'openvibe.ai', ['ai.complete']],// OpenVibe.Fun (contracts 0.236.0): its public quests and plans in Search through the owner API (owner fun), and// its account-event subscriptions; its export/deletion pair is above. Live and Chat are public reads and Today// uses the viewer's own token, so it holds no Live, Chat or Today grant. The optional OpenCoin grants@@@ -45,7 +45,7 @@ assert.ok(at('pics') === at('media-hub') + 1 && at('homes') === at('rent') + 1,assert.deepStrictEqual([p.nav[at('media-hub')].icon, p.nav[at('media-hub')].group, p.nav[at('homes')].group, p.nav[at('tools')].group], ['video', 'streams', 'shop', 'make'], 'Video draws the video icon (there is no media-hub icon)');assert.deepStrictEqual(['actor', 'bot', 'run', 'zone', 'codes'].map((id) => p.nav[at(id)].group), ['build', 'make', 'build', 'build', 'make'], 'robots and coding agents for people; the agent engine, compute and storage for developers (plan L39: Actor is the engine Codes, Vibe and the Dock call)');// Games together, items with the account, Alerts with the building blocks, files beside Video (each when its site is open).--[['games', 'games'], ['connect', 'make'], ['quest', 'games'], ['inventory', 'people'], ['watch', 'build'], ['download', 'streams'], ['search', 'answers'], ['chat', 'people'], ['domains', 'make'], ['report', 'answers'], ['design', 'make'], ['today', 'answers'], ['surf', 'games'], ['meme', 'games'], ['vision', 'answers'], ['exchange', 'shop'], ['fun', 'games'], ['cam', 'streams'], ['bench', 'build'], ['casino', 'games'], ['tours', 'streams'], ['shoes', 'shop']]++[['games', 'games'], ['connect', 'make'], ['quest', 'games'], ['inventory', 'people'], ['watch', 'build'], ['download', 'streams'], ['search', 'answers'], ['chat', 'people'], ['domains', 'make'], ['report', 'answers'], ['design', 'make'], ['today', 'answers'], ['surf', 'games'], ['meme', 'games'], ['vision', 'answers'], ['exchange', 'build'], ['fun', 'games'], ['cam', 'streams'], ['bench', 'build'], ['casino', 'games'], ['tours', 'streams'], ['shoes', 'shop']].filter(([id]) => at(id) >= 0).forEach(([id, group]) => assert.strictEqual(p.nav[at(id)].group, group, `${id} is in ${group}`));assert.strictEqual(p.nav.length, 55); // Connect since contracts 0.279.0, Tours and Shoes since contracts 0.265.0, Casino since contracts 0.264.0, Domains since contracts 0.226.0, Report since 0.227.0, Design since 0.228.0, Today since 0.230.0, Surf since 0.231.0, Meme since 0.234.0, Vision and Exchange since 0.235.0, Fun since 0.236.0, Cam since 0.237.0, Bench since 0.238.0assert.deepStrictEqual([p.nav[at('stream')].name, p.nav[at('pics')].name], ['Restream', 'Pics'], 'OpenRestream is "Restream" among the menu\'s short names (owner, 2026-10-28)');@@@ -150,7 +150,10 @@ const server = http.createServer(app);t = await token({ client_id: 'exchange', client_secret: 'exchange-secret', audience: 'openvibe.search' });assert.strictEqual(t.body.scope, 'search.document.write', 'Exchange indexes its public listings in Search');t = await token({ client_id: 'exchange', client_secret: 'exchange-secret', audience: 'openvibe.ai' });-- assert.strictEqual(t.status, 400, 'Exchange holds no AI capability in v1');++ assert.strictEqual(t.status, 200, 'the remake runs AI chat and Try it on a person’s behalf');++ const exchangeAiClaims = serviceAuth.verifyServiceToken(t.body.access_token, { publicKey: keys.publicKey, issuer: ISSUER, audience: 'openvibe.ai' });++ assert.ok(exchangeAiClaims.ok, exchangeAiClaims.reason);++ assert.deepStrictEqual([exchangeAiClaims.claims.cap, exchangeAiClaims.claims.ns], [['ai.run.create'], ['ai.complete']], 'Exchange runs ai.complete for a person');// OpenVibe.Fun (contracts 0.236.0): its public quests and plans in Search, its account-event subscriptions and the// export/deletion pair. Live and Chat are public reads and Today uses the viewer's token, so it holds no such grant.await db.prepare("UPDATE oauth_clients SET client_secret = 'fun-secret' WHERE client_id = 'fun'").run();
@@ -37,18 +37,18 @@ const fullName = (s) => (/^Open[A-Z]/.test(String(s && s.name)) ? s.name : `Open
@@ -37,18 +37,18 @@ const fullName = (s) => (/^Open[A-Z]/.test(String(s && s.name)) ? s.name : `Open
// each. A site missing here lands in the last group, so a new manifest is never left out of the menu.
// each. A site missing here lands in the last group, so a new manifest is never left out of the menu.
// The six neighbourhoods of the 2026-10-28 plan (§3.1): each label is two words, so it never collides with a site's own
// The six neighbourhoods of the 2026-10-28 plan (§3.1): each label is two words, so it never collides with a site's own
// name, and they follow what a person came to do. Rooms not open yet (Cam, Tours, Surf, Fun, Meme, Casino, Today, Report,
// name, and they follow what a person came to do. Rooms not open yet (Cam, Tours, Surf, Fun, Meme, Casino, Today, Report,
-// Vision, Website, Design, Domains, open.shoes, Exchange) are named here so they land in their neighbourhood the day
+// Vision, Website, Design, Domains, open.shoes) are named here so they land in their neighbourhood the day
// their manifest opens. The building blocks stay last, "For developers" (the tail of Make & build).
// their manifest opens. The building blocks stay last, "For developers" (the tail of Make & build).
const GROUPS = [
const GROUPS = [
{ id: 'streams', name: 'Streams & places', sites: ['live', 'media-hub', 'stream', 'pics', 'download', 'cam', 'tours'] },
{ id: 'streams', name: 'Streams & places', sites: ['live', 'media-hub', 'stream', 'pics', 'download', 'cam', 'tours'] },
{ id: 'games', name: 'Games & laughs', sites: ['games', 'surf', 'fun', 'meme', 'casino', 'quest'] },
{ id: 'games', name: 'Games & laughs', sites: ['games', 'surf', 'fun', 'meme', 'casino', 'quest'] },
{ id: 'answers', name: 'News & answers', sites: ['today', 'report', 'news', 'search', 'wiki', 'blog', 'reviews', 'vision'] },
{ id: 'answers', name: 'News & answers', sites: ['today', 'report', 'news', 'search', 'wiki', 'blog', 'reviews', 'vision'] },
{ id: 'make', name: 'Make & build', sites: ['website', 'connect', 'design', 'domains', 'codes', 'space', 'tools', 'host', 'bot'] },
{ id: 'make', name: 'Make & build', sites: ['website', 'connect', 'design', 'domains', 'codes', 'space', 'tools', 'host', 'bot'] },
- { id: 'shop', name: 'Shop & swap', sites: ['deals', 'coupons', 'shoes', 'exchange', 'rent', 'homes', 'work', 'food', 'trade'] },
+ { id: 'shop', name: 'Shop & swap', sites: ['deals', 'coupons', 'shoes', 'rent', 'homes', 'work', 'food', 'trade'] },
{ id: 'people', name: 'People & you', sites: ['chat', 'community', 'tips', 'vip', 'help', 'network', 'inventory'] },
{ id: 'people', name: 'People & you', sites: ['chat', 'community', 'tips', 'vip', 'help', 'network', 'inventory'] },
// Actor is the engine behind every agent (Codes' Auto, Vibe, the Dock call it): a developer service, not a second
// Actor is the engine behind every agent (Codes' Auto, Vibe, the Dock call it): a developer service, not a second
// place to chat with an agent (plan L39, 2026-10-31).
// place to chat with an agent (plan L39, 2026-10-31).
- { id: 'build', name: 'For developers', sites: ['services', 'ai', 'actor', 'bench', 'run', 'zone', 'events', 'media', 'watch', 'play'] },
+ { id: 'build', name: 'For developers', sites: ['services', 'ai', 'exchange', 'actor', 'bench', 'run', 'zone', 'events', 'media', 'watch', 'play'] },
];
];
// What each site is, in a few words, for the menus (the launcher's rows are two to a line on a phone: about 22
// What each site is, in a few words, for the menus (the launcher's rows are two to a line on a phone: about 22
// characters fit). The manifests' taglines stay the sites' own sentences everywhere else; a site missing here shows
// characters fit). The manifests' taglines stay the sites' own sentences everywhere else; a site missing here shows
@@ -62,7 +62,7 @@ const MENU_LINE = {
@@ -62,7 +62,7 @@ const MENU_LINE = {
deals: 'Deals people share', coupons: 'Codes that work', reviews: 'Honest reviews', work: 'Find a job', rent: 'Places to rent',
deals: 'Deals people share', coupons: 'Codes that work', reviews: 'Honest reviews', work: 'Find a job', rent: 'Places to rent',
homes: 'Homes for sale', food: 'Food near you', help: 'Help nearby', trade: 'Watch the markets', watch: 'Know when it changes',
homes: 'Homes for sale', food: 'Food near you', help: 'Help nearby', trade: 'Watch the markets', watch: 'Know when it changes',
search: 'Search everything', host: 'Compare hosting', report: 'Your numbers, honest', vision: 'Ask a picture',
search: 'Search everything', host: 'Compare hosting', report: 'Your numbers, honest', vision: 'Ask a picture',
- exchange: 'Apps, skills, compute',
+ exchange: 'AI models, one account',
network: 'Your account', inventory: 'Your items',
network: 'Your account', inventory: 'Your items',
services: 'APIs, SDK and keys', ai: 'AI router, your keys', bench: 'Which AI is best', run: 'Run jobs anywhere', zone: 'S3 storage', events: 'Event streams', media: 'Media API', play: 'Game services',
services: 'APIs, SDK and keys', ai: 'AI router, your keys', bench: 'Which AI is best', run: 'Run jobs anywhere', zone: 'S3 storage', events: 'Event streams', media: 'Media API', play: 'Game services',
};
};
@@ -693,8 +693,11 @@ const DEFAULT_GRANTS = [
@@ -693,8 +693,11 @@ const DEFAULT_GRANTS = [
['vision', 'network.notifications.push', SELF_AUDIENCE, ['vision']],
['vision', 'network.notifications.push', SELF_AUDIENCE, ['vision']],
// OpenVibe.Exchange (contracts 0.235.0): its public listings in Search through the owner API (owner exchange), and
// OpenVibe.Exchange (contracts 0.235.0): its public listings in Search through the owner API (owner exchange), and
// its account-event subscriptions; its export/deletion pair is above. v1 writes no grant and runs no sandbox code.
// its account-event subscriptions; its export/deletion pair is above. v1 writes no grant and runs no sandbox code.
+ // The 2026-11-06 remake makes Exchange the network's AI front: chat and Try it run ai.complete on a person's
+ // behalf (on_behalf_of), and the catalog/usage reads are public.
['exchange', 'search.document.write', 'openvibe.search', []],
['exchange', 'search.document.write', 'openvibe.search', []],
['exchange', 'events.subscription.manage', 'openvibe.events', []],
['exchange', 'events.subscription.manage', 'openvibe.events', []],
+ ['exchange', 'ai.run.create', 'openvibe.ai', ['ai.complete']],
// OpenVibe.Fun (contracts 0.236.0): its public quests and plans in Search through the owner API (owner fun), and
// OpenVibe.Fun (contracts 0.236.0): its public quests and plans in Search through the owner API (owner fun), and
// its account-event subscriptions; its export/deletion pair is above. Live and Chat are public reads and Today
// its account-event subscriptions; its export/deletion pair is above. Live and Chat are public reads and Today
// uses the viewer's own token, so it holds no Live, Chat or Today grant. The optional OpenCoin grants
// uses the viewer's own token, so it holds no Live, Chat or Today grant. The optional OpenCoin grants
@@ -45,7 +45,7 @@ assert.ok(at('pics') === at('media-hub') + 1 && at('homes') === at('rent') + 1,
@@ -45,7 +45,7 @@ assert.ok(at('pics') === at('media-hub') + 1 && at('homes') === at('rent') + 1,
assert.deepStrictEqual([p.nav[at('media-hub')].icon, p.nav[at('media-hub')].group, p.nav[at('homes')].group, p.nav[at('tools')].group], ['video', 'streams', 'shop', 'make'], 'Video draws the video icon (there is no media-hub icon)');
assert.deepStrictEqual([p.nav[at('media-hub')].icon, p.nav[at('media-hub')].group, p.nav[at('homes')].group, p.nav[at('tools')].group], ['video', 'streams', 'shop', 'make'], 'Video draws the video icon (there is no media-hub icon)');
assert.deepStrictEqual(['actor', 'bot', 'run', 'zone', 'codes'].map((id) => p.nav[at(id)].group), ['build', 'make', 'build', 'build', 'make'], 'robots and coding agents for people; the agent engine, compute and storage for developers (plan L39: Actor is the engine Codes, Vibe and the Dock call)');
assert.deepStrictEqual(['actor', 'bot', 'run', 'zone', 'codes'].map((id) => p.nav[at(id)].group), ['build', 'make', 'build', 'build', 'make'], 'robots and coding agents for people; the agent engine, compute and storage for developers (plan L39: Actor is the engine Codes, Vibe and the Dock call)');
// Games together, items with the account, Alerts with the building blocks, files beside Video (each when its site is open).
// Games together, items with the account, Alerts with the building blocks, files beside Video (each when its site is open).
-[['games', 'games'], ['connect', 'make'], ['quest', 'games'], ['inventory', 'people'], ['watch', 'build'], ['download', 'streams'], ['search', 'answers'], ['chat', 'people'], ['domains', 'make'], ['report', 'answers'], ['design', 'make'], ['today', 'answers'], ['surf', 'games'], ['meme', 'games'], ['vision', 'answers'], ['exchange', 'shop'], ['fun', 'games'], ['cam', 'streams'], ['bench', 'build'], ['casino', 'games'], ['tours', 'streams'], ['shoes', 'shop']]
+[['games', 'games'], ['connect', 'make'], ['quest', 'games'], ['inventory', 'people'], ['watch', 'build'], ['download', 'streams'], ['search', 'answers'], ['chat', 'people'], ['domains', 'make'], ['report', 'answers'], ['design', 'make'], ['today', 'answers'], ['surf', 'games'], ['meme', 'games'], ['vision', 'answers'], ['exchange', 'build'], ['fun', 'games'], ['cam', 'streams'], ['bench', 'build'], ['casino', 'games'], ['tours', 'streams'], ['shoes', 'shop']]
.filter(([id]) => at(id) >= 0).forEach(([id, group]) => assert.strictEqual(p.nav[at(id)].group, group, `${id} is in ${group}`));
.filter(([id]) => at(id) >= 0).forEach(([id, group]) => assert.strictEqual(p.nav[at(id)].group, group, `${id} is in ${group}`));
assert.strictEqual(p.nav.length, 55); // Connect since contracts 0.279.0, Tours and Shoes since contracts 0.265.0, Casino since contracts 0.264.0, Domains since contracts 0.226.0, Report since 0.227.0, Design since 0.228.0, Today since 0.230.0, Surf since 0.231.0, Meme since 0.234.0, Vision and Exchange since 0.235.0, Fun since 0.236.0, Cam since 0.237.0, Bench since 0.238.0
assert.strictEqual(p.nav.length, 55); // Connect since contracts 0.279.0, Tours and Shoes since contracts 0.265.0, Casino since contracts 0.264.0, Domains since contracts 0.226.0, Report since 0.227.0, Design since 0.228.0, Today since 0.230.0, Surf since 0.231.0, Meme since 0.234.0, Vision and Exchange since 0.235.0, Fun since 0.236.0, Cam since 0.237.0, Bench since 0.238.0
assert.deepStrictEqual([p.nav[at('stream')].name, p.nav[at('pics')].name], ['Restream', 'Pics'], 'OpenRestream is "Restream" among the menu\'s short names (owner, 2026-10-28)');
assert.deepStrictEqual([p.nav[at('stream')].name, p.nav[at('pics')].name], ['Restream', 'Pics'], 'OpenRestream is "Restream" among the menu\'s short names (owner, 2026-10-28)');
@@ -150,7 +150,10 @@ const server = http.createServer(app);
@@ -150,7 +150,10 @@ const server = http.createServer(app);
t = await token({ client_id: 'exchange', client_secret: 'exchange-secret', audience: 'openvibe.search' });
t = await token({ client_id: 'exchange', client_secret: 'exchange-secret', audience: 'openvibe.search' });
assert.strictEqual(t.body.scope, 'search.document.write', 'Exchange indexes its public listings in Search');
assert.strictEqual(t.body.scope, 'search.document.write', 'Exchange indexes its public listings in Search');
t = await token({ client_id: 'exchange', client_secret: 'exchange-secret', audience: 'openvibe.ai' });
t = await token({ client_id: 'exchange', client_secret: 'exchange-secret', audience: 'openvibe.ai' });
- assert.strictEqual(t.status, 400, 'Exchange holds no AI capability in v1');
+ assert.strictEqual(t.status, 200, 'the remake runs AI chat and Try it on a person’s behalf');
+ const exchangeAiClaims = serviceAuth.verifyServiceToken(t.body.access_token, { publicKey: keys.publicKey, issuer: ISSUER, audience: 'openvibe.ai' });
+ assert.ok(exchangeAiClaims.ok, exchangeAiClaims.reason);
+ assert.deepStrictEqual([exchangeAiClaims.claims.cap, exchangeAiClaims.claims.ns], [['ai.run.create'], ['ai.complete']], 'Exchange runs ai.complete for a person');
// OpenVibe.Fun (contracts 0.236.0): its public quests and plans in Search, its account-event subscriptions and the
// OpenVibe.Fun (contracts 0.236.0): its public quests and plans in Search, its account-event subscriptions and the
// export/deletion pair. Live and Chat are public reads and Today uses the viewer's token, so it holds no such grant.
// export/deletion pair. Live and Chat are public reads and Today uses the viewer's token, so it holds no such grant.
await db.prepare("UPDATE oauth_clients SET client_secret = 'fun-secret' WHERE client_id = 'fun'").run();
await db.prepare("UPDATE oauth_clients SET client_secret = 'fun-secret' WHERE client_id = 'fun'").run();