openvibers/openvibe.network Commits

Network: OpenVibe.Actor signs in (OAuth client actor, https://openvibe.actor/auth/callback), is on the home page and in the frame, and contracts move to v0.115.0. (#84)

OP OpenVibers committed 8 Oct 2026 · d58808b7e1 · 12 files, +24 −14

- The actor client gets its redirect URI (seed; created first as Actor's principal) and localhost:4950 in development.
- Home: a "Give a task to an agent" intent links openvibe.actor; the updates feed lists actor.
- openvibe-contracts v0.115.0: actor is a live service (site position 23), so the frame's cold-start order and the
  exposure summary include it.
- The agent tests that set up the actor client upsert it now that the seed creates it.


Claude-Session: https://claude.ai/code/session_01K8AYn3FF3HE7CPfM262qdh

Co-authored-by: Claude Opus 5.5 <[email protected]>

Changes

ddiff --git a/package-lock.json b/package-lock.json
iindex c64f2fd6..eed865f3 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -21,7 +21,7 @@
"iovalkey": "^0.4.0",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.114.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.115.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.1",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.13.2",
"pg": "^8.23.0",
@@@ -1556,9 +1556,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.114.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.114.0",
-- "integrity": "sha512-MLtahxYztHGzN09L0D6TuXzPoueuqb/6+v9QVivaaGMO92PDhGK3Q9mg9f0oWX6pYa+iL25Au5sI2MEK0spsRw==",
++ "version": "0.115.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.115.0",
++ "integrity": "sha512-2si6od4+RLgZEIAuWMmTrLZL65TzvCwB/sKoe24vSzC9+sNPt2RXWGXDCwE2fB90vmaRvX6pI+DQyu18QN2f0Q==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 429677ae..17035b5e 100644
---- a/package.json
++++ b/package.json
@@@ -30,7 +30,7 @@
"iovalkey": "^0.4.0",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.114.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.115.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.1",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.13.2",
"pg": "^8.23.0",
ddiff --git a/server/db/database.js b/server/db/database.js
iindex 5975ec76..15454923 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -108,6 +108,8 @@ async function seedDb(db, { log = console } = {}) {
{ client_id: 'billing', name: 'OpenVibe.Billing', redirect_uris: ['https://billing.openvibe.network/auth/callback'] },
{ client_id: 'ai', name: 'OpenVibe.AI', redirect_uris: ['https://ai.openvibe.services/auth/callback'] },
{ client_id: 'bot', name: 'OpenVibe.Bot', redirect_uris: ['https://openvibe.bot/auth/callback'] },
++ // OpenVibe's own agent and the router for agent work (plan T17); the client is created first as Actor's principal.
++ { client_id: 'actor', name: 'OpenVibe.Actor', redirect_uris: ['https://openvibe.actor/auth/callback'] },
];
let seededAny = false;
for (const c of contractClients) {
@@@ -133,6 +135,7 @@ async function seedDb(db, { log = console } = {}) {
{ clientId: 'media', extraUris: ['http://localhost:4100/auth/callback'] },
{ clientId: 'community', extraUris: ['http://localhost:4200/auth/callback'] },
{ clientId: 'space', extraUris: ['http://localhost:4940/auth/callback'] },
++ { clientId: 'actor', extraUris: ['http://localhost:4950/auth/callback'] },
];
for (const { clientId, extraUris } of localClients) {
try { await mergeRedirectUris(db, clientId, extraUris, { log, local: true }); }
ddiff --git a/server/home/render.js b/server/home/render.js
iindex 1c6ea131..718666fb 100644
---- a/server/home/render.js
++++ b/server/home/render.js
@@@ -47,6 +47,7 @@ const INTENTS = [
['wiki', 'fa-book-open', 'Look it up', 'Wiki pages with sources and history.', 'https://openvibe.wiki/', 'wiki learn read knowledge research look'],
['services', 'fa-server', 'Build on OpenVibe', 'One console for projects, keys, grants and the API docs of every service.', 'https://openvibe.services/', 'build api sdk developer developers oauth webhook webhooks app integrate console platform'],
['codes', 'fa-code', 'Code with any agent', 'Claude Code, Codex, OpenCode or your own model on one task, with hand-offs.', 'https://openvibe.codes/', 'code coding agent agents ai claude codex opencode deepseek harness contribute'],
++ ['actor', 'fa-user-astronaut', 'Give a task to an agent', 'Actor picks the best agent for it, checks the answer and shows the cost and why.', 'https://openvibe.actor/', 'agent agents ai assistant task tasks automate research router openrouter do it for me'],
];
function intents(catalog) {
ddiff --git a/server/updates/routes.js b/server/updates/routes.js
iindex 21abc2e2..cb972287 100644
---- a/server/updates/routes.js
++++ b/server/updates/routes.js
@@@ -69,7 +69,7 @@ function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', fetchImpl = gl
return { router: r, feed, queryOf };
}
--const SITES = ['live', 'network', 'tools', 'media', 'community', 'space', 'chat', 'games', 'blog', 'wiki', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes', 'services', 'host', 'ai', 'search', 'sources', 'events', 'billing', 'tips', 'vip', 'openre', 'bot', 'sites'];
++const SITES = ['live', 'network', 'tools', 'media', 'community', 'space', 'chat', 'games', 'blog', 'wiki', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes', 'services', 'host', 'ai', 'search', 'sources', 'events', 'billing', 'tips', 'vip', 'openre', 'bot', 'actor', 'sites'];
const NAMES = { ai: 'AI', vip: 'VIP', openre: 'OpenRe' };
const nameOf = (id) => NAMES[id] || (id ? id.charAt(0).toUpperCase() + id.slice(1) : '');
ddiff --git a/test/agent-grants.test.js b/test/agent-grants.test.js
iindex cbc1075a..1494f7dd 100644
---- a/test/agent-grants.test.js
++++ b/test/agent-grants.test.js
@@@ -22,7 +22,8 @@ console.log = log;
const PEOPLE = [[10, 'owner'], [11, 'dev'], [12, 'viewer'], [13, 'stranger'], [14, 'staff'], [15, 'admin']];
await db.prepare(`INSERT INTO users (id, username, password_hash, role) VALUES
(10, 'owner', 'x', 'user'), (11, 'dev', 'x', 'user'), (12, 'viewer', 'x', 'user'), (13, 'stranger', 'x', 'user'), (14, 'staff', 'x', 'admin'), (15, 'admin', 'x', 'user')`).run();
--await db.prepare("INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party) VALUES ('actor', 'x', 'OpenVibe.Actor', '[]', 1)").run();
++// The seed creates Actor's client (server/db/database.js); this file sets the fields it relies on.
++await db.prepare("INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party) VALUES ('actor', 'x', 'OpenVibe.Actor', '[]', 1) ON CONFLICT (client_id) DO UPDATE SET client_secret = EXCLUDED.client_secret, name = EXCLUDED.name, redirect_uris = EXCLUDED.redirect_uris, is_first_party = EXCLUDED.is_first_party").run();
const S = {};
for (const [id, name] of PEOPLE) S[name] = await subjects.ensureUserSubject(db, await db.prepare('SELECT * FROM users WHERE id = ?').get(id));
ddiff --git a/test/agent-tokens.test.js b/test/agent-tokens.test.js
iindex 37ab2685..2e5fd1f7 100644
---- a/test/agent-tokens.test.js
++++ b/test/agent-tokens.test.js
@@@ -24,7 +24,8 @@ const confirmations = require('../server/developer/confirmations');
const log = console.log; console.log = () => {};
const db = getDb();
console.log = log;
--await db.prepare("INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party) VALUES ('actor', 'actor-secret', 'OpenVibe.Actor', '[]', 1)").run();
++// The seed creates Actor's client (server/db/database.js); this file sets the fields it relies on.
++await db.prepare("INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party) VALUES ('actor', 'actor-secret', 'OpenVibe.Actor', '[]', 1) ON CONFLICT (client_id) DO UPDATE SET client_secret = EXCLUDED.client_secret, name = EXCLUDED.name, redirect_uris = EXCLUDED.redirect_uris, is_first_party = EXCLUDED.is_first_party").run();
await db.prepare("UPDATE oauth_clients SET client_secret = 'live-secret' WHERE client_id = 'live'").run();
await db.prepare(`INSERT INTO users (id, username, password_hash, role) VALUES (10, 'owner', 'x', 'user'), (14, 'staff', 'x', 'admin')`).run();
const owner = await subjects.ensureUserSubject(db, await db.prepare('SELECT * FROM users WHERE id = 10').get());
ddiff --git a/test/agents-concurrency.test.js b/test/agents-concurrency.test.js
iindex a82fcb18..bf2331dd 100644
---- a/test/agents-concurrency.test.js
++++ b/test/agents-concurrency.test.js
@@@ -22,7 +22,8 @@ console.log = log;
const PG = process.env.NETWORK_TEST_STORE === 'pg';
const USERS = [[10, 'owner'], [11, 'dev'], [12, 'leaver'], [13, 'stayer']];
await db.prepare(`INSERT INTO users (id, username, password_hash, role) VALUES ${USERS.map(([id, n]) => `(${id}, '${n}', 'x', 'user')`).join(', ')}`).run();
--await db.prepare("INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris) VALUES ('actor', 'x', 'OpenVibe.Actor', '[]')").run();
++// The seed creates Actor's client (server/db/database.js); this file sets the fields it relies on.
++await db.prepare("INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris) VALUES ('actor', 'x', 'OpenVibe.Actor', '[]') ON CONFLICT (client_id) DO UPDATE SET client_secret = EXCLUDED.client_secret, name = EXCLUDED.name, redirect_uris = EXCLUDED.redirect_uris, is_first_party = 0").run();
const S = {};
for (const [id, name] of USERS) S[name] = await subjects.ensureUserSubject(db, await db.prepare('SELECT * FROM users WHERE id = ?').get(id));
ddiff --git a/test/agents.test.js b/test/agents.test.js
iindex 22f4cf41..1e324bcb 100644
---- a/test/agents.test.js
++++ b/test/agents.test.js
@@@ -21,7 +21,8 @@ console.log = log;
await db.prepare(`INSERT INTO users (id, username, password_hash, role) VALUES
(10, 'owner', 'x', 'user'), (11, 'dev', 'x', 'user'), (12, 'viewer', 'x', 'user'), (13, 'stranger', 'x', 'user'),
(14, 'staff', 'x', 'admin'), (15, 'admin', 'x', 'user'), (16, 'leaver', 'x', 'user'), (17, 'erased', 'x', 'user')`).run();
--await db.prepare("INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris) VALUES ('actor', 'x', 'OpenVibe.Actor', '[]')").run();
++// The seed creates Actor's client (server/db/database.js); this file sets the fields it relies on.
++await db.prepare("INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris) VALUES ('actor', 'x', 'OpenVibe.Actor', '[]') ON CONFLICT (client_id) DO UPDATE SET client_secret = EXCLUDED.client_secret, name = EXCLUDED.name, redirect_uris = EXCLUDED.redirect_uris, is_first_party = 0").run();
const S = {};
for (const [id, name] of [[10, 'owner'], [11, 'dev'], [12, 'viewer'], [13, 'stranger'], [14, 'staff'], [15, 'admin'], [16, 'leaver'], [17, 'erased']]) {
S[name] = await subjects.ensureUserSubject(db, await db.prepare('SELECT * FROM users WHERE id = ?').get(id));
ddiff --git a/test/confirmations.test.js b/test/confirmations.test.js
iindex d4aac6ae..4e9eaa09 100644
---- a/test/confirmations.test.js
++++ b/test/confirmations.test.js
@@@ -26,7 +26,8 @@ console.log = log;
const PEOPLE = [[10, 'owner'], [11, 'admin'], [12, 'dev'], [13, 'stranger'], [14, 'staff'], [17, 'erased']];
await db.prepare(`INSERT INTO users (id, username, password_hash, role) VALUES
(10, 'owner', 'x', 'user'), (11, 'admin', 'x', 'user'), (12, 'dev', 'x', 'user'), (13, 'stranger', 'x', 'user'), (14, 'staff', 'x', 'admin'), (17, 'erased', 'x', 'user')`).run();
--await db.prepare("INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party) VALUES ('actor', 'x', 'OpenVibe.Actor', '[]', 1)").run();
++// The seed creates Actor's client (server/db/database.js); this file sets the fields it relies on.
++await db.prepare("INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party) VALUES ('actor', 'x', 'OpenVibe.Actor', '[]', 1) ON CONFLICT (client_id) DO UPDATE SET client_secret = EXCLUDED.client_secret, name = EXCLUDED.name, redirect_uris = EXCLUDED.redirect_uris, is_first_party = EXCLUDED.is_first_party").run();
// No service holds network.confirmation.manage by default (no receiver ships yet): Media and Tools get it here, Live does not.
for (const c of ['media', 'tools', 'live']) await db.prepare(`UPDATE oauth_clients SET client_secret = '${c}-secret' WHERE client_id = ?`).run(c);
for (const c of ['media', 'tools']) await db.prepare("INSERT INTO principal_grants (client_id, capability, audience, namespaces, granted_by) VALUES (?, 'network.confirmation.manage', 'openvibe.network', '[]', 'test')").run(c);
ddiff --git a/test/frame.test.js b/test/frame.test.js
iindex 3e174eaa..a708ae51 100644
---- a/test/frame.test.js
++++ b/test/frame.test.js
@@@ -14,7 +14,7 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');
assert.equal(siteForHost('evil.example'), null);
assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');
let p = svc.payloadFor('openvibe.media');
--assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'bot', 'codes'], 'cold start follows the base order');
++assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'bot', 'codes', 'actor'], 'cold start follows the base order');
assert.strictEqual(p.nav.find(n => n.id === 'space').url, 'https://openvibe.space/', 'the forum goes to Space');
assert.strictEqual(p.nav.find(n => n.id === 'community').url, 'https://openvibe.community/', 'Community remains for pastes and Pulse');
assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');
ddiff --git a/test/registry-exposure.test.js b/test/registry-exposure.test.js
iindex 53f0f846..b12e015e 100644
---- a/test/registry-exposure.test.js
++++ b/test/registry-exposure.test.js
@@@ -22,6 +22,7 @@ const PINNED = {
ai: 'live', // the public home and developer-app API at ai.openvibe.services since 2026-10-07 (contracts 0.110.0)
// openvibe-contracts v0.86-v0.90: Bot runs (its own exposure row); Zone is a charter only.
bot: 'live', zone: 'placeholder',
++ actor: 'live', // OpenVibe's own agent and the router for agent work at openvibe.actor since 2026-10-08 (contracts 0.115.0)
run: 'internal', // OpenVibe.Run runs on loopback since 2026-10-07 (contracts 0.109.0)
services: 'live', // OpenVibe.Services: the developer platform at openvibe.services since 2026-10-08 (contracts 0.113.0; the portal moved from Codes)
// openvibe-contracts v0.102.0: OpenVibe.Watch is a charter only (plan T18).
@@@ -110,7 +111,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
assert.strictEqual(sBy.shared.label, `not running (library, released v${require('openvibe-shared/package.json').version})`);
// Contracts 0.83.0 added two placeholder services (media-hub, space), v0.86-v0.90 Bot (live) and two more (zone, run),
// v0.95.0 Services; Space and AI went live in v0.110.0; none is retired
-- assert.deepStrictEqual(st.exposure_summary, { live: 19, internal: 11, library: 4, repository: 1, placeholder: 2, retired: 0 });
++ assert.deepStrictEqual(st.exposure_summary, { live: 20, internal: 11, library: 4, repository: 1, placeholder: 2, retired: 0 });
// The page: no row for a service whose domain serves a placeholder shows a bare "Up".
const html = await fetch(base + '/status').then(x => x.text());
@@@ -126,7 +127,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
// 3. The Frame: the nav lists only sites whose domain serves the service; the rest are soon with a reason.
const open = SITES.filter(s => s.status === 'open').map(s => s.id);
-- assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'space', 'bot', 'codes']); // by site position; bot: a site since openvibe-contracts 0.106.0; host: live since 0.107.0; space: live since 0.110.0 (the forum, 2026-10-07)
++ assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'space', 'bot', 'codes', 'actor']); // by site position; bot: a site since openvibe-contracts 0.106.0; host: live since 0.107.0; space: live since 0.110.0 (the forum, 2026-10-07)
for (const s of SITES) {
assert.ok(s.service && PINNED[s.service], `${s.id} names its service`);
assert.strictEqual(s.status === 'open', PINNED[s.service] === 'live', `${s.id} nav status follows exposure`);