openvibers/openvibe.actor Commits

OpenVibe Link rename: the machine, provider and page references (#382)

OP OpenVibers committed 4 days ago · f3021682c0 · 30 files, +103 −82

* Use Link credentials for Actor computer runs

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

* Move the Link model-prefs migration above main's newest

Main added 0055_personal_actor.sql, so the Link provider migration is 0058,
above the newest applied migration and still an expand phase.

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

* Emit Link machine URLs and pin contracts v0.373.0

* Record the contracts v0.373.0 pin in README and STATUS

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex f4fce60..74ef897 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -7,7 +7,7 @@ What changed in OpenVibe.Actor, newest first. Each site also publishes its patch
- **`actor.skill@2`: code in a sandbox** (plan HO8, ADR-060; contracts 0.343.0): a sandbox skill carries one to five
tools and its code as ordinary `.js` text resources. The registry reads the code, never runs it, and fails a tool
that reads `process.env`, starts a process, worker or `vm`, writes a file, builds code at run time, requires anything
-- but Node's safe modules, carries an encoded blob or minified code, or reaches the network outside the `net` its tool
++ but Link's safe modules, carries an encoded blob or minified code, or reaches the network outside the `net` its tool
declares; it flags any network use, timers over 5 s and loops over input. Every finding names its tool. The
cross-field rules (a tool's entry is a resource, every code resource is a tool's entry, tool names are unique) are
checked before anything is listed.
ddiff --git a/README.md b/README.md
iindex bb36979..5b114d0 100644
---- a/README.md
++++ b/README.md
@@@ -127,7 +127,7 @@ The resource index is for OpenVibe.Services on loopback and is blocked at the pu
which fits the result clip, so the tool is offered without a vision key.
- Reading, screenshots and closing run at every level; the rest wait for approval at `ask`. Below `full` the machine
refuses addresses on its own network.
-- - Approvals can come from the Link's tray or notification buttons, as well as `openvibe-node approve` (Link v0.11.0).
++ - Approvals can come from the Link's tray or notification buttons, as well as `openvibe-link approve` (Link v0.11.0).
- `computer_desk` (contracts 0.214.0, Link v0.15.0 on Linux) starts, stops or reports the machine's private desk: a
screen of its own (Xvfb with its own bus and window manager) where the agent works without touching the person's.
`computer_screen`, `computer_input` and `computer_browser` take `desk: "private"` to act there. Bot refuses that on
@@@ -196,7 +196,7 @@ Readiness requires the database and at least one available agent.
## Acceptance
--Run the suite with Node 22 (the Development block below):
++Run the suite with Link 22 (the Development block below):
- `npm test` — every [test/*.test.js](test/) in its own process on temp PGlite databases, with in-process mocks of OpenVibe.Network, OpenVibe.Events and OpenVibe.Media and stand-in model providers; nothing needs the network or a running site.
- `npm run test:pg` — the same suite against PostgreSQL instead of PGlite.
@@@ -261,7 +261,7 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md).
Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers).
<!-- versions:start -->
--- openvibe-contracts: v0.365.0
++- openvibe-contracts: v0.373.0
- openvibe-sdk: v0.123.0
- openvibe-shared: v4.173.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex 09cf162..f5f7b71 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -7,11 +7,11 @@
"runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4950,
"domain": "openvibe.actor",
-- "contracts": "openvibe-contracts v0.365.0",
++ "contracts": "openvibe-contracts v0.373.0",
"sdk": "openvibe-sdk v0.123.0",
"shared": "openvibe-shared v4.173.0",
"packages": [
-- "openvibe-contracts v0.365.0",
++ "openvibe-contracts v0.373.0",
"openvibe-sdk v0.123.0",
"openvibe-shared v4.173.0"
],
ddiff --git a/migrations/0058_actor_model_prefs_link.sql b/migrations/0058_actor_model_prefs_link.sql
nnew file mode 100644
iindex 0000000..023ff34
---- /dev/null
++++ b/migrations/0058_actor_model_prefs_link.sql
@@@ -0,0 +1,7 @@
++-- phase: expand
++-- The Link provider replaces the old paired-machine provider in existing choices.
++-- A later contract migration removes own:node from the check after all readers use Link.
++ALTER TABLE actor_model_prefs DROP CONSTRAINT IF EXISTS actor_model_prefs_choice_check;
++ALTER TABLE actor_model_prefs ADD CONSTRAINT actor_model_prefs_choice_check
++ CHECK (choice IN ('auto', 'openvibe', 'own:openai', 'own:anthropic', 'own:deepseek', 'own:google', 'own:openrouter', 'own:mistral', 'own:node', 'own:link'));
++UPDATE actor_model_prefs SET choice = 'own:link' WHERE choice = 'own:node';
ddiff --git a/package-lock.json b/package-lock.json
iindex 1f40144..a764d9c 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -15,7 +15,7 @@
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.365.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.373.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",
@@@ -1679,9 +1679,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.365.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.365.0",
-- "integrity": "sha512-J14GlNgevHVkD21d6ZWWtWjzsO5dEXMymZ3xGUj9QdykOYx+88nu5BK2yh4p9End7RaTr/fqvZ6UakGmhd6RGA==",
++ "version": "0.373.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.373.0",
++ "integrity": "sha512-ddW6yG5deFxRgS/MQLgV6pyYx8vco49Nt83DrE+m9XAPwuHxFSeSZgvZ5CznK0cjul3CMmB5NaVXA6jdEbXAfQ==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex ca0365e..4fe80ce 100644
---- a/package.json
++++ b/package.json
@@@ -28,7 +28,7 @@
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.365.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.373.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/agents/catalog.js b/server/agents/catalog.js
iindex 46126d3..e6c6af5 100644
---- a/server/agents/catalog.js
++++ b/server/agents/catalog.js
@@@ -78,7 +78,7 @@ const AGENTS = [
can: ['task:code'],
// The model use is on the person's own machine and keys: nothing for Actor to meter.
rate_card: { input_usd_per_mtok: 0, output_usd_per_mtok: 0, source: 'https://openvibe.codes/run', verified_at: VERIFIED },
-- blurb: 'Coding work goes to OpenVibe.Codes and runs on your own machine (add it on openvibe.bot/machines, then start one task on openvibe.codes/run). Name the repository; you get the summary, the changed files and the patch. The model use is on your machine\'s own keys, so Actor charges nothing for it.',
++ blurb: 'Coding work goes to OpenVibe.Codes and runs on your own machine (add it on openvibe.link/machines, then start one task on openvibe.codes/run). Name the repository; you get the summary, the changed files and the patch. The model use is on your machine\'s own keys, so Actor charges nothing for it.',
},
];
ddiff --git a/server/agents/codes.js b/server/agents/codes.js
iindex 6c12dbc..724866c 100644
---- a/server/agents/codes.js
++++ b/server/agents/codes.js
@@@ -2,7 +2,7 @@
/**
* The `codes` agent (plan T16/T17): a person's coding task goes to OpenVibe.Codes, which runs it on that person's own
-- * machine (openvibe.codes/run: an OpenVibe.Run job placed only on a machine of theirs, added on openvibe.bot/machines).
++ * machine (openvibe.codes/run: an OpenVibe.Run job placed only on a machine of theirs, added on openvibe.link/machines).
* Actor calls Codes' task API (Contracts 0.162 codes.task.*) with its own service token (audience openvibe.codes) and
* X-OV-Subject naming the person; Codes shows Actor only the tasks Actor started for that person. The model use is on
* the machine's own keys, so Actor meters nothing for it.
@@@ -92,7 +92,7 @@ function createCodes({ config, fetchImpl = globalThis.fetch, tokens: tokensOverr
await emit({ step: 'tool_call', tool: 'codes.task.start', input: repo });
const started = await call('POST', '/tasks', subject, { body: { repo, prompt: String(task).slice(0, 8000), ...(taskId ? { idempotency_key: String(taskId).slice(0, 128) } : {}) }, signal });
if (started.status === 409 && started.data && started.data.code === 'codes.no_project') {
-- throw new ProviderError(`Before Actor can send coding work to your machine: add the machine at https://openvibe.bot/machines, then start one task yourself at ${pages}/run (it picks the project your tasks run in).`, { code: 'actor.agent.unavailable' });
++ throw new ProviderError(`Before Actor can send coding work to your machine: add the machine at https://openvibe.link/machines, then start one task yourself at ${pages}/run (it picks the project your tasks run in).`, { code: 'actor.agent.unavailable' });
}
if (!started.ok || !started.data || !started.data.task) throw new ProviderError(why(started), { code: 'actor.agent.failed' });
if (!TASK_ID.test(String(started.data.task.id))) throw new ProviderError('OpenVibe.Codes answered with no task id', { code: 'actor.agent.failed' });
@@@ -122,7 +122,7 @@ function createCodes({ config, fetchImpl = globalThis.fetch, tokens: tokensOverr
if (!TERMINAL.has(t.state)) {
return { text: `${LABEL[t.state] || t.state}, and it is not done yet. Follow it live, and see the change when it ends: [the task on OpenVibe.Codes](${t.url})`, sources, model: 'openvibe-codes' };
}
-- if (t.state === 'expired') throw new ProviderError(`No machine of yours took the task in time. Is it online at https://openvibe.bot/machines, with the Codes job runner (install it with --worker)? The task: ${t.url}`, { code: 'actor.agent.failed' });
++ if (t.state === 'expired') throw new ProviderError(`No machine of yours took the task in time. Is it online at https://openvibe.link/machines, with the Codes job runner (install it with --worker)? The task: ${t.url}`, { code: 'actor.agent.failed' });
if (t.state !== 'succeeded') throw new ProviderError(`The task ${t.state === 'cancelled' ? 'was cancelled' : 'failed'}${t.error ? `: ${t.error}` : ''}. ${t.url}`, { code: 'actor.agent.failed' });
if (!t.result || !t.result.ok) throw new ProviderError(`The agents on your machine did not finish${t.result && t.result.error ? `: ${t.result.error}` : ''}. ${t.url}`, { code: 'actor.agent.failed' });
return { text: answerOf(t), sources, model: (t.result.agents || [])[0] || 'openvibe-codes' };
ddiff --git a/server/agents/computers.js b/server/agents/computers.js
iindex bc9e84a..4aa88b6 100644
---- a/server/agents/computers.js
++++ b/server/agents/computers.js
@@@ -55,7 +55,7 @@ const BROWSER_KEYS = new Set(['Enter', 'Tab', 'Escape', 'Backspace', 'Delete', '
function createComputers({ config, fetchImpl = globalThis.fetch, tokens: tokensOverride = null }) {
const base = config.services.botUrl;
-- const pages = config.services.botPublicUrl || base;
++ const pages = config.services.linkPublicUrl || 'https://openvibe.link';
const enabled = !!(base && (tokensOverride || config.oauth.clientSecret));
const tokens = !enabled ? null : tokensOverride || serviceAuth.createTokenClient({
tokenUrl: `${config.networkInternalUrl}/oauth/token`, clientId: config.oauth.clientId, clientSecret: config.oauth.clientSecret,
ddiff --git a/server/config.js b/server/config.js
iindex c54fc34..fa6b90c 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -170,8 +170,10 @@ function load(env = process.env) {
// OpenVibe.Media: where a sandbox skill's code bytes are stored, private to Actor's tenant (ADR-060).
// Empty means a sandbox skill cannot be published (a prompt skill is unaffected).
mediaUrl: trim(env.ACTOR_MEDIA_URL != null ? env.ACTOR_MEDIA_URL : (selfHosted ? '' : 'http://127.0.0.1:4100')),
-- // Where "Run it on my machine" goes: Link's setup, which turns the skill runner on a person's Node.
-- linkUrl: trim(env.ACTOR_LINK_URL || 'https://openvibe.bot/link'),
++ // Where "Run it on my machine" goes: Link's setup, which turns the skill runner on a person's Link machine.
++ linkUrl: trim(env.ACTOR_LINK_URL || 'https://openvibe.link'),
++ // Where a person's Link machines and desks live (the panel's /machines pages).
++ linkPublicUrl: trim(env.ACTOR_LINK_PUBLIC_URL || 'https://openvibe.link'),
// OpenVibe.Space: the person's own spaces, which their agent builds (plan 2026-10-22 §Z stage Z1).
// Empty turns the space tools off.
spaceUrl: trim(env.ACTOR_SPACE_URL != null ? env.ACTOR_SPACE_URL : (selfHosted ? '' : 'http://127.0.0.1:4940')),
ddiff --git a/server/http/chat-pages.js b/server/http/chat-pages.js
iindex 196bd6f..1c98305 100644
---- a/server/http/chat-pages.js
++++ b/server/http/chat-pages.js
@@@ -37,10 +37,10 @@ const canvases = require('../canvases');
// The model a person's tasks run on (plan L55 C3; the owner, 11-01: "use your own API keys, or OpenVibe's"). Auto is
// OpenVibe's own; "own:<provider>" is the person's key, sealed in OpenVibe.AI, which Actor never sees.
--const PROVIDER_NAMES = { openai: 'OpenAI', anthropic: 'Anthropic', deepseek: 'DeepSeek', google: 'Google Gemini', openrouter: 'OpenRouter', mistral: 'Mistral', node: 'computer' };
++const PROVIDER_NAMES = { openai: 'OpenAI', anthropic: 'Anthropic', deepseek: 'DeepSeek', google: 'Google Gemini', openrouter: 'OpenRouter', mistral: 'Mistral', link: 'computer' };
const AI_SITE = 'https://ai.openvibe.services';
const allowUrl = (provider) => `${AI_SITE}/you?allow=actor&provider=${encodeURIComponent(provider)}`;
--const modelLabel = (m) => (m && m.provider === 'node' ? 'Your computer' : m && m.provider ? `Your ${PROVIDER_NAMES[m.provider] || m.provider} key` : 'Auto');
++const modelLabel = (m) => (m && m.provider === 'link' ? 'Your computer' : m && m.provider ? `Your ${PROVIDER_NAMES[m.provider] || m.provider} key` : 'Auto');
const glyphs = require('openvibe-shared/glyphs');
// One conversation UI for the network (plan L55 C4): the sidebar, the turns, the work, the answer and the composer.
const thread = require('openvibe-shared/thread');
@@@ -518,7 +518,7 @@ ${shown.length ? html`<div class="ct-trig-runs">${shown}</div>` : ''}`;
<p class="ccm-head">Your own key</p>
<ul class="ccm-list">${modelPrefs.KEY_PROVIDERS.map((p) => html`<li class="ccm-own" data-ccm-provider="${p}">${item(`own:${p}`, PROVIDER_NAMES[p] || p, 'Your key, kept in OpenVibe.AI')}<a class="ccm-setup" href="${allowUrl(p)}" target="_blank" rel="noopener" data-ccm-setup>Set up</a></li>`)}</ul>
<p class="ccm-head">Your computer</p>
--<ul class="ccm-list"><li class="ccm-own" data-ccm-provider="node">${item('own:node', 'Your computer', 'A model you run at home, through OpenVibe Link. Nothing billed.')}<a class="ccm-setup" href="${allowUrl('node')}" target="_blank" rel="noopener" data-ccm-setup>Set up</a></li></ul>
++<ul class="ccm-list"><li class="ccm-own" data-ccm-provider="node">${item('own:link', 'Your computer', 'A model you run at home, through OpenVibe Link. Nothing billed.')}<a class="ccm-setup" href="${allowUrl('link')}" target="_blank" rel="noopener" data-ccm-setup>Set up</a></li></ul>
<p class="ccm-fine" data-ccm-fine>A key you add stays sealed in OpenVibe.AI: Actor never sees it, and only uses it once you allow Actor there. <a href="/chat/model">All settings</a></p>
</div></details>`;
}
@@@ -666,7 +666,7 @@ ${radio('auto', 'Auto, OpenVibe\'s', 'Actor picks a model for each step, on Open
<p class="cm-head">Your own key</p>
${modelPrefs.KEY_PROVIDERS.map((p) => radio(`own:${p}`, PROVIDER_NAMES[p] || p, 'Billed by the provider to you, not counted against Actor\'s allowance.', `<a class="cm-setup" href="${allowUrl(p).replace(/&/g, '&amp;')}" target="_blank" rel="noopener">Add or allow the key</a>`))}
<p class="cm-head">Your computer</p>
--${radio('own:node', 'Your computer', 'A model you run at home (Ollama, LM Studio or llama.cpp) through OpenVibe Link v0.36.0 or newer: nothing is billed, and it answers only while that computer is on. Usually slower than a cloud model.', `<a class="cm-setup" href="${allowUrl('node').replace(/&/g, '&amp;')}" target="_blank" rel="noopener">Add your computer's model and allow Actor</a>`)}
++${radio('own:link', 'Your computer', 'A model you run at home (Ollama, LM Studio or llama.cpp) through OpenVibe Link v0.36.0 or newer: nothing is billed, and it answers only while that computer is on. Usually slower than a cloud model.', `<a class="cm-setup" href="${allowUrl('link').replace(/&/g, '&amp;')}" target="_blank" rel="noopener">Add your computer's model and allow Actor</a>`)}
${modelField(aiModels, model.model || '')}
<button class="cm-save" type="submit">Save</button>
</form>
@@@ -675,7 +675,7 @@ ${modelField(aiModels, model.model || '')}
${runsSaved ? html`<p class="cm-ok" role="status">Saved. Your next task runs on ${model.runs_on === 'computer' ? 'your computer' : 'OpenVibe'}.</p>` : ''}
<form class="cm-form" method="post" action="/chat/runs-on">
<label class="cm-opt"><input type="radio" name="runs_on" value="openvibe"${model.runs_on !== 'computer' ? raw(' checked') : ''}><span><b>OpenVibe</b><small>Quick, from anywhere, on the model you chose above.</small></span></label>
--<label class="cm-opt"><input type="radio" name="runs_on" value="computer"${model.runs_on === 'computer' ? raw(' checked') : ''}><span><b>My computer</b><small>Nothing is billed for the model, and it answers only while your computer is on. Needs OpenVibe Link v0.36.0 or newer with a local model (Ollama, LM Studio or llama.cpp). Usually slower.</small></span><span class="cm-setups"><a class="cm-setup" href="${allowUrl('node')}" target="_blank" rel="noopener">Add your computer's model</a><a class="cm-setup" href="https://openvibe.bot/machines" target="_blank" rel="noopener">Add your machine for coding</a></span></label>
++<label class="cm-opt"><input type="radio" name="runs_on" value="computer"${model.runs_on === 'computer' ? raw(' checked') : ''}><span><b>My computer</b><small>Nothing is billed for the model, and it answers only while your computer is on. Needs OpenVibe Link v0.36.0 or newer with a local model (Ollama, LM Studio or llama.cpp). Usually slower.</small></span><span class="cm-setups"><a class="cm-setup" href="${allowUrl('link')}" target="_blank" rel="noopener">Add your computer's model</a><a class="cm-setup" href="https://openvibe.link/machines" target="_blank" rel="noopener">Add your machine for coding</a></span></label>
<button class="cm-save" type="submit">Save</button>
</form></main>`,
});
ddiff --git a/server/http/console-pages.js b/server/http/console-pages.js
iindex bab5c15..2960bbe 100644
---- a/server/http/console-pages.js
++++ b/server/http/console-pages.js
@@@ -42,6 +42,7 @@ function createConsolePages(ctx, { robots = null, computers = null } = {}) {
const page = (req, res, o, status = 200) => send(res, status, { viewer: req.viewer, config, path: req.originalUrl, css: ['css/console.css'], ...o });
const signedIn = (req) => req.viewer && req.viewer.kind === 'user' && req.viewer.subject;
const pages = config.services.botPublicUrl || 'https://openvibe.bot';
++ const linkPages = config.services.linkPublicUrl || 'https://openvibe.link';
/** One Bot read, bounded so a slow Bot never holds the page; { ok, value } or { ok: false, why }. */
async function fromBot(fn) {
@@@ -114,13 +115,13 @@ ${today ? html`<p class="cn-note" data-allowance>${leftLine(tier, today)}</p>` :
function computersCard(result) {
const body = !bot.computers.enabled ? html`<p class="cn-empty">Not connected to OpenVibe.Bot on this server.</p>`
: !result.ok ? html`<p class="cn-empty">${result.why}</p>`
-- : !result.value.computers.length ? html`<p class="cn-empty">No computer is open to your agent. Add one on <a href="${pages}/machines">openvibe.bot/machines</a> with <code>--computer ask</code>, then turn on “Let my agent use this computer”.</p>`
++ : !result.value.computers.length ? html`<p class="cn-empty">No computer is open to your agent. Add one on <a href="${linkPages}/machines">openvibe.link/machines</a> with <code>--computer ask</code>, then turn on “Let my agent use this computer”.</p>`
: html`<ul class="cn-list">${result.value.computers.map((c) => html`<li><span class="cn-dot${c.online ? ' on' : ''}" aria-hidden="true"></span><b>${c.name || c.id}</b>
<span class="cn-meta">${c.online ? 'Online' : 'Offline'} · ${LEVEL_TEXT[c.level] || 'On'}${c.can.length ? html` · ${c.can.join(', ')}` : ''}</span>
${c.waiting_for_approval ? html`<span class="pill pill-running">${String(c.waiting_for_approval)} waiting on the machine</span>` : ''}
--${c.online && c.can.includes('screen') ? html`<a class="cn-link" href="${pages}/machines/${encodeURIComponent(c.id)}/screen">Watch the screen</a>` : ''}</li>`)}</ul>`;
++${c.online && c.can.includes('screen') ? html`<a class="cn-link" href="${linkPages}/machines/${encodeURIComponent(c.id)}/screen">Watch the screen</a>` : ''}</li>`)}</ul>`;
return html`<section class="cn-card" aria-labelledby="cn-computers"><h2 id="cn-computers">Your computers</h2>${body}
--<p class="cn-more"><a href="${pages}/machines">Machines on OpenVibe.Bot</a></p></section>`;
++<p class="cn-more"><a href="${linkPages}/machines">Machines on OpenVibe Link</a></p></section>`;
}
function robotsCard(result) {
@@@ -144,7 +145,7 @@ ${c.online && c.can.includes('screen') ? html`<a class="cn-link" href="${pages}/
['Waiting for you', 'What an agent asks before it acts. Approve or decline here, in Link\'s tray, or from a notification.', null, false],
['Today', `How many of your ${String(tier.tasksPerDay)} free tasks a day you have used and what they cost, with the last 7 days.`, null, false],
['Your agents', 'Tasks you set up once, with a memory and a schedule.', ['/agents', 'See the agents'], false],
-- ['Your computers', 'Your own machines running jobs for your agents.', [`${pages}/machines`, 'Machines on OpenVibe.Bot'], false],
++ ['Your computers', 'Your own machines running jobs for your agents.', [`${linkPages}/machines`, 'Machines on OpenVibe Link'], false],
['Your robots', 'Robots your agents may drive, with their battery and e-stop.', [`${pages}/robots`, 'Robots on OpenVibe.Bot'], false],
];
return html`<div class="cn-grid cn-preview" aria-label="What the console shows">${cards.map(([title, what, link, wide], i) => html`<section class="cn-card${wide ? ' cn-wide' : ''}" aria-labelledby="cn-p${String(i)}"><h2 id="cn-p${String(i)}">${title}</h2>
@@@ -189,7 +190,7 @@ ${commandBox(prefill(req), today)}
${live.length ? html`<ul class="cn-tasks">${live.map(taskRow)}</ul><p class="cn-note">This page refreshes every 10 seconds while something runs.</p>` : html`<p class="cn-empty">Nothing is running.</p>`}</section>
<section class="cn-card${nWaiting ? ' cn-attn' : ''}" aria-labelledby="cn-waiting"><h2 id="cn-waiting">Waiting for you${nWaiting ? html` <span class="cn-count">${String(nWaiting)}</span>` : ''}</h2>
${waiting.length ? html`<ul class="cn-list">${waiting.slice(0, 4).map((a) => html`<li><a href="/me/approvals/${a.id}"><b>${clip(a.summary, 80)}</b></a><span class="cn-meta">Asked ${time(a.created_at)}</span></li>`)}</ul>` : ''}
--${onMachines ? html`<p class="cn-note">${String(onMachines)} on your computers: answer them there, in the Link's tray, with a notification's Approve button, or with <code>openvibe-node approvals</code>.</p>` : ''}
++${onMachines ? html`<p class="cn-note">${String(onMachines)} on your computers: answer them there, in the Link's tray, with a notification's Approve button, or with <code>openvibe-link approvals</code>.</p>` : ''}
${nWaiting ? '' : html`<p class="cn-empty">Nothing waits for you.</p>`}
<p class="cn-more"><a href="/me/approvals">All approvals</a></p></section>
<section class="cn-card" aria-labelledby="cn-today"><h2 id="cn-today">Today</h2>
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex a74875f..b90c698 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -502,8 +502,8 @@ ${today ? html`<p class="task-allowance muted small">${leftLine(tier, today)}</p
[html`Tasks a watch hands over when its condition fires (<a href="https://openvibe.watch">OpenVibe.Watch</a>)`, 'Live: the task is yours, with your allowance; Watch sees only the tasks it started.'],
[html`Your own agents (<a href="/me/agents">Your agents</a>): instructions, memory you can read and edit, a schedule (hourly, daily or weekly), a budget per run and a daily cap`, 'Live.'],
[html`Your own robots on <a href="https://openvibe.bot">OpenVibe.Bot</a>: see them and drive the ones you opened to your agent, through Bot's limits and e-stop`, 'Live: turn on "Let my OpenVibe.Actor agent drive this robot" on the robot\'s panel.'],
-- [html`Coding tasks on your own machine, through <a href="https://openvibe.codes/run">OpenVibe.Codes</a>: name the repository and what to change, and get the summary, the files and the patch`, html`Live: add the machine on <a href="https://openvibe.bot/machines">openvibe.bot/machines</a> and start one task on openvibe.codes/run first. The model use is on your machine's own keys.`],
-- [html`Your own computers, through <a href="https://openvibe.bot/docs/computers">OpenVibe.Bot</a>: list and read files, run programs and write files there, as you, at the level you set on the computer`, html`Live: install OpenVibe Link with <code>--computer ask</code> and turn on "Let my agent use this computer" on <a href="https://openvibe.bot/machines">openvibe.bot/machines</a>. At <code>ask</code>, you approve each run or write on the computer.`],
++ [html`Coding tasks on your own machine, through <a href="https://openvibe.codes/run">OpenVibe.Codes</a>: name the repository and what to change, and get the summary, the files and the patch`, html`Live: add the machine on <a href="https://openvibe.link/machines">openvibe.link/machines</a> and start one task on openvibe.codes/run first. The model use is on your machine's own keys.`],
++ [html`Your own computers, through <a href="https://openvibe.bot/docs/computers">OpenVibe.Bot</a>: list and read files, run programs and write files there, as you, at the level you set on the computer`, html`Live: install OpenVibe Link with <code>--computer ask</code> and turn on "Let my agent use this computer" on <a href="https://openvibe.link/machines">openvibe.link/machines</a>. At <code>ask</code>, you approve each run or write on the computer.`],
[html`<a href="/console">The Console</a>: what runs now, what waits for you, today's spend, your agents, computers and robots, and one box to start a task`, 'Live, for signed-in people.'],
[html`Ask in chat: write <code>@actor</code> and your question in OpenVibe's global chat or your own channel, and the answer comes back in the room`, 'Live: public tools only (never your computers, robots or code), 5 an hour, on your free tier.'],
[html`Run your own: Actor on your server with your own model (<a href="https://github.com/OpenVibers/OpenVibe.Actor/blob/main/docs/self-host.md">how</a>), signed in with your password, data on your disk`, 'Live: Docker or plain Node, Ollama or any OpenAI-compatible model.'],
@@@ -1128,7 +1128,7 @@ ${!shared ? relatedRunsBlock(row, { forks, others }) : ''}
${open ? planCard(plan, { editable: !shared && (live || (waiting && !(question && question.kind === 'acting'))), taskId: row.id }) : ''}
${!shared && open ? html`<section class="desk-card" id="desk-card" aria-live="polite"${desk ? '' : raw(' hidden')}>
<p><strong>Your agent is working on a desk of its own</strong><span id="desk-kind">${desk && desk.kind === 'browser' ? ' (a browser)' : ''}</span>. Watch it live, and take it over whenever you like: while you use it, the agent waits; when you stop, it goes on.</p>
--<a class="sc-btn sc-primary" id="desk-link" href="${desk ? desk.url : botPages}" rel="noopener" target="_blank">Watch and take over</a>
++<a class="sc-btn sc-primary" id="desk-link" href="${desk ? desk.url : linkPages}" rel="noopener" target="_blank">Watch and take over</a>
</section>` : ''}
${open || look ? html`<figure class="last-look" id="last-look"${look ? '' : raw(' hidden')}>
<a id="last-look-link" href="${look ? `/tasks/${row.id}/look?at=${look.at}` : '#'}" target="_blank" rel="noopener" title="Open it full size"><img id="last-look-img" src="${look ? `/tasks/${row.id}/look?at=${look.at}` : ''}" alt="The screen your agent last looked at"${look && look.width ? raw(` width="${Number(look.width)}" height="${Number(look.height)}"`) : ''} loading="lazy"></a>
@@@ -1348,10 +1348,11 @@ ${a.refused ? html`<details class="refused"><summary>See the answer that didn't
</section>`;
}
-- // A step's links to the person's own desk on OpenVibe.Bot (Desks plan D4: Watch, Take over) are made clickable;
++ // A step's links to the person's own desk on OpenVibe Link (Desks plan D4: Watch, Take over) are made clickable;
// every other address in a step stays text (a model writes steps too, and its links are never vouched for).
const botPages = String((config.services && config.services.botPublicUrl) || 'https://openvibe.bot').replace(/\/+$/, '');
-- const deskLink = new RegExp(`${botPages.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/machines/dev_[0-9A-HJKMNP-TV-Z]{26}/screen(?:\\?desk=[a-z0-9-]{1,40})?`, 'g');
++ const linkPages = String((config.services && config.services.linkPublicUrl) || 'https://openvibe.link').replace(/\/+$/, '');
++ const deskLink = new RegExp(`${linkPages.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/machines/dev_[0-9A-HJKMNP-TV-Z]{26}/screen(?:\\?desk=[a-z0-9-]{1,40})?`, 'g');
function withDeskLinks(text) {
const t = String(text || '');
const parts = [];
ddiff --git a/server/runtime/model-prefs.js b/server/runtime/model-prefs.js
iindex 9cc5ed0..0b8112a 100644
---- a/server/runtime/model-prefs.js
++++ b/server/runtime/model-prefs.js
@@@ -7,7 +7,7 @@
* auto Actor decides, as today (the operator's tiers and keys)
* openvibe OpenVibe's own keys, through OpenVibe.AI (the gateway: its routing, quota and cost)
* own:<provider> the person's OWN key for that provider, sealed in OpenVibe.AI's vault
-- * own:node a model on the person's own computer (OpenVibe.AI's "Your computer": their OpenVibe Link, through
++ * own:link a model on the person's own computer (OpenVibe.AI's "Your computer": their OpenVibe Link, through
* OpenVibe.Bot; no key, nothing billed), used only after they allow Actor there
*
* The provider is one of the six OpenVibe.AI knows a person's key for. The model id is optional: without one,
@@@ -22,8 +22,8 @@ const CHOICES = ['auto', 'openvibe'];
// OpenVibe.Codes (the codes agent), for every turn but robot and computer-control tasks.
const RUNS_ON = ['openvibe', 'computer'];
const KEY_PROVIDERS = ['openai', 'anthropic', 'deepseek', 'google', 'openrouter', 'mistral'];
--// Plus their own computer (plan L55 C3): node is OpenVibe.AI's name for it.
--const OWN_PROVIDERS = [...KEY_PROVIDERS, 'node'];
++// Plus their own computer (plan L55 C3): Link is OpenVibe.AI's name for it.
++const OWN_PROVIDERS = [...KEY_PROVIDERS, 'link'];
// A model id: letters, digits and the marks providers use (gpt-5.1, claude-sonnet-4-5, deepseek/flash, org/model).
const MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,119}$/;
// An exact id (D3: org/model[:quant]@revision), accepted only while the AI catalog is on: `@` and `_` are marks an
@@@ -89,10 +89,10 @@ async function runsOnFor(s, requester) {
return row && RUNS_ON.includes(row.runs_on) ? row.runs_on : 'openvibe';
}
--/** The model choice "Runs on: my computer" means: the person's own computer, through OpenVibe.AI's node credential. */
++/** The model choice "Runs on: my computer" means: the person's own computer, through OpenVibe.AI's Link credential. */
function computerChoice(requester) {
const subject = String(requester || '').startsWith('user:') ? requester.slice(5) : null;
-- return subject ? { subject, provider: 'node', model: null } : null;
++ return subject ? { subject, provider: 'link', model: null } : null;
}
/** Set where the person's tasks run. Returns { runs_on } or { error }; the model choice is left as it is. */
ddiff --git a/server/skills/run.js b/server/skills/run.js
iindex cca8527..bf6362f 100644
---- a/server/skills/run.js
++++ b/server/skills/run.js
@@@ -96,7 +96,7 @@ function createSkillRun({ config, fetchImpl = globalThis.fetch, tokens: tokensOv
const services = (config && config.services) || {};
const skills = (config && config.skills) || {};
const base = String(services.runUrl || '').replace(/\/+$/, '');
-- const link = String(services.linkUrl || 'https://openvibe.bot/link');
++ const link = String(services.linkUrl || 'https://openvibe.link');
const enabled = !!(base && (tokensOverride || (config.oauth && config.oauth.clientSecret)));
const tokens = !enabled ? null : tokensOverride || serviceAuth.createTokenClient({
tokenUrl: `${config.networkInternalUrl}/oauth/token`,
ddiff --git a/server/skills/scan.js b/server/skills/scan.js
iindex d5be653..9b3ad11 100644
---- a/server/skills/scan.js
++++ b/server/skills/scan.js
@@@ -197,7 +197,7 @@ function scanCode(resource, tool) {
}
const badModules = new Set();
for (const m of code.matchAll(REQUIRE)) {
-- const name = String(m[2] || '').replace(/^node:/, '');
++ const name = String(m[2] || '').replace(/^link:/, '');
if (!SAFE_REQUIRE.has(name)) badModules.add(name);
}
if (badModules.size) {
ddiff --git a/server/tasks/engine.js b/server/tasks/engine.js
iindex c59542f..67852fc 100644
---- a/server/tasks/engine.js
++++ b/server/tasks/engine.js
@@@ -537,7 +537,7 @@ function createEngine({ config, s, adapters, stream, webhooks = null, now = () =
if (dayRun) choice = await day.modelChoice(id);
// "Runs on: my computer": the model on the person's computer, whatever else they chose above it (a
// model id they gave for their computer is kept).
-- if (runsOn === 'computer' && !(choice && choice.provider === 'node')) choice = modelPrefs.computerChoice(row.requester);
++ if (runsOn === 'computer' && !(choice && choice.provider === 'link')) choice = modelPrefs.computerChoice(row.requester);
// The person the task belongs to rides on every router call (on_behalf_of): their allowance pays,
// even when they chose no key. A task with no person (a watch, a webhook) names nobody, and AI
// serves it from free or untolled capacity only.
@@@ -745,7 +745,7 @@ function createEngine({ config, s, adapters, stream, webhooks = null, now = () =
if (!placement.selected) {
if (attempt > 1) break;
const why = pinnedAgent === 'codes' && !row.agent
-- ? { code: 'actor.no_agent', detail: 'Your coding tasks run on your own machine (Runs on: my computer) through OpenVibe.Codes, and it is not connected here. Add your machine on https://openvibe.bot/machines, or switch Runs on back to OpenVibe at https://openvibe.actor/chat/model.' }
++ ? { code: 'actor.no_agent', detail: 'Your coding tasks run on your own machine (Runs on: my computer) through OpenVibe.Codes, and it is not connected here. Add your machine on https://openvibe.link/machines, or switch Runs on back to OpenVibe at https://openvibe.actor/chat/model.' }
: whyNone(placement, { cls, mode: row.mode, budgetUsd: budgetLeft() });
return await setState(id, 'failed', { error_code: why.code, error_detail: why.detail, finished_at: iso() });
}
ddiff --git a/skills/README.md b/skills/README.md
iindex d395ba8..7edd203 100644
---- a/skills/README.md
++++ b/skills/README.md
@@@ -48,7 +48,7 @@ A sandbox skill's `.js` resources are read by the code rules instead (`code.env`
`code.vm`, `code.worker`, `code.fs_write`, `code.network`, `code.encoded`, `code.minified`, `code.require`,
`code.timer`, `code.loop`): the registry reads the code, never runs it, and every finding names the tool whose entry
runs that code. The scan fails a tool that reads the environment, starts a process or worker, loads `vm`, writes a
--file, builds code at run time, requires anything but Node's safe modules, carries an encoded blob or minified code, or
++file, builds code at run time, requires anything but Link's safe modules, carries an encoded blob or minified code, or
reaches the network outside what its `net` declares; it flags any network use, timers over 5 s and loops over input.
A sentence that says "never", "not" or "without" says the opposite, so no rule fires on it: the scan reads what a
ddiff --git a/test/chat-live-desk.test.js b/test/chat-live-desk.test.js
iindex 042f1b4..c29c03f 100644
---- a/test/chat-live-desk.test.js
++++ b/test/chat-live-desk.test.js
@@@ -28,20 +28,20 @@ const MACHINE = 'dev_01JZ0000000000000000000000';
const deskEvent = (watch, running = true) => store.appendEvent(s, id, { step: 'tool_result', tool: 'computer_desk', is_error: false, text: JSON.stringify({ watch, running, kind: 'browser' }) });
await check('Bot\'s screen page address names the machine and the desk', async () => {
-- assert.deepStrictEqual(store.deskPlace(`https://openvibe.bot/machines/${MACHINE}/screen?desk=desk-trains`), { machine: MACHINE, desk: 'desk-trains' });
-- assert.deepStrictEqual(store.deskPlace(`https://openvibe.bot/machines/${MACHINE}/screen`), { machine: MACHINE, desk: 'main' }, 'no desk named is the machine\'s own screen');
++ assert.deepStrictEqual(store.deskPlace(`https://openvibe.link/machines/${MACHINE}/screen?desk=desk-trains`), { machine: MACHINE, desk: 'desk-trains' });
++ assert.deepStrictEqual(store.deskPlace(`https://openvibe.link/machines/${MACHINE}/screen`), { machine: MACHINE, desk: 'main' }, 'no desk named is the machine\'s own screen');
assert.strictEqual(store.deskPlace('https://example.com/elsewhere'), null);
});
await check('a working turn on an agent desk offers the live desk; the owner\'s screen does not', async () => {
await s.db.query("UPDATE tasks SET state = 'running' WHERE id = $1", [id]);
-- await deskEvent(`https://openvibe.bot/machines/${MACHINE}/screen?desk=desk-trains`);
++ await deskEvent(`https://openvibe.link/machines/${MACHINE}/screen?desk=desk-trains`);
let page = await t.get(`/chat/${id}`, { as: kim });
assert.match(page.text, new RegExp(`data-watch="/chat/${id}/watch"`), 'the figure says where to ask');
-- await deskEvent(`https://openvibe.bot/machines/${MACHINE}/screen`);
++ await deskEvent(`https://openvibe.link/machines/${MACHINE}/screen`);
page = await t.get(`/chat/${id}`, { as: kim });
assert.ok(!page.text.includes('data-watch='), 'the owner\'s own screen is never framed');
-- await deskEvent(`https://openvibe.bot/machines/${MACHINE}/screen?desk=desk-trains`);
++ await deskEvent(`https://openvibe.link/machines/${MACHINE}/screen?desk=desk-trains`);
});
await check('only the requester, from openvibe.actor, while it works, gets the link; it is reused, not remade', async () => {
@@@ -63,7 +63,7 @@ const MACHINE = 'dev_01JZ0000000000000000000000';
const other = (await t.get('/api/v1/tasks', { as: kim, json: { task: 'And back on Sunday' }, headers: SAME })).json().id;
await t.waitFor(other);
await s.db.query("UPDATE tasks SET state = 'running' WHERE id = $1", [other]);
-- await store.appendEvent(s, other, { step: 'tool_result', tool: 'computer_desk', is_error: false, text: JSON.stringify({ watch: `https://openvibe.bot/machines/${MACHINE}/screen?desk=private`, running: true }) });
++ await store.appendEvent(s, other, { step: 'tool_result', tool: 'computer_desk', is_error: false, text: JSON.stringify({ watch: `https://openvibe.link/machines/${MACHINE}/screen?desk=private`, running: true }) });
answer = () => ({ error: 'capability.denied', code: 'capability.denied' });
const no = await t.get(`/chat/${other}/watch`, { as: kim, method: 'POST', headers: SAME });
assert.strictEqual(no.status, 502);
ddiff --git a/test/chat-model.test.js b/test/chat-model.test.js
iindex c4c3359..9534500 100644
---- a/test/chat-model.test.js
++++ b/test/chat-model.test.js
@@@ -28,8 +28,8 @@ const SAME = { 'sec-fetch-site': 'same-origin' };
assert.ok(page.text.includes('href="/chat/model"'), 'the plain form, for no script');
// Their own computer (L55 C3): its own group, set up and allowed at OpenVibe.AI.
assert.ok(page.text.includes('<p class="ccm-head">Your computer</p>'));
-- assert.ok(page.text.includes('data-ccm-choice="own:node" aria-pressed="false"'));
-- assert.ok(page.text.includes('https://ai.openvibe.services/you?allow=actor&amp;provider=node'));
++ assert.ok(page.text.includes('data-ccm-choice="own:link" aria-pressed="false"'));
++ assert.ok(page.text.includes('https://ai.openvibe.services/you?allow=actor&amp;provider=link'));
});
await check('a tap saves through the API from the page; the chip says the person\'s own key after', async () => {
@@@ -57,11 +57,11 @@ const SAME = { 'sec-fetch-site': 'same-origin' };
const bad = await t.get('/chat/model', { as: kim, form: { choice: 'own:openai', model: 'bad model!' }, headers: SAME });
assert.match(bad.headers.get('location'), /\/chat\/model\?error=/);
assert.strictEqual((await t.get('/chat/model')).status, 303, 'signed out: sign in first');
-- const pc = await t.get('/chat/model', { as: kim, form: { choice: 'own:node' }, headers: SAME });
++ const pc = await t.get('/chat/model', { as: kim, form: { choice: 'own:link' }, headers: SAME });
assert.strictEqual(pc.status, 303);
const pcPage = await t.get('/chat/model?saved=1', { as: kim });
assert.match(pcPage.text, /Saved\. Your next task runs on Your computer\./);
-- assert.match(pcPage.text, /<input type="radio" name="choice" value="own:node" checked>/);
++ assert.match(pcPage.text, /<input type="radio" name="choice" value="own:link" checked>/);
assert.match((await t.get('/chat', { as: kim })).text, /<span data-ccm-label>Your computer<\/span>/);
});
ddiff --git a/test/codes.test.js b/test/codes.test.js
iindex 43de824..d7f73ec 100644
---- a/test/codes.test.js
++++ b/test/codes.test.js
@@@ -71,7 +71,7 @@ const run = (codes, extra = {}) => {
await assert.rejects(codes.adapter({ task: 'https://github.com/a/b fix', requester: 'ip:1.2.3.4', emit: async () => {} }), /signed in/);
await assert.rejects(codes.adapter({ task: 'fix my code', requester: `user:${ME}`, emit: async () => {} }), (e) => e.code === 'actor.task.no_repo');
const none = fakeCodes({ start: 409, startBody: { code: 'codes.no_project', detail: 'no /run task yet' } });
-- await assert.rejects(run(createCodes({ config, fetchImpl: none.fetchImpl, tokens, sleep: async () => {}, pollMs: 0 })), /openvibe\.bot\/machines.*openvibe\.codes\/run/);
++ await assert.rejects(run(createCodes({ config, fetchImpl: none.fetchImpl, tokens, sleep: async () => {}, pollMs: 0 })), /openvibe\.link\/machines.*openvibe\.codes\/run/);
});
await check('past Actor\'s own time the answer links the live page; an expired task says the machine never took it', async () => {
ddiff --git a/test/computer-handoff.test.js b/test/computer-handoff.test.js
iindex a4c13ae..455c642 100644
---- a/test/computer-handoff.test.js
++++ b/test/computer-handoff.test.js
@@@ -45,7 +45,7 @@ const COMPUTER = { session_id: SESSION, machine: 'a shared desk host', where: 'n
const id = await runningTask('Compare three hosting plans under $10 a month and tell me which one to pick.');
await store.appendEvent(s, id, {
at: new Date().toISOString(), kind: 'output', step: 'tool_result', tool: 'computer_desk', is_error: false,
-- text: JSON.stringify({ watch: 'https://openvibe.bot/machines/dev_01J8Z4M2Q0R7T9YV3K6N8P1W2A/screen?desk=desk-trains', running: true, kind: 'browser' }),
++ text: JSON.stringify({ watch: 'https://openvibe.link/machines/dev_01J8Z4M2Q0R7T9YV3K6N8P1W2A/screen?desk=desk-trains', running: true, kind: 'browser' }),
computer: COMPUTER,
});
@@@ -56,7 +56,7 @@ const COMPUTER = { session_id: SESSION, machine: 'a shared desk host', where: 'n
const { desk: _desk, needs_you: _needs, ...task } = r.json();
valid(task);
assert.deepStrictEqual(r.json().computer, { ...COMPUTER, driving: false });
-- assert.ok(r.json().desk && /openvibe\.bot/.test(r.json().desk.url), 'the watch link is still there');
++ assert.ok(r.json().desk && /openvibe\.link/.test(r.json().desk.url), 'the watch link is still there');
});
await check('the still is its person\'s alone: another person and a signed-out visitor get 404', async () => {
ddiff --git a/test/computers.test.js b/test/computers.test.js
iindex 99ff603..ad5ac60 100644
---- a/test/computers.test.js
++++ b/test/computers.test.js
@@@ -76,7 +76,7 @@ const as = { requester: `user:${ME}` };
assert.ok(bot.calls.every((c) => c.subject === ME && c.auth === 'Bearer svc-token'), 'as the person, with Actor\'s token');
const none = JSON.parse(await tools(fakeBot({ machines: [] })).call('list_my_computers', {}, as));
assert.deepStrictEqual(none.computers, []);
-- assert.match(none.note, /https:\/\/openvibe\.bot\/machines.*--computer.*Let my agent use this computer/);
++ assert.match(none.note, /https:\/\/openvibe\.link\/machines.*--computer.*Let my agent use this computer/);
});
await check('files and programs go to Bot as the person; refusals and an unanswered approval come back as errors', async () => {
@@@ -221,7 +221,7 @@ const as = { requester: `user:${ME}` };
assert.ok(defs.some((d) => d.function.name === 'computer_desk'), 'offered with the computer tools');
['computer_screen', 'computer_input', 'computer_browser'].forEach((n) => assert.deepStrictEqual(defs.find((d) => d.function.name === n).function.parameters.properties.desk.enum, ['main', 'private'], n));
assert.deepStrictEqual(JSON.parse(await t.call('list_my_computers', {}, as)).computers[0].can, ['screen', 'input', 'browser', 'desk'], 'the model sees the machine has a desk');
-- assert.deepStrictEqual(JSON.parse(await t.call('computer_desk', { machine: PC, action: 'start', width: 1920, height: 1080 }, as)), { watch: `https://openvibe.bot/machines/${PC}/screen?desk=private`, running: true, display: ':91', width: 1920, height: 1080 }, 'with the link to watch and take it over (Desks plan D4)');
++ assert.deepStrictEqual(JSON.parse(await t.call('computer_desk', { machine: PC, action: 'start', width: 1920, height: 1080 }, as)), { watch: `https://openvibe.link/machines/${PC}/screen?desk=private`, running: true, display: ':91', width: 1920, height: 1080 }, 'with the link to watch and take it over (Desks plan D4)');
const started = bot.calls.filter((c) => c.body && c.body.kind === 'desk').at(-1);
assert.deepStrictEqual([started.subject, started.body.value, started.body.ms], [ME, { action: 'start', width: 1920, height: 1080 }, 120000], 'as the person, with time for an approval');
const look = JSON.parse(await t.call('computer_screen', { machine: PC, question: 'What is open?', desk: 'private' }, as));
ddiff --git a/test/desk-handoff.test.js b/test/desk-handoff.test.js
iindex ad98f18..84b4046 100644
---- a/test/desk-handoff.test.js
++++ b/test/desk-handoff.test.js
@@@ -63,11 +63,11 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
});
assert.strictEqual(out.done, true);
assert.match(out.note, /Look at it again/);
-- assert.strictEqual(out.watch, `https://openvibe.bot/machines/${PC}/screen?desk=${run.name}`);
++ assert.strictEqual(out.watch, `https://openvibe.link/machines/${PC}/screen?desk=${run.name}`);
assert.ok(asked > 0, 'the task\'s clock was asked to wait');
assert.strictEqual(said.length, 1);
assert.strictEqual(said[0].step, 'text');
-- assert.match(said[0].text, new RegExp(`^Needs you: Sign in to your Google account in the browser\\. .*https://openvibe\\.bot/machines/${PC}/screen\\?desk=${run.name}$`));
++ assert.match(said[0].text, new RegExp(`^Needs you: Sign in to your Google account in the browser\\. .*https://openvibe\\.link/machines/${PC}/screen\\?desk=${run.name}$`));
assert.ok(bot.asked.every((d) => d === run.name), 'it watched the task\'s own desk');
assert.deepStrictEqual(told.at(-1), { subject: ME, what: 'Sign in to your Google account in the browser.', url: out.watch }, 'the person is told wherever they are, with the link');
});
@@@ -93,7 +93,7 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
const run = t.deskRun();
const started = JSON.parse(await t.call('computer_desk', { machine: PC, action: 'start' }, { requester: `user:${ME}`, desks: run }));
assert.strictEqual(Object.keys(started)[0], 'watch', 'the link comes first, so a clipped result keeps it');
-- assert.strictEqual(started.watch, `https://openvibe.bot/machines/${PC}/screen?desk=${run.name}`);
++ assert.strictEqual(started.watch, `https://openvibe.link/machines/${PC}/screen?desk=${run.name}`);
const stopped = JSON.parse(await t.call('computer_desk', { machine: PC, action: 'stop' }, { requester: `user:${ME}`, desks: run }));
assert.strictEqual(stopped.watch, undefined);
});
@@@ -103,7 +103,7 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
const alice = t.network.addUser('desk-alice');
const id = t.ctx.s.newId('tsk');
await store.insertTask(t.ctx.s, { id, requester: `user:${alice.subject}`, task: 'Book it', mode: 'balanced', budget_task: 0.05, budget_day: 1, created_at: t.ctx.s.iso() });
-- const watch = `https://openvibe.bot/machines/${PC}/screen?desk=desk-0123456789`;
++ const watch = `https://openvibe.link/machines/${PC}/screen?desk=desk-0123456789`;
await store.appendEvent(t.ctx.s, id, { kind: 'output', step: 'plan', agent: 'openvibe-runtime', text: 'OpenVibe runtime takes it.' });
await store.appendEvent(t.ctx.s, id, { kind: 'output', step: 'text', text: `Needs you: Sign in to the airline. Open your agent's desk, take it over, do it, then let go: ${watch}` });
await store.appendEvent(t.ctx.s, id, { kind: 'output', step: 'text', text: `See https://evil.example/machines/${PC}/screen?desk=desk-0123456789 too` });
@@@ -124,7 +124,7 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
const bea = t.network.addUser('desk-bea');
const id = t.ctx.s.newId('tsk');
await store.insertTask(t.ctx.s, { id, requester: `user:${bea.subject}`, task: 'Book it', mode: 'balanced', budget_task: 0.05, budget_day: 1, created_at: t.ctx.s.iso() });
-- const watch = `https://openvibe.bot/machines/${PC}/screen?desk=desk-0123456789`;
++ const watch = `https://openvibe.link/machines/${PC}/screen?desk=desk-0123456789`;
await store.appendEvent(t.ctx.s, id, { kind: 'output', step: 'plan', agent: 'openvibe-runtime', text: 'OpenVibe runtime takes it.' });
assert.strictEqual((await t.get(`/api/v1/tasks/${id}`, { as: bea })).json().needs_you, undefined, 'nothing waits yet');
const e = await store.appendEvent(t.ctx.s, id, { kind: 'output', step: 'text', text: `Needs you: Sign in to the airline. Open your agent's desk, take it over, do it, then let go: ${watch}` });
@@@ -137,7 +137,7 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
const cy = t.network.addUser('desk-cy');
const id = t.ctx.s.newId('tsk');
await store.insertTask(t.ctx.s, { id, requester: `user:${cy.subject}`, task: 'Find a flight', mode: 'balanced', budget_task: 0.05, budget_day: 1, created_at: t.ctx.s.iso() });
-- const watch = `https://openvibe.bot/machines/${PC}/screen?desk=desk-0123456789`;
++ const watch = `https://openvibe.link/machines/${PC}/screen?desk=desk-0123456789`;
await store.appendEvent(t.ctx.s, id, { kind: 'output', step: 'plan', agent: 'openvibe-runtime', text: 'OpenVibe runtime takes it.' });
const before = await t.get(`/tasks/${id}`, { as: cy });
assert.ok(/<section class="desk-card" id="desk-card" aria-live="polite" hidden>/.test(before.text), 'no desk yet: the card waits, hidden');
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/memory-loop.test.js b/test/memory-loop.test.js
iindex 413a407..c02769f 100644
---- a/test/memory-loop.test.js
++++ b/test/memory-loop.test.js
@@@ -229,7 +229,7 @@ function fakeModels(script) {
await check('a hand-off asks the person through the same deduped path', async () => {
const asked = [];
const ctx = { state: { desks: null }, waitForPerson: () => 1, notifyTask: async (kind, detail) => { asked.push({ kind, detail }); } };
-- const core = { pcs: { watchUrl: () => `https://openvibe.bot/machines/dev_${'0'.repeat(26)}/screen`, deskTurn: async () => ({ error: 'no desk in this test' }) } };
++ const core = { pcs: { watchUrl: () => `https://openvibe.link/machines/dev_${'0'.repeat(26)}/screen`, deskTurn: async () => ({ error: 'no desk in this test' }) } };
const out = await handToPerson(u.subject, { machine: `dev_${'0'.repeat(26)}`, what: 'Sign in to the airline.' }, ctx, core);
assert.strictEqual(out.done, false);
assert.strictEqual(asked.length, 1);
ddiff --git a/test/model-prefs.test.js b/test/model-prefs.test.js
iindex dd3c89b..db87bd0 100644
---- a/test/model-prefs.test.js
++++ b/test/model-prefs.test.js
@@@ -84,18 +84,18 @@ function fakeFetch(calls, chatAnswer) {
assert.ok(!JSON.stringify(map.columns).includes('key'), 'no key material is exported');
});
-- await check('your computer (L55 C3): own:node runs through OpenVibe.AI on the person\'s own machine, with the tools', async () => {
-- const put = await t.get('/api/v1/me/model', { as: alice, method: 'PUT', json: { choice: 'own:node' }, headers: SAME });
++ await check('your computer (L55 C3): own:link runs through OpenVibe.AI on the person\'s own machine, with the tools', async () => {
++ const put = await t.get('/api/v1/me/model', { as: alice, method: 'PUT', json: { choice: 'own:link' }, headers: SAME });
assert.strictEqual(put.status, 200, put.text);
-- assert.deepStrictEqual([put.json().choice, put.json().provider], ['own:node', 'node']);
++ assert.deepStrictEqual([put.json().choice, put.json().provider], ['own:link', 'link']);
const calls = [];
const models = createModels({ config: aiConfig(), fetchImpl: fakeFetch(calls), log: { log() {}, warn() {}, error() {} } });
-- await models.withChoice({ subject: PERSON, provider: 'node', model: null }, () => models.chat({
++ await models.withChoice({ subject: PERSON, provider: 'link', model: null }, () => models.chat({
tier: 'smart', messages: [{ role: 'user', content: 'hello' }],
tools: [{ type: 'function', function: { name: 'noop', parameters: { type: 'object', properties: {} } } }],
}));
assert.strictEqual(calls.length, 1);
-- assert.strictEqual(calls[0].headers['x-openvibe-credential'], 'node:' + PERSON, 'AI\'s name for their computer');
++ assert.strictEqual(calls[0].headers['x-openvibe-credential'], 'link:' + PERSON, 'AI\'s name for their computer');
assert.ok(Array.isArray(calls[0].body.tools), 'the loop\'s tools go with it');
await t.get('/api/v1/me/model', { as: alice, method: 'PUT', json: { choice: 'auto' }, headers: SAME });
});
ddiff --git a/test/notify-client.test.js b/test/notify-client.test.js
iindex 0feb867..0138ef8 100644
---- a/test/notify-client.test.js
++++ b/test/notify-client.test.js
@@@ -19,7 +19,7 @@ const reply = (status, body) => new Response(JSON.stringify(body), { status, hea
return reply(200, { ok: true });
};
const n = createNotifier({ config, fetchImpl, tokens, log: { warn() {} } });
-- const url = 'https://openvibe.bot/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-0123456789';
++ const url = 'https://openvibe.link/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-0123456789';
assert.deepStrictEqual(await n.needsYou({ subject: ME, what: 'Sign in to the airline.', url }), { sent: true });
assert.strictEqual(calls[0].url, `http://network.test/internal/identity/resolve?subject_id=${ME}`);
assert.deepStrictEqual(calls[1].body, { user_id: 42, type: 'AGENT_NEEDS_YOU', category: 'service', priority: 'high', icon: '🙋', title: 'Your agent needs you', message: 'Sign in to the airline.', url, service: 'actor', rich_content: { step: 'Sign in to the airline.' } });
ddiff --git a/test/runs-on.test.js b/test/runs-on.test.js
iindex 21b91a0..921cbc6 100644
---- a/test/runs-on.test.js
++++ b/test/runs-on.test.js
@@@ -1,7 +1,7 @@
'use strict';
// "Runs on: my computer" (plan L55 C3; the owner, 11-01: "a remote session, or a local agent session"): a person
// chooses where their tasks run, beside the model they chose. On their computer, every model call of the agent loop
--// goes to the model on their own computer (OpenVibe.AI's node credential, whatever key they chose above it), and a
++// goes to the model on their own computer (OpenVibe.AI's Link credential, whatever key they chose above it), and a
// coding task is pinned to Codes on their machine. Private mode and a public chat's ask stay on OpenVibe.
// node test/runs-on.test.js
const assert = require('assert');
@@@ -70,7 +70,7 @@ function aiModels(calls) {
assert.strictEqual(row.state, 'succeeded', JSON.stringify([row.error_code, row.error_detail]));
const loopCalls = calls.filter((c) => c.url.endsWith('/chat/completions'));
assert.ok(loopCalls.length >= 1, 'the loop called a model');
-- assert.ok(loopCalls.every((c) => c.headers['x-openvibe-credential'] === `node:${kim.subject || kim.id}`), JSON.stringify(loopCalls.map((c) => c.headers['x-openvibe-credential'])));
++ assert.ok(loopCalls.every((c) => c.headers['x-openvibe-credential'] === `link:${kim.subject || kim.id}`), JSON.stringify(loopCalls.map((c) => c.headers['x-openvibe-credential'])));
const why = JSON.stringify(row.explanation);
assert.ok(why.includes('on the model on your computer'), 'the explanation says where it ran');
});
@@@ -91,7 +91,7 @@ function aiModels(calls) {
calls.length = 0;
const made = await t.get('/api/v1/tasks', { as: kim, json: { task: 'What is two plus two?', mode: 'private' }, headers: SAME });
await t.waitFor(made.json().id);
-- assert.ok(calls.every((c) => !String(c.headers['x-openvibe-credential'] || '').startsWith('node:')), 'never their computer');
++ assert.ok(calls.every((c) => !String(c.headers['x-openvibe-credential'] || '').startsWith('link:')), 'never their computer');
await t.get('/api/v1/me/model', { as: kim, method: 'PUT', json: { runs_on: 'openvibe' }, headers: SAME });
});
@@@ -100,8 +100,8 @@ function aiModels(calls) {
assert.strictEqual(page.status, 200);
assert.ok(page.text.includes('id="runs-on"'));
assert.ok(page.text.includes('name="runs_on" value="openvibe" checked'));
-- assert.ok(page.text.includes('https://ai.openvibe.services/you?allow=actor&amp;provider=node'), 'set up the computer\'s model at OpenVibe.AI');
-- assert.ok(page.text.includes('https://openvibe.bot/machines'), 'add the machine for coding');
++ assert.ok(page.text.includes('https://ai.openvibe.services/you?allow=actor&amp;provider=link'), 'set up the computer\'s model at OpenVibe.AI');
++ assert.ok(page.text.includes('https://openvibe.link/machines'), 'add the machine for coding');
const saved = await t.get('/chat/runs-on', { as: kim, form: { runs_on: 'computer' }, headers: SAME });
assert.strictEqual(saved.status, 303);
assert.strictEqual(saved.headers.get('location'), '/chat/model?saved=runs#runs-on');
@@@ -118,8 +118,8 @@ function aiModels(calls) {
assert.match(anon.headers.get('location'), /^\/auth\/login/);
});
-- await check('the choice "Runs on: my computer" means: AI\'s node credential, for a person only', async () => {
-- assert.deepStrictEqual(prefs.computerChoice('user:usr_1'), { subject: 'usr_1', provider: 'node', model: null });
++ await check('the choice "Runs on: my computer" means: AI\'s Link credential, for a person only', async () => {
++ assert.deepStrictEqual(prefs.computerChoice('user:usr_1'), { subject: 'usr_1', provider: 'link', model: null });
assert.strictEqual(prefs.computerChoice('ip:1.2.3.4'), null);
assert.deepStrictEqual(prefs.RUNS_ON, ['openvibe', 'computer']);
assert.deepStrictEqual(prefs.wire(null).runs_on, 'openvibe');
ddiff --git a/test/skill-sandbox.test.js b/test/skill-sandbox.test.js
iindex ea0149a..f8829b0 100644
---- a/test/skill-sandbox.test.js
++++ b/test/skill-sandbox.test.js
@@@ -83,7 +83,7 @@ function fakeRun({ job = null, submitStatus = null, read = null } = {}) {
function runOf(fetchImpl, { waitMs = 1000, clock = { t: 0 } } = {}) {
return createSkillRun({
-- config: { services: { runUrl: 'https://run.test', linkUrl: 'https://openvibe.bot/link' }, skills: { placementWaitMs: waitMs, ttlMs: 120000 }, oauth: { clientId: 'actor', clientSecret: 's' }, networkInternalUrl: 'http://network.test' },
++ config: { services: { runUrl: 'https://run.test', linkUrl: 'https://openvibe.link' }, skills: { placementWaitMs: waitMs, ttlMs: 120000 }, oauth: { clientId: 'actor', clientSecret: 's' }, networkInternalUrl: 'http://network.test' },
fetchImpl,
tokens: { authHeaders: async () => ({ Authorization: 'Bearer actor-token' }), invalidate() {} },
projects: { forSubject: async () => [{ project_id: PRJ, role: 'owner', archived: false }] },
@@@ -300,14 +300,14 @@ function runOf(fetchImpl, { waitMs = 1000, clock = { t: 0 } } = {}) {
});
await check('no node: the step says so plainly, links Link\'s setup, and nothing runs elsewhere', async () => {
-- const run = { execute: async () => ({ ok: false, no_node: true, error: 'No machine is offering to run this skill right now. Run it on my machine: https://openvibe.bot/link — turn the skill runner on there, then try again. Nothing ran anywhere else.' }) };
++ const run = { execute: async () => ({ ok: false, no_node: true, error: 'No machine is offering to run this skill right now. Run it on my machine: https://openvibe.link — turn the skill runner on there, then try again. Nothing ran anywhere else.' }) };
const tools = await toolModule.toolsFor(t.ctx.s, { owner: `user:${alice.subject}`, agentId, run });
const runtime = tools.byAlias.get(toolModule.aliasOf('slugify-a-title', 'slugify'));
const out = await runtime.run({ title: 'A title' }, { store: t.ctx.s });
assert.strictEqual(out.isError, true);
assert.match(out.text, /No machine is offering to run this skill/);
assert.match(out.text, /Run it on my machine/);
-- assert.match(out.text, /openvibe\.bot\/link/);
++ assert.match(out.text, /openvibe\.link/);
assert.strictEqual(JSON.parse(out.text).untrusted_tool_result.no_node, true);
});
} finally { await t.close(); }
@@@ -399,7 +399,7 @@ function runOf(fetchImpl, { waitMs = 1000, clock = { t: 0 } } = {}) {
assert.strictEqual(out.no_node, true);
assert.match(out.error, /No machine is offering to run this skill/);
assert.match(out.error, /Run it on my machine/);
-- assert.match(out.error, /openvibe\.bot\/link/);
++ assert.match(out.error, /openvibe\.link/);
assert.ok(stub.calls.some((c) => c.method === 'POST' && /\/cancel$/.test(c.url)), 'the queued job is cancelled');
assert.ok(!stub.calls.some((c) => c.method === 'POST' && !/\/api\/v1\/jobs$|\/cancel$/.test(c.url)), 'nothing else was called');
});