openvibers/openvibe.ai Commits

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

OP OpenVibers committed 5 days ago · c756fa73a6 · 17 files, +36 −36

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

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

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

Changes

ddiff --git a/README.md b/README.md
iindex d6182bb..799b734 100644
---- a/README.md
++++ b/README.md
@@@ -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
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
-- 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 is
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`
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 model
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.
ddiff --git a/docs/ai-exchange-baseline.md b/docs/ai-exchange-baseline.md
iindex a70e69d..e654580 100644
---- a/docs/ai-exchange-baseline.md
++++ b/docs/ai-exchange-baseline.md
@@@ -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. |
ddiff --git a/docs/community-offers.md b/docs/community-offers.md
iindex 1439803..2604d2f 100644
---- a/docs/community-offers.md
++++ b/docs/community-offers.md
@@@ -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 capacity
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.
## 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 even
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`.
@@@ -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. The
seller 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 three
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
account, or the project that registered it earns 0). Its answers carry `provider: community-endpoint:<id>`,
ddiff --git a/docs/memory-m5.md b/docs/memory-m5.md
iindex 8e7e2bd..6241913 100644
---- a/docs/memory-m5.md
++++ b/docs/memory-m5.md
@@@ -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 keep
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,
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.
## Tests
ddiff --git a/docs/private-ai.md b/docs/private-ai.md
iindex e96eff2..ce95111 100644
---- a/docs/private-ai.md
++++ b/docs/private-ai.md
@@@ -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). |
ddiff --git a/server/api/seller.js b/server/api/seller.js
iindex 2d48aa6..ed9d119 100644
---- a/server/api/seller.js
++++ b/server/api/seller.js
@@@ -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
ddiff --git a/server/app.js b/server/app.js
iindex fab4d27..ff32f21 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -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).
ddiff --git a/server/community/endpoints.js b/server/community/endpoints.js
iindex ab22224..e9ddc01 100644
---- a/server/community/endpoints.js
++++ b/server/community/endpoints.js
@@@ -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.
*/
ddiff --git a/server/community/index.js b/server/community/index.js
iindex 90ec6b9..765e221 100644
---- a/server/community/index.js
++++ b/server/community/index.js
@@@ -142,7 +142,7 @@ function createCommunity({ db, config, env = process.env, machines = null, endpo
if (!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, endpo
const 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;
ddiff --git a/server/exchange/privacy.js b/server/exchange/privacy.js
iindex 38ed5d7..3a136b2 100644
---- a/server/exchange/privacy.js
++++ b/server/exchange/privacy.js
@@@ -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' };
}
ddiff --git a/server/machines.js b/server/machines.js
iindex 3949934..f7f606f 100644
---- a/server/machines.js
++++ b/server/machines.js
@@@ -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 = globalThis
const 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 = globalThis
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
-- // 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 [];
ddiff --git a/server/memory/index.js b/server/memory/index.js
iindex d02d87c..03392d3 100644
---- a/server/memory/index.js
++++ b/server/memory/index.js
@@@ -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);
ddiff --git a/server/openapi.js b/server/openapi.js
iindex 4425cd8..303ae03 100644
---- a/server/openapi.js
++++ b/server/openapi.js
@@@ -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]) },
});
ddiff --git a/server/you-page.js b/server/you-page.js
iindex 4701c76..cc8edff 100644
---- a/server/you-page.js
++++ b/server/you-page.js
@@@ -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 &ldquo;Let my AI use this computer&rsquo;s models&rdquo; 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 &ldquo;Let my AI use this computer&rsquo;s models&rdquo; 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>` : '';
ddiff --git a/test/community-endpoints.test.js b/test/community-endpoints.test.js
iindex 22875cd..c3bd494 100644
---- a/test/community-endpoints.test.js
++++ b/test/community-endpoints.test.js
@@@ -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');
ddiff --git a/test/machine-credential.test.js b/test/machine-credential.test.js
iindex 390f739..8161f5e 100644
---- a/test/machine-credential.test.js
++++ b/test/machine-credential.test.js
@@@ -188,7 +188,7 @@ t.test('machine any (contracts 0.221.0): the fastest of the person\'s own machin
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: '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;
ddiff --git a/test/you-page.test.js b/test/you-page.test.js
iindex c275f78..5021578 100644
---- a/test/you-page.test.js
++++ b/test/you-page.test.js
@@@ -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 } });
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' });