openvibers/openvibe.network Commits
Grant Exchange AI console access (#689)
Changes
@@@ -34,7 +34,7 @@ const AREAS = [] },{ area: 'Queues', items: [{ item: 'Events: subscriptions, dead letters, replay', where: [{ kind: 'tab', tab: 'eventsops' }, { kind: 'url', url: 'https://openvibe.events/' }], service: 'events' },-- { item: 'AI runs, providers and routes', where: [{ kind: 'url', url: 'https://ai.openvibe.services/console' }, { kind: 'tab', tab: 'ai' }], service: 'ai' },++ { item: 'AI runs, providers and routes', where: [{ kind: 'url', url: 'https://openvibe.exchange/console' }, { kind: 'tab', tab: 'ai' }], service: 'ai' },] },{ area: 'Moderation, money and loyalty', items: [{ item: 'Moderation audit across services', where: [{ kind: 'tab', tab: 'modlog' }], service: 'network' },@@@ -65,7 +65,7 @@ const TASKS_SCHEMA = Object.freeze({properties: {provider_topup: task('Top up the prepaid AI balances','Some AI providers run on a prepaid balance. When one runs out, OpenVibe pauses that provider and uses the next one. Mark it done after a top-up.',-- { group: 'money', unlocks: 'Keeps the AI providers that run on a prepaid balance answering.', get_it_at: 'https://ai.openvibe.services/console', get_it_how: 'Open Providers in the AI console, see which balance is low, and top it up on that provider\'s own site.' }),++ { group: 'money', unlocks: 'Keeps the AI providers that run on a prepaid balance answering.', get_it_at: 'https://openvibe.exchange/console/providers', get_it_how: 'Open Providers in the AI console, see which balance is low, and top it up on that provider\'s own site.' }),dnssec_records: task('Add the DNSSEC records at the registrar','Signs OpenVibe\'s domains so nobody can answer for them with false addresses. Cloudflare makes one DS record per domain; it works once the registrar has it.',{ group: 'domains', unlocks: 'Protects every OpenVibe domain from forged DNS answers.', get_it_at: 'https://developers.cloudflare.com/dns/dnssec/', get_it_how: 'Turn on DNSSEC for each domain in Cloudflare (DNS, then Settings), then copy its DS record to the registrar.' }),@@@ -1,7 +1,6 @@'use strict';/**-- * OpenAI and Anthropic (phase 1): the keys go to OpenVibe.AI's own-key (BYOK) page, and this entry is-- * the link to it.++ * OpenAI and Anthropic (phase 1): keys go to OpenVibe.Exchange's "Your keys and accounts" page.** These two are one catalogue entry, because they are one thing: a provider key that OpenVibe.AI* already holds the way it holds every person's own key, and runs with. Connect does not seal them and@@@ -14,7 +13,7 @@ module.exports = {key: 'ai',name: 'OpenAI & Anthropic',vendor: 'OpenVibe.AI',-- blurb: 'Provider keys live in OpenVibe.AI’s own-key page; Connect links there.',++ blurb: 'Provider keys live in OpenVibe.AI under your account; manage them on OpenVibe.Exchange.',apiHost: null,docsUrl: 'https://openvibe.exchange/api',byok: {@@@ -27,13 +26,13 @@ module.exports = {},auth: {kind: 'byok_link',-- label: 'Your own key, in OpenVibe.AI',++ label: 'Your own key, on OpenVibe.Exchange',hint: 'This key is never stored in Connect. OpenVibe.AI holds it, under your account.',fields: [],scopes: [],},walkthrough: [-- { title: 'Open your AI keys', text: 'Your own provider keys live in OpenVibe.AI, with the sign-in of your account; the public pages are OpenVibe.Exchange now.', link: 'https://ai.openvibe.services/you', linkLabel: 'Open your AI keys' },++ { title: 'Open your AI keys', text: 'Your provider keys and supported browser session cookies are managed on OpenVibe.Exchange.', link: 'https://openvibe.exchange/privacy/keys', linkLabel: 'Your keys and accounts' },{ title: 'Add your key there', text: 'The key is sealed by AI, under your own account, and AI runs on it alone.', linkLabel: 'The next step' },{ title: 'Come back here', text: 'Connect has nothing to keep for these two: no credential, no grant, no tool.', linkLabel: 'The next step' },],@@@ -742,6 +742,10 @@ const DEFAULT_GRANTS = [['exchange', 'search.document.write', 'openvibe.search', []],['exchange', 'events.subscription.manage', 'openvibe.events', []],['exchange', 'ai.run.create', 'openvibe.ai', ['ai.complete']],++ // OpenVibe.Exchange's owner-only /console calls AI's admin JSON API. Exchange checks the signed-in Network++ // owner's role before every console route; the service token keeps only the read and operator-write caps needed++ // by the console, scoped to its existing AI audience.++ ...['ai.usage.read', 'ai.workflow.manage', 'ai.provider.manage'].map((cap) => ['exchange', cap, 'openvibe.ai', []]),// The Exchange's Arena (11-06): a blind pair is a Bench round Exchange creates, each person's vote is a judgement it// casts for them (X-OV-Subject), and /rankings reads the chat.arena boards.['exchange', 'bench.round.create', 'openvibe.bench', []],@@@ -39,6 +39,8 @@ for (const [client, cap, aud] of principals.DEFAULT_GRANTS) {assert.deepStrictEqual(principals.DEFAULT_GRANTS.filter(([, cap]) => cap === 'network.secret.use').map(([client, , aud]) => [client, aud]).sort(),[['actor', principals.SELF_AUDIENCE], ['ai', principals.SELF_AUDIENCE], ['domains', principals.SELF_AUDIENCE], ['watch', principals.SELF_AUDIENCE]], 'project secrets: Actor signs, Watch and AI (an app\'s own provider, AI#74) use them, Domains reveals a person\'s provider token');assert.ok(principals.DEFAULT_GRANTS.some(([client, cap, aud, scopes]) => client === 'trade' && cap === 'events.subscription.manage' && aud === 'openvibe.events' && Array.isArray(scopes) && !scopes.length), 'Trade holds events.subscription.manage on openvibe.events');++assert.deepStrictEqual(principals.DEFAULT_GRANTS.filter(([client, cap, aud]) => client === 'exchange' && cap.startsWith('ai.') && aud === 'openvibe.ai').map(([, cap]) => cap).sort(),++ ['ai.provider.manage', 'ai.run.create', 'ai.usage.read', 'ai.workflow.manage'], 'Exchange gets only the AI run capability and console admin capabilities');assert.strictEqual((await events()).length, 0, 'the seed is not announced');// Grant, update, revoke.@@@ -157,7 +157,7 @@ const server = http.createServer(app);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');++ assert.deepStrictEqual([exchangeAiClaims.claims.cap, exchangeAiClaims.claims.ns], [['ai.provider.manage', 'ai.run.create', 'ai.usage.read', 'ai.workflow.manage'], ['ai.complete']], 'Exchange runs AI workflows and manages the operator console 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();
@@ -34,7 +34,7 @@ const AREAS = [
@@ -34,7 +34,7 @@ const AREAS = [
] },
] },
{ area: 'Queues', items: [
{ area: 'Queues', items: [
{ item: 'Events: subscriptions, dead letters, replay', where: [{ kind: 'tab', tab: 'eventsops' }, { kind: 'url', url: 'https://openvibe.events/' }], service: 'events' },
{ item: 'Events: subscriptions, dead letters, replay', where: [{ kind: 'tab', tab: 'eventsops' }, { kind: 'url', url: 'https://openvibe.events/' }], service: 'events' },
- { item: 'AI runs, providers and routes', where: [{ kind: 'url', url: 'https://ai.openvibe.services/console' }, { kind: 'tab', tab: 'ai' }], service: 'ai' },
+ { item: 'AI runs, providers and routes', where: [{ kind: 'url', url: 'https://openvibe.exchange/console' }, { kind: 'tab', tab: 'ai' }], service: 'ai' },
] },
] },
{ area: 'Moderation, money and loyalty', items: [
{ area: 'Moderation, money and loyalty', items: [
{ item: 'Moderation audit across services', where: [{ kind: 'tab', tab: 'modlog' }], service: 'network' },
{ item: 'Moderation audit across services', where: [{ kind: 'tab', tab: 'modlog' }], service: 'network' },
@@ -65,7 +65,7 @@ const TASKS_SCHEMA = Object.freeze({
@@ -65,7 +65,7 @@ const TASKS_SCHEMA = Object.freeze({
properties: {
properties: {
provider_topup: task('Top up the prepaid AI balances',
provider_topup: task('Top up the prepaid AI balances',
'Some AI providers run on a prepaid balance. When one runs out, OpenVibe pauses that provider and uses the next one. Mark it done after a top-up.',
'Some AI providers run on a prepaid balance. When one runs out, OpenVibe pauses that provider and uses the next one. Mark it done after a top-up.',
- { group: 'money', unlocks: 'Keeps the AI providers that run on a prepaid balance answering.', get_it_at: 'https://ai.openvibe.services/console', get_it_how: 'Open Providers in the AI console, see which balance is low, and top it up on that provider\'s own site.' }),
+ { group: 'money', unlocks: 'Keeps the AI providers that run on a prepaid balance answering.', get_it_at: 'https://openvibe.exchange/console/providers', get_it_how: 'Open Providers in the AI console, see which balance is low, and top it up on that provider\'s own site.' }),
dnssec_records: task('Add the DNSSEC records at the registrar',
dnssec_records: task('Add the DNSSEC records at the registrar',
'Signs OpenVibe\'s domains so nobody can answer for them with false addresses. Cloudflare makes one DS record per domain; it works once the registrar has it.',
'Signs OpenVibe\'s domains so nobody can answer for them with false addresses. Cloudflare makes one DS record per domain; it works once the registrar has it.',
{ group: 'domains', unlocks: 'Protects every OpenVibe domain from forged DNS answers.', get_it_at: 'https://developers.cloudflare.com/dns/dnssec/', get_it_how: 'Turn on DNSSEC for each domain in Cloudflare (DNS, then Settings), then copy its DS record to the registrar.' }),
{ group: 'domains', unlocks: 'Protects every OpenVibe domain from forged DNS answers.', get_it_at: 'https://developers.cloudflare.com/dns/dnssec/', get_it_how: 'Turn on DNSSEC for each domain in Cloudflare (DNS, then Settings), then copy its DS record to the registrar.' }),
@@ -1,7 +1,6 @@
@@ -1,7 +1,6 @@
'use strict';
'use strict';
/**
/**
- * OpenAI and Anthropic (phase 1): the keys go to OpenVibe.AI's own-key (BYOK) page, and this entry is
- * the link to it.
+ * OpenAI and Anthropic (phase 1): keys go to OpenVibe.Exchange's "Your keys and accounts" page.
*
*
* These two are one catalogue entry, because they are one thing: a provider key that OpenVibe.AI
* These two are one catalogue entry, because they are one thing: a provider key that OpenVibe.AI
* already holds the way it holds every person's own key, and runs with. Connect does not seal them and
* already holds the way it holds every person's own key, and runs with. Connect does not seal them and
@@ -14,7 +13,7 @@ module.exports = {
@@ -14,7 +13,7 @@ module.exports = {
key: 'ai',
key: 'ai',
name: 'OpenAI & Anthropic',
name: 'OpenAI & Anthropic',
vendor: 'OpenVibe.AI',
vendor: 'OpenVibe.AI',
- blurb: 'Provider keys live in OpenVibe.AI’s own-key page; Connect links there.',
+ blurb: 'Provider keys live in OpenVibe.AI under your account; manage them on OpenVibe.Exchange.',
apiHost: null,
apiHost: null,
docsUrl: 'https://openvibe.exchange/api',
docsUrl: 'https://openvibe.exchange/api',
byok: {
byok: {
@@ -27,13 +26,13 @@ module.exports = {
@@ -27,13 +26,13 @@ module.exports = {
},
},
auth: {
auth: {
kind: 'byok_link',
kind: 'byok_link',
- label: 'Your own key, in OpenVibe.AI',
+ label: 'Your own key, on OpenVibe.Exchange',
hint: 'This key is never stored in Connect. OpenVibe.AI holds it, under your account.',
hint: 'This key is never stored in Connect. OpenVibe.AI holds it, under your account.',
fields: [],
fields: [],
scopes: [],
scopes: [],
},
},
walkthrough: [
walkthrough: [
- { title: 'Open your AI keys', text: 'Your own provider keys live in OpenVibe.AI, with the sign-in of your account; the public pages are OpenVibe.Exchange now.', link: 'https://ai.openvibe.services/you', linkLabel: 'Open your AI keys' },
+ { title: 'Open your AI keys', text: 'Your provider keys and supported browser session cookies are managed on OpenVibe.Exchange.', link: 'https://openvibe.exchange/privacy/keys', linkLabel: 'Your keys and accounts' },
{ title: 'Add your key there', text: 'The key is sealed by AI, under your own account, and AI runs on it alone.', linkLabel: 'The next step' },
{ title: 'Add your key there', text: 'The key is sealed by AI, under your own account, and AI runs on it alone.', linkLabel: 'The next step' },
{ title: 'Come back here', text: 'Connect has nothing to keep for these two: no credential, no grant, no tool.', linkLabel: 'The next step' },
{ title: 'Come back here', text: 'Connect has nothing to keep for these two: no credential, no grant, no tool.', linkLabel: 'The next step' },
],
],
@@ -742,6 +742,10 @@ const DEFAULT_GRANTS = [
@@ -742,6 +742,10 @@ const DEFAULT_GRANTS = [
['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']],
['exchange', 'ai.run.create', 'openvibe.ai', ['ai.complete']],
+ // OpenVibe.Exchange's owner-only /console calls AI's admin JSON API. Exchange checks the signed-in Network
+ // owner's role before every console route; the service token keeps only the read and operator-write caps needed
+ // by the console, scoped to its existing AI audience.
+ ...['ai.usage.read', 'ai.workflow.manage', 'ai.provider.manage'].map((cap) => ['exchange', cap, 'openvibe.ai', []]),
// The Exchange's Arena (11-06): a blind pair is a Bench round Exchange creates, each person's vote is a judgement it
// The Exchange's Arena (11-06): a blind pair is a Bench round Exchange creates, each person's vote is a judgement it
// casts for them (X-OV-Subject), and /rankings reads the chat.arena boards.
// casts for them (X-OV-Subject), and /rankings reads the chat.arena boards.
['exchange', 'bench.round.create', 'openvibe.bench', []],
['exchange', 'bench.round.create', 'openvibe.bench', []],
@@ -39,6 +39,8 @@ for (const [client, cap, aud] of principals.DEFAULT_GRANTS) {
@@ -39,6 +39,8 @@ for (const [client, cap, aud] of principals.DEFAULT_GRANTS) {
assert.deepStrictEqual(principals.DEFAULT_GRANTS.filter(([, cap]) => cap === 'network.secret.use').map(([client, , aud]) => [client, aud]).sort(),
assert.deepStrictEqual(principals.DEFAULT_GRANTS.filter(([, cap]) => cap === 'network.secret.use').map(([client, , aud]) => [client, aud]).sort(),
[['actor', principals.SELF_AUDIENCE], ['ai', principals.SELF_AUDIENCE], ['domains', principals.SELF_AUDIENCE], ['watch', principals.SELF_AUDIENCE]], 'project secrets: Actor signs, Watch and AI (an app\'s own provider, AI#74) use them, Domains reveals a person\'s provider token');
[['actor', principals.SELF_AUDIENCE], ['ai', principals.SELF_AUDIENCE], ['domains', principals.SELF_AUDIENCE], ['watch', principals.SELF_AUDIENCE]], 'project secrets: Actor signs, Watch and AI (an app\'s own provider, AI#74) use them, Domains reveals a person\'s provider token');
assert.ok(principals.DEFAULT_GRANTS.some(([client, cap, aud, scopes]) => client === 'trade' && cap === 'events.subscription.manage' && aud === 'openvibe.events' && Array.isArray(scopes) && !scopes.length), 'Trade holds events.subscription.manage on openvibe.events');
assert.ok(principals.DEFAULT_GRANTS.some(([client, cap, aud, scopes]) => client === 'trade' && cap === 'events.subscription.manage' && aud === 'openvibe.events' && Array.isArray(scopes) && !scopes.length), 'Trade holds events.subscription.manage on openvibe.events');
+assert.deepStrictEqual(principals.DEFAULT_GRANTS.filter(([client, cap, aud]) => client === 'exchange' && cap.startsWith('ai.') && aud === 'openvibe.ai').map(([, cap]) => cap).sort(),
+ ['ai.provider.manage', 'ai.run.create', 'ai.usage.read', 'ai.workflow.manage'], 'Exchange gets only the AI run capability and console admin capabilities');
assert.strictEqual((await events()).length, 0, 'the seed is not announced');
assert.strictEqual((await events()).length, 0, 'the seed is not announced');
// Grant, update, revoke.
// Grant, update, revoke.
@@ -157,7 +157,7 @@ const server = http.createServer(app);
@@ -157,7 +157,7 @@ const server = http.createServer(app);
assert.strictEqual(t.status, 200, 'the remake runs AI chat and Try it on a person’s behalf');
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' });
const exchangeAiClaims = serviceAuth.verifyServiceToken(t.body.access_token, { publicKey: keys.publicKey, issuer: ISSUER, audience: 'openvibe.ai' });
assert.ok(exchangeAiClaims.ok, exchangeAiClaims.reason);
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');
+ assert.deepStrictEqual([exchangeAiClaims.claims.cap, exchangeAiClaims.claims.ns], [['ai.provider.manage', 'ai.run.create', 'ai.usage.read', 'ai.workflow.manage'], ['ai.complete']], 'Exchange runs AI workflows and manages the operator console 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();