openvibers/openvibe.network Commits

Grant Exchange AI console access (#689)

OP OpenVibers committed 5 days ago · 3ec6b70f4a · 6 files, +13 −8

* Grant Exchange the AI console capabilities

* Update Exchange console grant assertion

Changes

ddiff --git a/server/admin/operator-checklist.js b/server/admin/operator-checklist.js
iindex c217207b..188d2666 100644
---- a/server/admin/operator-checklist.js
++++ b/server/admin/operator-checklist.js
@@@ -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' },
ddiff --git a/server/admin/setup.js b/server/admin/setup.js
iindex 9e48c6b7..e7333a7d 100644
---- a/server/admin/setup.js
++++ b/server/admin/setup.js
@@@ -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.' }),
ddiff --git a/server/connect/apps/ai.js b/server/connect/apps/ai.js
iindex f3f89f0a..ba4eb5c5 100644
---- a/server/connect/apps/ai.js
++++ b/server/connect/apps/ai.js
@@@ -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' },
],
ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex 5f1f393a..3ee49313 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -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', []],
ddiff --git a/test/grants-admin.test.js b/test/grants-admin.test.js
iindex 05b8fcd3..da4797d0 100644
---- a/test/grants-admin.test.js
++++ b/test/grants-admin.test.js
@@@ -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.
ddiff --git a/test/principals.test.js b/test/principals.test.js
iindex 82d653ef..991a67ed 100644
---- a/test/principals.test.js
++++ b/test/principals.test.js
@@@ -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();