openvibers/openvibe.actor Commits
OpenVibe Link: the new name in pages, docs and messages (#355)
Changes
@@@ -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 aprofile 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 thepage, 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 machinerefuses 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: 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 ona machine without a desk, so it never falls back to the person's screen.@@@ -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: tokensOconst 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: tokensOconst 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: tokensOif (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: tokensOif (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 : '';@@@ -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.@@@ -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 deska = 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, BROWSERconst 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']);@@@ -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)),@@@ -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, '&')}" 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, '&')}" 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, '&')}" 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>`,});@@@ -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>@@@ -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>@@@ -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@@@ -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,@@@ -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) {@@@ -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 deskconst 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 };}@@@ -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.@@@ -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,@@@ -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*\(/;@@@ -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}"`);@@@ -82,7 +82,7 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImplconst 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$/);});
@@ -106,7 +106,7 @@ The resource index is for OpenVibe.Services on loopback and is blocked at the pu
@@ -106,7 +106,7 @@ The resource index is for OpenVibe.Services on loopback and is blocked at the pu
## Does not own
## Does not own
- Coding: **OpenVibe.Codes** (the coding-agent harness).
- 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**.
- Accounts, apps and grants: **OpenVibe.Network** and **OpenVibe.Services**.
- The tools the runtime uses: **OpenVibe.Tools**. Search: **OpenVibe.Search**.
- 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
@@ -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.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.
- **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`).
- **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.
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
- 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),
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.
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 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.
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
a machine without a desk, so it never falls back to the person's screen.
a machine without a desk, so it never falls back to the person's screen.
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'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,
* 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,
* 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
* 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 @@
@@ -16,8 +16,8 @@
* screen → { format, width, height, content_b64 } (the screen now, scaled to max_width)
* 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)
* 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)
* 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"),
* 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.
* 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']);
@@ -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 BROWSER_ACTIONS = new Set(['open', 'click', 'fill', 'press', 'back', 'read', 'close']);
const PAGE_TEXT = 3500;
const PAGE_TEXT = 3500;
const PAGE_LINKS = 25;
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).
// none: one tool result stays whole (tools.js clips results at 6000 characters).
const PAGE_TEXT_WITH_ELEMENTS = 2200;
const PAGE_TEXT_WITH_ELEMENTS = 2200;
const PAGE_ELEMENTS = 60;
const PAGE_ELEMENTS = 60;
@@ -186,14 +186,14 @@ function createComputers({ config, fetchImpl = globalThis.fetch, tokens: tokensO
@@ -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) => ({
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,
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]),
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 } : {}),
...(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 } : {}),
...(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)) } : {}),
...(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 } : {}),
...(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 } : {}),
...(m.computer.terminal && m.computer.terminal.desks === true ? { apps_on_desk: true } : {}),
folders: (m.computer.files && m.computer.files.roots) || [],
folders: (m.computer.files && m.computer.files.roots) || [],
...(m.computer.waiting ? { waiting_for_approval: m.computer.waiting } : {}),
...(m.computer.waiting ? { waiting_for_approval: m.computer.waiting } : {}),
@@ -232,7 +232,7 @@ function createComputers({ config, fetchImpl = globalThis.fetch, tokens: tokensO
@@ -232,7 +232,7 @@ function createComputers({ config, fetchImpl = globalThis.fetch, tokens: tokensO
const value = { argv };
const value = { argv };
if (cwd) value.cwd = String(cwd);
if (cwd) value.cwd = String(cwd);
if (Number.isInteger(timeoutS) && timeoutS > 0) value.timeout_s = Math.min(timeoutS, 600);
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 (desk && desk !== 'main') value.desk = desk;
if (detach) {
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' };
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
@@ -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' };
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;
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 (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' };
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}"]`;
a.selector = `[data-ov-ref="${a.ref}"]`;
@@ -345,7 +345,7 @@ function createComputers({ config, fetchImpl = globalThis.fetch, tokens: tokensO
@@ -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 === 'fill') return { done: !!d.done, chars: d.chars };
if (a.action === 'close') return { done: true };
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
// 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 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 room = elements ? PAGE_TEXT_WITH_ELEMENTS : PAGE_TEXT;
const text = typeof d.text === 'string' ? d.text : '';
const text = typeof d.text === 'string' ? d.text : '';
@@ -26,7 +26,7 @@ const KINDS = [
@@ -26,7 +26,7 @@ const KINDS = [
'install', // install a skill or connect a connector
'install', // install a skill or connect a connector
'routine', // set up a task that keeps running
'routine', // set up a task that keeps running
'money', // spend the person's money
'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.
// The plain words a person reads for each kind. Never an id, never a code: this is the sentence on the card.
@@ -39,7 +39,7 @@ const { stepFor } = require('./actions');
@@ -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,
* 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
* back, read, close; pages answer as text and links, so no vision is needed
* computer_desk(machine, action)
* 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
* 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)
* 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 = () =>
@@ -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
* 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
* 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.
* 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 = () =>
@@ -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,
* 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
* 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) {
function deskKind(run, machine, asked) {
if (asked != null) return asked; // checked where the desk starts (computers.desk)
if (asked != null) return asked; // checked where the desk starts (computers.desk)
@@ -270,7 +270,7 @@ function createOpenVibeTools({ config, fetchImpl = globalThis.fetch, now = () =>
@@ -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 } = {}) {
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);
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' };
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 desk = desks && desks.used.has(a.machine) ? desks.name : 'private';
const watch = pcs.watchUrl(a.machine, desk);
const watch = pcs.watchUrl(a.machine, desk);
const granted = waitForPerson ? waitForPerson(PERSON_WAIT_MS) : 0;
const granted = waitForPerson ? waitForPerson(PERSON_WAIT_MS) : 0;
@@ -341,7 +341,7 @@ function createOpenVibeTools({ config, fetchImpl = globalThis.fetch, now = () =>
@@ -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, 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})` });
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"' });
} 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 = () =>
@@ -352,7 +352,7 @@ function createOpenVibeTools({ config, fetchImpl = globalThis.fetch, now = () =>
const { kind, ...rest } = a;
const { kind, ...rest } = a;
const chosen = a.desk === 'private' ? kind : deskKind(desks, a.machine, kind); // private: a whole desk
const chosen = a.desk === 'private' ? kind : deskKind(desks, a.machine, kind); // private: a whole desk
a = chosen ? { ...rest, kind: chosen } : rest;
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
// browser profile kept for their agent on that machine, and saves it back when it stops. Never
// the model's choice.
// the model's choice.
delete a.profile;
delete a.profile;
@@ -421,7 +421,7 @@ const COMPUTER_TOOLS = new Set([...COMPUTER_DEFS, SCREEN_DEF, INPUT_DEF, BROWSER
@@ -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_WAIT_MS = 10 * 60_000;
const PERSON_POLL_MS = 3000;
const PERSON_POLL_MS = 3000;
const PERSON_QUIET_MS = 20_000;
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';
const AGENT_PROFILE = 'agent';
// The tools that work on a screen: their desk "private" is the task's own desk (deskRun).
// 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']);
const DESK_TOOLS = new Set(['computer_screen', 'computer_input', 'computer_browser', 'computer_desk', 'computer_run']);
@@ -190,7 +190,7 @@ function load(env = process.env) {
@@ -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
// 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
// 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: {
skills: {
mediaTenant: trim(env.ACTOR_MEDIA_TENANT || env.OV_OAUTH_CLIENT_ID || 'actor'),
mediaTenant: trim(env.ACTOR_MEDIA_TENANT || env.OV_OAUTH_CLIENT_ID || 'actor'),
placementWaitMs: Math.max(1000, int(env.ACTOR_SKILL_PLACEMENT_WAIT_MS, 12000)),
placementWaitMs: Math.max(1000, int(env.ACTOR_SKILL_PLACEMENT_WAIT_MS, 12000)),
@@ -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 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>
<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 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, '&')}" 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, '&')}" 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 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>
<button class="cm-save" type="submit">Save</button>
</form></main>`,
</form></main>`,
});
});
@@ -189,7 +189,7 @@ ${commandBox(prefill(req), today)}
@@ -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>
${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 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>`}
${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>
@@ -88,10 +88,10 @@ const TEMPLATES = [
@@ -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: [
{ 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.' },
{ 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.' },
{ 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.' },
{ 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: [
{ 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
@@ -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 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`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`<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.'],
@@ -1414,7 +1414,7 @@ ${a.available ? '' : html`<p class="muted small">${a.reason}</p>`}
@@ -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>
<p class="ag-id muted small"><code>${a.id}</code>${a.model ? html` · ${a.model}` : ''} · prices checked ${a.rate_card.verified_at}</p>
</li>`;
</li>`;
})}</ul>
})}</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>
<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>
${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>
<details class="ag-fold" id="classes"><summary>What a task needs (5 kinds)</summary>
@@ -4,7 +4,7 @@
@@ -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
* 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
* 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 }`,
* 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
* 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
* `{ error }`; `enabled` is false without Media's URL or Actor's client secret, and publishing a sandbox skill then
@@ -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 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
* 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,
@@ -16,7 +16,7 @@ const { personOf } = require('./_openvibe');
@@ -16,7 +16,7 @@ const { personOf } = require('./_openvibe');
const PERSON_WAIT_MS = 10 * 60_000;
const PERSON_WAIT_MS = 10 * 60_000;
const PERSON_POLL_MS = 3000;
const PERSON_POLL_MS = 3000;
const PERSON_QUIET_MS = 20_000;
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';
const AGENT_PROFILE = 'agent';
function deskRun(taskId = null) {
function deskRun(taskId = null) {
@@ -35,7 +35,7 @@ async function startDesk(a, person, core, ctx) {
@@ -35,7 +35,7 @@ async function startDesk(a, person, core, ctx) {
const { kind, ...rest } = a;
const { kind, ...rest } = a;
const chosen = a.desk === 'private' ? kind : deskKind(desks, a.machine, kind); // private: a whole desk
const chosen = a.desk === 'private' ? kind : deskKind(desks, a.machine, kind); // private: a whole desk
const body = chosen ? { ...rest, kind: chosen } : rest;
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.
// kept for their agent on that machine, and saves it back when it stops. Never the model's choice.
delete body.profile;
delete body.profile;
if (body.desk !== 'private' && desks && desks.keeps && desks.keeps.has(a.machine)) body.profile = AGENT_PROFILE;
if (body.desk !== 'private' && desks && desks.keeps && desks.keeps.has(a.machine)) body.profile = AGENT_PROFILE;
@@ -82,7 +82,7 @@ module.exports = {
@@ -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, 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 };
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 };
} else return { text: json({ error: 'no desk of yours runs yet: start one with computer_desk start and machine "any"' }), isError: true };
}
}
@@ -9,7 +9,7 @@
@@ -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
* 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
* 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
* 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
* 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.
* touches, and the agent's level and approvals still decide every step.
@@ -5,7 +5,7 @@
@@ -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
* 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
* (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],
* class: function, artifact [email protected],
* args: { tool, entry, input }, inputs: the listing's { media_id, sha256 } pins,
* args: { tool, entry, input }, inputs: the listing's { media_id, sha256 } pins,
@@ -118,7 +118,7 @@ const CODE_RULES = ['code.env', 'code.process', 'code.dynamic', 'code.vm', 'code
@@ -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'];
'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`
// 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 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 = /\brequire\s*\(\s*(['"])([^'"]+)\1\s*\)/g;
const REQUIRE_COMPUTED = /\brequire\s*\(\s*(?!['"])|\bmodule\s*\.\s*require\s*\(|\bcreateRequire\s*\(/;
const REQUIRE_COMPUTED = /\brequire\s*\(\s*(?!['"])|\bmodule\s*\.\s*require\s*\(|\bcreateRequire\s*\(/;
@@ -272,14 +272,14 @@ const as = { requester: `user:${ME}` };
@@ -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');
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);
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');
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 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 });
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() });
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' });
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 desk = { level: 'trusted', window_manager: 'openbox', input: true, screen: true, named: true, max_desks: 2 };
const machines = [
const machines = [
{ id: PC, name: 'desk-pc', online: true, agent_computer: true, computer: { level: 'trusted', browser: { level: 'trusted' }, desk: { ...desk, kinds: ['desktop', 'browser'] } } },
{ 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}` };
@@ -300,10 +300,10 @@ const as = { requester: `user:${ME}` };
await t.call('computer_desk', { machine: MAC, action: 'start' }, { ...as, desks: t.deskRun() });
await t.call('computer_desk', { machine: MAC, action: 'start' }, { ...as, desks: t.deskRun() });
assert.strictEqual(last().kind, 'browser');
assert.strictEqual(last().kind, 'browser');
await t.call('computer_desk', { machine: OLD, action: 'start' }, { ...as, desks: t.deskRun() });
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 });
await t.call('computer_desk', { machine: PC, action: 'stop' }, { ...as, desks: run });
assert.deepStrictEqual(last(), { action: 'stop', desk: run.name }, 'stop takes no kind');
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 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 }) });
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);
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}` };
@@ -311,7 +311,7 @@ const as = { requester: `user:${ME}` };
await t3.call('computer_desk', { machine: PC, action: 'start', profile: 'someone-else' }, { ...as, desks: kept });
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.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');
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 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 }) });
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);
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}` };
@@ -358,7 +358,7 @@ const as = { requester: `user:${ME}` };
assert.strictEqual(deskCommands(), before, 'a bad kind never reaches the machine');
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 bot = fakeBot({ machines: [{ id: PC, name: 'desk-pc', online: true, agent_computer: true, computer: { level: 'trusted', browser: { level: 'trusted' } } }] });
const inner = bot.fetchImpl;
const inner = bot.fetchImpl;
const elements = Array.from({ length: 90 }, (_, i) => `e${i + 1} button "Item ${i + 1}"`);
const elements = Array.from({ length: 90 }, (_, i) => `e${i + 1} button "Item ${i + 1}"`);
@@ -82,7 +82,7 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
@@ -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 });
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.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/);
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 });
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$/);
assert.match(old.watch, /\?desk=private$/);
});
});