openvibers/openvibe.actor Commits
OpenVibe Link rename: the machine, provider and page references (#382)
Changes
@@@ -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 fivetools and its code as ordinary `.js` text resources. The registry reads the code, never runs it, and fails a toolthat 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 tooldeclares; it flags any network use, timers over 5 s and loops over input. Every finding names its tool. Thecross-field rules (a tool's entry is a resource, every code resource is a tool's entry, tool names are unique) arechecked before anything is listed.@@@ -127,7 +127,7 @@ The resource index is for OpenVibe.Services on loopback and is blocked at the puwhich 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 machinerefuses 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: ascreen 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 -->@@@ -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"],nnew file mode 100644@@@ -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';@@@ -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",@@@ -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",@@@ -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.',},];@@@ -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: tokensOverrawait 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: tokensOverrif (!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' };@@@ -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,@@@ -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')),@@@ -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, '&')}" 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, '&')}" 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, '&')}" 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>`,});@@@ -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>@@@ -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 = [];@@@ -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. */@@@ -96,7 +96,7 @@ function createSkillRun({ config, fetchImpl = globalThis.fetch, tokens: tokensOvconst 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`,@@@ -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) {@@@ -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() });}@@@ -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 entryruns 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, orreaches 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@@@ -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);@@@ -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&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&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>/);});@@@ -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 () => {@@@ -45,7 +45,7 @@ const COMPUTER = { session_id: SESSION, machine: 'a shared desk host', where: 'nconst 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: 'nconst { 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 () => {@@@ -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));@@@ -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.fetchImplconst 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.fetchImplconst 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.fetchImplconst 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.fetchImplconst 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');nnew file mode 100644@@@ -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');@@@ -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);@@@ -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 });});@@@ -19,7 +19,7 @@ const reply = (status, body) => new Response(JSON.stringify(body), { status, heareturn 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.' } });@@@ -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.jsconst 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&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&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');@@@ -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');});
@@ -7,7 +7,7 @@ What changed in OpenVibe.Actor, newest first. Each site also publishes its patch
@@ -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
- **`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
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
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
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
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.
checked before anything is listed.
@@ -127,7 +127,7 @@ The resource index is for OpenVibe.Services on loopback and is blocked at the pu
@@ -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.
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
- 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.
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
- `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.
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
`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.
@@ -196,7 +196,7 @@ Readiness requires the database and at least one available agent.
## Acceptance
## 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 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.
- `npm run test:pg` — the same suite against PostgreSQL instead of PGlite.
@@ -261,7 +261,7 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md).
@@ -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).
Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers).
<!-- versions:start -->
<!-- versions:start -->
-- openvibe-contracts: v0.365.0
+- openvibe-contracts: v0.373.0
- openvibe-sdk: v0.123.0
- openvibe-sdk: v0.123.0
- openvibe-shared: v4.173.0
- openvibe-shared: v4.173.0
<!-- versions:end -->
<!-- versions:end -->
@@ -7,11 +7,11 @@
@@ -7,11 +7,11 @@
"runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4950,
"port": 4950,
"domain": "openvibe.actor",
"domain": "openvibe.actor",
- "contracts": "openvibe-contracts v0.365.0",
+ "contracts": "openvibe-contracts v0.373.0",
"sdk": "openvibe-sdk v0.123.0",
"sdk": "openvibe-sdk v0.123.0",
"shared": "openvibe-shared v4.173.0",
"shared": "openvibe-shared v4.173.0",
"packages": [
"packages": [
- "openvibe-contracts v0.365.0",
+ "openvibe-contracts v0.373.0",
"openvibe-sdk v0.123.0",
"openvibe-sdk v0.123.0",
"openvibe-shared v4.173.0"
"openvibe-shared v4.173.0"
],
],
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -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';
@@ -15,7 +15,7 @@
@@ -15,7 +15,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.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-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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.173.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
@@ -1679,9 +1679,9 @@
@@ -1679,9 +1679,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"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",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -28,7 +28,7 @@
@@ -28,7 +28,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.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-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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.173.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
@@ -78,7 +78,7 @@ const AGENTS = [
@@ -78,7 +78,7 @@ const AGENTS = [
can: ['task:code'],
can: ['task:code'],
// The model use is on the person's own machine and keys: nothing for Actor to meter.
// 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 },
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.',
},
},
];
];
@@ -2,7 +2,7 @@
@@ -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
* 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
* 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
* 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.
* the machine's own keys, so Actor meters nothing for it.
@@ -92,7 +92,7 @@ function createCodes({ config, fetchImpl = globalThis.fetch, tokens: tokensOverr
@@ -92,7 +92,7 @@ function createCodes({ config, fetchImpl = globalThis.fetch, tokens: tokensOverr
await emit({ step: 'tool_call', tool: 'codes.task.start', input: repo });
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 });
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') {
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 (!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' });
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
@@ -122,7 +122,7 @@ function createCodes({ config, fetchImpl = globalThis.fetch, tokens: tokensOverr
if (!TERMINAL.has(t.state)) {
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' };
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.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' });
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' };
return { text: answerOf(t), sources, model: (t.result.agents || [])[0] || 'openvibe-codes' };
@@ -55,7 +55,7 @@ const BROWSER_KEYS = new Set(['Enter', 'Tab', 'Escape', 'Backspace', 'Delete', '
@@ -55,7 +55,7 @@ const BROWSER_KEYS = new Set(['Enter', 'Tab', 'Escape', 'Backspace', 'Delete', '
function createComputers({ config, fetchImpl = globalThis.fetch, tokens: tokensOverride = null }) {
function createComputers({ config, fetchImpl = globalThis.fetch, tokens: tokensOverride = null }) {
const base = config.services.botUrl;
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 enabled = !!(base && (tokensOverride || config.oauth.clientSecret));
const tokens = !enabled ? null : tokensOverride || serviceAuth.createTokenClient({
const tokens = !enabled ? null : tokensOverride || serviceAuth.createTokenClient({
tokenUrl: `${config.networkInternalUrl}/oauth/token`, clientId: config.oauth.clientId, clientSecret: config.oauth.clientSecret,
tokenUrl: `${config.networkInternalUrl}/oauth/token`, clientId: config.oauth.clientId, clientSecret: config.oauth.clientSecret,
@@ -170,8 +170,10 @@ function load(env = process.env) {
@@ -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).
// 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).
// 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')),
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).
// OpenVibe.Space: the person's own spaces, which their agent builds (plan 2026-10-22 §Z stage Z1).
// Empty turns the space tools off.
// Empty turns the space tools off.
spaceUrl: trim(env.ACTOR_SPACE_URL != null ? env.ACTOR_SPACE_URL : (selfHosted ? '' : 'http://127.0.0.1:4940')),
spaceUrl: trim(env.ACTOR_SPACE_URL != null ? env.ACTOR_SPACE_URL : (selfHosted ? '' : 'http://127.0.0.1:4940')),
@@ -37,10 +37,10 @@ const canvases = require('../canvases');
@@ -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
// 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.
// 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 AI_SITE = 'https://ai.openvibe.services';
const allowUrl = (provider) => `${AI_SITE}/you?allow=actor&provider=${encodeURIComponent(provider)}`;
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');
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.
// 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');
const thread = require('openvibe-shared/thread');
@@ -518,7 +518,7 @@ ${shown.length ? html`<div class="ct-trig-runs">${shown}</div>` : ''}`;
@@ -518,7 +518,7 @@ ${shown.length ? html`<div class="ct-trig-runs">${shown}</div>` : ''}`;
<p class="ccm-head">Your own key</p>
<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>
<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>
<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>
<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>`;
</div></details>`;
}
}
@@ -666,7 +666,7 @@ ${radio('auto', 'Auto, OpenVibe\'s', 'Actor picks a model for each step, on Open
@@ -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>
<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, '&')}" target="_blank" rel="noopener">Add or allow the key</a>`))}
${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, '&')}" target="_blank" rel="noopener">Add or allow the key</a>`))}
<p class="cm-head">Your computer</p>
<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, '&')}" 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, '&')}" target="_blank" rel="noopener">Add your computer's model and allow Actor</a>`)}
${modelField(aiModels, model.model || '')}
${modelField(aiModels, model.model || '')}
<button class="cm-save" type="submit">Save</button>
<button class="cm-save" type="submit">Save</button>
</form>
</form>
@@ -675,7 +675,7 @@ ${modelField(aiModels, model.model || '')}
@@ -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>` : ''}
${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">
<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="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>
<button class="cm-save" type="submit">Save</button>
</form></main>`,
</form></main>`,
});
});
@@ -42,6 +42,7 @@ function createConsolePages(ctx, { robots = null, computers = null } = {}) {
@@ -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 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 signedIn = (req) => req.viewer && req.viewer.kind === 'user' && req.viewer.subject;
const pages = config.services.botPublicUrl || 'https://openvibe.bot';
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 }. */
/** One Bot read, bounded so a slow Bot never holds the page; { ok, value } or { ok: false, why }. */
async function fromBot(fn) {
async function fromBot(fn) {
@@ -114,13 +115,13 @@ ${today ? html`<p class="cn-note" data-allowance>${leftLine(tier, today)}</p>` :
@@ -114,13 +115,13 @@ ${today ? html`<p class="cn-note" data-allowance>${leftLine(tier, today)}</p>` :
function computersCard(result) {
function computersCard(result) {
const body = !bot.computers.enabled ? html`<p class="cn-empty">Not connected to OpenVibe.Bot on this server.</p>`
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.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>
: 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>
<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.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}
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) {
function robotsCard(result) {
@@ -144,7 +145,7 @@ ${c.online && c.can.includes('screen') ? html`<a class="cn-link" href="${pages}/
@@ -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],
['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],
['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 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],
['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>
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)}
@@ -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>
${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>
<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>` : ''}
${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>`}
${nWaiting ? '' : html`<p class="cn-empty">Nothing waits for you.</p>`}
<p class="cn-more"><a href="/me/approvals">All approvals</a></p></section>
<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>
<section class="cn-card" aria-labelledby="cn-today"><h2 id="cn-today">Today</h2>
@@ -502,8 +502,8 @@ ${today ? html`<p class="task-allowance muted small">${leftLine(tier, today)}</p
@@ -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`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 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`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`<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`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.'],
[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 }) : ''}
@@ -1128,7 +1128,7 @@ ${!shared ? relatedRunsBlock(row, { forks, others }) : ''}
${open ? planCard(plan, { editable: !shared && (live || (waiting && !(question && question.kind === 'acting'))), taskId: row.id }) : ''}
${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')}>
${!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>
<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>` : ''}
</section>` : ''}
${open || look ? html`<figure class="last-look" id="last-look"${look ? '' : raw(' hidden')}>
${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>
<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
@@ -1348,10 +1348,11 @@ ${a.refused ? html`<details class="refused"><summary>See the answer that didn't
</section>`;
</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).
// 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 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) {
function withDeskLinks(text) {
const t = String(text || '');
const t = String(text || '');
const parts = [];
const parts = [];
@@ -7,7 +7,7 @@
@@ -7,7 +7,7 @@
* auto Actor decides, as today (the operator's tiers and keys)
* 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)
* 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:<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
* 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,
* 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'];
@@ -22,8 +22,8 @@ const CHOICES = ['auto', 'openvibe'];
// OpenVibe.Codes (the codes agent), for every turn but robot and computer-control tasks.
// OpenVibe.Codes (the codes agent), for every turn but robot and computer-control tasks.
const RUNS_ON = ['openvibe', 'computer'];
const RUNS_ON = ['openvibe', 'computer'];
const KEY_PROVIDERS = ['openai', 'anthropic', 'deepseek', 'google', 'openrouter', 'mistral'];
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).
// 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}$/;
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
// 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) {
@@ -89,10 +89,10 @@ async function runsOnFor(s, requester) {
return row && RUNS_ON.includes(row.runs_on) ? row.runs_on : 'openvibe';
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) {
function computerChoice(requester) {
const subject = String(requester || '').startsWith('user:') ? requester.slice(5) : null;
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. */
/** Set where the person's tasks run. Returns { runs_on } or { error }; the model choice is left as it is. */
@@ -96,7 +96,7 @@ function createSkillRun({ config, fetchImpl = globalThis.fetch, tokens: tokensOv
@@ -96,7 +96,7 @@ function createSkillRun({ config, fetchImpl = globalThis.fetch, tokens: tokensOv
const services = (config && config.services) || {};
const services = (config && config.services) || {};
const skills = (config && config.skills) || {};
const skills = (config && config.skills) || {};
const base = String(services.runUrl || '').replace(/\/+$/, '');
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 enabled = !!(base && (tokensOverride || (config.oauth && config.oauth.clientSecret)));
const tokens = !enabled ? null : tokensOverride || serviceAuth.createTokenClient({
const tokens = !enabled ? null : tokensOverride || serviceAuth.createTokenClient({
tokenUrl: `${config.networkInternalUrl}/oauth/token`,
tokenUrl: `${config.networkInternalUrl}/oauth/token`,
@@ -197,7 +197,7 @@ function scanCode(resource, tool) {
@@ -197,7 +197,7 @@ function scanCode(resource, tool) {
}
}
const badModules = new Set();
const badModules = new Set();
for (const m of code.matchAll(REQUIRE)) {
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 (!SAFE_REQUIRE.has(name)) badModules.add(name);
}
}
if (badModules.size) {
if (badModules.size) {
@@ -537,7 +537,7 @@ function createEngine({ config, s, adapters, stream, webhooks = null, now = () =
@@ -537,7 +537,7 @@ function createEngine({ config, s, adapters, stream, webhooks = null, now = () =
if (dayRun) choice = await day.modelChoice(id);
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
// "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).
// 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,
// 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
// 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.
// serves it from free or untolled capacity only.
@@ -745,7 +745,7 @@ function createEngine({ config, s, adapters, stream, webhooks = null, now = () =
@@ -745,7 +745,7 @@ function createEngine({ config, s, adapters, stream, webhooks = null, now = () =
if (!placement.selected) {
if (!placement.selected) {
if (attempt > 1) break;
if (attempt > 1) break;
const why = pinnedAgent === 'codes' && !row.agent
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() });
: whyNone(placement, { cls, mode: row.mode, budgetUsd: budgetLeft() });
return await setState(id, 'failed', { error_code: why.code, error_detail: why.detail, finished_at: iso() });
return await setState(id, 'failed', { error_code: why.code, error_detail: why.detail, finished_at: iso() });
}
}
@@ -48,7 +48,7 @@ A sandbox skill's `.js` resources are read by the code rules instead (`code.env`
@@ -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.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
`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
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.
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
A sentence that says "never", "not" or "without" says the opposite, so no rule fires on it: the scan reads what a
@@ -28,20 +28,20 @@ const MACHINE = 'dev_01JZ0000000000000000000000';
@@ -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' }) });
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 () => {
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);
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 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 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 });
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');
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 });
page = await t.get(`/chat/${id}`, { as: kim });
assert.ok(!page.text.includes('data-watch='), 'the owner\'s own screen is never framed');
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 () => {
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';
@@ -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;
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 t.waitFor(other);
await s.db.query("UPDATE tasks SET state = 'running' WHERE id = $1", [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' });
answer = () => ({ error: 'capability.denied', code: 'capability.denied' });
const no = await t.get(`/chat/${other}/watch`, { as: kim, method: 'POST', headers: SAME });
const no = await t.get(`/chat/${other}/watch`, { as: kim, method: 'POST', headers: SAME });
assert.strictEqual(no.status, 502);
assert.strictEqual(no.status, 502);
@@ -28,8 +28,8 @@ const SAME = { 'sec-fetch-site': 'same-origin' };
@@ -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');
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.
// 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('<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&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&provider=link'));
});
});
await check('a tap saves through the API from the page; the chip says the person\'s own key after', async () => {
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' };
@@ -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 });
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.match(bad.headers.get('location'), /\/chat\/model\?error=/);
assert.strictEqual((await t.get('/chat/model')).status, 303, 'signed out: sign in first');
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);
assert.strictEqual(pc.status, 303);
const pcPage = await t.get('/chat/model?saved=1', { as: kim });
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, /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>/);
assert.match((await t.get('/chat', { as: kim })).text, /<span data-ccm-label>Your computer<\/span>/);
});
});
@@ -71,7 +71,7 @@ const run = (codes, extra = {}) => {
@@ -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: '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');
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' } });
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 () => {
await check('past Actor\'s own time the answer links the live page; an expired task says the machine never took it', async () => {
@@ -45,7 +45,7 @@ const COMPUTER = { session_id: SESSION, machine: 'a shared desk host', where: 'n
@@ -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.');
const id = await runningTask('Compare three hosting plans under $10 a month and tell me which one to pick.');
await store.appendEvent(s, id, {
await store.appendEvent(s, id, {
at: new Date().toISOString(), kind: 'output', step: 'tool_result', tool: 'computer_desk', is_error: false,
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,
computer: COMPUTER,
});
});
@@ -56,7 +56,7 @@ const COMPUTER = { session_id: SESSION, machine: 'a shared desk host', where: 'n
@@ -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();
const { desk: _desk, needs_you: _needs, ...task } = r.json();
valid(task);
valid(task);
assert.deepStrictEqual(r.json().computer, { ...COMPUTER, driving: false });
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 () => {
await check('the still is its person\'s alone: another person and a signed-out visitor get 404', async () => {
@@ -76,7 +76,7 @@ const as = { requester: `user:${ME}` };
@@ -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');
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));
const none = JSON.parse(await tools(fakeBot({ machines: [] })).call('list_my_computers', {}, as));
assert.deepStrictEqual(none.computers, []);
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 () => {
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}` };
@@ -221,7 +221,7 @@ const as = { requester: `user:${ME}` };
assert.ok(defs.some((d) => d.function.name === 'computer_desk'), 'offered with the computer tools');
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));
['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('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);
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');
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));
const look = JSON.parse(await t.call('computer_screen', { machine: PC, question: 'What is open?', desk: 'private' }, as));
@@ -63,11 +63,11 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
@@ -63,11 +63,11 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
});
});
assert.strictEqual(out.done, true);
assert.strictEqual(out.done, true);
assert.match(out.note, /Look at it again/);
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.ok(asked > 0, 'the task\'s clock was asked to wait');
assert.strictEqual(said.length, 1);
assert.strictEqual(said.length, 1);
assert.strictEqual(said[0].step, 'text');
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.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');
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
@@ -93,7 +93,7 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
const run = t.deskRun();
const run = t.deskRun();
const started = JSON.parse(await t.call('computer_desk', { machine: PC, action: 'start' }, { requester: `user:${ME}`, desks: run }));
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(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 }));
const stopped = JSON.parse(await t.call('computer_desk', { machine: PC, action: 'stop' }, { requester: `user:${ME}`, desks: run }));
assert.strictEqual(stopped.watch, undefined);
assert.strictEqual(stopped.watch, undefined);
});
});
@@ -103,7 +103,7 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
@@ -103,7 +103,7 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
const alice = t.network.addUser('desk-alice');
const alice = t.network.addUser('desk-alice');
const id = t.ctx.s.newId('tsk');
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() });
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: '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: `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` });
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
@@ -124,7 +124,7 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
const bea = t.network.addUser('desk-bea');
const bea = t.network.addUser('desk-bea');
const id = t.ctx.s.newId('tsk');
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() });
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.' });
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');
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}` });
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
@@ -137,7 +137,7 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
const cy = t.network.addUser('desk-cy');
const cy = t.network.addUser('desk-cy');
const id = t.ctx.s.newId('tsk');
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() });
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.' });
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 });
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');
assert.ok(/<section class="desk-card" id="desk-card" aria-live="polite" hidden>/.test(before.text), 'no desk yet: the card waits, hidden');
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -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');
@@ -229,7 +229,7 @@ function fakeModels(script) {
@@ -229,7 +229,7 @@ function fakeModels(script) {
await check('a hand-off asks the person through the same deduped path', async () => {
await check('a hand-off asks the person through the same deduped path', async () => {
const asked = [];
const asked = [];
const ctx = { state: { desks: null }, waitForPerson: () => 1, notifyTask: async (kind, detail) => { asked.push({ kind, detail }); } };
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);
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(out.done, false);
assert.strictEqual(asked.length, 1);
assert.strictEqual(asked.length, 1);
@@ -84,18 +84,18 @@ function fakeFetch(calls, chatAnswer) {
@@ -84,18 +84,18 @@ function fakeFetch(calls, chatAnswer) {
assert.ok(!JSON.stringify(map.columns).includes('key'), 'no key material is exported');
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.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 calls = [];
const models = createModels({ config: aiConfig(), fetchImpl: fakeFetch(calls), log: { log() {}, warn() {}, error() {} } });
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' }],
tier: 'smart', messages: [{ role: 'user', content: 'hello' }],
tools: [{ type: 'function', function: { name: 'noop', parameters: { type: 'object', properties: {} } } }],
tools: [{ type: 'function', function: { name: 'noop', parameters: { type: 'object', properties: {} } } }],
}));
}));
assert.strictEqual(calls.length, 1);
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');
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 });
await t.get('/api/v1/me/model', { as: alice, method: 'PUT', json: { choice: 'auto' }, headers: SAME });
});
});
@@ -19,7 +19,7 @@ const reply = (status, body) => new Response(JSON.stringify(body), { status, hea
@@ -19,7 +19,7 @@ const reply = (status, body) => new Response(JSON.stringify(body), { status, hea
return reply(200, { ok: true });
return reply(200, { ok: true });
};
};
const n = createNotifier({ config, fetchImpl, tokens, log: { warn() {} } });
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.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.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.' } });
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.' } });
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
// "Runs on: my computer" (plan L55 C3; the owner, 11-01: "a remote session, or a local agent session"): a person
// "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
// 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.
// 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
// node test/runs-on.test.js
const assert = require('assert');
const assert = require('assert');
@@ -70,7 +70,7 @@ function aiModels(calls) {
@@ -70,7 +70,7 @@ function aiModels(calls) {
assert.strictEqual(row.state, 'succeeded', JSON.stringify([row.error_code, row.error_detail]));
assert.strictEqual(row.state, 'succeeded', JSON.stringify([row.error_code, row.error_detail]));
const loopCalls = calls.filter((c) => c.url.endsWith('/chat/completions'));
const loopCalls = calls.filter((c) => c.url.endsWith('/chat/completions'));
assert.ok(loopCalls.length >= 1, 'the loop called a model');
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);
const why = JSON.stringify(row.explanation);
assert.ok(why.includes('on the model on your computer'), 'the explanation says where it ran');
assert.ok(why.includes('on the model on your computer'), 'the explanation says where it ran');
});
});
@@ -91,7 +91,7 @@ function aiModels(calls) {
@@ -91,7 +91,7 @@ function aiModels(calls) {
calls.length = 0;
calls.length = 0;
const made = await t.get('/api/v1/tasks', { as: kim, json: { task: 'What is two plus two?', mode: 'private' }, headers: SAME });
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);
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 });
await t.get('/api/v1/me/model', { as: kim, method: 'PUT', json: { runs_on: 'openvibe' }, headers: SAME });
});
});
@@ -100,8 +100,8 @@ function aiModels(calls) {
@@ -100,8 +100,8 @@ function aiModels(calls) {
assert.strictEqual(page.status, 200);
assert.strictEqual(page.status, 200);
assert.ok(page.text.includes('id="runs-on"'));
assert.ok(page.text.includes('id="runs-on"'));
assert.ok(page.text.includes('name="runs_on" value="openvibe" checked'));
assert.ok(page.text.includes('name="runs_on" value="openvibe" checked'));
- assert.ok(page.text.includes('https://ai.openvibe.services/you?allow=actor&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&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 });
const saved = await t.get('/chat/runs-on', { as: kim, form: { runs_on: 'computer' }, headers: SAME });
assert.strictEqual(saved.status, 303);
assert.strictEqual(saved.status, 303);
assert.strictEqual(saved.headers.get('location'), '/chat/model?saved=runs#runs-on');
assert.strictEqual(saved.headers.get('location'), '/chat/model?saved=runs#runs-on');
@@ -118,8 +118,8 @@ function aiModels(calls) {
@@ -118,8 +118,8 @@ function aiModels(calls) {
assert.match(anon.headers.get('location'), /^\/auth\/login/);
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.strictEqual(prefs.computerChoice('ip:1.2.3.4'), null);
assert.deepStrictEqual(prefs.RUNS_ON, ['openvibe', 'computer']);
assert.deepStrictEqual(prefs.RUNS_ON, ['openvibe', 'computer']);
assert.deepStrictEqual(prefs.wire(null).runs_on, 'openvibe');
assert.deepStrictEqual(prefs.wire(null).runs_on, 'openvibe');
@@ -83,7 +83,7 @@ function fakeRun({ job = null, submitStatus = null, read = null } = {}) {
@@ -83,7 +83,7 @@ function fakeRun({ job = null, submitStatus = null, read = null } = {}) {
function runOf(fetchImpl, { waitMs = 1000, clock = { t: 0 } } = {}) {
function runOf(fetchImpl, { waitMs = 1000, clock = { t: 0 } } = {}) {
return createSkillRun({
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,
fetchImpl,
tokens: { authHeaders: async () => ({ Authorization: 'Bearer actor-token' }), invalidate() {} },
tokens: { authHeaders: async () => ({ Authorization: 'Bearer actor-token' }), invalidate() {} },
projects: { forSubject: async () => [{ project_id: PRJ, role: 'owner', archived: false }] },
projects: { forSubject: async () => [{ project_id: PRJ, role: 'owner', archived: false }] },
@@ -300,14 +300,14 @@ function runOf(fetchImpl, { waitMs = 1000, clock = { t: 0 } } = {}) {
@@ -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 () => {
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 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 runtime = tools.byAlias.get(toolModule.aliasOf('slugify-a-title', 'slugify'));
const out = await runtime.run({ title: 'A title' }, { store: t.ctx.s });
const out = await runtime.run({ title: 'A title' }, { store: t.ctx.s });
assert.strictEqual(out.isError, true);
assert.strictEqual(out.isError, true);
assert.match(out.text, /No machine is offering to run this skill/);
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, /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);
assert.strictEqual(JSON.parse(out.text).untrusted_tool_result.no_node, true);
});
});
} finally { await t.close(); }
} finally { await t.close(); }
@@ -399,7 +399,7 @@ function runOf(fetchImpl, { waitMs = 1000, clock = { t: 0 } } = {}) {
@@ -399,7 +399,7 @@ function runOf(fetchImpl, { waitMs = 1000, clock = { t: 0 } } = {}) {
assert.strictEqual(out.no_node, true);
assert.strictEqual(out.no_node, true);
assert.match(out.error, /No machine is offering to run this skill/);
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, /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' && /\/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');
assert.ok(!stub.calls.some((c) => c.method === 'POST' && !/\/api\/v1\/jobs$|\/cancel$/.test(c.url)), 'nothing else was called');
});
});