openvibers/openvibe.ai Commits
OpenVibe Link: the new name in pages, docs and messages (#372)
Changes
@@@ -39,7 +39,7 @@ owns publication truth: Wiki, Blog, News, Live, … decide what to publish.key failing and publishes `ai.credential.failed` (`/api/v1/me/*`, `server/me.js`; only the person's own token orAI-site session, never a service token or `on_behalf_of`)- a person's own computer as their provider (credential provider `node`, contracts 0.220.0): a model on their own-- machine (Ollama, LM Studio, llama.cpp through the OpenVibe Node), reached through OpenVibe.Bot with `bot.machine.ai`++ machine (Ollama, LM Studio, llama.cpp through the OpenVibe Link), reached through OpenVibe.Bot with `bot.machine.ai`for that person only, free and with no key (`server/machines.js`; Bot on this host by default, `OV_BOT_INTERNAL_URL`);machine `any` (contracts 0.221.0) uses the fastest of their machines that is on with the model, then the next- the `ai.run.queued|succeeded|failed|cached` events and the `ai.preferences` / `ai.usage_summary`@@@ -392,7 +392,7 @@ receipt and in the answer's `x_openvibe`.Memory M2a (spec `research-2026-10-30/memory-m2-spec.md`): a person's own notes, sealed with `AI_MEMORY_KEY`(AES-256-GCM, AAD `memory|<owner>|<id>|<scope>`, `m1:`), deduped by an HMAC of the normalized text (HKDF-SHA256, info`memory-mac`), and scoped by `user:<usr_…>` on every query. The server redacts every item again with--`openvibe-sdk/redact` (the Node's shapes and corpus) and refuses an item that is only a secret; what an agent writes is++`openvibe-sdk/redact` (the Link's shapes and corpus) and refuses an item that is only a secret; what an agent writes isa suggestion, and every item a service writes passes `noLooks`. An agent's search is active-only, at most 8 items, inthe scopes its token's grant names (`user` always; `project:*` when the grant lists it), and leaves a `memory_reads`row with ids, never text. The profile cache rebuilds only when its `items_hash` moves, at most once an hour, in-process@@@ -818,7 +818,7 @@ logged or stored.### The CLI--`openvibe-node exchange serve --model qwen3:32b --public-url https://…/v1` (OpenVibe.Node) serves the local model++`openvibe-node exchange serve --model qwen3:32b --public-url https://…/v1` (OpenVibe Link) serves the local modelthrough an OpenAI-compatible listener and registers it on the existing endpoint-seller API with the person's`OPENVIBE_DEVELOPER_KEY`; Ctrl-C removes the offer. See that repo's README.@@@ -24,7 +24,7 @@ This document records the current state of the repositories that implement (or w| OpenVibe.Exchange | `main` | `b562b0fd7d018dfa036219585a7d591330a50f3a` | 0.1.0 | v0.235.0 | v0.82.0 | v4.109.2 || OpenVibe.Host | `main` | `46421d082f7d38c370cd9050df2135d7800e24f8` | 0.3.0 | v0.272.0 | v0.82.0 | v4.109.2 || OpenVibe.Run | `main` | `1bacc978ab25d7294a6f5f75a339645b47fe6406` | 0.1.0 | v0.250.0 | v0.82.0 | v4.109.2 |--| OpenVibe.Node | `main` | `c8564f0b58f567dd909daaadd2f2a7b91f29a7a7` | v0.38.0 (tag) | N/A (Go) | N/A | N/A |++| OpenVibe Link | `main` | `c8564f0b58f567dd909daaadd2f2a7b91f29a7a7` | v0.38.0 (tag) | N/A (Go) | N/A | N/A || OpenVibe.Watch | `main` | `52bfa57a83fdf3cb1b0879071c02ae6f5c601fa4` | 0.1.0 | v0.222.0 | v0.82.0 | v4.109.2 || OpenVibe.Sources | `main` | `7b40905dd943a7ed33ed7afe4ed637f547f48df5` | 0.1.0 | v0.224.0 | v0.82.0 | v4.109.2 || OpenVibe.Codes | `main` | `f955eebafd2d71c5660037cfe5f98631d4160c65` | 0.7.0 | v0.261.0 | v0.82.0 | v4.109.2 |@@@ -145,7 +145,7 @@ Notes:| **OpenVibe.Bench** | Pinned evaluation rounds, Elo/Pareto/cost-per-solved rankings, blind judgments, MCP (`docs/api.md`, `docs/benchmarks.md`, `server/pick/pick.js`, `server/rating/*`). | Model-variant family schemas, throughput/TTFT quantitative evidence types, inference-specific benchmark gates. || **OpenVibe.Report** | Time-series/aggregation views, consumes Bench AI boards. | Supply-side profitability summaries tied to the new inference subledger; real model-catalog analytics. || **OpenVibe.Host / Run** | Worker/deployment orchestration, async jobs. | Pinned GGUF/vLLM/SGLang inference worker images, GPU rental orchestration with budget gates. |--| **OpenVibe.Node / Bot** | Owner-machine inference path in AI (`server/machines.js`). | Commercial node-pool isolation, DNS-rebinding/SSRF tests for node routing. |++| **OpenVibe Link / Bot** | Owner-machine inference path in AI (`server/machines.js`). | Commercial node-pool isolation, DNS-rebinding/SSRF tests for node routing. || **OpenVibe.Exchange** | v1 free/swap-only marketplace (`README.md`, `STATUS.json`, migrations). | Paid listing v2; inference API/model-pack marketplace (explicitly out of scope per D6). || **OpenVibe.Codes / Actor** | Coding harness, provider integrations, proof-of-work. | Hosted runner completion; integration with new catalog/aliases. || **OpenVibe.Watch / Sources** | Market-data sourcing, rate/model change alerts. | Official provider price/terms ingestion for inference providers; stale-data warnings. |@@@ -1,17 +1,17 @@# Community AI offers ("Offer to the network")The design's community machines (`EXCHANGE-REMAKE-2026-11-06.md`, plan §3, stage 4): a person turns on **Offer to the--network** in OpenVibe.Node's `ai` plugin and their local models (Ollama, LM Studio, llama.cpp) or GPU become capacity++network** in OpenVibe Link's `ai` plugin and their local models (Ollama, LM Studio, llama.cpp) or GPU become capacityOpenVibe.AI can place inference on. It is opt-in twice — the owner turns the offer on, and the caller asks for communitycapacity per request — because the machine sees the prompt.## The chain--1. **Node** (`plugins/ai`, 0.3.0) advertises `status.capabilities.ai.offer`: `enabled`, `models`, `hours`,++1. **Link** (`plugins/ai`, 0.3.0) advertises `status.capabilities.ai.offer`: `enabled`, `models`, `hours`,`concurrency`, `price` (credits per million in/out), `audience`, `open_now`, `ready`. It enforces `enabled`, `hours`,`models` and `concurrency` on the machine, and accepts a community command only as a `community/<model>` request,which it strips before the local server sees it.--2. **Bot** stores the Node's capabilities and reports every machine with an enabled offer to Network:++2. **Bot** stores the Link's capabilities and reports every machine with an enabled offer to Network:`POST /internal/ai/offers/report` (source `bot`, `network.resource.report`). A community run reaches a machine evenwith "Let my AI use this computer's models" off, because the offer itself is the opt-in; the machine still onlyaccepts `kind: ai`.@@@ -36,7 +36,7 @@ that answer carries no community label.## Community endpoints--The design's SDK gap: any server that speaks the OpenAI Chat Completions API can sell, not only a Node machine. The++The design's SDK gap: any server that speaks the OpenAI Chat Completions API can sell, not only a machine running Link. Theseller registers it with their own Network developer key on the Exchange's public API host, documented in`/openapi.json`:@@@ -69,7 +69,7 @@ service token is refused.OpenVibe as it is (put it behind an unguessable base path if you want it hard to find);- the seller's declared `concurrency` is enforced in-process: over it the call is busy and the router falls back.--**Same trust as a Node machine.** The endpoint joins `server/community/index.js`'s offer list, so it gets the same three++**Same trust as a machine running Link.** The endpoint joins `server/community/index.js`'s offer list, so it gets the same threecanaries, the same model fingerprint (the `model` its answer reports must be the declared one), the same ~1% spotre-runs against a verified peer, the free tier only until verified, and the self-dealing rule (the owner, a mergedaccount, or the project that registered it earns 0). Its answers carry `provider: community-endpoint:<id>`,@@@ -1,6 +1,6 @@# Memory M5: services, sharing and the portable file--This is the fifth stage of the Memory plan (`../../OpenVibe_Memory_Plan_2026-10-30.md`). M1–M4 are the Node readers,++This is the fifth stage of the Memory plan (`../../OpenVibe_Memory_Plan_2026-10-30.md`). M1–M4 are the Link readers,the store, the cloud imports, the write-back and the agent API. M5 adds three things to the same store:- the memory-service adapters (mem0, Letta, Zep) with the person's own key;@@@ -102,7 +102,7 @@ forwards the person's token, the method, the body and `If-Match` to AI, and keepanother account without carrying anyone's identity, and a profile is derived from the notes and rebuilt.- The round trip is proved in `test/memory-m5.test.js`: export A's store, import it as B, and compare text, kind,scope and source; importing the same file again changes nothing.--- The Node writes and reads the same file (`openvibe-node memory export|import`); Codes offers a download and an++- The Link writes and reads the same file (`openvibe-node memory export|import`); Codes offers a download and animport form. The format is the contract, not either client.## Tests@@@ -9,7 +9,7 @@ API both use one definition of private here; nothing in Exchange re-implements i| label | when || --- | --- |--| `private` | the provider's own terms say zero retention **and** no training, or the offer is a person's own machine (a Node credential), or it is the host's own deployment (`metadata.local`). |++| `private` | the provider's own terms say zero retention **and** no training, or the offer is a person's own machine (a Link credential), or it is the host's own deployment (`metadata.local`). || `community` | the offer runs on a community machine (a person's hardware, Node's "Offer to the network"): that machine sees the prompt. || `anonymized` | everything else, including a provider that declares no policy (`known: false` says so). |@@@ -2,7 +2,7 @@/*** The public seller API (design EXCHANGE-REMAKE-2026-11-06, "Gap found while building the SDK"): a standalone* OpenAI-compatible server can sell to the Exchange with the seller's own Network developer key, not only an-- * OpenVibe Node machine. Served on the Exchange's public API host (api.openvibe.exchange) and documented in++ * OpenVibe Link machine. Served on the Exchange's public API host (api.openvibe.exchange) and documented in* /openapi.json.** POST /api/v1/exchange/seller/offers ai.app.run (a developer key), or a person's own token@@@ -187,7 +187,7 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys// The budget of a Codes run token (Codes C3): Codes registers it when it mints the token.if (runBudgets) app.use(require('./api/run-budgets').runBudgetsRouter({ runBudgets, auth, log, sendError: (res, err, ctx) => sendError(res, err, ctx, log) }));// Community capacity (design: "Offer to the network"): the public offers Exchange lists and the seller's /sell-- // dashboard, an API for Exchange. Nothing here writes an offer; the offer belongs to Network and the Node owner.++ // dashboard, an API for Exchange. Nothing here writes an offer; the offer belongs to Network and the Link owner.if (community) app.use('/api/v1', require('./api/community').communityRouter({ community, auth, log }));app.use(adminRouter({ config, db, registry, pool, quotas, cache, runs, auth, settings, ops, log }));// The operator console (/console, /auth/*): Network staff, server-rendered, no scripts (server/console).@@@ -1,7 +1,7 @@'use strict';/*** Community endpoints (design EXCHANGE-REMAKE-2026-11-06, "Gap found while building the SDK"): a standalone-- * OpenAI-compatible server can sell to the Exchange, not only an OpenVibe Node machine. A seller registers its own++ * OpenAI-compatible server can sell to the Exchange, not only an OpenVibe Link machine. A seller registers its own* https address with their Network developer key on the public seller API (server/api/seller.js); AI stores the* offer here, reads it beside Network's Node offers, and reaches it through this module only.*@@@ -17,7 +17,7 @@* - the request is built fresh: the model, the conversation, the output limit and the tools. Never the buyer's* subject, on_behalf_of, run id, key, cookies or forwarding headers.*-- * Trust is the Node machine's (server/community/index.js): the same canaries, the same model fingerprint, the same++ * Trust is the Link machine's (server/community/index.js): the same canaries, the same model fingerprint, the same* ~1% spot re-runs, the free tier only until verified, and the self-dealing rule. An endpoint is a community source:* it sees the prompt, and only requests that opt in ever reach it.*/@@@ -142,7 +142,7 @@ function createCommunity({ db, config, env = process.env, machines = null, endpoif (!identityTokens && secret && network) {identityTokens = createServiceTokenClient({ tokenUrl: `${network}/oauth/token`, clientId, clientSecret: secret, scope: { 'openvibe.network': 'identity.subject.resolve' }, fetch: fetchImpl });}-- // A Node offer comes from Network and is reached through Bot; a community endpoint is registered here and reached++ // A Link offer comes from Network and is reached through Bot; a community endpoint is registered here and reached// through its own https address. Each works without the other.const registryEnabled = Boolean(cfg.enabled && db && machines && network && registryTokens);const endpointsEnabled = Boolean(cfg.enabled && db && endpoints && endpoints.enabled);@@@ -438,7 +438,7 @@ function createCommunity({ db, config, env = process.env, machines = null, endpoconst wanted = ctx.communityOfferId ? String(ctx.communityOfferId) : null;const out = [];for (const offer of list) {-- // The Node's own ready flag may be a few minutes stale (status is sent on changes); the hours are read here++ // The Link's own ready flag may be a few minutes stale (status is sent on changes); the hours are read here// too, so a request outside the owner's window never reaches the machine.if (offer.health === 'down' || offer.ready !== true || !withinHours(offer, clock.now())) continue;if (wanted && String(offer.offer_id) !== wanted) continue;@@@ -46,7 +46,7 @@ function dataPolicyOf(record) {* credential the person's own key's descriptor, when the answer came from their key or their machine*/function labelFor({ record = null, credential = null } = {}) {-- // A person's own machine (a Node credential): the prompt never left their hardware.++ // A person's own machine (a Link credential): the prompt never left their hardware.if (credential && credential.provider === 'node') {return { label: 'private', retention: 'none', training: false, region: null, trust: 'own_machine', known: true, source: 'your own machine' };}@@@ -3,12 +3,12 @@* A person's own machine as their model (AI on your own machines: contracts 0.219.0 bot.command@1 kind ai and the* capability bot.machine.ai; 0.220.0 the credential provider node). One chat completion goes through OpenVibe.Bot,* POST /api/v1/machines/:id/commands with kind ai, as OpenVibe.AI acting for the person (X-OV-Subject), to the machine's-- * own model server (OpenVibe Node's ai plugin: Ollama, LM Studio, llama.cpp). Bot checks that the machine is theirs and++ * own model server (OpenVibe Link's ai plugin: Ollama, LM Studio, llama.cpp). Bot checks that the machine is theirs and* that they turned on "Let my AI use this computer's models"; the prompt never reaches anyone else's machine.** Machine `any` (contracts 0.221.0): whichever of the person's own machines is on, lets their AI use its models and* offers the model, from Bot's list for OpenVibe.AI (GET /api/v1/machines, only those with the switch on). The fastest-- * at that model goes first (computer.ai_speed, OpenVibe.Node v0.29.0), then the others; one that is busy, away or fails++ * at that model goes first (computer.ai_speed, OpenVibe Link v0.29.0), then the others; one that is busy, away or fails* hands over to the next. Never anyone else's machine, never another provider.** Off without OV_BOT_INTERNAL_URL or a client secret: a credential with provider node is then refused, never sent@@@ -17,7 +17,7 @@** Tools (contracts 0.277.0, plan L55 C3): an agent's loop on the person's own model. The command carries the tools and* the calls already made; the machine hands back the calls its model asked for (message.tool_calls) and runs none. Only-- * a machine whose Node says computer.ai.tools (v0.36.0) is asked: with machine any the others are passed over, and a++ * a machine whose Link says computer.ai.tools (v0.36.0) is asked: with machine any the others are passed over, and a* named machine without it is refused with what to update.*/const { AiError } = require('./util');@@@ -78,14 +78,14 @@ function createMachineModels({ config, env = process.env, fetchImpl = globalThisconst tools = Array.isArray(args.tools) && args.tools.length > 0;if (machine !== ANY) {if (tools) {-- // A Node older than v0.36.0 would drop the tools and answer in words: say what to update instead.++ // A Link older than v0.36.0 would drop the tools and answer in words: say what to update instead.const m = (await listed(subject, signal)).find((x) => x && x.id === machine);-- if (m && m.computer && m.computer.ai && m.computer.ai.tools !== true) throw new AiError(422, 'machine.no_tools', 'this computer\'s OpenVibe Node cannot pass tools to its model yet: update it to v0.36.0 or newer');++ if (m && m.computer && m.computer.ai && m.computer.ai.tools !== true) throw new AiError(422, 'machine.no_tools', 'this computer\'s OpenVibe Link cannot pass tools to its model yet: update it to v0.36.0 or newer');}return { ...(await once(machine, args)), machine, tried: [] };}const ids = (await candidates(subject, model, signal, { tools })).slice(0, MAX_TRIED);-- if (!ids.length) throw new AiError(503, 'machine.none', `none of the person's own machines is on with ${String(model).slice(0, 120)} and lets their AI use it${tools ? ', with an OpenVibe Node that can pass tools (v0.36.0 or newer)' : ''}`);++ if (!ids.length) throw new AiError(503, 'machine.none', `none of the person's own machines is on with ${String(model).slice(0, 120)} and lets their AI use it${tools ? ', with an OpenVibe Link that can pass tools (v0.36.0 or newer)' : ''}`);const tried = [];for (const id of ids) {try {@@@ -101,7 +101,7 @@ function createMachineModels({ config, env = process.env, fetchImpl = globalThisasync function once(machine, { subject, model, messages, maxTokens = null, temperature = null, signal = null, timeoutMs = 120000, tools = null, toolChoice = null, community = false }) {// The ack wire has no extra field for "this is the machine's offer to the network" (bot.command@1's value is-- // closed), so the model carries the community/ prefix; the Node ai plugin strips it before its model server++ // closed), so the model carries the community/ prefix; the Link ai plugin strips it before its model server// sees it and enforces the offer. The prefix never reaches the local model.const asked = community && !String(model).startsWith('community/') ? `community/${model}` : model;const value = { model: asked, messages, ...(maxTokens ? { max_tokens: Math.min(32768, Math.max(1, Math.round(maxTokens))) } : {}), ...(temperature != null ? { temperature: Math.min(2, Math.max(0, Number(temperature))) } : {}),@@@ -136,7 +136,7 @@ function createMachineModels({ config, env = process.env, fetchImpl = globalThis/*** The person's computers that let their AI use their models, for their own settings page: name, whether it is on,-- * its models and whether its Node can pass tools. [] when Bot cannot be reached.++ * its models and whether its Link can pass tools. [] when Bot cannot be reached.*/async function mine(subject, signal = null) {if (!enabled || !SUBJECT_RE.test(String(subject))) return [];@@@ -483,7 +483,7 @@ function createMemory({ db, config, clock = { now: () => Date.now() }, log = con: String(req.principal.sub || '').replace(/^svc:/, '');const source = validateSource(serviceCaller ? serviceSource : (raw.source || 'person'));// A service is an agent: what it writes is a suggestion, stored as written. A person may mark their own item-- // as AI-produced too (the Node imports what a coding tool wrote).++ // as AI-produced too (the Link imports what a coding tool wrote).const aiMade = serviceCaller || raw.no_looks === true || AI_SOURCES.has(source);let text = clean;text = String(text == null ? '' : text);@@@ -634,7 +634,7 @@ const sellerEndpointInput = {};route('post', '/exchange/seller/offers', {operationId: 'createSellerEndpointOffer', summary: 'Register a standalone OpenAI-compatible server to sell', tags: ['exchange'], security: SERVICE_OR_APP,-- description: 'ai.app.run (a Network developer key), or a person\'s own token; a first-party service token is refused. The owner is the person the key acts for: an app\'s project owner, or the person themselves. The router reaches base_url only through the egress guard: https, a public DNS answer, never an IP literal, a timeout, a response byte cap, and never the buyer\'s identity in a header. A new endpoint serves the free tier only until its canaries, model fingerprint and spot re-runs verify it, exactly as a Node machine does; a request on the seller\'s own account is self-served and earns 0. An answer from it is labelled a community endpoint on the receipt (provider community-endpoint:<id>, provider_access COMMUNITY_ENDPOINT) and only requests that opt in to community capacity reach it.',++ description: 'ai.app.run (a Network developer key), or a person\'s own token; a first-party service token is refused. The owner is the person the key acts for: an app\'s project owner, or the person themselves. The router reaches base_url only through the egress guard: https, a public DNS answer, never an IP literal, a timeout, a response byte cap, and never the buyer\'s identity in a header. A new endpoint serves the free tier only until its canaries, model fingerprint and spot re-runs verify it, exactly as a machine running Link does; a request on the seller\'s own account is self-served and earns 0. An answer from it is labelled a community endpoint on the receipt (provider community-endpoint:<id>, provider_access COMMUNITY_ENDPOINT) and only requests that opt in to community capacity reach it.',requestBody: body(sellerEndpointInput),responses: { 201: json('The registered offer', { type: 'object', required: ['offer'], properties: { offer: sellerEndpointOffer } }), 409: problem, 422: problem, ...errors([401, 403, 503]) },});@@@ -18,7 +18,7 @@* POST /you/computer a model on your own computer (model; any of your computers)* POST /you/computer/remove forget it*-- * Your computer (plan L55 C3): a model the person runs at home, reached through OpenVibe.Bot and their OpenVibe Node,++ * Your computer (plan L55 C3): a model the person runs at home, reached through OpenVibe.Bot and their OpenVibe Link,* kept here like a key but with no key: the model, and any of their computers that lets their AI use its models. An* app uses it only when the person allows that app below (provider node), and it never stands in for a key.*/@@@ -145,10 +145,10 @@ function allowRequest(query) {function computerSection(pc, machines) {const models = [...new Set(machines.flatMap((m) => m.models))].slice(0, 60);const onNow = machines.filter((m) => m.online);-- const line = (m) => `<li><b>${esc(m.name)}</b>${m.online ? '' : ' <span class="you-pill">away</span>'}${m.models.length ? ` · ${esc(m.models.slice(0, 6).join(', '))}${m.models.length > 6 ? ` and ${m.models.length - 6} more` : ''}` : ' · no models listed'}${m.tools ? '' : ' · <span class="you-pill">update OpenVibe Node to v0.36.0 for agents</span>'}</li>`;++ const line = (m) => `<li><b>${esc(m.name)}</b>${m.online ? '' : ' <span class="you-pill">away</span>'}${m.models.length ? ` · ${esc(m.models.slice(0, 6).join(', '))}${m.models.length > 6 ? ` and ${m.models.length - 6} more` : ''}` : ' · no models listed'}${m.tools ? '' : ' · <span class="you-pill">update OpenVibe Link to v0.36.0 for agents</span>'}</li>`;const where = machines.length? `<ul class="you-pcs">${machines.map(line).join('')}</ul>`-- : '<p class="you-empty">None of your computers lets OpenVibe use its models yet. On the computer, run a model server (Ollama, LM Studio or llama.cpp) and OpenVibe Node with <code>--ai</code>, then turn on “Let my AI use this computer’s models” at <a href="https://openvibe.bot/machines">openvibe.bot/machines</a>.</p>';++ : '<p class="you-empty">None of your computers lets OpenVibe use its models yet. On the computer, run a model server (Ollama, LM Studio or llama.cpp) and OpenVibe Link with <code>--ai</code>, then turn on “Let my AI use this computer’s models” at <a href="https://openvibe.bot/machines">openvibe.bot/machines</a>.</p>';const saved = pc ? `<ul class="you-keys"><li class="you-key"><div class="you-key-t"><b>Any of your computers</b><span class="you-pill${onNow.some((m) => m.models.includes(pc.model)) ? ' is-ok' : ''}">${esc(pc.model || 'no model')}</span></div><div class="you-acts"><form method="post" action="/you/computer/remove"><button class="you-btn" type="submit">Remove</button></form></div><div class="you-key-n">${onNow.some((m) => m.models.includes(pc.model)) ? 'On now, with this model' : 'None of your computers with this model is on right now'}${pc.last_used_at ? ` · used ${esc(ago(pc.last_used_at))}` : ''}</div></li></ul>` : '';@@@ -2,7 +2,7 @@// Community endpoints (the design's SDK gap): a standalone OpenAI-compatible server is registered by its own https// address, and AI reaches it only through the egress guard. The tests are the guard's own: https only, no IP literal// (127.0.0.1, 10/8, 169.254.169.254, [::1], a NAT64 spelling), no private DNS answer, no redirect to one; a timeout and--// a response cap; and never the buyer's identity in a header. Then the same trust as a Node machine: the canaries, the++// a response cap; and never the buyer's identity in a header. Then the same trust as a machine running Link: the canaries, the// model fingerprint, the free tier only until verified, and self-dealing earns 0.const assert = require('assert');const { boot, request, token, suite, ALL } = require('./helpers');@@@ -188,7 +188,7 @@ t.test('machine any (contracts 0.221.0): the fastest of the person\'s own machinlisted = [];await assert.rejects(m.chat({ machine: 'any', subject: FRAN, model: 'llama3.1:8b', messages: [] }), (e) => e.status === 503 && e.code === 'machine.none' && /is on with/.test(e.message));await assert.rejects(m.chat({ machine: 'anyone', subject: FRAN, model: 'm', messages: [] }), (e) => e.status === 422);-- // Tools (contracts 0.277.0): only a machine whose Node says it can pass them; a named one without it is refused.++ // Tools (contracts 0.277.0): only a machine whose Link says it can pass them; a named one without it is refused.listed = list.map((x) => (x.id === id('CC') ? { ...x, computer: { ...x.computer, ai: { ...x.computer.ai, tools: true } } } : x));busy = new Set();asked.length = 0;@@@ -109,7 +109,7 @@ t.test('your computer (L55 C3): a model at home, no key; an app reaches it onlylet r = await page('/you', { headers: { Cookie: cookie } });assert.ok(r.text.includes('<h2 id="you-pc">Your computer</h2>'));assert.ok(r.text.includes('<b>Desk PC</b> · qwen3:8b, llama3.1:8b') && r.text.includes('<b>Old laptop</b> <span class="you-pill">away</span>'), 'its computers and their models');-- assert.ok(r.text.includes('update OpenVibe Node to v0.36.0'), 'a Node that cannot pass tools says what to update');++ assert.ok(r.text.includes('update OpenVibe Link to v0.36.0'), 'a Link that cannot pass tools says what to update');assert.ok(r.text.includes('<option value="qwen3:8b">'), 'the models to pick from');assert.ok(flash(await post('/you/computer', { model: 'two words' })).error, 'a model name has no spaces');r = await post('/you/computer', { model: 'qwen3:8b' });
@@ -39,7 +39,7 @@ owns publication truth: Wiki, Blog, News, Live, … decide what to publish.
@@ -39,7 +39,7 @@ owns publication truth: Wiki, Blog, News, Live, … decide what to publish.
key failing and publishes `ai.credential.failed` (`/api/v1/me/*`, `server/me.js`; only the person's own token or
key failing and publishes `ai.credential.failed` (`/api/v1/me/*`, `server/me.js`; only the person's own token or
AI-site session, never a service token or `on_behalf_of`)
AI-site session, never a service token or `on_behalf_of`)
- a person's own computer as their provider (credential provider `node`, contracts 0.220.0): a model on their own
- a person's own computer as their provider (credential provider `node`, contracts 0.220.0): a model on their own
- machine (Ollama, LM Studio, llama.cpp through the OpenVibe Node), reached through OpenVibe.Bot with `bot.machine.ai`
+ machine (Ollama, LM Studio, llama.cpp through the OpenVibe Link), reached through OpenVibe.Bot with `bot.machine.ai`
for that person only, free and with no key (`server/machines.js`; Bot on this host by default, `OV_BOT_INTERNAL_URL`);
for that person only, free and with no key (`server/machines.js`; Bot on this host by default, `OV_BOT_INTERNAL_URL`);
machine `any` (contracts 0.221.0) uses the fastest of their machines that is on with the model, then the next
machine `any` (contracts 0.221.0) uses the fastest of their machines that is on with the model, then the next
- the `ai.run.queued|succeeded|failed|cached` events and the `ai.preferences` / `ai.usage_summary`
- the `ai.run.queued|succeeded|failed|cached` events and the `ai.preferences` / `ai.usage_summary`
@@ -392,7 +392,7 @@ receipt and in the answer's `x_openvibe`.
@@ -392,7 +392,7 @@ receipt and in the answer's `x_openvibe`.
Memory M2a (spec `research-2026-10-30/memory-m2-spec.md`): a person's own notes, sealed with `AI_MEMORY_KEY`
Memory M2a (spec `research-2026-10-30/memory-m2-spec.md`): a person's own notes, sealed with `AI_MEMORY_KEY`
(AES-256-GCM, AAD `memory|<owner>|<id>|<scope>`, `m1:`), deduped by an HMAC of the normalized text (HKDF-SHA256, info
(AES-256-GCM, AAD `memory|<owner>|<id>|<scope>`, `m1:`), deduped by an HMAC of the normalized text (HKDF-SHA256, info
`memory-mac`), and scoped by `user:<usr_…>` on every query. The server redacts every item again with
`memory-mac`), and scoped by `user:<usr_…>` on every query. The server redacts every item again with
-`openvibe-sdk/redact` (the Node's shapes and corpus) and refuses an item that is only a secret; what an agent writes is
+`openvibe-sdk/redact` (the Link's shapes and corpus) and refuses an item that is only a secret; what an agent writes is
a suggestion, and every item a service writes passes `noLooks`. An agent's search is active-only, at most 8 items, in
a suggestion, and every item a service writes passes `noLooks`. An agent's search is active-only, at most 8 items, in
the scopes its token's grant names (`user` always; `project:*` when the grant lists it), and leaves a `memory_reads`
the scopes its token's grant names (`user` always; `project:*` when the grant lists it), and leaves a `memory_reads`
row with ids, never text. The profile cache rebuilds only when its `items_hash` moves, at most once an hour, in-process
row with ids, never text. The profile cache rebuilds only when its `items_hash` moves, at most once an hour, in-process
@@ -818,7 +818,7 @@ logged or stored.
@@ -818,7 +818,7 @@ logged or stored.
### The CLI
### The CLI
-`openvibe-node exchange serve --model qwen3:32b --public-url https://…/v1` (OpenVibe.Node) serves the local model
+`openvibe-node exchange serve --model qwen3:32b --public-url https://…/v1` (OpenVibe Link) serves the local model
through an OpenAI-compatible listener and registers it on the existing endpoint-seller API with the person's
through an OpenAI-compatible listener and registers it on the existing endpoint-seller API with the person's
`OPENVIBE_DEVELOPER_KEY`; Ctrl-C removes the offer. See that repo's README.
`OPENVIBE_DEVELOPER_KEY`; Ctrl-C removes the offer. See that repo's README.
@@ -24,7 +24,7 @@ This document records the current state of the repositories that implement (or w
@@ -24,7 +24,7 @@ This document records the current state of the repositories that implement (or w
| OpenVibe.Exchange | `main` | `b562b0fd7d018dfa036219585a7d591330a50f3a` | 0.1.0 | v0.235.0 | v0.82.0 | v4.109.2 |
| OpenVibe.Exchange | `main` | `b562b0fd7d018dfa036219585a7d591330a50f3a` | 0.1.0 | v0.235.0 | v0.82.0 | v4.109.2 |
| OpenVibe.Host | `main` | `46421d082f7d38c370cd9050df2135d7800e24f8` | 0.3.0 | v0.272.0 | v0.82.0 | v4.109.2 |
| OpenVibe.Host | `main` | `46421d082f7d38c370cd9050df2135d7800e24f8` | 0.3.0 | v0.272.0 | v0.82.0 | v4.109.2 |
| OpenVibe.Run | `main` | `1bacc978ab25d7294a6f5f75a339645b47fe6406` | 0.1.0 | v0.250.0 | v0.82.0 | v4.109.2 |
| OpenVibe.Run | `main` | `1bacc978ab25d7294a6f5f75a339645b47fe6406` | 0.1.0 | v0.250.0 | v0.82.0 | v4.109.2 |
-| OpenVibe.Node | `main` | `c8564f0b58f567dd909daaadd2f2a7b91f29a7a7` | v0.38.0 (tag) | N/A (Go) | N/A | N/A |
+| OpenVibe Link | `main` | `c8564f0b58f567dd909daaadd2f2a7b91f29a7a7` | v0.38.0 (tag) | N/A (Go) | N/A | N/A |
| OpenVibe.Watch | `main` | `52bfa57a83fdf3cb1b0879071c02ae6f5c601fa4` | 0.1.0 | v0.222.0 | v0.82.0 | v4.109.2 |
| OpenVibe.Watch | `main` | `52bfa57a83fdf3cb1b0879071c02ae6f5c601fa4` | 0.1.0 | v0.222.0 | v0.82.0 | v4.109.2 |
| OpenVibe.Sources | `main` | `7b40905dd943a7ed33ed7afe4ed637f547f48df5` | 0.1.0 | v0.224.0 | v0.82.0 | v4.109.2 |
| OpenVibe.Sources | `main` | `7b40905dd943a7ed33ed7afe4ed637f547f48df5` | 0.1.0 | v0.224.0 | v0.82.0 | v4.109.2 |
| OpenVibe.Codes | `main` | `f955eebafd2d71c5660037cfe5f98631d4160c65` | 0.7.0 | v0.261.0 | v0.82.0 | v4.109.2 |
| OpenVibe.Codes | `main` | `f955eebafd2d71c5660037cfe5f98631d4160c65` | 0.7.0 | v0.261.0 | v0.82.0 | v4.109.2 |
@@ -145,7 +145,7 @@ Notes:
@@ -145,7 +145,7 @@ Notes:
| **OpenVibe.Bench** | Pinned evaluation rounds, Elo/Pareto/cost-per-solved rankings, blind judgments, MCP (`docs/api.md`, `docs/benchmarks.md`, `server/pick/pick.js`, `server/rating/*`). | Model-variant family schemas, throughput/TTFT quantitative evidence types, inference-specific benchmark gates. |
| **OpenVibe.Bench** | Pinned evaluation rounds, Elo/Pareto/cost-per-solved rankings, blind judgments, MCP (`docs/api.md`, `docs/benchmarks.md`, `server/pick/pick.js`, `server/rating/*`). | Model-variant family schemas, throughput/TTFT quantitative evidence types, inference-specific benchmark gates. |
| **OpenVibe.Report** | Time-series/aggregation views, consumes Bench AI boards. | Supply-side profitability summaries tied to the new inference subledger; real model-catalog analytics. |
| **OpenVibe.Report** | Time-series/aggregation views, consumes Bench AI boards. | Supply-side profitability summaries tied to the new inference subledger; real model-catalog analytics. |
| **OpenVibe.Host / Run** | Worker/deployment orchestration, async jobs. | Pinned GGUF/vLLM/SGLang inference worker images, GPU rental orchestration with budget gates. |
| **OpenVibe.Host / Run** | Worker/deployment orchestration, async jobs. | Pinned GGUF/vLLM/SGLang inference worker images, GPU rental orchestration with budget gates. |
-| **OpenVibe.Node / Bot** | Owner-machine inference path in AI (`server/machines.js`). | Commercial node-pool isolation, DNS-rebinding/SSRF tests for node routing. |
+| **OpenVibe Link / Bot** | Owner-machine inference path in AI (`server/machines.js`). | Commercial node-pool isolation, DNS-rebinding/SSRF tests for node routing. |
| **OpenVibe.Exchange** | v1 free/swap-only marketplace (`README.md`, `STATUS.json`, migrations). | Paid listing v2; inference API/model-pack marketplace (explicitly out of scope per D6). |
| **OpenVibe.Exchange** | v1 free/swap-only marketplace (`README.md`, `STATUS.json`, migrations). | Paid listing v2; inference API/model-pack marketplace (explicitly out of scope per D6). |
| **OpenVibe.Codes / Actor** | Coding harness, provider integrations, proof-of-work. | Hosted runner completion; integration with new catalog/aliases. |
| **OpenVibe.Codes / Actor** | Coding harness, provider integrations, proof-of-work. | Hosted runner completion; integration with new catalog/aliases. |
| **OpenVibe.Watch / Sources** | Market-data sourcing, rate/model change alerts. | Official provider price/terms ingestion for inference providers; stale-data warnings. |
| **OpenVibe.Watch / Sources** | Market-data sourcing, rate/model change alerts. | Official provider price/terms ingestion for inference providers; stale-data warnings. |
@@ -1,17 +1,17 @@
@@ -1,17 +1,17 @@
# Community AI offers ("Offer to the network")
# Community AI offers ("Offer to the network")
The design's community machines (`EXCHANGE-REMAKE-2026-11-06.md`, plan §3, stage 4): a person turns on **Offer to the
The design's community machines (`EXCHANGE-REMAKE-2026-11-06.md`, plan §3, stage 4): a person turns on **Offer to the
-network** in OpenVibe.Node's `ai` plugin and their local models (Ollama, LM Studio, llama.cpp) or GPU become capacity
+network** in OpenVibe Link's `ai` plugin and their local models (Ollama, LM Studio, llama.cpp) or GPU become capacity
OpenVibe.AI can place inference on. It is opt-in twice — the owner turns the offer on, and the caller asks for community
OpenVibe.AI can place inference on. It is opt-in twice — the owner turns the offer on, and the caller asks for community
capacity per request — because the machine sees the prompt.
capacity per request — because the machine sees the prompt.
## The chain
## The chain
-1. **Node** (`plugins/ai`, 0.3.0) advertises `status.capabilities.ai.offer`: `enabled`, `models`, `hours`,
+1. **Link** (`plugins/ai`, 0.3.0) advertises `status.capabilities.ai.offer`: `enabled`, `models`, `hours`,
`concurrency`, `price` (credits per million in/out), `audience`, `open_now`, `ready`. It enforces `enabled`, `hours`,
`concurrency`, `price` (credits per million in/out), `audience`, `open_now`, `ready`. It enforces `enabled`, `hours`,
`models` and `concurrency` on the machine, and accepts a community command only as a `community/<model>` request,
`models` and `concurrency` on the machine, and accepts a community command only as a `community/<model>` request,
which it strips before the local server sees it.
which it strips before the local server sees it.
-2. **Bot** stores the Node's capabilities and reports every machine with an enabled offer to Network:
+2. **Bot** stores the Link's capabilities and reports every machine with an enabled offer to Network:
`POST /internal/ai/offers/report` (source `bot`, `network.resource.report`). A community run reaches a machine even
`POST /internal/ai/offers/report` (source `bot`, `network.resource.report`). A community run reaches a machine even
with "Let my AI use this computer's models" off, because the offer itself is the opt-in; the machine still only
with "Let my AI use this computer's models" off, because the offer itself is the opt-in; the machine still only
accepts `kind: ai`.
accepts `kind: ai`.
@@ -36,7 +36,7 @@ that answer carries no community label.
@@ -36,7 +36,7 @@ that answer carries no community label.
## Community endpoints
## Community endpoints
-The design's SDK gap: any server that speaks the OpenAI Chat Completions API can sell, not only a Node machine. The
+The design's SDK gap: any server that speaks the OpenAI Chat Completions API can sell, not only a machine running Link. The
seller registers it with their own Network developer key on the Exchange's public API host, documented in
seller registers it with their own Network developer key on the Exchange's public API host, documented in
`/openapi.json`:
`/openapi.json`:
@@ -69,7 +69,7 @@ service token is refused.
@@ -69,7 +69,7 @@ service token is refused.
OpenVibe as it is (put it behind an unguessable base path if you want it hard to find);
OpenVibe as it is (put it behind an unguessable base path if you want it hard to find);
- the seller's declared `concurrency` is enforced in-process: over it the call is busy and the router falls back.
- the seller's declared `concurrency` is enforced in-process: over it the call is busy and the router falls back.
-**Same trust as a Node machine.** The endpoint joins `server/community/index.js`'s offer list, so it gets the same three
+**Same trust as a machine running Link.** The endpoint joins `server/community/index.js`'s offer list, so it gets the same three
canaries, the same model fingerprint (the `model` its answer reports must be the declared one), the same ~1% spot
canaries, the same model fingerprint (the `model` its answer reports must be the declared one), the same ~1% spot
re-runs against a verified peer, the free tier only until verified, and the self-dealing rule (the owner, a merged
re-runs against a verified peer, the free tier only until verified, and the self-dealing rule (the owner, a merged
account, or the project that registered it earns 0). Its answers carry `provider: community-endpoint:<id>`,
account, or the project that registered it earns 0). Its answers carry `provider: community-endpoint:<id>`,
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
# Memory M5: services, sharing and the portable file
# Memory M5: services, sharing and the portable file
-This is the fifth stage of the Memory plan (`../../OpenVibe_Memory_Plan_2026-10-30.md`). M1–M4 are the Node readers,
+This is the fifth stage of the Memory plan (`../../OpenVibe_Memory_Plan_2026-10-30.md`). M1–M4 are the Link readers,
the store, the cloud imports, the write-back and the agent API. M5 adds three things to the same store:
the store, the cloud imports, the write-back and the agent API. M5 adds three things to the same store:
- the memory-service adapters (mem0, Letta, Zep) with the person's own key;
- the memory-service adapters (mem0, Letta, Zep) with the person's own key;
@@ -102,7 +102,7 @@ forwards the person's token, the method, the body and `If-Match` to AI, and keep
@@ -102,7 +102,7 @@ forwards the person's token, the method, the body and `If-Match` to AI, and keep
another account without carrying anyone's identity, and a profile is derived from the notes and rebuilt.
another account without carrying anyone's identity, and a profile is derived from the notes and rebuilt.
- The round trip is proved in `test/memory-m5.test.js`: export A's store, import it as B, and compare text, kind,
- The round trip is proved in `test/memory-m5.test.js`: export A's store, import it as B, and compare text, kind,
scope and source; importing the same file again changes nothing.
scope and source; importing the same file again changes nothing.
-- The Node writes and reads the same file (`openvibe-node memory export|import`); Codes offers a download and an
+- The Link writes and reads the same file (`openvibe-node memory export|import`); Codes offers a download and an
import form. The format is the contract, not either client.
import form. The format is the contract, not either client.
## Tests
## Tests
@@ -9,7 +9,7 @@ API both use one definition of private here; nothing in Exchange re-implements i
@@ -9,7 +9,7 @@ API both use one definition of private here; nothing in Exchange re-implements i
| label | when |
| label | when |
| --- | --- |
| --- | --- |
-| `private` | the provider's own terms say zero retention **and** no training, or the offer is a person's own machine (a Node credential), or it is the host's own deployment (`metadata.local`). |
+| `private` | the provider's own terms say zero retention **and** no training, or the offer is a person's own machine (a Link credential), or it is the host's own deployment (`metadata.local`). |
| `community` | the offer runs on a community machine (a person's hardware, Node's "Offer to the network"): that machine sees the prompt. |
| `community` | the offer runs on a community machine (a person's hardware, Node's "Offer to the network"): that machine sees the prompt. |
| `anonymized` | everything else, including a provider that declares no policy (`known: false` says so). |
| `anonymized` | everything else, including a provider that declares no policy (`known: false` says so). |
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
/**
/**
* The public seller API (design EXCHANGE-REMAKE-2026-11-06, "Gap found while building the SDK"): a standalone
* The public seller API (design EXCHANGE-REMAKE-2026-11-06, "Gap found while building the SDK"): a standalone
* OpenAI-compatible server can sell to the Exchange with the seller's own Network developer key, not only an
* OpenAI-compatible server can sell to the Exchange with the seller's own Network developer key, not only an
- * OpenVibe Node machine. Served on the Exchange's public API host (api.openvibe.exchange) and documented in
+ * OpenVibe Link machine. Served on the Exchange's public API host (api.openvibe.exchange) and documented in
* /openapi.json.
* /openapi.json.
*
*
* POST /api/v1/exchange/seller/offers ai.app.run (a developer key), or a person's own token
* POST /api/v1/exchange/seller/offers ai.app.run (a developer key), or a person's own token
@@ -187,7 +187,7 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
@@ -187,7 +187,7 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
// The budget of a Codes run token (Codes C3): Codes registers it when it mints the token.
// The budget of a Codes run token (Codes C3): Codes registers it when it mints the token.
if (runBudgets) app.use(require('./api/run-budgets').runBudgetsRouter({ runBudgets, auth, log, sendError: (res, err, ctx) => sendError(res, err, ctx, log) }));
if (runBudgets) app.use(require('./api/run-budgets').runBudgetsRouter({ runBudgets, auth, log, sendError: (res, err, ctx) => sendError(res, err, ctx, log) }));
// Community capacity (design: "Offer to the network"): the public offers Exchange lists and the seller's /sell
// Community capacity (design: "Offer to the network"): the public offers Exchange lists and the seller's /sell
- // dashboard, an API for Exchange. Nothing here writes an offer; the offer belongs to Network and the Node owner.
+ // dashboard, an API for Exchange. Nothing here writes an offer; the offer belongs to Network and the Link owner.
if (community) app.use('/api/v1', require('./api/community').communityRouter({ community, auth, log }));
if (community) app.use('/api/v1', require('./api/community').communityRouter({ community, auth, log }));
app.use(adminRouter({ config, db, registry, pool, quotas, cache, runs, auth, settings, ops, log }));
app.use(adminRouter({ config, db, registry, pool, quotas, cache, runs, auth, settings, ops, log }));
// The operator console (/console, /auth/*): Network staff, server-rendered, no scripts (server/console).
// The operator console (/console, /auth/*): Network staff, server-rendered, no scripts (server/console).
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
/**
/**
* Community endpoints (design EXCHANGE-REMAKE-2026-11-06, "Gap found while building the SDK"): a standalone
* Community endpoints (design EXCHANGE-REMAKE-2026-11-06, "Gap found while building the SDK"): a standalone
- * OpenAI-compatible server can sell to the Exchange, not only an OpenVibe Node machine. A seller registers its own
+ * OpenAI-compatible server can sell to the Exchange, not only an OpenVibe Link machine. A seller registers its own
* https address with their Network developer key on the public seller API (server/api/seller.js); AI stores the
* https address with their Network developer key on the public seller API (server/api/seller.js); AI stores the
* offer here, reads it beside Network's Node offers, and reaches it through this module only.
* offer here, reads it beside Network's Node offers, and reaches it through this module only.
*
*
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
* - the request is built fresh: the model, the conversation, the output limit and the tools. Never the buyer's
* - the request is built fresh: the model, the conversation, the output limit and the tools. Never the buyer's
* subject, on_behalf_of, run id, key, cookies or forwarding headers.
* subject, on_behalf_of, run id, key, cookies or forwarding headers.
*
*
- * Trust is the Node machine's (server/community/index.js): the same canaries, the same model fingerprint, the same
+ * Trust is the Link machine's (server/community/index.js): the same canaries, the same model fingerprint, the same
* ~1% spot re-runs, the free tier only until verified, and the self-dealing rule. An endpoint is a community source:
* ~1% spot re-runs, the free tier only until verified, and the self-dealing rule. An endpoint is a community source:
* it sees the prompt, and only requests that opt in ever reach it.
* it sees the prompt, and only requests that opt in ever reach it.
*/
*/
@@ -142,7 +142,7 @@ function createCommunity({ db, config, env = process.env, machines = null, endpo
@@ -142,7 +142,7 @@ function createCommunity({ db, config, env = process.env, machines = null, endpo
if (!identityTokens && secret && network) {
if (!identityTokens && secret && network) {
identityTokens = createServiceTokenClient({ tokenUrl: `${network}/oauth/token`, clientId, clientSecret: secret, scope: { 'openvibe.network': 'identity.subject.resolve' }, fetch: fetchImpl });
identityTokens = createServiceTokenClient({ tokenUrl: `${network}/oauth/token`, clientId, clientSecret: secret, scope: { 'openvibe.network': 'identity.subject.resolve' }, fetch: fetchImpl });
}
}
- // A Node offer comes from Network and is reached through Bot; a community endpoint is registered here and reached
+ // A Link offer comes from Network and is reached through Bot; a community endpoint is registered here and reached
// through its own https address. Each works without the other.
// through its own https address. Each works without the other.
const registryEnabled = Boolean(cfg.enabled && db && machines && network && registryTokens);
const registryEnabled = Boolean(cfg.enabled && db && machines && network && registryTokens);
const endpointsEnabled = Boolean(cfg.enabled && db && endpoints && endpoints.enabled);
const endpointsEnabled = Boolean(cfg.enabled && db && endpoints && endpoints.enabled);
@@ -438,7 +438,7 @@ function createCommunity({ db, config, env = process.env, machines = null, endpo
@@ -438,7 +438,7 @@ function createCommunity({ db, config, env = process.env, machines = null, endpo
const wanted = ctx.communityOfferId ? String(ctx.communityOfferId) : null;
const wanted = ctx.communityOfferId ? String(ctx.communityOfferId) : null;
const out = [];
const out = [];
for (const offer of list) {
for (const offer of list) {
- // The Node's own ready flag may be a few minutes stale (status is sent on changes); the hours are read here
+ // The Link's own ready flag may be a few minutes stale (status is sent on changes); the hours are read here
// too, so a request outside the owner's window never reaches the machine.
// too, so a request outside the owner's window never reaches the machine.
if (offer.health === 'down' || offer.ready !== true || !withinHours(offer, clock.now())) continue;
if (offer.health === 'down' || offer.ready !== true || !withinHours(offer, clock.now())) continue;
if (wanted && String(offer.offer_id) !== wanted) continue;
if (wanted && String(offer.offer_id) !== wanted) continue;
@@ -46,7 +46,7 @@ function dataPolicyOf(record) {
@@ -46,7 +46,7 @@ function dataPolicyOf(record) {
* credential the person's own key's descriptor, when the answer came from their key or their machine
* credential the person's own key's descriptor, when the answer came from their key or their machine
*/
*/
function labelFor({ record = null, credential = null } = {}) {
function labelFor({ record = null, credential = null } = {}) {
- // A person's own machine (a Node credential): the prompt never left their hardware.
+ // A person's own machine (a Link credential): the prompt never left their hardware.
if (credential && credential.provider === 'node') {
if (credential && credential.provider === 'node') {
return { label: 'private', retention: 'none', training: false, region: null, trust: 'own_machine', known: true, source: 'your own machine' };
return { label: 'private', retention: 'none', training: false, region: null, trust: 'own_machine', known: true, source: 'your own machine' };
}
}
@@ -3,12 +3,12 @@
@@ -3,12 +3,12 @@
* A person's own machine as their model (AI on your own machines: contracts 0.219.0 bot.command@1 kind ai and the
* A person's own machine as their model (AI on your own machines: contracts 0.219.0 bot.command@1 kind ai and the
* capability bot.machine.ai; 0.220.0 the credential provider node). One chat completion goes through OpenVibe.Bot,
* capability bot.machine.ai; 0.220.0 the credential provider node). One chat completion goes through OpenVibe.Bot,
* POST /api/v1/machines/:id/commands with kind ai, as OpenVibe.AI acting for the person (X-OV-Subject), to the machine's
* POST /api/v1/machines/:id/commands with kind ai, as OpenVibe.AI acting for the person (X-OV-Subject), to the machine's
- * own model server (OpenVibe Node's ai plugin: Ollama, LM Studio, llama.cpp). Bot checks that the machine is theirs and
+ * own model server (OpenVibe Link's ai plugin: Ollama, LM Studio, llama.cpp). Bot checks that the machine is theirs and
* that they turned on "Let my AI use this computer's models"; the prompt never reaches anyone else's machine.
* that they turned on "Let my AI use this computer's models"; the prompt never reaches anyone else's machine.
*
*
* Machine `any` (contracts 0.221.0): whichever of the person's own machines is on, lets their AI use its models and
* Machine `any` (contracts 0.221.0): whichever of the person's own machines is on, lets their AI use its models and
* offers the model, from Bot's list for OpenVibe.AI (GET /api/v1/machines, only those with the switch on). The fastest
* offers the model, from Bot's list for OpenVibe.AI (GET /api/v1/machines, only those with the switch on). The fastest
- * at that model goes first (computer.ai_speed, OpenVibe.Node v0.29.0), then the others; one that is busy, away or fails
+ * at that model goes first (computer.ai_speed, OpenVibe Link v0.29.0), then the others; one that is busy, away or fails
* hands over to the next. Never anyone else's machine, never another provider.
* hands over to the next. Never anyone else's machine, never another provider.
*
*
* Off without OV_BOT_INTERNAL_URL or a client secret: a credential with provider node is then refused, never sent
* Off without OV_BOT_INTERNAL_URL or a client secret: a credential with provider node is then refused, never sent
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
*
*
* Tools (contracts 0.277.0, plan L55 C3): an agent's loop on the person's own model. The command carries the tools and
* Tools (contracts 0.277.0, plan L55 C3): an agent's loop on the person's own model. The command carries the tools and
* the calls already made; the machine hands back the calls its model asked for (message.tool_calls) and runs none. Only
* the calls already made; the machine hands back the calls its model asked for (message.tool_calls) and runs none. Only
- * a machine whose Node says computer.ai.tools (v0.36.0) is asked: with machine any the others are passed over, and a
+ * a machine whose Link says computer.ai.tools (v0.36.0) is asked: with machine any the others are passed over, and a
* named machine without it is refused with what to update.
* named machine without it is refused with what to update.
*/
*/
const { AiError } = require('./util');
const { AiError } = require('./util');
@@ -78,14 +78,14 @@ function createMachineModels({ config, env = process.env, fetchImpl = globalThis
@@ -78,14 +78,14 @@ function createMachineModels({ config, env = process.env, fetchImpl = globalThis
const tools = Array.isArray(args.tools) && args.tools.length > 0;
const tools = Array.isArray(args.tools) && args.tools.length > 0;
if (machine !== ANY) {
if (machine !== ANY) {
if (tools) {
if (tools) {
- // A Node older than v0.36.0 would drop the tools and answer in words: say what to update instead.
+ // A Link older than v0.36.0 would drop the tools and answer in words: say what to update instead.
const m = (await listed(subject, signal)).find((x) => x && x.id === machine);
const m = (await listed(subject, signal)).find((x) => x && x.id === machine);
- if (m && m.computer && m.computer.ai && m.computer.ai.tools !== true) throw new AiError(422, 'machine.no_tools', 'this computer\'s OpenVibe Node cannot pass tools to its model yet: update it to v0.36.0 or newer');
+ if (m && m.computer && m.computer.ai && m.computer.ai.tools !== true) throw new AiError(422, 'machine.no_tools', 'this computer\'s OpenVibe Link cannot pass tools to its model yet: update it to v0.36.0 or newer');
}
}
return { ...(await once(machine, args)), machine, tried: [] };
return { ...(await once(machine, args)), machine, tried: [] };
}
}
const ids = (await candidates(subject, model, signal, { tools })).slice(0, MAX_TRIED);
const ids = (await candidates(subject, model, signal, { tools })).slice(0, MAX_TRIED);
- if (!ids.length) throw new AiError(503, 'machine.none', `none of the person's own machines is on with ${String(model).slice(0, 120)} and lets their AI use it${tools ? ', with an OpenVibe Node that can pass tools (v0.36.0 or newer)' : ''}`);
+ if (!ids.length) throw new AiError(503, 'machine.none', `none of the person's own machines is on with ${String(model).slice(0, 120)} and lets their AI use it${tools ? ', with an OpenVibe Link that can pass tools (v0.36.0 or newer)' : ''}`);
const tried = [];
const tried = [];
for (const id of ids) {
for (const id of ids) {
try {
try {
@@ -101,7 +101,7 @@ function createMachineModels({ config, env = process.env, fetchImpl = globalThis
@@ -101,7 +101,7 @@ function createMachineModels({ config, env = process.env, fetchImpl = globalThis
async function once(machine, { subject, model, messages, maxTokens = null, temperature = null, signal = null, timeoutMs = 120000, tools = null, toolChoice = null, community = false }) {
async function once(machine, { subject, model, messages, maxTokens = null, temperature = null, signal = null, timeoutMs = 120000, tools = null, toolChoice = null, community = false }) {
// The ack wire has no extra field for "this is the machine's offer to the network" (bot.command@1's value is
// The ack wire has no extra field for "this is the machine's offer to the network" (bot.command@1's value is
- // closed), so the model carries the community/ prefix; the Node ai plugin strips it before its model server
+ // closed), so the model carries the community/ prefix; the Link ai plugin strips it before its model server
// sees it and enforces the offer. The prefix never reaches the local model.
// sees it and enforces the offer. The prefix never reaches the local model.
const asked = community && !String(model).startsWith('community/') ? `community/${model}` : model;
const asked = community && !String(model).startsWith('community/') ? `community/${model}` : model;
const value = { model: asked, messages, ...(maxTokens ? { max_tokens: Math.min(32768, Math.max(1, Math.round(maxTokens))) } : {}), ...(temperature != null ? { temperature: Math.min(2, Math.max(0, Number(temperature))) } : {}),
const value = { model: asked, messages, ...(maxTokens ? { max_tokens: Math.min(32768, Math.max(1, Math.round(maxTokens))) } : {}), ...(temperature != null ? { temperature: Math.min(2, Math.max(0, Number(temperature))) } : {}),
@@ -136,7 +136,7 @@ function createMachineModels({ config, env = process.env, fetchImpl = globalThis
@@ -136,7 +136,7 @@ function createMachineModels({ config, env = process.env, fetchImpl = globalThis
/**
/**
* The person's computers that let their AI use their models, for their own settings page: name, whether it is on,
* The person's computers that let their AI use their models, for their own settings page: name, whether it is on,
- * its models and whether its Node can pass tools. [] when Bot cannot be reached.
+ * its models and whether its Link can pass tools. [] when Bot cannot be reached.
*/
*/
async function mine(subject, signal = null) {
async function mine(subject, signal = null) {
if (!enabled || !SUBJECT_RE.test(String(subject))) return [];
if (!enabled || !SUBJECT_RE.test(String(subject))) return [];
@@ -483,7 +483,7 @@ function createMemory({ db, config, clock = { now: () => Date.now() }, log = con
@@ -483,7 +483,7 @@ function createMemory({ db, config, clock = { now: () => Date.now() }, log = con
: String(req.principal.sub || '').replace(/^svc:/, '');
: String(req.principal.sub || '').replace(/^svc:/, '');
const source = validateSource(serviceCaller ? serviceSource : (raw.source || 'person'));
const source = validateSource(serviceCaller ? serviceSource : (raw.source || 'person'));
// A service is an agent: what it writes is a suggestion, stored as written. A person may mark their own item
// A service is an agent: what it writes is a suggestion, stored as written. A person may mark their own item
- // as AI-produced too (the Node imports what a coding tool wrote).
+ // as AI-produced too (the Link imports what a coding tool wrote).
const aiMade = serviceCaller || raw.no_looks === true || AI_SOURCES.has(source);
const aiMade = serviceCaller || raw.no_looks === true || AI_SOURCES.has(source);
let text = clean;
let text = clean;
text = String(text == null ? '' : text);
text = String(text == null ? '' : text);
@@ -634,7 +634,7 @@ const sellerEndpointInput = {
@@ -634,7 +634,7 @@ const sellerEndpointInput = {
};
};
route('post', '/exchange/seller/offers', {
route('post', '/exchange/seller/offers', {
operationId: 'createSellerEndpointOffer', summary: 'Register a standalone OpenAI-compatible server to sell', tags: ['exchange'], security: SERVICE_OR_APP,
operationId: 'createSellerEndpointOffer', summary: 'Register a standalone OpenAI-compatible server to sell', tags: ['exchange'], security: SERVICE_OR_APP,
- description: 'ai.app.run (a Network developer key), or a person\'s own token; a first-party service token is refused. The owner is the person the key acts for: an app\'s project owner, or the person themselves. The router reaches base_url only through the egress guard: https, a public DNS answer, never an IP literal, a timeout, a response byte cap, and never the buyer\'s identity in a header. A new endpoint serves the free tier only until its canaries, model fingerprint and spot re-runs verify it, exactly as a Node machine does; a request on the seller\'s own account is self-served and earns 0. An answer from it is labelled a community endpoint on the receipt (provider community-endpoint:<id>, provider_access COMMUNITY_ENDPOINT) and only requests that opt in to community capacity reach it.',
+ description: 'ai.app.run (a Network developer key), or a person\'s own token; a first-party service token is refused. The owner is the person the key acts for: an app\'s project owner, or the person themselves. The router reaches base_url only through the egress guard: https, a public DNS answer, never an IP literal, a timeout, a response byte cap, and never the buyer\'s identity in a header. A new endpoint serves the free tier only until its canaries, model fingerprint and spot re-runs verify it, exactly as a machine running Link does; a request on the seller\'s own account is self-served and earns 0. An answer from it is labelled a community endpoint on the receipt (provider community-endpoint:<id>, provider_access COMMUNITY_ENDPOINT) and only requests that opt in to community capacity reach it.',
requestBody: body(sellerEndpointInput),
requestBody: body(sellerEndpointInput),
responses: { 201: json('The registered offer', { type: 'object', required: ['offer'], properties: { offer: sellerEndpointOffer } }), 409: problem, 422: problem, ...errors([401, 403, 503]) },
responses: { 201: json('The registered offer', { type: 'object', required: ['offer'], properties: { offer: sellerEndpointOffer } }), 409: problem, 422: problem, ...errors([401, 403, 503]) },
});
});
@@ -18,7 +18,7 @@
@@ -18,7 +18,7 @@
* POST /you/computer a model on your own computer (model; any of your computers)
* POST /you/computer a model on your own computer (model; any of your computers)
* POST /you/computer/remove forget it
* POST /you/computer/remove forget it
*
*
- * Your computer (plan L55 C3): a model the person runs at home, reached through OpenVibe.Bot and their OpenVibe Node,
+ * Your computer (plan L55 C3): a model the person runs at home, reached through OpenVibe.Bot and their OpenVibe Link,
* kept here like a key but with no key: the model, and any of their computers that lets their AI use its models. An
* kept here like a key but with no key: the model, and any of their computers that lets their AI use its models. An
* app uses it only when the person allows that app below (provider node), and it never stands in for a key.
* app uses it only when the person allows that app below (provider node), and it never stands in for a key.
*/
*/
@@ -145,10 +145,10 @@ function allowRequest(query) {
@@ -145,10 +145,10 @@ function allowRequest(query) {
function computerSection(pc, machines) {
function computerSection(pc, machines) {
const models = [...new Set(machines.flatMap((m) => m.models))].slice(0, 60);
const models = [...new Set(machines.flatMap((m) => m.models))].slice(0, 60);
const onNow = machines.filter((m) => m.online);
const onNow = machines.filter((m) => m.online);
- const line = (m) => `<li><b>${esc(m.name)}</b>${m.online ? '' : ' <span class="you-pill">away</span>'}${m.models.length ? ` · ${esc(m.models.slice(0, 6).join(', '))}${m.models.length > 6 ? ` and ${m.models.length - 6} more` : ''}` : ' · no models listed'}${m.tools ? '' : ' · <span class="you-pill">update OpenVibe Node to v0.36.0 for agents</span>'}</li>`;
+ const line = (m) => `<li><b>${esc(m.name)}</b>${m.online ? '' : ' <span class="you-pill">away</span>'}${m.models.length ? ` · ${esc(m.models.slice(0, 6).join(', '))}${m.models.length > 6 ? ` and ${m.models.length - 6} more` : ''}` : ' · no models listed'}${m.tools ? '' : ' · <span class="you-pill">update OpenVibe Link to v0.36.0 for agents</span>'}</li>`;
const where = machines.length
const where = machines.length
? `<ul class="you-pcs">${machines.map(line).join('')}</ul>`
? `<ul class="you-pcs">${machines.map(line).join('')}</ul>`
- : '<p class="you-empty">None of your computers lets OpenVibe use its models yet. On the computer, run a model server (Ollama, LM Studio or llama.cpp) and OpenVibe Node with <code>--ai</code>, then turn on “Let my AI use this computer’s models” at <a href="https://openvibe.bot/machines">openvibe.bot/machines</a>.</p>';
+ : '<p class="you-empty">None of your computers lets OpenVibe use its models yet. On the computer, run a model server (Ollama, LM Studio or llama.cpp) and OpenVibe Link with <code>--ai</code>, then turn on “Let my AI use this computer’s models” at <a href="https://openvibe.bot/machines">openvibe.bot/machines</a>.</p>';
const saved = pc ? `<ul class="you-keys"><li class="you-key"><div class="you-key-t"><b>Any of your computers</b><span class="you-pill${onNow.some((m) => m.models.includes(pc.model)) ? ' is-ok' : ''}">${esc(pc.model || 'no model')}</span></div>
const saved = pc ? `<ul class="you-keys"><li class="you-key"><div class="you-key-t"><b>Any of your computers</b><span class="you-pill${onNow.some((m) => m.models.includes(pc.model)) ? ' is-ok' : ''}">${esc(pc.model || 'no model')}</span></div>
<div class="you-acts"><form method="post" action="/you/computer/remove"><button class="you-btn" type="submit">Remove</button></form></div>
<div class="you-acts"><form method="post" action="/you/computer/remove"><button class="you-btn" type="submit">Remove</button></form></div>
<div class="you-key-n">${onNow.some((m) => m.models.includes(pc.model)) ? 'On now, with this model' : 'None of your computers with this model is on right now'}${pc.last_used_at ? ` · used ${esc(ago(pc.last_used_at))}` : ''}</div></li></ul>` : '';
<div class="you-key-n">${onNow.some((m) => m.models.includes(pc.model)) ? 'On now, with this model' : 'None of your computers with this model is on right now'}${pc.last_used_at ? ` · used ${esc(ago(pc.last_used_at))}` : ''}</div></li></ul>` : '';
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
// Community endpoints (the design's SDK gap): a standalone OpenAI-compatible server is registered by its own https
// Community endpoints (the design's SDK gap): a standalone OpenAI-compatible server is registered by its own https
// address, and AI reaches it only through the egress guard. The tests are the guard's own: https only, no IP literal
// address, and AI reaches it only through the egress guard. The tests are the guard's own: https only, no IP literal
// (127.0.0.1, 10/8, 169.254.169.254, [::1], a NAT64 spelling), no private DNS answer, no redirect to one; a timeout and
// (127.0.0.1, 10/8, 169.254.169.254, [::1], a NAT64 spelling), no private DNS answer, no redirect to one; a timeout and
-// a response cap; and never the buyer's identity in a header. Then the same trust as a Node machine: the canaries, the
+// a response cap; and never the buyer's identity in a header. Then the same trust as a machine running Link: the canaries, the
// model fingerprint, the free tier only until verified, and self-dealing earns 0.
// model fingerprint, the free tier only until verified, and self-dealing earns 0.
const assert = require('assert');
const assert = require('assert');
const { boot, request, token, suite, ALL } = require('./helpers');
const { boot, request, token, suite, ALL } = require('./helpers');
@@ -188,7 +188,7 @@ t.test('machine any (contracts 0.221.0): the fastest of the person\'s own machin
@@ -188,7 +188,7 @@ t.test('machine any (contracts 0.221.0): the fastest of the person\'s own machin
listed = [];
listed = [];
await assert.rejects(m.chat({ machine: 'any', subject: FRAN, model: 'llama3.1:8b', messages: [] }), (e) => e.status === 503 && e.code === 'machine.none' && /is on with/.test(e.message));
await assert.rejects(m.chat({ machine: 'any', subject: FRAN, model: 'llama3.1:8b', messages: [] }), (e) => e.status === 503 && e.code === 'machine.none' && /is on with/.test(e.message));
await assert.rejects(m.chat({ machine: 'anyone', subject: FRAN, model: 'm', messages: [] }), (e) => e.status === 422);
await assert.rejects(m.chat({ machine: 'anyone', subject: FRAN, model: 'm', messages: [] }), (e) => e.status === 422);
- // Tools (contracts 0.277.0): only a machine whose Node says it can pass them; a named one without it is refused.
+ // Tools (contracts 0.277.0): only a machine whose Link says it can pass them; a named one without it is refused.
listed = list.map((x) => (x.id === id('CC') ? { ...x, computer: { ...x.computer, ai: { ...x.computer.ai, tools: true } } } : x));
listed = list.map((x) => (x.id === id('CC') ? { ...x, computer: { ...x.computer, ai: { ...x.computer.ai, tools: true } } } : x));
busy = new Set();
busy = new Set();
asked.length = 0;
asked.length = 0;
@@ -109,7 +109,7 @@ t.test('your computer (L55 C3): a model at home, no key; an app reaches it only
@@ -109,7 +109,7 @@ t.test('your computer (L55 C3): a model at home, no key; an app reaches it only
let r = await page('/you', { headers: { Cookie: cookie } });
let r = await page('/you', { headers: { Cookie: cookie } });
assert.ok(r.text.includes('<h2 id="you-pc">Your computer</h2>'));
assert.ok(r.text.includes('<h2 id="you-pc">Your computer</h2>'));
assert.ok(r.text.includes('<b>Desk PC</b> · qwen3:8b, llama3.1:8b') && r.text.includes('<b>Old laptop</b> <span class="you-pill">away</span>'), 'its computers and their models');
assert.ok(r.text.includes('<b>Desk PC</b> · qwen3:8b, llama3.1:8b') && r.text.includes('<b>Old laptop</b> <span class="you-pill">away</span>'), 'its computers and their models');
- assert.ok(r.text.includes('update OpenVibe Node to v0.36.0'), 'a Node that cannot pass tools says what to update');
+ assert.ok(r.text.includes('update OpenVibe Link to v0.36.0'), 'a Link that cannot pass tools says what to update');
assert.ok(r.text.includes('<option value="qwen3:8b">'), 'the models to pick from');
assert.ok(r.text.includes('<option value="qwen3:8b">'), 'the models to pick from');
assert.ok(flash(await post('/you/computer', { model: 'two words' })).error, 'a model name has no spaces');
assert.ok(flash(await post('/you/computer', { model: 'two words' })).error, 'a model name has no spaces');
r = await post('/you/computer', { model: 'qwen3:8b' });
r = await post('/you/computer', { model: 'qwen3:8b' });