openvibers/openvibe.chat Commits

Point Chat machine flows to OpenVibe Link (#424)

OP OpenVibers committed 4 days ago · faaf71698b · 17 files, +64 −54

* Point Chat machine flows to OpenVibe Link

* Speak Link on the machine and provider paths

A paired machine token is a link principal (actor_type link), the desktop
party routes accept it, and the person's own-model provider is link in the AI
settings and the model picker, matching Contracts. The chat.preferences
fixture reports the namespace's current schema version.

* Merge remote-tracking branch 'origin/main' into rename/link-1109-Chat

* Merge remote-tracking branch 'origin/main' into rename/link-1109-Chat

* Merge main and pin openvibe-contracts v0.372.0

Keeps the link principal on the desktop party routes and the link provider in
AI settings, and moves the Contracts pin to the released Link tag.

Changes

ddiff --git a/README.md b/README.md
iindex 3835285..75afb04 100644
---- a/README.md
++++ b/README.md
@@@ -356,7 +356,7 @@ API: `GET|PUT|DELETE /api/chat/me/ai-key` (own sign-in only, not API tokens); pa
linked from the channel on the server's settings page.
**A model on their own computer** (AI on your own machines; contracts 0.220.0/0.221.0). The key may instead be provider
--`node`: a `machine` (a `dev_` id from openvibe.bot/machines, or `any`, the fastest of theirs that is on) and a model,
++`node`: a `machine` (a `dev_` id from openvibe.link/machines, or `any`, the fastest of theirs that is on) and a model,
with no key and nothing spent. OpenVibe.AI reaches it through OpenVibe.Bot and the person's OpenVibe Link, only where they
turned on "Let my AI use this computer's models".
- **The form** shows only the fields the chosen provider takes (`chat.css` `:has()`). It comes prefilled from Bot's
ddiff --git a/package-lock.json b/package-lock.json
iindex 3bdd835..eec0ce3 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -17,7 +17,7 @@
"express-rate-limit": "^7.5.0",
"iovalkey": "^0.4.0",
"multer": "^2.0.2",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.338.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.372.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.123.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.173.0",
"pg": "^8.23.0",
@@@ -1557,9 +1557,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.338.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.338.0",
-- "integrity": "sha512-75FD6IxMsXoud3NFXKDs0/D0rhJtF2D+WL8M92ArIyabkUnM/sZvw6E6L97EqwQ3O7M/kB+uwsNSxGudbil+ng==",
++ "version": "0.372.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.372.0",
++ "integrity": "sha512-HnfBnkhfMXYNbW4NnpTAMqZc0uV9d4XvwA20VaZXZFbBTPZiClYfoZ0/TWWWh24BFANx9TSzC+4jE0WDHKuNhA==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 6b47bfa..93831a2 100644
---- a/package.json
++++ b/package.json
@@@ -29,7 +29,7 @@
"express-rate-limit": "^7.5.0",
"iovalkey": "^0.4.0",
"multer": "^2.0.2",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.338.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.372.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.123.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.173.0",
"pg": "^8.23.0",
ddiff --git a/server/ai/credentials.js b/server/ai/credentials.js
iindex db344c0..537dfe9 100644
---- a/server/ai/credentials.js
++++ b/server/ai/credentials.js
@@@ -13,14 +13,14 @@
* Needs Network's grant [chat, ai.credential.manage, openvibe.ai]. Refusals come back as CredentialError (status, code).
*
* Provider node (contracts 0.220.0) is a model on the person's own computer instead: `machine` names it (a dev_ id on
-- * openvibe.bot/machines), there is no key and nothing is spent; OpenVibe.AI reaches it through OpenVibe.Bot, and only
++ * openvibe.link/machines), there is no key and nothing is spent; OpenVibe.AI reaches it through OpenVibe.Bot, and only
* while the person lets their AI use that computer's models there.
*/
const config = require('../config');
const aiClient = require('./client');
const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
--const PROVIDERS = { openai: 'OpenAI (or any OpenAI-compatible endpoint)', anthropic: 'Anthropic', node: 'A model on your own computer' };
++const PROVIDERS = { openai: 'OpenAI (or any OpenAI-compatible endpoint)', anthropic: 'Anthropic', link: 'A model on your own computer' };
const MACHINE_RE = /^dev_[0-9A-HJKMNP-TV-Z]{26}$/;
class CredentialError extends Error { constructor(status, code, message) { super(message); this.status = status; this.code = code; } }
@@@ -53,13 +53,13 @@ async function put(subject, { provider, base_url, api_key, model, budget_usd_per
if (!PROVIDERS[provider]) fail(422, 'credentials.provider', 'Choose OpenAI (or an OpenAI-compatible endpoint), Anthropic, or a model on your own computer.');
const m = String(model || '').trim();
if (!m || m.length > 120 || /\s/.test(m)) fail(422, 'credentials.model', 'Name the model, for example gpt-4o-mini or claude-haiku-4-5.');
-- if (provider === 'node') {
++ if (provider === 'link') {
// A model on the person's own computer: the machine, no key, no budget (nothing is spent).
const id = String(machine || '').trim();
// `any` (contracts 0.221.0): whichever of their computers is on with the model, the fastest first.
-- if (!MACHINE_RE.test(id) && id !== 'any') fail(422, 'credentials.machine', 'Name your computer (its id, dev_…, from openvibe.bot/machines), or choose any of your computers.');
++ if (!MACHINE_RE.test(id) && id !== 'any') fail(422, 'credentials.machine', 'Name your computer (its id, dev_…, from openvibe.link/machines), or choose any of your computers.');
try {
-- return view(await call('PUT', subject, { provider: 'node', machine: id, models: { chat: m } }));
++ return view(await call('PUT', subject, { provider: 'link', machine: id, models: { chat: m } }));
} catch (err) {
if (err instanceof CredentialError) throw err;
if (err && err.status >= 400 && err.status < 500) throw new CredentialError(err.status, 'credentials.refused', (err.detail || err.message || 'OpenVibe.AI refused it.').slice(0, 200));
@@@ -93,12 +93,12 @@ async function remove(subject) {
}
}
--const label = (c) => (c && c.provider === 'node' ? `${c.machine === 'any' ? 'any of your own computers' : 'your own computer'} · ${c.model || 'its model'}` : c ? `${c.provider}${c.base_url ? ` (${(() => { try { return new URL(c.base_url).hostname; } catch { return 'custom'; } })()})` : ''} · ${c.model || 'default model'}` : null);
++const label = (c) => (c && c.provider === 'link' ? `${c.machine === 'any' ? 'any of your own computers' : 'your own computer'} · ${c.model || 'its model'}` : c ? `${c.provider}${c.base_url ? ` (${(() => { try { return new URL(c.base_url).hostname; } catch { return 'custom'; } })()})` : ''} · ${c.model || 'default model'}` : null);
/** What a room's members see an AI member think with: "<model> on Fran's computer", or "openai · gpt-4o-mini, Fran's key". */
const memberLabel = (c, name) => {
const who = String(name || 'a member').slice(0, 40);
-- return c && c.provider === 'node' ? `${c.model || 'a model'} on ${who}'s computer${c.machine === 'any' ? 's' : ''}` : `${label(c)}, ${who}'s key`;
++ return c && c.provider === 'link' ? `${c.model || 'a model'} on ${who}'s computer${c.machine === 'any' ? 's' : ''}` : `${label(c)}, ${who}'s key`;
};
module.exports = { get, put, remove, label, memberLabel, CredentialError, PROVIDERS };
ddiff --git a/server/rooms/ai-members.js b/server/rooms/ai-members.js
iindex 3a0a864..31039b5 100644
---- a/server/rooms/ai-members.js
++++ b/server/rooms/ai-members.js
@@@ -487,7 +487,7 @@ async function afterPost(room, message, user, deps = {}) {
return await say(resting ? 'AI is resting for a moment. Try again in a minute.' : 'I could not answer just now. Try again in a minute.');
}
if (onComputer) return await say(vibe
-- ? 'I could not reach the model on your computer just now. Is it on, with its model server running (Ollama, LM Studio or llama.cpp), and "Let my AI use this computer\'s models" on at openvibe.bot/machines? You can switch me back to OpenVibe\'s model in Settings, under "Your AI key".'
++ ? 'I could not reach the model on your computer just now. Is it on, with its model server running (Ollama, LM Studio or llama.cpp), and "Let my AI use this computer\'s models" on at openvibe.link/machines? You can switch me back to OpenVibe\'s model in Settings, under "Your AI key".'
: 'I could not reach the model on my manager\'s computer just now. Try again later, or the room\'s owner can switch me to OpenVibe\'s model.');
return await say(vibe
? 'I could not reach your model just now (your key may be out of today\'s budget, or the provider did not answer). Try again later, or switch me back to OpenVibe\'s model in Settings, under "Your AI key".'
ddiff --git a/server/rooms/party-desktop.js b/server/rooms/party-desktop.js
iindex cde74e3..6df7d30 100644
---- a/server/rooms/party-desktop.js
++++ b/server/rooms/party-desktop.js
@@@ -12,7 +12,7 @@ const controlLease = require('./control-lease');
const rooms = require('./rooms');
const parties = require('./parties');
--const NODE = /^node:(nod_[0-9A-HJKMNP-TV-Z]{26})$/;
++const NODE = /^link:(lnk_[0-9A-HJKMNP-TV-Z]{26})$/;
const SHARE = /^shr_[0-9A-HJKMNP-TV-Z]{26}$/;
async function nodeOf(req) {
@@@ -21,7 +21,7 @@ async function nodeOf(req) {
const r = await verifyServiceToken(header.slice(7).trim(), {
...keys().verifyOptions, issuer: config.networkUrl, audience: 'openvibe.chat', contracts,
});
-- if (!r.ok || r.claims.actor_type !== 'node') return null;
++ if (!r.ok || r.claims.actor_type !== 'link') return null;
const m = NODE.exec(String(r.claims.sub || ''));
const owner = String(r.claims.on_behalf_of || '');
return m && /^usr_[0-9A-HJKMNP-TV-Z]{26}$/.test(owner) ? { id: m[1], owner } : null;
ddiff --git a/server/rooms/vibe.js b/server/rooms/vibe.js
iindex 06f634e..729c9ae 100644
---- a/server/rooms/vibe.js
++++ b/server/rooms/vibe.js
@@@ -64,7 +64,7 @@ function titleOf(text) {
const newSlug = () => `v-${crypto.randomBytes(8).toString('hex').slice(0, 12)}`;
/** What Vibe thinks with, said to its person: "llama3.1:8b on your computer", "your openai key (gpt-4o-mini)". */
--const youOf = (cred) => (cred.provider === 'node'
++const youOf = (cred) => (cred.provider === 'link'
? `${cred.model || 'a model'} on your computer${cred.machine === 'any' ? 's' : ''}`
: `your ${cred.provider} key (${cred.model || 'its default model'})`);
ddiff --git a/server/web/pages.js b/server/web/pages.js
iindex f5e63de..c0204f0 100644
---- a/server/web/pages.js
++++ b/server/web/pages.js
@@@ -662,21 +662,21 @@ ${await aiKeySection(actor, req.query || {})}`,
try { key = await credentials.get(actor.user.subject_id); } catch { unreachable = true; }
const opt = (v, label, cur) => `<option value="${v}"${v === cur ? ' selected' : ''}>${label}</option>`;
const spent = key ? `$${key.spent_usd_today.toFixed(2)} of ${key.budget_usd_per_day ? `$${key.budget_usd_per_day.toFixed(2)}` : 'no cap'} used today` : '';
-- // From openvibe.bot/machines, "Use these models in OpenVibe.Chat": a model on your own computer, filled in.
-- const offered = q.own === 'node' && /^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$/.test(String(q.machine || '')) ? { machine: String(q.machine), model: /^[A-Za-z0-9._:/-]{1,120}$/.test(String(q.model || '')) ? String(q.model) : '' } : null;
-- const provider = offered ? 'node' : key ? key.provider : 'openai';
++ // From openvibe.link/machines, "Use these models in OpenVibe.Chat": a model on your own computer, filled in.
++ const offered = q.own === 'link' && /^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$/.test(String(q.machine || '')) ? { machine: String(q.machine), model: /^[A-Za-z0-9._:/-]{1,120}$/.test(String(q.model || '')) ? String(q.model) : '' } : null;
++ const provider = offered ? 'link' : key ? key.provider : 'openai';
const machineValue = offered ? offered.machine : (key && key.machine) || '';
const modelValue = offered && offered.model ? offered.model : (key && key.model) || '';
-- const keyLine = key && key.provider === 'node' ? `${key.machine === 'any' ? 'on whichever of your computers is on, the fastest first' : `on your computer <code>${esc(key.machine || '')}</code>`} · nothing spent` : key ? `key ending ${esc(key.key_hint || '····')} · ${esc(spent)}` : '';
++ const keyLine = key && key.provider === 'link' ? `${key.machine === 'any' ? 'on whichever of your computers is on, the fastest first' : `on your computer <code>${esc(key.machine || '')}</code>`} · nothing spent` : key ? `key ending ${esc(key.key_hint || '····')} · ${esc(spent)}` : '';
const vibeOwn = key ? (await require('../rooms/vibe').engineOf(actor.user)).own : false;
const vibeBox = require('../rooms/vibe').enabled() ? `<label class="oc-check"><input type="checkbox" name="vibe" value="1"${vibeOwn || offered ? ' checked' : ''}> Vibe thinks with it too (in all your chats with Vibe; its answers then have no daily limit)</label>` : '';
const open = offered || q.saved || q.error ? ' open' : '';
return `<details class="oc-card oc-fold oc-aikey" id="oc-ai-key"${open}><summary><span>Your AI key${key ? ` <span class="oc-muted">· ${esc(credentials.label(key))}</span>` : ''}</span>${IC.chevron}</summary>
--<p class="oc-muted">AI members you add to a room, your Vibe among them, can think with your own provider and model instead of OpenVibe's: OpenAI, Anthropic, any OpenAI-compatible endpoint (OpenRouter, Groq, Together, your own server), or <strong>a model on your own computer</strong> (Ollama, LM Studio or llama.cpp through the OpenVibe Link, free, with no port opened: turn on "Let my AI use this computer's models" for it on <a href="https://openvibe.bot/machines">openvibe.bot/machines</a>). A key is kept encrypted in OpenVibe.AI and never shown again; the room's messages go to your provider or your computer when one of those members answers, and a key's replies count against your daily budget, not the room's limit.</p>
++<p class="oc-muted">AI members you add to a room, your Vibe among them, can think with your own provider and model instead of OpenVibe's: OpenAI, Anthropic, any OpenAI-compatible endpoint (OpenRouter, Groq, Together, your own server), or <strong>a model on your own computer</strong> (Ollama, LM Studio or llama.cpp through the OpenVibe Link, free, with no port opened: turn on "Let my AI use this computer's models" for it on <a href="https://openvibe.link/machines">openvibe.link/machines</a>). A key is kept encrypted in OpenVibe.AI and never shown again; the room's messages go to your provider or your computer when one of those members answers, and a key's replies count against your daily budget, not the room's limit.</p>
${offered ? notice('ok', `Save to let your AI members think with ${offered.model || 'a model'} on ${offered.machine === 'any' ? 'whichever of your computers is on' : 'your own computer'}.`) : ''}
${unreachable ? notice('error', 'OpenVibe.AI did not answer, so your key cannot be shown or changed right now.') : `${key ? `<p><strong>${esc(credentials.label(key))}</strong> <span class="oc-muted">· ${keyLine}${vibeOwn ? ' · Vibe thinks with it' : ''}</span></p>` : '<p class="oc-muted">No key yet.</p>'}
<form class="oc-form oc-ai-form" method="post" action="/settings/ai-key">
--<label>Provider <select name="provider">${opt('openai', 'OpenAI or OpenAI-compatible', provider)}${opt('anthropic', 'Anthropic', provider)}${opt('node', 'A model on my own computer', provider)}</select></label>
++<label>Provider <select name="provider">${opt('openai', 'OpenAI or OpenAI-compatible', provider)}${opt('anthropic', 'Anthropic', provider)}${opt('link', 'A model on my own computer', provider)}</select></label>
<label class="oc-if-node">Computer <input name="machine" maxlength="40" pattern="dev_[0-9A-Z]{26}" placeholder="dev_… (a model on your own computer only)" value="${esc(machineValue === 'any' ? '' : machineValue)}" autocomplete="off" spellcheck="false"></label>
<label class="oc-check oc-if-node"><input type="checkbox" name="machine_any" value="1"${machineValue === 'any' ? ' checked' : ''}> Any of my computers that is on with this model (the fastest first, the next when one is busy)</label>
<label class="oc-if-openai">Endpoint <input name="base_url" type="url" maxlength="300" inputmode="url" placeholder="https://openrouter.ai/api/v1 (leave empty for OpenAI itself)" value="${esc((key && key.base_url) || '')}"></label>
@@@ -1077,7 +1077,7 @@ ${composer}${work ? `</div>${queuePanel(room, can, me, work)}</div>` : ''}`,
if (can.manage && room.kind !== 'system' && aiMembers.enabled()) { try { myKey = await credentials.get(actor.user.subject_id); } catch { myKey = null; } }
const engineField = (a) => {
const cur = a ? (a.engine || 'openvibe') : 'openvibe';
-- const ownLabel = a && cur === 'own' ? aiMembers.modelOf(a) : myKey && myKey.provider === 'node' ? `Your own computer: ${myKey.model || 'its model'}` : myKey ? `Your own key: ${credentials.label(myKey)}` : 'Your own key (add one in your chat settings first)';
++ const ownLabel = a && cur === 'own' ? aiMembers.modelOf(a) : myKey && myKey.provider === 'link' ? `Your own computer: ${myKey.model || 'its model'}` : myKey ? `Your own key: ${credentials.label(myKey)}` : 'Your own key (add one in your chat settings first)';
return `<label>Thinks with <select name="engine"><option value="openvibe"${cur === 'openvibe' ? ' selected' : ''}>OpenVibe's model</option><option value="own"${cur === 'own' ? ' selected' : ''}${cur === 'own' || myKey ? '' : ' disabled'}>${esc(ownLabel)}</option></select></label>
${myKey || cur === 'own' ? '' : '<p class="oc-muted">To give it a different model (yours, from OpenAI, Anthropic or an OpenAI-compatible provider, or a model on your own computer), <a href="/settings#oc-ai-key">add your AI key</a>.</p>'}`;
};
ddiff --git a/server/web/vibe-pages.js b/server/web/vibe-pages.js
iindex e509a8a..f660ea2 100644
---- a/server/web/vibe-pages.js
++++ b/server/web/vibe-pages.js
@@@ -101,7 +101,7 @@ function rail({ chats, current, signedIn, allowance, query = '', here = null })
const allow = own ? `<p class="vb-allow" id="vb-allow" data-own="1">No daily limit: Vibe thinks with ${esc(own.you)}</p>`
: left != null ? `<p class="vb-allow" id="vb-allow">${left} of ${allowance.per_day} answers left today</p>` : '';
// Chat's other places, quietly at the foot (the network's navbar reaches them too).
-- const more = [['https://openvibe.bot/machines', 'Computers & robots'], ['/work', 'Work together'], ['/messages', 'Messages'], ['/rooms', 'Rooms and servers'], ['/', 'Global chat']];
++ const more = [['https://openvibe.link/machines', 'Computers & robots'], ['/work', 'Work together'], ['/messages', 'Messages'], ['/rooms', 'Rooms and servers'], ['/', 'Global chat']];
const html = kit.sidebar({
id: 'vb-rail', label: 'Your chats with Vibe',
action: { href: '/vibe', label: 'New chat', icon: 'pencil' },
ddiff --git a/server/web/workspace-pages.js b/server/web/workspace-pages.js
iindex 74c2410..94e0a20 100644
---- a/server/web/workspace-pages.js
++++ b/server/web/workspace-pages.js
@@@ -177,7 +177,7 @@ ${tasks.length ? `<ol class="oc-tasks">${tasks.map((t) => taskItem(t, room, can,
`<a class="oc-pill" href="${back.href}">${ICONS.chat}<span>${vibeChat ? 'Chat' : 'Room'}</span></a>`,
t.result_url ? `<a class="oc-pill oc-pill-strong" href="${esc(t.result_url)}" rel="noopener nofollow">${ICONS.open}<span>Open where it landed</span></a>` : '',
info && info.actor_url ? `<a class="oc-pill" href="${esc(info.actor_url)}" title="Every step the agent took, on OpenVibe.Actor">${ICONS.steps}<span>Every step</span></a>` : '',
-- t.kind === 'computer' || t.kind === 'code' ? `<a class="oc-pill" href="https://openvibe.bot/machines" title="Your computers: pair one, see its screen, set what agents may do on it">${ICONS.computer}<span>Your computers</span></a>` : '',
++ t.kind === 'computer' || t.kind === 'code' ? `<a class="oc-pill" href="https://openvibe.link/machines" title="Your computers: pair one, see its screen, set what agents may do on it">${ICONS.computer}<span>Your computers</span></a>` : '',
].filter(Boolean).join('');
const checked = info ? info.lines.find((l) => l.startsWith('Checked by')) : null;
const sources = t.sources.length ? `<section class="oc-tp-card" aria-labelledby="oc-src-h"><h2 id="oc-src-h">Sources <span class="oc-muted">${t.sources.length}</span></h2>
ddiff --git a/test/link-brand.test.js b/test/link-brand.test.js
nnew file mode 100644
iindex 0000000..eaa904f
---- /dev/null
++++ b/test/link-brand.test.js
@@@ -0,0 +1,10 @@
++'use strict';
++const assert = require('node:assert/strict');
++const { spawnSync } = require('node:child_process');
++const path = require('node:path');
++const root = path.resolve(__dirname, '..');
++const oldName = ['OpenVibe[ .-]No' + 'de', 'openvibe-' + 'node', 'no' + 'd_', 'node[.]openvibe[.]network', 'node:lnk_', 'openvibe[.](bot|my|network)/mach' + 'ines'].join('|');
++const result = spawnSync('git', ['grep', '-n', '-I', '-i', '-E', oldName, '--', '.', ':!migrations/**', ':!test/link-brand.test.js'], { cwd: root, encoding: 'utf8' });
++assert.ok(result.status === 0 || result.status === 1, result.stderr || 'git grep failed');
++assert.equal(result.status, 1, `Old Link brand or principal returned:\n${(result.stdout || '').split('\n').slice(0, 10).join('\n')}`);
++console.log(' ok old Link brand and principal IDs are absent');
ddiff --git a/test/party-media.test.js b/test/party-media.test.js
iindex 1947e16..a03e7ba 100644
---- a/test/party-media.test.js
++++ b/test/party-media.test.js
@@@ -162,9 +162,9 @@ t('a desktop party gates members, claims one Link machine, and issues member tic
const seen = await api('GET', `/parties/${slug}`, bob);
assert.strictEqual(seen.body.party.lease.holder.subject, bob.subject_id);
assert.strictEqual((await api('GET', `/parties/${slug}/desktop/ticket`, ann)).status, 404);
-- const nodeId = ids.newId('node');
++ const nodeId = ids.newId('link');
const now = Math.floor(Date.now() / 1000);
-- const nodeToken = (owner) => serviceAuth.signServiceToken({ iss: h.ISS, sub: `node:${nodeId}`, actor_type: 'node', on_behalf_of: owner,
++ const nodeToken = (owner) => serviceAuth.signServiceToken({ iss: h.ISS, sub: `link:${nodeId}`, actor_type: 'link', on_behalf_of: owner,
aud: ['openvibe.chat'], cap: [], iat: now, exp: now + 300, jti: `party_${owner}` }, h.keys.privateKey);
const wrong = await h.http('POST', `/api/chat/parties/${slug}/desktop/claim`, { token: nodeToken(ann.subject_id), body: { port: 18180 } });
assert.strictEqual(wrong.status, 404);
ddiff --git a/test/preferences.test.js b/test/preferences.test.js
iindex 6a87637..24d3f9a 100644
---- a/test/preferences.test.js
++++ b/test/preferences.test.js
@@@ -40,7 +40,7 @@ t('reading needs a signed-in person; nothing stored yet is {} at revision 0', as
assert.strictEqual(r.status, 401);
r = await h.http('GET', '/api/chat/preferences', { token: ann.token });
assert.strictEqual(r.status, 200, r.text);
-- assert.deepStrictEqual(r.body, { namespace: NS, version: 2, revision: 0, preferences: {} });
++ assert.deepStrictEqual(r.body, { namespace: NS, version: 3, revision: 0, preferences: {} });
assert.strictEqual(r.headers.get('etag'), '"0"');
assert.ok(r.headers.get('cache-control').includes('no-store'));
const tok = h.tokenRequests.find((x) => x.audience === 'openvibe.network');
ddiff --git a/test/room-ai-models.test.js b/test/room-ai-models.test.js
iindex 95a2fe0..e548b09 100644
---- a/test/room-ai-models.test.js
++++ b/test/room-ai-models.test.js
@@@ -35,8 +35,8 @@ const fakeHttp = {
if (method === 'GET') { if (!store.has(subject)) throw problem(404, 'credential.not_found'); return store.get(subject); }
if (method === 'DELETE') { if (!store.delete(subject)) throw problem(404, 'credential.not_found'); return null; }
const prev = store.get(subject);
-- if (json.provider === 'node') { // a model on the person's own computer: no key (contracts 0.220.0)
-- const row = { owner: 'chat', subject, provider: 'node', machine: json.machine, base_url: null, models: json.models, budget_usd_per_day: null, spent_usd_today: 0 };
++ if (json.provider === 'link') { // a model on the person's own computer: no key (contracts 0.220.0)
++ const row = { owner: 'chat', subject, provider: 'link', machine: json.machine, base_url: null, models: json.models, budget_usd_per_day: null, spent_usd_today: 0 };
store.set(subject, row);
return row;
}
@@@ -154,40 +154,40 @@ t('a new model relabels its members; removing the key sends them back to OpenVib
assert.strictEqual((await api('POST', '/rooms/model-night/ai-members', ann, { name: 'Echo', engine: 'own' })).body.code, 'rooms.ai_no_key', 'no key, no own engine');
});
--t('a model on your own computer: the machine and no key; the settings form comes prefilled from openvibe.bot', async () => {
++t('a model on your own computer: the machine and no key; the settings form comes prefilled from openvibe.link', async () => {
const MACHINE = 'dev_01M5YGM39VC2RRGDQPRD6D4SHD';
const cy = h.addUser('cy', { subject: ids.newId('user') });
await h.ctx.upsertUser(h.live.users.get(cy.id));
-- assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'node', model: 'llama3.1:8b' })).status, 422, 'it names the computer');
-- assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'node', machine: 'dev_nope', model: 'llama3.1:8b' })).status, 422);
-- const put = await api('PUT', '/me/ai-key', cy, { provider: 'node', machine: MACHINE, model: 'llama3.1:8b' });
++ assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'link', model: 'llama3.1:8b' })).status, 422, 'it names the computer');
++ assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'link', machine: 'dev_nope', model: 'llama3.1:8b' })).status, 422);
++ const put = await api('PUT', '/me/ai-key', cy, { provider: 'link', machine: MACHINE, model: 'llama3.1:8b' });
assert.strictEqual(put.status, 200, JSON.stringify(put.body));
-- assert.deepStrictEqual([put.body.key.provider, put.body.key.machine, put.body.key.model], ['node', MACHINE, 'llama3.1:8b']);
++ assert.deepStrictEqual([put.body.key.provider, put.body.key.machine, put.body.key.model], ['link', MACHINE, 'llama3.1:8b']);
const { label, memberLabel } = require('../server/ai/credentials');
assert.strictEqual(label(put.body.key), 'your own computer · llama3.1:8b');
assert.strictEqual(memberLabel(put.body.key, 'Cy'), 'llama3.1:8b on Cy\'s computer', 'what a room sees: whose computer, never "your own"');
assert.strictEqual(memberLabel({ provider: 'openai', model: 'gpt-4o-mini' }, 'Cy'), 'openai · gpt-4o-mini, Cy\'s key');
-- // From openvibe.bot/machines ("Use these models in OpenVibe.Chat"): the form is filled in, nothing is saved yet.
-- const page = await web('GET', `/settings?own=node&machine=${MACHINE}&model=qwen2.5:7b`, cy);
++ // From openvibe.link/machines ("Use these models in OpenVibe.Chat"): the form is filled in, nothing is saved yet.
++ const page = await web('GET', `/settings?own=link&machine=${MACHINE}&model=qwen2.5:7b`, cy);
const sect = page.text.slice(page.text.indexOf('id="oc-ai-key"'));
-- assert.ok(sect.includes('<option value="node" selected>A model on my own computer</option>'), 'own computer chosen');
++ assert.ok(sect.includes('<option value="link" selected>A model on my own computer</option>'), 'own computer chosen');
assert.ok(sect.includes(`name="machine" maxlength="40"`) && sect.includes(`value="${MACHINE}"`));
assert.ok(sect.includes('class="oc-form oc-ai-form"') && sect.includes('<label class="oc-if-node">Computer') && sect.includes('<label class="oc-not-node">API key') && sect.includes('<label class="oc-if-openai">Endpoint'), 'each field says which providers take it (chat.css hides the rest)');
assert.ok(sect.includes('value="qwen2.5:7b"'), 'the model from the link');
assert.ok(sect.includes('Save to let your AI members think with qwen2.5:7b on your own computer.'));
assert.ok(sect.includes('your own computer · llama3.1:8b') && sect.includes('nothing spent'), 'what is saved now');
// Any of their computers (contracts 0.221.0): the box, the label, what a room sees.
-- const any = await web('POST', '/settings/ai-key', cy, { provider: 'node', machine: '', machine_any: '1', model: 'llama3.1:8b' });
++ const any = await web('POST', '/settings/ai-key', cy, { provider: 'link', machine: '', machine_any: '1', model: 'llama3.1:8b' });
assert.strictEqual(any.status, 303);
const anyKey = (await api('GET', '/me/ai-key', cy)).body.key;
assert.deepStrictEqual([anyKey.machine, label(anyKey), memberLabel(anyKey, 'Cy')], ['any', 'any of your own computers · llama3.1:8b', 'llama3.1:8b on Cy\'s computers']);
const anyPage = (await web('GET', '/settings', cy)).text;
assert.ok(anyPage.includes('name="machine_any" value="1" checked') && anyPage.includes('on whichever of your computers is on, the fastest first'), 'the box, checked, and the saved line');
-- assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'node', machine: 'anyone', model: 'llama3.1:8b' })).status, 422);
-- // From openvibe.bot when the person has several computers with models: any of them, the box checked.
-- const anyLink = (await web('GET', '/settings?own=node&machine=any&model=qwen2.5:7b', cy)).text;
++ assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'link', machine: 'anyone', model: 'llama3.1:8b' })).status, 422);
++ // From openvibe.link when the person has several computers with models: any of them, the box checked.
++ const anyLink = (await web('GET', '/settings?own=link&machine=any&model=qwen2.5:7b', cy)).text;
assert.ok(anyLink.includes('think with qwen2.5:7b on whichever of your computers is on') && anyLink.includes('name="machine_any" value="1" checked'), 'any, from the link');
-- const bad = await web('GET', '/settings?own=node&machine=../x&model=<b>', cy);
++ const bad = await web('GET', '/settings?own=link&machine=../x&model=<b>', cy);
assert.ok(!bad.text.includes('Save to let your AI members'), 'a bad link fills nothing');
assert.strictEqual((await api('DELETE', '/me/ai-key', cy)).status, 200);
});
@@@ -206,7 +206,7 @@ t('Vibe thinks with your own key or computer when you choose it: every chat of y
const first = await vibeApi('POST', '', { message: 'hello there' });
assert.strictEqual(first.status, 201, first.text);
assert.strictEqual((await memberOf(first.body.room.slug)).engine, 'openvibe', 'OpenVibe\'s model by default');
-- assert.strictEqual((await api('PUT', '/me/ai-key', di, { provider: 'node', machine: MACHINE, model: 'llama3.1:8b' })).status, 200);
++ assert.strictEqual((await api('PUT', '/me/ai-key', di, { provider: 'link', machine: MACHINE, model: 'llama3.1:8b' })).status, 200);
const on = await vibeApi('PUT', '/engine', { own: true });
assert.deepStrictEqual(on.body, { own: true, label: 'llama3.1:8b on Di\'s computer' }, on.text);
assert.deepStrictEqual(await memberOf(first.body.room.slug), { engine: 'own', engine_subject: h.live.users.get(di.id).subject_id, engine_label: 'llama3.1:8b on Di\'s computer' }, 'the chats they have');
@@@ -226,15 +226,15 @@ t('Vibe thinks with your own key or computer when you choose it: every chat of y
// The settings form says so and keeps it; unchecked, Vibe goes back to OpenVibe's model.
const page = await web('GET', '/settings', di);
assert.ok(page.text.includes('name="vibe" value="1" checked') && page.text.includes('· Vibe thinks with it'), 'the box, checked');
-- const saved = await web('POST', '/settings/ai-key', di, { provider: 'node', machine: MACHINE, model: 'qwen2.5:7b' });
++ const saved = await web('POST', '/settings/ai-key', di, { provider: 'link', machine: MACHINE, model: 'qwen2.5:7b' });
assert.strictEqual(saved.status, 303);
assert.deepStrictEqual((await vibeApi('GET', '/engine')).body, { own: false, label: null }, 'unchecked: OpenVibe\'s model');
assert.ok((await web('GET', '/vibe', di)).text.includes('answers left today'), 'the free answers left again');
assert.strictEqual((await memberOf(first.body.room.slug)).engine, 'openvibe');
-- await web('POST', '/settings/ai-key', di, { provider: 'node', machine: MACHINE, model: 'qwen2.5:7b', vibe: '1' });
++ await web('POST', '/settings/ai-key', di, { provider: 'link', machine: MACHINE, model: 'qwen2.5:7b', vibe: '1' });
assert.strictEqual((await memberOf(next.body.room.slug)).engine_label, 'qwen2.5:7b on Di\'s computer', 'checked: their computer, the new model');
-- // From openvibe.bot's link the box comes checked.
-- assert.ok((await web('GET', `/settings?own=node&machine=${MACHINE}&model=phi3`, di)).text.includes('name="vibe" value="1" checked'));
++ // From openvibe.link's link the box comes checked.
++ assert.ok((await web('GET', `/settings?own=link&machine=${MACHINE}&model=phi3`, di)).text.includes('name="vibe" value="1" checked'));
// The key goes: so does Vibe's use of it.
assert.strictEqual((await api('DELETE', '/me/ai-key', di)).status, 200);
assert.strictEqual((await memberOf(first.body.room.slug)).engine, 'openvibe');
ddiff --git a/test/task-page.test.js b/test/task-page.test.js
iindex 97f01de..f310b8e 100644
---- a/test/task-page.test.js
++++ b/test/task-page.test.js
@@@ -13,7 +13,7 @@ const { plain, took } = require('../server/web/workspace-pages');
const { ICONS } = require('../server/web/icons');
assert.strictEqual(
-- plain("I couldn't do it: **no computer is open to me**.\n\n**What I did**\n1. Asked for `list_my_computers`\n- see [the machines page](https://openvibe.bot/machines)"),
++ plain("I couldn't do it: **no computer is open to me**.\n\n**What I did**\n1. Asked for `list_my_computers`\n- see [the machines page](https://openvibe.link/machines)"),
"I couldn't do it: no computer is open to me. What I did Asked for list_my_computers see the machines page");
assert.strictEqual(plain('```\ncode\n```\nafter'), 'after');
assert.strictEqual(plain('snake_case and 2*3*4 stay'), 'snake_case and 2*3*4 stay');
ddiff --git a/test/vibe-modes.test.js b/test/vibe-modes.test.js
iindex dd3a059..66249d3 100644
---- a/test/vibe-modes.test.js
++++ b/test/vibe-modes.test.js
@@@ -103,7 +103,7 @@ t('an agent that hands its person a step on its desk: Vibe asks them in the chat
const r = await vibeApi('POST', `/${slug}/ask`, ann, { message: 'Book the cheapest flight to Lisbon', mode: 'agent' });
assert.strictEqual(r.status, 201, r.text);
await new Promise((res) => setTimeout(res, 150));
-- const desk = 'https://openvibe.bot/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-0123456789';
++ const desk = 'https://openvibe.link/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-0123456789';
actorTask = { id: 'tsk_h1', state: 'running', needs_you: { seq: 7, what: 'Sign in to the airline.', url: desk } };
await workspace.poll();
await workspace.poll();
@@@ -128,7 +128,7 @@ t('an agent working on a desk: Vibe offers to watch it and take it over, once pe
const r = await vibeApi('POST', `/${slug}/ask`, bob, { message: 'Compare three laptops on the store site', mode: 'agent' });
assert.strictEqual(r.status, 201, r.text);
await new Promise((res) => setTimeout(res, 150));
-- const desk = 'https://openvibe.bot/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-9876543210';
++ const desk = 'https://openvibe.link/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-9876543210';
actorTask = { id: 'tsk_d1', state: 'running' };
await workspace.poll();
const lines = async () => (await messagesOf(slug, bob)).filter((m) => m.ai_member_id && /Your agent is working on/.test(m.message)).map((m) => m.message);
ddiff --git a/test/vibe.test.js b/test/vibe.test.js
iindex c8476e8..375a043 100644
---- a/test/vibe.test.js
++++ b/test/vibe.test.js
@@@ -152,7 +152,7 @@ t('the app page: signed out it invites a sign-in, signed in it lists the chats a
// The rail leads to the rest of the person's agent: their tasks, their computers and robots, team workspaces.
assert.ok(/href="https:\/\/openvibe\.actor\/tasks"[^>]*>[\s\S]*?<span>Tasks<\/span>/.test(signed.text), 'Tasks');
assert.ok(/href="https:\/\/openvibe\.actor\/me\/agents"[^>]*>[\s\S]*?<span>Your agents<\/span>/.test(signed.text), 'Your agents: recurring work');
-- assert.ok(signed.text.includes('<a href="https://openvibe.bot/machines">Computers &amp; robots</a>'), 'Computers & robots, at the foot');
++ assert.ok(signed.text.includes('<a href="https://openvibe.link/machines">Computers &amp; robots</a>'), 'Computers & robots, at the foot');
assert.ok(signed.text.includes('<a href="/work">Work together</a>'), 'Work together, at the foot');
const form = await web('POST', '/vibe', ann, { message: 'hello from a form' });
assert.strictEqual(form.status, 303);
@@@ -200,7 +200,7 @@ t('answers render as escaped Markdown with http(s) links only', () => {
assert.ok(!md.render('[x](https://a.com" onmouseover="alert(1))').includes('onmouseover="'));
assert.strictEqual(md.render('```\n<i>\n```'), '<pre><code>&lt;i&gt;</code></pre>');
// <https://…> (Markdown's autolink, which agents write): the address is the link; anything else in brackets stays text.
-- assert.strictEqual(md.render('Go to <https://openvibe.bot/machines>.'), '<p>Go to <a href="https://openvibe.bot/machines" rel="nofollow ugc noopener" target="_blank">https://openvibe.bot/machines</a>.</p>');
++ assert.strictEqual(md.render('Go to <https://openvibe.link/machines>.'), '<p>Go to <a href="https://openvibe.link/machines" rel="nofollow ugc noopener" target="_blank">https://openvibe.link/machines</a>.</p>');
assert.ok(!md.render('<javascript:alert(1)>').includes('href'));
// Tables and nested lists (answers are model-written Markdown, which uses both): escaped like everything else.
assert.strictEqual(md.render('| Plan | Cost |\n|---|--:|\n| Rice | $2 |\n| <b>x</b> | 1 \\| 2 |'),