openvibers/openvibe.actor Commits

OpenVibe Link: the new name in pages, docs and messages (#355)

OP OpenVibers committed 5 days ago · 70eb42b6a7 · 17 files, +49 −49

* OpenVibe Link: the new name in pages, docs and messages

OpenVibe Node is now OpenVibe Link. Pages, emails, notifications, docs, READMEs, error messages and tool descriptions say OpenVibe Link (or Link), and installer links point at openvibe.link. Code identifiers, the openvibe-node command, paths, contract ids and the repository name are unchanged.

* Migrations: keep applied files byte-identical (comments unchanged)

Changes

ddiff --git a/README.md b/README.md
iindex d89d33b..8166be1 100644
---- a/README.md
++++ b/README.md
@@@ -106,7 +106,7 @@ The resource index is for OpenVibe.Services on loopback and is blocked at the pu
## Does not own
- Coding: **OpenVibe.Codes** (the coding-agent harness).
--- Execution sandboxes, browsers and desktops: **OpenVibe.Run** / **OpenVibe.Node** (next).
++- Execution sandboxes, browsers and desktops: **OpenVibe.Run** / **OpenVibe Link** (next).
- Accounts, apps and grants: **OpenVibe.Network** and **OpenVibe.Services**.
- The tools the runtime uses: **OpenVibe.Tools**. Search: **OpenVibe.Search**.
@@@ -119,16 +119,16 @@ The resource index is for OpenVibe.Services on loopback and is blocked at the pu
- **OpenVibe.Tools** (`ACTOR_TOOLS_URL`): the catalog, the run API and the page reader, for `find_tools`, `run_tool` and `read_page`. Called as any anonymous caller would.
- **OpenVibe.Bot** (`ACTOR_BOT_URL`, default `http://127.0.0.1:4630`; empty turns it off): a signed-in person's own robots, for `list_my_robots` and `robot_steps`. Actor calls Bot with its own client-credentials token (audience `openvibe.bot`, `bot.robot.read` + `bot.robot.control`) and `X-OV-Subject` naming the person. Bot answers only for the robot's owner, and only on robots where that owner turned on "Let my OpenVibe.Actor agent drive this robot". Bot's gate, limits and e-stop judge every step. App, agent and service tasks never see these tools.
- **The Console** (`/console`, plan T17 layer 3): one page for a signed-in person. It has a command box (the ordinary task form) and their running and recent tasks; it refreshes itself while something runs, never while they type. It shows what waits for them: their approvals, and approvals waiting on their own computers. It also shows today's spend against the free tier, their agents, and their computers and robots, read from OpenVibe.Bot as them with a 4 s bound, so a slow Bot never holds the page. Links go to Services, Watch and Codes for the rest. It works without JavaScript (`server/http/console-pages.js`, `test/console.test.js`).
--- **A person's own computers, also through OpenVibe.Bot** (plan T14/T17, contracts 0.166.0): `list_my_computers`, `computer_list`, `computer_read`, `computer_run` (argv, never a shell line) and `computer_write`, on machines installed with OpenVibe Node's `--computer` plugin. Actor calls Bot's machine API with its own token (`bot.computer.control`) and `X-OV-Subject` naming the person. Bot answers only for the machine's owner, and only where that owner turned on "Let my agent use this computer". The level the owner set on the machine decides what may happen there; at `ask`, each run or write waits up to two minutes for the owner's approval on the machine. Only a signed-in person's task sees these tools (`server/agents/computers.js`, `test/computers.test.js`). `computer_screen` (when an OpenAI key is set) takes a picture of the machine's screen through Bot (1280 px wide) and has OpenAI's vision model answer the agent's question about it. The runtime model gets that answer, never the picture; the picture is not stored; secrets visible on screen are named, not repeated; and each look is metered on the task like any model call (refused once the task's budget is spent). `computer_input` (offered with `computer_screen`; contracts 0.171.0) uses the mouse and keyboard: a click, double or right click or move at a position in the last screen picture (its width and height are passed, and the Node scales them to the real screen), a scroll, typed text, or one key or chord. It is an acting command, so at level `ask` each one waits for the person's approval on the machine; the runtime is told to look first, act in small steps, look again, and never type a secret the task did not give it.
-- - `computer_browser` (contracts 0.174.0, Node v0.10.0) drives a browser of the agent's own on the machine. It is a
++- **A person's own computers, also through OpenVibe.Bot** (plan T14/T17, contracts 0.166.0): `list_my_computers`, `computer_list`, `computer_read`, `computer_run` (argv, never a shell line) and `computer_write`, on machines installed with OpenVibe Link's `--computer` plugin. Actor calls Bot's machine API with its own token (`bot.computer.control`) and `X-OV-Subject` naming the person. Bot answers only for the machine's owner, and only where that owner turned on "Let my agent use this computer". The level the owner set on the machine decides what may happen there; at `ask`, each run or write waits up to two minutes for the owner's approval on the machine. Only a signed-in person's task sees these tools (`server/agents/computers.js`, `test/computers.test.js`). `computer_screen` (when an OpenAI key is set) takes a picture of the machine's screen through Bot (1280 px wide) and has OpenAI's vision model answer the agent's question about it. The runtime model gets that answer, never the picture; the picture is not stored; secrets visible on screen are named, not repeated; and each look is metered on the task like any model call (refused once the task's budget is spent). `computer_input` (offered with `computer_screen`; contracts 0.171.0) uses the mouse and keyboard: a click, double or right click or move at a position in the last screen picture (its width and height are passed, and the Link scales them to the real screen), a scroll, typed text, or one key or chord. It is an acting command, so at level `ask` each one waits for the person's approval on the machine; the runtime is told to look first, act in small steps, look again, and never type a secret the task did not give it.
++ - `computer_browser` (contracts 0.174.0, Link v0.10.0) drives a browser of the agent's own on the machine. It is a
profile of its own, so it never sees the person's sessions.
- It can open a URL, click a selector or a visible link or button, fill a field, press a key, go back, read the
page, take a screenshot and close. The page comes back as its text (up to 3,500 characters) and links (up to 25),
which fits the result clip, so the tool is offered without a vision key.
- Reading, screenshots and closing run at every level; the rest wait for approval at `ask`. Below `full` the machine
refuses addresses on its own network.
-- - Approvals can come from the Node's tray or notification buttons, as well as `openvibe-node approve` (Node v0.11.0).
-- - `computer_desk` (contracts 0.214.0, Node v0.15.0 on Linux) starts, stops or reports the machine's private desk: a
++ - Approvals can come from the Link's tray or notification buttons, as well as `openvibe-node approve` (Link v0.11.0).
++ - `computer_desk` (contracts 0.214.0, Link v0.15.0 on Linux) starts, stops or reports the machine's private desk: a
screen of its own (Xvfb with its own bus and window manager) where the agent works without touching the person's.
`computer_screen`, `computer_input` and `computer_browser` take `desk: "private"` to act there. Bot refuses that on
a machine without a desk, so it never falls back to the person's screen.
ddiff --git a/server/agents/computers.js b/server/agents/computers.js
iindex 37618b3..a200713 100644
---- a/server/agents/computers.js
++++ b/server/agents/computers.js
@@@ -1,7 +1,7 @@
'use strict';
/**
-- * The person's own computers on OpenVibe.Bot, as runtime tools (plan T14/T17): OpenVibe.Node's computer plugin, reached
++ * The person's own computers on OpenVibe.Bot, as runtime tools (plan T14/T17): OpenVibe Link's computer plugin, reached
* only through Bot. Actor calls Bot's public API with its own service token (audience openvibe.bot,
* bot.computer.control) and X-OV-Subject naming the person the task is for. Bot answers only for the machine's owner,
* and only where that owner turned on "Let my agent use this computer"; the level the owner set on the machine itself
@@@ -16,8 +16,8 @@
* screen → { format, width, height, content_b64 } (the screen now, scaled to max_width)
* input → { done, action, x?, y? } (mouse, scroll, typing or a key, as the person)
* desk → { running, kind?, display?, width?, height? } (a desk: start, stop, status; kind browser|desktop)
-- * screen, input and browser take desk: "private" to act on the private desk (contracts 0.214.0, Node v0.15.0 on Linux),
-- * or a named desk "desk-<name>" (contracts 0.215.0, Node v0.19.0: several desks per machine; agents/tools.js gives each
++ * screen, input and browser take desk: "private" to act on the private desk (contracts 0.214.0, Link v0.15.0 on Linux),
++ * or a named desk "desk-<name>" (contracts 0.215.0, Link v0.19.0: several desks per machine; agents/tools.js gives each
* task one, the model only ever says "private"),
* a screen of its own where the agent works without touching the person's; Bot refuses it on a machine without one.
*
@@@ -42,7 +42,7 @@ const POINT_ACTIONS = new Set(['move', 'click', 'double_click', 'right_click']);
const BROWSER_ACTIONS = new Set(['open', 'click', 'fill', 'press', 'back', 'read', 'close']);
const PAGE_TEXT = 3500;
const PAGE_LINKS = 25;
--// With the page's elements as refs (Node v0.21.0) the links are among them, so the text gets less room and the links
++// With the page's elements as refs (Link v0.21.0) the links are among them, so the text gets less room and the links
// none: one tool result stays whole (tools.js clips results at 6000 characters).
const PAGE_TEXT_WITH_ELEMENTS = 2200;
const PAGE_ELEMENTS = 60;
@@@ -186,14 +186,14 @@ function createComputers({ config, fetchImpl = globalThis.fetch, tokens: tokensO
const machines = ((r.data && r.data.machines) || []).filter((m) => m.computer).map((m) => ({
id: m.id, name: m.name, online: m.online, level: m.computer.level,
can: ['terminal', 'files', 'screen', 'input', 'browser', 'desk'].filter((k) => m.computer[k]),
-- // Several desks at once (Node v0.19.0): each task then works on a desk of its own there.
++ // Several desks at once (Link v0.19.0): each task then works on a desk of its own there.
...(m.computer.desk && m.computer.desk.named === true ? { desks_at_once: Number(m.computer.desk.max_desks) || 1 } : {}),
...(Number.isInteger(m.computer.desks_running) ? { desks_running: m.computer.desks_running } : {}),
-- // The kinds of desk it runs (Node v0.23.0, Desks plan D2): browser (a browser alone) and desktop (a whole screen).
++ // The kinds of desk it runs (Link v0.23.0, Desks plan D2): browser (a browser alone) and desktop (a whole screen).
...(m.computer.desk && Array.isArray(m.computer.desk.kinds) ? { desk_kinds: m.computer.desk.kinds.filter((k) => DESK_KINDS.has(k)) } : {}),
-- // It keeps the person's browser profile from desk to desk (Node v0.24.0): sign in once.
++ // It keeps the person's browser profile from desk to desk (Link v0.24.0): sign in once.
...(m.computer.desk && m.computer.desk.profiles === true ? { keeps_sign_ins: true } : {}),
-- // It runs programs on a desk (Node v0.26.0): computer_run with desk "private", and detach for an app.
++ // It runs programs on a desk (Link v0.26.0): computer_run with desk "private", and detach for an app.
...(m.computer.terminal && m.computer.terminal.desks === true ? { apps_on_desk: true } : {}),
folders: (m.computer.files && m.computer.files.roots) || [],
...(m.computer.waiting ? { waiting_for_approval: m.computer.waiting } : {}),
@@@ -232,7 +232,7 @@ function createComputers({ config, fetchImpl = globalThis.fetch, tokens: tokensO
const value = { argv };
if (cwd) value.cwd = String(cwd);
if (Number.isInteger(timeoutS) && timeoutS > 0) value.timeout_s = Math.min(timeoutS, 600);
-- // On a desk (Node v0.26.0): its session, so a program's windows open there; detach keeps an app running there.
++ // On a desk (Link v0.26.0): its session, so a program's windows open there; detach keeps an app running there.
if (desk && desk !== 'main') value.desk = desk;
if (detach) {
if (!value.desk) return { error: 'detach is for your own desk only (desk "private"): an app is never left running on the person\'s screen' };
@@@ -320,7 +320,7 @@ function createComputers({ config, fetchImpl = globalThis.fetch, tokens: tokensO
if (typeof a.url !== 'string' || !/^https?:\/\//i.test(a.url) || a.url.length > 2048) return { error: 'open needs url: an http or https address' };
value.url = a.url;
}
-- // A ref (e7) from the page's elements names exactly that element: Node keeps it on the element as data-ov-ref.
++ // A ref (e7) from the page's elements names exactly that element: Link keeps it on the element as data-ov-ref.
if (a.ref != null) {
if (!['click', 'fill'].includes(a.action) || !REF.test(String(a.ref))) return { error: 'ref is an element\'s ref from the page (for example e7), for click or fill' };
a.selector = `[data-ov-ref="${a.ref}"]`;
@@@ -345,7 +345,7 @@ function createComputers({ config, fetchImpl = globalThis.fetch, tokens: tokensO
if (a.action === 'fill') return { done: !!d.done, chars: d.chars };
if (a.action === 'close') return { done: true };
// Sized to fit one tool result whole (tools.js clips results at 6000 characters): what the action changed (its
-- // receipt), the page's elements as refs to act on (Node v0.21.0), then its text; links when there are no refs.
++ // receipt), the page's elements as refs to act on (Link v0.21.0), then its text; links when there are no refs.
const elements = Array.isArray(d.elements) ? d.elements.slice(0, PAGE_ELEMENTS).map((e) => String(e).slice(0, 110)) : null;
const room = elements ? PAGE_TEXT_WITH_ELEMENTS : PAGE_TEXT;
const text = typeof d.text === 'string' ? d.text : '';
ddiff --git a/server/agents/kinds.js b/server/agents/kinds.js
iindex c33a7f9..500fcc5 100644
---- a/server/agents/kinds.js
++++ b/server/agents/kinds.js
@@@ -26,7 +26,7 @@ const KINDS = [
'install', // install a skill or connect a connector
'routine', // set up a task that keeps running
'money', // spend the person's money
-- 'skill.run', // run a skill's code in a sandbox on a Node (ADR-060)
++ 'skill.run', // run a skill's code in a sandbox on a Link (ADR-060)
];
// The plain words a person reads for each kind. Never an id, never a code: this is the sentence on the card.
ddiff --git a/server/agents/tools.js b/server/agents/tools.js
iindex d4b7abb..d786521 100644
---- a/server/agents/tools.js
++++ b/server/agents/tools.js
@@@ -39,7 +39,7 @@ const { stepFor } = require('./actions');
* the agent's own browser on the machine (a profile of its own): open, click, fill, press,
* back, read, close; pages answer as text and links, so no vision is needed
* computer_desk(machine, action)
-- * the machine's private desk (contracts 0.214.0, Node v0.15.0 on Linux): start, stop or
++ * the machine's private desk (contracts 0.214.0, Link v0.15.0 on Linux): start, stop or
* status; computer_screen, computer_input and computer_browser take desk: "private" to work
* there, never on the person's own screen (agent mode on a desk of its own)
*
@@@ -200,7 +200,7 @@ function createOpenVibeTools({ config, fetchImpl = globalThis.fetch, now = () =>
/**
* A task's own desk (Desks plan D1). The model only ever says desk "private"; on a machine that runs several desks
-- * (Node v0.19.0) that is a desk named for this run, so two tasks at once on one machine never see or drive each
++ * (Link v0.19.0) that is a desk named for this run, so two tasks at once on one machine never see or drive each
* other's screen, browser or files; elsewhere it is the machine's one private desk, as before. close() stops every
* desk the run used. A model's own desk name is refused, so a run can never reach another task's desk.
*/
@@@ -223,10 +223,10 @@ function createOpenVibeTools({ config, fetchImpl = globalThis.fetch, now = () =>
}
/**
-- * The kind of the task's desk (Desks plan D2, Node v0.23.0). Web work goes on a browser desk (a browser alone, half
++ * The kind of the task's desk (Desks plan D2, Link v0.23.0). Web work goes on a browser desk (a browser alone, half
* the cost of a whole desk) where the machine runs them; a whole desk (desktop) when the model asks for one (apps,
* a terminal on the desk) or the machine runs only those. The model's own ask goes through as asked (the machine
-- * says when it cannot). A machine that does not list its kinds (an older Node) is sent none.
++ * says when it cannot). A machine that does not list its kinds (an older Link) is sent none.
*/
function deskKind(run, machine, asked) {
if (asked != null) return asked; // checked where the desk starts (computers.desk)
@@@ -270,7 +270,7 @@ function createOpenVibeTools({ config, fetchImpl = globalThis.fetch, now = () =>
async function handToPerson(person, a, { signal, desks, emit, waitForPerson, pollMs = PERSON_POLL_MS, quietMs = PERSON_QUIET_MS } = {}) {
const what = String(a.what || '').replace(/\s+/g, ' ').trim().slice(0, 300);
if (!what) return { error: 'what is the step the person should do, in a sentence' };
-- // The task's own desk on a machine that runs several; else the machine's one private desk (an older Node).
++ // The task's own desk on a machine that runs several; else the machine's one private desk (an older Link).
const desk = desks && desks.used.has(a.machine) ? desks.name : 'private';
const watch = pcs.watchUrl(a.machine, desk);
const granted = waitForPerson ? waitForPerson(PERSON_WAIT_MS) : 0;
@@@ -341,7 +341,7 @@ function createOpenVibeTools({ config, fetchImpl = globalThis.fetch, now = () =>
}
if (!order.length) return json({ error: `none of the person's computers runs desks for you now, and ${ses.error}` });
}
-- if (!order.length) return json({ error: 'none of the person\'s computers runs desks for you now: it needs OpenVibe Node v0.19.0 or newer, online, with "Let my agent use this computer" on' });
++ if (!order.length) return json({ error: 'none of the person\'s computers runs desks for you now: it needs OpenVibe Link v0.19.0 or newer, online, with "Let my agent use this computer" on' });
return json({ error: `every computer of the person's that runs desks is full right now (${out.error})` });
} else return json({ error: 'no desk of yours runs yet: start one with computer_desk start and machine "any"' });
}
@@@ -352,7 +352,7 @@ function createOpenVibeTools({ config, fetchImpl = globalThis.fetch, now = () =>
const { kind, ...rest } = a;
const chosen = a.desk === 'private' ? kind : deskKind(desks, a.machine, kind); // private: a whole desk
a = chosen ? { ...rest, kind: chosen } : rest;
-- // The person's sign-ins carry from task to task (Node v0.24.0): every task's desk starts from the
++ // The person's sign-ins carry from task to task (Link v0.24.0): every task's desk starts from the
// browser profile kept for their agent on that machine, and saves it back when it stops. Never
// the model's choice.
delete a.profile;
@@@ -421,7 +421,7 @@ const COMPUTER_TOOLS = new Set([...COMPUTER_DEFS, SCREEN_DEF, INPUT_DEF, BROWSER
const PERSON_WAIT_MS = 10 * 60_000;
const PERSON_POLL_MS = 3000;
const PERSON_QUIET_MS = 20_000;
--// The browser profile kept for the person's agent on each of their machines (Node v0.24.0: sign in once).
++// The browser profile kept for the person's agent on each of their machines (Link v0.24.0: sign in once).
const AGENT_PROFILE = 'agent';
// The tools that work on a screen: their desk "private" is the task's own desk (deskRun).
const DESK_TOOLS = new Set(['computer_screen', 'computer_input', 'computer_browser', 'computer_desk', 'computer_run']);
ddiff --git a/server/config.js b/server/config.js
iindex 55a29a2..c54fc34 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -190,7 +190,7 @@ function load(env = process.env) {
// Skills that run code in a sandbox (plan HO8, ADR-060). A tool's code is stored in Media under Actor's
// tenant; a run is one OpenVibe.Run function job ([email protected]) with a 120 s lifetime. The
-- // placement wait is how long the step waits for a Node before it says, plainly, that none offered to run it.
++ // placement wait is how long the step waits for a Link before it says, plainly, that none offered to run it.
skills: {
mediaTenant: trim(env.ACTOR_MEDIA_TENANT || env.OV_OAUTH_CLIENT_ID || 'actor'),
placementWaitMs: Math.max(1000, int(env.ACTOR_SKILL_PLACEMENT_WAIT_MS, 12000)),
ddiff --git a/server/http/chat-pages.js b/server/http/chat-pages.js
iindex 99a059e..196bd6f 100644
---- a/server/http/chat-pages.js
++++ b/server/http/chat-pages.js
@@@ -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 Node. 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: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>
<p class="ccm-fine" data-ccm-fine>A key you add stays sealed in OpenVibe.AI: Actor never sees it, and only uses it once you allow Actor there. <a href="/chat/model">All settings</a></p>
</div></details>`;
}
@@@ -666,7 +666,7 @@ ${radio('auto', 'Auto, OpenVibe\'s', 'Actor picks a model for each step, on Open
<p class="cm-head">Your own key</p>
${modelPrefs.KEY_PROVIDERS.map((p) => radio(`own:${p}`, PROVIDER_NAMES[p] || p, 'Billed by the provider to you, not counted against Actor\'s allowance.', `<a class="cm-setup" href="${allowUrl(p).replace(/&/g, '&amp;')}" target="_blank" rel="noopener">Add or allow the key</a>`))}
<p class="cm-head">Your computer</p>
--${radio('own:node', 'Your computer', 'A model you run at home (Ollama, LM Studio or llama.cpp) through OpenVibe Node v0.36.0 or newer: nothing is billed, and it answers only while that computer is on. Usually slower than a cloud model.', `<a class="cm-setup" href="${allowUrl('node').replace(/&/g, '&amp;')}" target="_blank" rel="noopener">Add your computer's model and allow Actor</a>`)}
++${radio('own:node', 'Your computer', 'A model you run at home (Ollama, LM Studio or llama.cpp) through OpenVibe Link v0.36.0 or newer: nothing is billed, and it answers only while that computer is on. Usually slower than a cloud model.', `<a class="cm-setup" href="${allowUrl('node').replace(/&/g, '&amp;')}" target="_blank" rel="noopener">Add your computer's model and allow Actor</a>`)}
${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 Node 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('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>
<button class="cm-save" type="submit">Save</button>
</form></main>`,
});
ddiff --git a/server/http/console-pages.js b/server/http/console-pages.js
iindex 6d856e6..7d862e2 100644
---- a/server/http/console-pages.js
++++ b/server/http/console-pages.js
@@@ -189,7 +189,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 Node'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-node approvals</code>.</p>` : ''}
${nWaiting ? '' : html`<p class="cn-empty">Nothing waits for you.</p>`}
<p class="cn-more"><a href="/me/approvals">All approvals</a></p></section>
<section class="cn-card" aria-labelledby="cn-today"><h2 id="cn-today">Today</h2>
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex abcecd7..b2e79ec 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -88,10 +88,10 @@ const TEMPLATES = [
{ group: 'Your robot', badge: 'Robot', type: 'toy', needs: 'Turn on Your agent on the robot\'s panel first.', items: [
{ title: 'Drive your rover and say hello', text: 'Drive my rover forward for two seconds, turn on its headlights, then say hello.' },
] },
-- { group: 'Your computer', badge: 'Computer', type: 'site', needs: 'Needs the OpenVibe Node\'s computer plugin on that machine.', items: [
++ { group: 'Your computer', badge: 'Computer', type: 'site', needs: 'Needs the OpenVibe Link\'s computer plugin on that machine.', items: [
{ title: 'The ten largest files in your Downloads', text: 'On my computer, list the ten largest files in my Downloads folder.' },
] },
-- { group: 'On a desk of its own', badge: 'Desk', type: 'cam', needs: 'Needs a computer or desk host with the OpenVibe Node\'s computer plugin. Watch the desk and take it over from the task.', items: [
++ { group: 'On a desk of its own', badge: 'Desk', type: 'cam', needs: 'Needs a computer or desk host with the OpenVibe Link\'s computer plugin. Watch the desk and take it over from the task.', items: [
{ title: 'Sign in on its desk, then read your account', text: 'On my computer, open the sign-in page of <a site I use> on your desk and hand it to me to sign in; then tell me what my account page shows.' },
] },
{ group: 'Private', badge: 'Private', type: 'person', needs: 'Answered on OpenVibe\'s own model; nothing leaves OpenVibe.', items: [
@@@ -505,7 +505,7 @@ ${today ? html`<p class="task-allowance muted small">${leftLine(tier, today)}</p
[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 Node 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`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`<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.'],
@@@ -1414,7 +1414,7 @@ ${a.available ? '' : html`<p class="muted small">${a.reason}</p>`}
<p class="ag-id muted small"><code>${a.id}</code>${a.model ? html` · ${a.model}` : ''} · prices checked ${a.rate_card.verified_at}</p>
</li>`;
})}</ul>
--<p class="ag-soon">Coming: your own models and API keys, and your own computer through the OpenVibe Node.</p>
++<p class="ag-soon">Coming: your own models and API keys, and your own computer through the OpenVibe Link.</p>
<details class="ag-fold" id="modes"><summary>Modes: how Actor picks (${MODES.length})</summary>
${table(['Mode', 'What it picks'], MODES.map((m) => [html`<strong>${MODE_LABEL[m]}</strong> <code>${m}</code>`, MODE_TEXT[m]]))}</details>
<details class="ag-fold" id="classes"><summary>What a task needs (5 kinds)</summary>
ddiff --git a/server/media/client.js b/server/media/client.js
iindex 8327138..73bd318 100644
---- a/server/media/client.js
++++ b/server/media/client.js
@@@ -4,7 +4,7 @@
* OpenVibe.Media for Actor (plan HO8, ADR-060): where a sandbox skill's code bytes live. Each code resource is one
* PRIVATE object under Actor's own tenant (`config.skills.mediaTenant`, normally Actor's client id). The registry
* hashes the exact bytes it scans and uploads those bytes with that hash; the listing then pins `{ media_id, sha256 }`,
-- * so a Run job names the object and the digest and the Node refuses anything that changed.
++ * so a Run job names the object and the digest and the Link refuses anything that changed.
*
* There is no public URL here: a skill's code is not served to anybody. `put()` answers `{ id, sha256 }` or
* `{ error }`; `enabled` is false without Media's URL or Actor's client secret, and publishing a sandbox skill then
ddiff --git a/server/runtime/model-prefs.js b/server/runtime/model-prefs.js
iindex 192fef8..9cc5ed0 100644
---- a/server/runtime/model-prefs.js
++++ b/server/runtime/model-prefs.js
@@@ -7,7 +7,7 @@
* auto Actor decides, as today (the operator's tiers and keys)
* openvibe OpenVibe's own keys, through OpenVibe.AI (the gateway: its routing, quota and cost)
* own:<provider> the person's OWN key for that provider, sealed in OpenVibe.AI's vault
-- * own:node a model on the person's own computer (OpenVibe.AI's "Your computer": their OpenVibe Node, through
++ * own:node 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,
ddiff --git a/server/runtime/tools/_person.js b/server/runtime/tools/_person.js
iindex a2a2ecc..4e651c0 100644
---- a/server/runtime/tools/_person.js
++++ b/server/runtime/tools/_person.js
@@@ -16,7 +16,7 @@ const { personOf } = require('./_openvibe');
const PERSON_WAIT_MS = 10 * 60_000;
const PERSON_POLL_MS = 3000;
const PERSON_QUIET_MS = 20_000;
--// The browser profile kept for the person's agent on each of their machines (Node v0.24.0: sign in once).
++// The browser profile kept for the person's agent on each of their machines (Link v0.24.0: sign in once).
const AGENT_PROFILE = 'agent';
function deskRun(taskId = null) {
ddiff --git a/server/runtime/tools/computer_desk.js b/server/runtime/tools/computer_desk.js
iindex f37880a..3ac52bc 100644
---- a/server/runtime/tools/computer_desk.js
++++ b/server/runtime/tools/computer_desk.js
@@@ -35,7 +35,7 @@ async function startDesk(a, person, core, ctx) {
const { kind, ...rest } = a;
const chosen = a.desk === 'private' ? kind : deskKind(desks, a.machine, kind); // private: a whole desk
const body = chosen ? { ...rest, kind: chosen } : rest;
-- // The person's sign-ins carry from task to task (Node v0.24.0): every task's desk starts from the browser profile
++ // The person's sign-ins carry from task to task (Link v0.24.0): every task's desk starts from the browser profile
// kept for their agent on that machine, and saves it back when it stops. Never the model's choice.
delete body.profile;
if (body.desk !== 'private' && desks && desks.keeps && desks.keeps.has(a.machine)) body.profile = AGENT_PROFILE;
@@@ -82,7 +82,7 @@ module.exports = {
}
if (!order.length) return { text: json({ error: `none of the person's computers runs desks for you now, and ${ses.error}` }), isError: true };
}
-- if (!order.length) return { text: json({ error: 'none of the person\'s computers runs desks for you now: it needs OpenVibe Node v0.19.0 or newer, online, with "Let my agent use this computer" on' }), isError: true };
++ if (!order.length) return { text: json({ error: 'none of the person\'s computers runs desks for you now: it needs OpenVibe Link v0.19.0 or newer, online, with "Let my agent use this computer" on' }), isError: true };
return { text: json({ error: `every computer of the person's that runs desks is full right now (${out && out.error})` }), isError: true };
} else return { text: json({ error: 'no desk of yours runs yet: start one with computer_desk start and machine "any"' }), isError: true };
}
ddiff --git a/server/skills/document.js b/server/skills/document.js
iindex 666c0be..b948c0f 100644
---- a/server/skills/document.js
++++ b/server/skills/document.js
@@@ -9,7 +9,7 @@
* one to five TOOLS, each naming the resource its `entry` runs, its `args` (a JSON Schema subset), the network it may
* reach (`net`: deny by default, openvibe-only, public) and its limits (wall_ms at most 30 s; mem_bytes at most
* 256 MiB). The code never runs on an OpenVibe server: Actor stores the scanned bytes in Media and submits a Run job
-- * to a Node that declared the skill runner (ADR-060).
++ * to a Link that declared the skill runner (ADR-060).
*
* A skill GRANTS NOTHING in either major: `requires` DECLARES the HO1 kinds it may ask for, the apps and hosts it
* touches, and the agent's level and approvals still decide every step.
ddiff --git a/server/skills/run.js b/server/skills/run.js
iindex ecd7750..cca8527 100644
---- a/server/skills/run.js
++++ b/server/skills/run.js
@@@ -5,7 +5,7 @@
*
* A skill's code never runs on Actor's server. This module submits one Run job AS THE PERSON — Actor's service token
* (audience openvibe.run), `X-OV-Subject`, and the person's `X-OV-Project` (project delegation, ADR-003 amendment
-- * 2026-10-12) — and, if a Node takes it, waits for the result:
++ * 2026-10-12) — and, if a Link takes it, waits for the result:
*
* class: function, artifact [email protected],
* args: { tool, entry, input }, inputs: the listing's { media_id, sha256 } pins,
ddiff --git a/server/skills/scan.js b/server/skills/scan.js
iindex a9b50d8..d5be653 100644
---- a/server/skills/scan.js
++++ b/server/skills/scan.js
@@@ -118,7 +118,7 @@ const CODE_RULES = ['code.env', 'code.process', 'code.dynamic', 'code.vm', 'code
'code.network', 'code.encoded', 'code.minified', 'code.require', 'code.timer', 'code.loop'];
// The only modules a v1 tool may require: Node's own, with no process, no child, no worker and no raw socket. `fs`
--// is here because reading the job directory is allowed by the Node sandbox; the scan blocks every write below.
++// is here because reading the job directory is allowed by the Link sandbox; the scan blocks every write below.
const SAFE_REQUIRE = new Set(['assert', 'buffer', 'crypto', 'events', 'fs', 'path', 'punycode', 'querystring', 'stream', 'string_decoder', 'url', 'util', 'zlib']);
const REQUIRE = /\brequire\s*\(\s*(['"])([^'"]+)\1\s*\)/g;
const REQUIRE_COMPUTED = /\brequire\s*\(\s*(?!['"])|\bmodule\s*\.\s*require\s*\(|\bcreateRequire\s*\(/;
ddiff --git a/test/computers.test.js b/test/computers.test.js
iindex 7b59b7c..de3d9ed 100644
---- a/test/computers.test.js
++++ b/test/computers.test.js
@@@ -272,14 +272,14 @@ const as = { requester: `user:${ME}` };
assert.strictEqual(bot.calls.filter((c) => c.body && c.body.kind === 'desk' && c.body.value.action === 'stop').length, 1, 'b keeps its desk');
await t.closeDesks(a, as.requester);
assert.strictEqual(bot.calls.filter((c) => c.body && c.body.kind === 'desk' && c.body.value.action === 'stop').length, 1, 'closing twice stops nothing more');
-- // A machine on an older Node: the run's desk is the machine's one private desk, as before.
++ // A machine on an older Link: the run's desk is the machine's one private desk, as before.
const old = fakeBot({ machines: [{ id: PC, name: 'desk-pc', online: true, agent_computer: true, computer: { ...many, desk: { level: 'trusted', window_manager: 'openbox', input: true, screen: true } } }] });
const t2 = createOpenVibeTools({ config, fetchImpl: old.fetchImpl, robots: createRobots({ config, fetchImpl: old.fetchImpl, tokens }), computers: createComputers({ config, fetchImpl: old.fetchImpl, tokens }), vision });
await t2.call('computer_desk', { machine: PC, action: 'start' }, { ...as, desks: t2.deskRun() });
assert.deepStrictEqual(old.calls.filter((c) => c.body && c.body.kind === 'desk').at(-1).body.value, { action: 'start' });
});
-- await check('browser desks (Desks plan D2, Node v0.23.0): a task\'s desk is a browser alone where the machine runs them, a whole desk when the model asks or the machine has no browser desks; an older Node is sent no kind', async () => {
++ await check('browser desks (Desks plan D2, Link v0.23.0): a task\'s desk is a browser alone where the machine runs them, a whole desk when the model asks or the machine has no browser desks; an older Link is sent no kind', async () => {
const desk = { level: 'trusted', window_manager: 'openbox', input: true, screen: true, named: true, max_desks: 2 };
const machines = [
{ id: PC, name: 'desk-pc', online: true, agent_computer: true, computer: { level: 'trusted', browser: { level: 'trusted' }, desk: { ...desk, kinds: ['desktop', 'browser'] } } },
@@@ -300,10 +300,10 @@ const as = { requester: `user:${ME}` };
await t.call('computer_desk', { machine: MAC, action: 'start' }, { ...as, desks: t.deskRun() });
assert.strictEqual(last().kind, 'browser');
await t.call('computer_desk', { machine: OLD, action: 'start' }, { ...as, desks: t.deskRun() });
-- assert.strictEqual(last().kind, undefined, 'an older Node is sent no kind');
++ assert.strictEqual(last().kind, undefined, 'an older Link is sent no kind');
await t.call('computer_desk', { machine: PC, action: 'stop' }, { ...as, desks: run });
assert.deepStrictEqual(last(), { action: 'stop', desk: run.name }, 'stop takes no kind');
-- // Kept sign-ins (Node v0.24.0): a machine that keeps profiles starts every task's desk from the agent's profile.
++ // Kept sign-ins (Link v0.24.0): a machine that keeps profiles starts every task's desk from the agent's profile.
const keeps = fakeBot({ machines: [{ ...machines[0], computer: { ...machines[0].computer, desk: { ...machines[0].computer.desk, profiles: true } } }] });
const t3 = createOpenVibeTools({ config, fetchImpl: keeps.fetchImpl, robots: createRobots({ config, fetchImpl: keeps.fetchImpl, tokens }), computers: createComputers({ config, fetchImpl: keeps.fetchImpl, tokens }), vision: async () => ({ text: 'x', cost_usd: 0 }) });
assert.strictEqual(JSON.parse(await t3.call('list_my_computers', {}, as)).computers[0].keeps_sign_ins, true);
@@@ -311,7 +311,7 @@ const as = { requester: `user:${ME}` };
await t3.call('computer_desk', { machine: PC, action: 'start', profile: 'someone-else' }, { ...as, desks: kept });
assert.deepStrictEqual(keeps.calls.filter((c) => c.body && c.body.kind === 'desk').at(-1).body.value, { action: 'start', desk: kept.name, kind: 'browser', profile: 'agent' }, 'the agent\'s kept profile, never the model\'s choice');
assert.strictEqual(last().profile, undefined, 'a machine that keeps none is sent none');
-- // Apps on a desk (Node v0.26.0): computer_run on the task's own desk, detached; never detached on the person's screen.
++ // Apps on a desk (Link v0.26.0): computer_run on the task's own desk, detached; never detached on the person's screen.
const apps = fakeBot({ machines: [{ ...machines[0], computer: { ...machines[0].computer, terminal: { level: 'trusted', desks: true }, desk: { ...machines[0].computer.desk } } }] });
const t4 = createOpenVibeTools({ config, fetchImpl: apps.fetchImpl, robots: createRobots({ config, fetchImpl: apps.fetchImpl, tokens }), computers: createComputers({ config, fetchImpl: apps.fetchImpl, tokens }), vision: async () => ({ text: 'x', cost_usd: 0 }) });
assert.strictEqual(JSON.parse(await t4.call('list_my_computers', {}, as)).computers[0].apps_on_desk, true);
@@@ -358,7 +358,7 @@ const as = { requester: `user:${ME}` };
assert.strictEqual(deskCommands(), before, 'a bad kind never reaches the machine');
});
-- await check('computer_browser with element refs and receipts (Desks plan D3, Node v0.21.0): a ref clicks exactly that element; the result says what changed and lists the page\'s elements', async () => {
++ await check('computer_browser with element refs and receipts (Desks plan D3, Link v0.21.0): a ref clicks exactly that element; the result says what changed and lists the page\'s elements', async () => {
const bot = fakeBot({ machines: [{ id: PC, name: 'desk-pc', online: true, agent_computer: true, computer: { level: 'trusted', browser: { level: 'trusted' } } }] });
const inner = bot.fetchImpl;
const elements = Array.from({ length: 90 }, (_, i) => `e${i + 1} button "Item ${i + 1}"`);
ddiff --git a/test/desk-handoff.test.js b/test/desk-handoff.test.js
iindex a1ffb82..ad98f18 100644
---- a/test/desk-handoff.test.js
++++ b/test/desk-handoff.test.js
@@@ -82,7 +82,7 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
const spent = await t.handToPerson(ME, { machine: PC, what: 'Solve the CAPTCHA.' }, { desks: run, emit: async () => {}, waitForPerson: () => 0 });
assert.deepStrictEqual([spent.done, /already waited/.test(spent.note)], [false, true]);
assert.match((await t.handToPerson(ME, { machine: PC, what: ' ' }, { desks: run })).error, /what is the step/);
-- // A machine with one private desk (an older Node): the hand-off is to that desk.
++ // A machine with one private desk (an older Link): the hand-off is to that desk.
const old = await t.handToPerson(ME, { machine: PC, what: 'Sign in.' }, { desks: t.deskRun(), emit: async () => {}, waitForPerson: () => 20, pollMs: 5 });
assert.match(old.watch, /\?desk=private$/);
});