openvibers/openvibe.sdk Commits
OpenVibe Link: the new name in docs and comments (#139)
Changes
@@@ -91,19 +91,19 @@idempotent per source and task ref) and `read()` gets one the caller may see. The Report service redacts every textfield; the client never sends a raw transcript.- Add `examples/cli-agent-stream.js`: about 40 lines that start a stream, spawn any CLI command, post its steps and end-- the stream with the command's result, using the same ticket path a Node desk uses.++ the stream with the command's result, using the same ticket path a Link desk uses.## 0.93.0 (2026-11-06)- `openvibe-sdk/exchange`: the seller side gains the **endpoint form**, so any OpenAI-compatible server can sell, not-- only an OpenVibe Node machine. `seller.offer({ endpoint, models, price, hours, concurrency, audience, name })` — with++ only an OpenVibe Link machine. `seller.offer({ endpoint, models, price, hours, concurrency, audience, name })` — withno `machineId` — registers the server on OpenVibe.Exchange's public seller API(`POST /api/v1/exchange/seller/offers`) with the same developer key the buyer side uses. The owner is the person thekey acts for (a developer app's project owner), never a body field, and the SDK refuses an endpoint that is not httpsbefore anything is sent.- The platform reaches that address only through its egress guard (https, a public DNS answer checked inside theconnection, never an IP literal, redirects judged again, a timeout and a response byte cap, no buyer identity in a-- header) and verifies it exactly as a Node machine (canaries, the model fingerprint, about 1% spot re-runs, the free++ header) and verifies it exactly as a machine running Link (canaries, the model fingerprint, about 1% spot re-runs, the freetier until verified, self-dealing earns 0). Its answers are labelled `community-endpoint:<id>` on the receipt.- `seller.endpointOffers()` lists the caller's endpoint offers with their verification;`seller.removeEndpointOffer(id)` removes one. `machineId` still selects the Network registry form.@@@ -137,7 +137,7 @@- `state(id, { ticket })` is what the desk polls every ten seconds: `{ viewers, encode, curtain, ends_at }`. While `encode` is false the desk stops encoding and sends a snapshot every five seconds with `postFrame(id, bytes, { ticket, contentType })` (JPEG or WebP, at most 200 KB).- `setCurtain(id, up, reason)` raises the desk's privacy screen (while it is up Live pauses every viewer's consumer, so a viewer sees "Private screen" and never why), `setVisibility(id, v)` switches who may watch, `end(id)` ends the stream (the card stays, with the last frame and the VOD) and `viewTicket(id)` mints the player's view ticket (ten minutes).- The publish ticket is a bearer of its own: on the desk's routes (`state`, `postFrame`) the desk sends the ticket, never the person's token. Every body is checked in the module before it leaves, and a bad id, visibility, curtain reason or an oversized frame is refused with a TypeError.-- - The capability behind it is `live.agent_stream.publish` for the audience `openvibe.live` (Contracts 0.298.0): a Node principal acting for its owner, Codes or Actor. Never a developer-app token, never another person's stream.++ - The capability behind it is `live.agent_stream.publish` for the audience `openvibe.live` (Contracts 0.298.0): a Link principal acting for its owner, Codes or Actor. Never a developer-app token, never another person's stream.## 0.90.0 (2026-11-06)@@@ -158,7 +158,7 @@- Add `openvibe-sdk/exchange`: OpenVibe.Exchange, the buyer and the seller side of the model exchange (the plan of 2026-11-06, "For builders", item 7).- The buyer side speaks the Exchange’s v1 API with a Network developer key: `chat()` (streaming and not, the OpenAI shape), `models()`, `offers(model)`, `catalog()`, `presets()` (list, create, update, delete), `usage()` and `receipt(id)`. Every call takes an `AbortSignal`, and a 429 or a 5xx is retried with a jittered backoff: a safe method always, a write only when it is idempotent, so a completion is never run twice.-- - The seller side registers an offer in the Network’s user-owned AI offers — the same registry OpenVibe.Node’s "Offer to the network" writes to (`POST /internal/ai/offers/report`, the capability `network.resource.report`), so any server that speaks the OpenAI API can be a provider and not only a Node. The SDK sends the document that registry validates, field for field, and refuses an offer the registry would refuse before anything is sent; `earnings()` and `verification()` read the seller dashboard (the tokens served, the credits earned, the canaries, the spot re-runs, the reputation, the uptime).++ - The seller side registers an offer in the Network’s user-owned AI offers — the same registry OpenVibe Link’s "Offer to the network" writes to (`POST /internal/ai/offers/report`, the capability `network.resource.report`), so any server that speaks the OpenAI API can be a provider and not only a Node. The SDK sends the document that registry validates, field for field, and refuses an offer the registry would refuse before anything is sent; `earnings()` and `verification()` read the seller dashboard (the tokens served, the credits earned, the canaries, the spot re-runs, the reputation, the uptime).- The webhooks (usage, earnings, an offer going down) carry `X-OpenVibe-Signature-V2`; `webhooks.verify` checks the raw body in constant time and refuses a timestamp more than 300 s either way.## 0.88.0 (2026-11-06)@@@ -381,8 +381,8 @@of the step.- **Streaming machines.** A step that asks for a GPU or 2 or more cores excludes an offer whose`health.streaming` is `true`, with the reason "it is streaming". The field is not in-- `platform.resource-offer@1` yet (Node reports `screen.state` in its own status, Bot's offer does not carry it),-- so this release fixes the name the Node can start sending; a top-level `offer.streaming` is tolerated too.++ `platform.resource-offer@1` yet (Link reports `screen.state` in its own status, Bot's offer does not carry it),++ so this release fixes the name Link can start sending; a top-level `offer.streaming` is tolerated too.- README and [docs/placement.md](docs/placement.md): the API, the rules, the estimate, escalation and the linetemplates.@@@ -391,8 +391,8 @@ release may change an API and says so here.## 0.75.0 (2026-10-30)--- **`openvibe-sdk/redact`: one secret scrubber for the Node and AI (Memory M2).**-- - `redact(text)` → `{ text, count }` ports OpenVibe.Node's `internal/memory/redact.go` (Node #87): the same shapes,++- **`openvibe-sdk/redact`: one secret scrubber for Link and AI (Memory M2).**++ - `redact(text)` → `{ text, count }` ports OpenVibe Link's `internal/memory/redact.go` (Node #87): the same shapes,the whole-text pass with a sentinel before the per-item pass, the unterminated private key block, Luhn cardnumbers, and the deliberately wide environment-assignment rule. `onlySecret(text)` says an item was nothing but asecret, so AI refuses it.@@@ -485,13 +485,13 @@ release may change an API and says so here.## 0.67.0 (2026-10-27)--- **`openvibe-sdk/bot`: a machine's own models** (AI on your own machines; OpenVibe.Node v0.27.0's ai plugin).++- **`openvibe-sdk/bot`: a machine's own models** (AI on your own machines; OpenVibe Link v0.27.0's ai plugin).- `bot.machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (bot.command@1kind `ai`) and answers `{ model, message, usage?, ms? }`. A machine that does not answer throws an `OpenVibeError`with code `bot.ai_nack`.- `bot.machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".- Types: `Machine` for `machines.list()`, with `computer.ai` (the models and whether a server answers) and-- `computer.ai_speed` (how fast the last model answered there, Node v0.29.0); `MachineChatInput` and `MachineChatResult`;++ `computer.ai_speed` (how fast the last model answered there, Link v0.29.0); `MachineChatInput` and `MachineChatResult`;`MachineCommandInput.kind` takes `ai`.## 0.66.0 (2026-10-27)@@@ -499,7 +499,7 @@ release may change an API and says so here.- **`openvibe-sdk/bot`: machines and desks** (the Desks plan; the roadmap's SDK section). `bot.machines.list()`,`.computer(id)`, `.command(id, { kind, value })` (bot.command@1's computer kinds) and `.deskTurn(id, desk)`.- **`bot.machines.desk(id, { name?, kind?, profile? })`** is a desk of your own on a machine: a browser alone or a whole-- screen, with its own files and sign-ins (OpenVibe.Node v0.19.0 to v0.26.0).++ screen, with its own files and sign-ins (OpenVibe Link v0.19.0 to v0.26.0).- `start`, `stop` and `status`.- `browser`, `screen`, `input` and `exec` on that desk only, never on the person's own screen. `exec` takes`detach` for an app that keeps running until the desk stops.@@@ -206,7 +206,7 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client| `openvibe-sdk/trade` | server | `createTradeClient(client)`: OpenVibe.Trade. `instruments.resolve(q)/list/get/observations(symbol, {metric, before, limit})/documents`, `sources()`; the person's `watchlists.list/get/create/rename/add/removeItem/remove` and `alerts.list/deliveries/create/remove`. Public capabilities since openvibe-contracts 0.208.0. Audience `openvibe.trade` || `openvibe-sdk/space` | server | `createSpaceClient(client)`: OpenVibe.Space, static sites for apps and CI. `deploys.uploadArchive(site, tarGz, {activate, preview, root})`, `uploadFiles(site, [{path, data}], …)`, `uploadSourceBuild(site, archive, {ref, commit_sha})`, `list/get/log/activate/rollback(site, {deploy_id})/remove`; `projects.list/create/get/remove/quota/sites/members.set(p, principal, role)/remove`; `sites.create/get/remove`, `sites.config.get/set/reset` (`{ headers, redirects, spa }`), `sites.source.get/set/remove`; `domains.list/add/verify/remove`. An app reaches only the projects a person added it to (deployer, maintainer or owner). Public capabilities since openvibe-contracts 0.209.0. Audience `openvibe.space` || `openvibe-sdk/host` | server | `createHostClient(client)`: OpenVibe.Host's public hosting catalog, sent without a token. `providers.list/get(slug)`, `plans.list({kind, min_vcpu, min_ram_mb, min_disk_gb, max_price, currency, location, provider, sort, cursor})/get(id)`, `promos.list`, `offers.list({sort, currency, max_monthly, source, priced, cursor})`, `domains.list/tld('dev')/check('example.dev')`, `benchmarks.list/get`. Any other filter is refused (422). Editing the catalog is first-party |--| `openvibe-sdk/redact` | any | The shared secret scrubber (Memory M2): `redact(text)` → `{ text, count }` removes the tool and service keys, PEM private key blocks (including one with no END line), authorization headers, bearer tokens, chat webhooks, connection-string passwords, credential-named assignments and Luhn card numbers, keeping the words around them (`password=[redacted]`); `onlySecret(text)` says an item was nothing but a secret. The same shapes and corpus as OpenVibe.Node's `internal/memory/redact.go`, so the Node and AI cannot drift |++| `openvibe-sdk/redact` | any | The shared secret scrubber (Memory M2): `redact(text)` → `{ text, count }` removes the tool and service keys, PEM private key blocks (including one with no END line), authorization headers, bearer tokens, chat webhooks, connection-string passwords, credential-named assignments and Luhn card numbers, keeping the words around them (`password=[redacted]`); `onlySecret(text)` says an item was nothing but a secret. The same shapes and corpus as OpenVibe Link's `internal/memory/redact.go`, so Link and AI cannot drift || `openvibe-sdk/explicit` | any | `isExplicit(...texts)`: true when AI text says a scene shows sexual content (body parts, sex acts, pornography, nudity); such a moment, clip or paste is not published || `openvibe-sdk/no-looks` | any | **Deprecated (2026-10-31):** AI text may describe people. `noLooks`, `noLooksTags` and `hasLooks` return what they are given; `isExplicit` is re-exported from `openvibe-sdk/explicit` || `openvibe-sdk/front-page` | any | What may lead a front page (a latest or most viewed list, a quote strip, a feed): `frontPageOk(text)` is false for a built-in slur category (OpenVibe.Chat's, spaced out or leet-spelled included), strong profanity in English, Portuguese, Spanish, French or German, or a hate topic out of context; `containsCoreSlur(text)` is the slur check alone. Leave such an item off the front page and show the next one; it stays on its own page |@@@ -223,7 +223,7 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client| `openvibe-sdk/usage` | server | `usageSample(fields)` builds a `platform.usage-sample@1` record (required `id`, `idempotency_key`, `service`, `operation`, `quantity`, `unit`; `at` defaults to now ISO, `source` to `openvibe-sdk/usage`; null fields stripped; money fields never invented); `usageKey(service, ...parts)` joins an idempotency key with `:`; `validateUsageSample(record)` → `{ok, errors}` (openvibe-contracts, required lazily). `createUsageReporter({ db, service, source, table, billingUrl, tokenClient, audience, requireProject, ... })` owns one service's readings: `key(...parts)`/`sample(fields)`, and `record(t, reading)` queues the reading idempotency-keyed in the outbox inside the caller's transaction, relayed to Billing's `billing.usage.record` with backoff (a retry is never dropped; only Billing refusing the reading itself — any 4xx but 401/403/404/408/425/429, which stay pending — marks a row rejected; a 401 drops the cached token). The reporter bills whatever reading it is given: never record first-party or sandbox traffic (pass `requireProject: true` to refuse a reading without a `prj_…` project). `openvibe-sdk/govern` builds its `onUsage` record with it || `openvibe-sdk/commerce` | server | `createCommerceClient(config, {caps, fetchImpl?, tokenClients?, timeoutMs?})`: the Billing client Tips and VIP share. `config` = `{billing: {url, audience, timeoutMs = 10000}, network: {internalUrl}, oauth: {clientId, clientSecret}}`; `caps` maps `intent`, `transfer`, `subscription`, `entitlement` (and optional `rates`, default `entitlement`) to capability strings, one lazily created `serviceAuth` token client per distinct string (`scope` = the string) unless `tokenClients` supplies one, keyed by logical key or capability. Methods: `createIntent` (`kind` purchase with `bits`, or subscription with `creator`/`tier`/`autoRenew`), `createTransfer`, `refundTransfer`, `subscribeWithCredit` (`tier`), `cancelSubscription`, `getSubscription`, `listSubscriptions`, `entitlement`, `rates`, `listTiers(creator)` and `setTier({creator, tier, priceCents, status?, key})` (a creator's tier prices, kept by Billing; optional `caps.tier` = `billing.tier.manage`, which `setTier` needs), `baseUrl`. `Idempotency-Key` is sent verbatim; a 401 invalidates the token and retries once with the same key; problem+json becomes `CommerceError` (`status`, `code`, `body`, `retryable`). `intentKey(prefix, parts)` joins the parts that are set; `receipt(kind, fields)` → `{kind, at, ...fields}` (null stripped, never posted). `openvibe-contracts` is required lazily || `openvibe-sdk/vibes` | server | `createVibesClient({appToken \| tokenProvider, billingUrl, fetchImpl?, timeoutMs?})`: Vibes Pay for developer apps (D12). `charges.create({amount, description, reference, return_url})` sends a whole number of Vibes with the reference as the `Idempotency-Key`, → `{id, pay_url, status, expires_at}`; `charges.get(id)`, `charges.refund(id, {reason?})`. `webhooks.verify(rawBody, signatureHeader, secret, {toleranceSec = 300, now?})` checks `X-OpenVibe-Signature-V2` (`t=<unix>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">`, constant time, replay window) and the legacy `sha256=<hex>` body-only form; also exported as `verifyWebhook`. A float, zero or negative amount is refused before any request; problem+json becomes `VibesError` (`status`, `code`, `body`, `retryable`), a 401 invalidates a token provider and retries once with the same key |--| `openvibe-sdk/exchange` | server | `createExchangeClient({developerKey, baseUrl?, networkUrl?, fetchImpl?, timeoutMs?, maxRetries?, owner?, paths?})`: OpenVibe.Exchange, the buyer and the seller side of the model exchange (the plan of 2026-11-06, item 7). Buyer: `chat(body, {signal})` → the OpenAI shape (`stream: true` answers an async iterable of chunks), `models()`, `offers(model)` (the offers that serve one model, with the price, the speed and the uptime), `catalog()`, `presets.list/create/update/remove` (a preset is stored under its name; there is no PATCH), `usage({days})` (the public aggregate; with `from`/`to`/`requester` the platform usage for that requester) and `receipt(id)`. Seller: `seller.offer({model, price, hours, concurrency, endpoint, machineId, owner})` registers the offer in the Network’s user-owned AI offers (the same registry OpenVibe.Node’s "Offer to the network" writes to: `POST /internal/ai/offers/report`, the capability `network.resource.report`), `seller.earnings()`, `seller.verification()` (canaries, spot re-runs, reputation, uptime) and `seller.offers()`. A 429 or a 5xx is retried with a jittered backoff (a write only when it is idempotent); every call takes an AbortSignal. `webhooks.verify(rawBody, signatureHeader, secret, {toleranceSec = 300, now?})` checks `X-OpenVibe-Signature-V2` (`t=<unix>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">`, constant time, replay window) and the legacy `sha256=<hex>` body-only form; also exported as `verifyWebhook` |++| `openvibe-sdk/exchange` | server | `createExchangeClient({developerKey, baseUrl?, networkUrl?, fetchImpl?, timeoutMs?, maxRetries?, owner?, paths?})`: OpenVibe.Exchange, the buyer and the seller side of the model exchange (the plan of 2026-11-06, item 7). Buyer: `chat(body, {signal})` → the OpenAI shape (`stream: true` answers an async iterable of chunks), `models()`, `offers(model)` (the offers that serve one model, with the price, the speed and the uptime), `catalog()`, `presets.list/create/update/remove` (a preset is stored under its name; there is no PATCH), `usage({days})` (the public aggregate; with `from`/`to`/`requester` the platform usage for that requester) and `receipt(id)`. Seller: `seller.offer({model, price, hours, concurrency, endpoint, machineId, owner})` registers the offer in the Network’s user-owned AI offers (the same registry OpenVibe Link’s "Offer to the network" writes to: `POST /internal/ai/offers/report`, the capability `network.resource.report`), `seller.earnings()`, `seller.verification()` (canaries, spot re-runs, reputation, uptime) and `seller.offers()`. A 429 or a 5xx is retried with a jittered backoff (a write only when it is idempotent); every call takes an AbortSignal. `webhooks.verify(rawBody, signatureHeader, secret, {toleranceSec = 300, now?})` checks `X-OpenVibe-Signature-V2` (`t=<unix>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">`, constant time, replay window) and the legacy `sha256=<hex>` body-only form; also exported as `verifyWebhook` || `openvibe-sdk/telemetry` | server | `telemetrySample(fields)` / `validateTelemetrySample(record)` for `platform.telemetry-sample@1`; `createTelemetry({service, instance?, sink, intervalMs = 15000, now?, log?, maxBuffered = 1000})` → `{record(name, value, labels), gauge(name, value, labels), count(name, labels), flush(), stop()}`: buffered samples flushed to `await sink(samples)` on an unref'd interval and on `stop()` (a `gracefulStop` stop step); `record` builds `{service, instance, operation, at, latency_ms, ...labels}` (a non-number value goes to `extra[name]`); a rejecting sink is logged once and its samples kept; past `maxBuffered` the oldest sample is dropped, with a warning at most once a minute; the per-service PostgreSQL sink: `telemetrySchema(table = 'telemetry_samples')` (migration DDL), `createPgTelemetrySink({db, table?, log?})` (one multi-row INSERT per flush, malformed samples dropped with a warning), `pruneTelemetry({db, table?, olderThanDays = 30})`, `startTelemetryRetention({db, table?, olderThanDays = 30, everyMs = 24 h, delayMs = 60 s, log?})` → `{run(), stop()}` (prunes soon after start and then daily on unref'd timers, so a service redeployed more than daily still prunes) || `openvibe-sdk/cost` | server | Build and validate `platform.rate-card@1` and `platform.cost-snapshot@1` records with `rateCard()`, `costSnapshot()`, `validateRateCard()`, and `validateCostSnapshot()`; `estimate()` calculates a charge from a rate card; the AI Exchange money math in integer nano-USD with `nanoUsd()`, `chargeTokens()`, `expectedCostNano()`, `costPerSuccessNano()`, `formatNanoUsd()` || `openvibe-sdk/runtime` | server | Build and validate `platform.runtime-offer@1` records with `runtimeOffer()` and `validateRuntimeOffer()`; `runtimeClass()`, `RUNTIME_CLASSES`, and `validateRuntimeClass()` handle known runtime classes |@@@ -14,11 +14,11 @@"streams (openvibe-sdk/streams, S7): schedule(), listSchedules(), cancelSchedule() and remindSchedule() for scheduled public agent streams; postStep() and steps.post() from any harness; digest.read() and poll.open()/poll.result() for a Co-pilot service token; onControl() for pause, curtain, mute and end","reports (openvibe-sdk/reports, S7): write() a session report with the person's own token, private by default and idempotent per source and task ref, and read() one the caller may see; examples/cli-agent-stream.js streams any CLI command's run with steps","chat-apps (openvibe-sdk/chat-apps): the three chat apps (Telegram, Discord, Slack) in one place — the delivery checks (Telegram's secret token, Slack's v0 signing, Discord's Ed25519), the button callback (signed once, used once, expiring), each app's payload as the same shape and splitText() for a long answer",-- "exchange (openvibe-sdk/exchange): the buyer side (chat, models, offers, presets, usage, receipts) and the seller side (offer, endpointOffers, removeEndpointOffer, earnings, verification) of OpenVibe.Exchange, with the platform webhook signature check; an offer is a Node machine's Network report or a standalone OpenAI-compatible endpoint registered on the public seller API",++ "exchange (openvibe-sdk/exchange): the buyer side (chat, models, offers, presets, usage, receipts) and the seller side (offer, endpointOffers, removeEndpointOffer, earnings, verification) of OpenVibe.Exchange, with the platform webhook signature check; an offer is a Link machine's Network report or a standalone OpenAI-compatible endpoint registered on the public seller API","locker (openvibe-sdk/locker, openvibe-sdk/locker/server): the Locker envelope (locker.envelope@1) for end-to-end encryption, WebCrypto only, in Node and the browser: seal/open for text and bytes, sealStream/openStream, the three locks (people, password, link), person key pairs (ECDH P-256, ECDSA P-256), the eight-word fingerprint, wrapFor and rewrap, recoveryWords for the phrase to write down, opening a people lock by key, inspect for what an envelope says about itself, and the header checks a service runs without a key","the 1.0 core surface is frozen now, before 1.0 (core, auth, events, db, media): docs/one-point-oh.json gives every export its tier, test/surface.test.js pins the exact names, and removing or renaming one needs a major release; createCodeVerifier and pkceChallenge left in 0.86.0","share (openvibe-sdk/share): transactional local sharing, audience and membership checks, exact-name resolution, recipient-bound ownership transfers, audit, durable events/notifications, uniform API and agent client; locked items (Locker): the lock kind, per-member key slots checked against the key directory, a re-seal on removal through onReseal, and the transfer offer slot",-- "redact (openvibe-sdk/redact): the shared secret scrubber for memory, the same shapes and corpus as OpenVibe.Node's internal/memory/redact.go (redact, onlySecret)",++ "redact (openvibe-sdk/redact): the shared secret scrubber for memory, the same shapes and corpus as OpenVibe Link's internal/memory/redact.go (redact, onlySecret)","core client: retries, timeouts, problem+json errors, trace propagation","auth: service and app tokens, PKCE, offline verification, the Network signing keys (createNetworkKeys), revocation cutoffs","registry discovery and contracts version checks",@@@ -2663,7 +2663,7 @@ function subscribe(topics, onEvent, opts = {}) {* for await (const { event, data, id } of parseSSE(res.body)) { … }** `body` is a ReadableStream<Uint8Array> (fetch), or any async iterable of Uint8Array/string chunks-- * (a Node stream). `event` defaults to 'message'; `id` is the id field of that event (undefined when++ * (a Link stream). `event` defaults to 'message'; `id` is the id field of that event (undefined when* it had none); comments are skipped; `retry: <ms>` is passed to onRetry. A line split across chunks,* including a \r\n pair, is reassembled. Breaking out of the loop releases the stream.*/@@@ -3300,13 +3300,13 @@ function (module, exports) {* and the call is never retried (a code is one-time, so a lost answer cannot be safely repeated).* Every other POST carries an Idempotency-Key (the client generates one when retries are on).*-- * Machines (the OpenVibe Node's computer plugin): `machines.list`, `machines.computer`, `machines.command` (bot.command@1's++ * Machines (the OpenVibe Link's computer plugin): `machines.list`, `machines.computer`, `machines.command` (bot.command@1's* computer kinds) and `machines.deskTurn`; `machines.desk(id, { name?, kind?, profile? })` is a desk of your own on a* machine (the Desks plan: its own screen or browser, files and sign-ins), with start/stop/status and browser, screen,* input and exec on it. A desk command that the machine does not do throws an OpenVibeError with code `bot.desk_nack`* (the machine's own fault code is `fault`), so a desk reads as plain awaits.*-- * A machine's own models (AI on your own machines, contracts 0.219.0, OpenVibe.Node v0.27.0's ai plugin):++ * A machine's own models (AI on your own machines, contracts 0.219.0, OpenVibe Link v0.27.0's ai plugin):* `machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (kind ai) and answers* `{ model, message: { role, content }, usage?, ms? }`; a machine that does not answer throws `bot.ai_nack`.* `machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".@@@ -3401,7 +3401,7 @@ function createBotClient(client, defaults = {}) {},};-- // ── Machines (the OpenVibe Node's computer plugin) and desks of your own on them ──++ // ── Machines (the OpenVibe Link's computer plugin) and desks of your own on them ──const machines = {/** { machines } — the person's machines with their computer capabilities (a service sees the ones opened to it). */list: (o = {}) => call({ path: '/api/v1/machines' }, o),@@@ -4097,13 +4097,13 @@ module.exports = { createThemesClient, followTheme, readTheme, applyTheme, seasofunction (module, exports) {'use strict';/**-- * openvibe-sdk/redact: the shared secret scrubber (Memory M2). It is the JavaScript port of OpenVibe.Node's-- * internal/memory/redact.go (Node #87), so the Node and OpenVibe.AI run one list of shapes against one corpus-- * (test/redact-corpus.json). The Node scrubs a memory file before it uploads it; AI scrubs every item again before it++ * openvibe-sdk/redact: the shared secret scrubber (Memory M2). It is the JavaScript port of OpenVibe Link's++ * internal/memory/redact.go (Node #87), so Link and OpenVibe.AI run one list of shapes against one corpus++ * (test/redact-corpus.json). Link scrubs a memory file before it uploads it; AI scrubs every item again before it* seals it, and refuses an item that was nothing but a secret.** - redact(text) -> { text, count }: every secret shape is replaced with [redacted], and the count says how many were-- * removed. The whole-text pass uses an internal sentinel first, the way the Node scrubs a file before it splits it++ * removed. The whole-text pass uses an internal sentinel first, the way Link scrubs a file before it splits it* into items; the sentinel is resolved to [redacted] before the result is returned. A text that is already redacted* is left alone.* - onlySecret(text): true when nothing but redacted secrets is left ("the item was only a secret").@@@ -4315,7 +4315,7 @@ function redactWith(text, marker) {/*** The whole-text pass with the sentinel, then the per-item pass. A secret that spans two items (a key block in a-- * fence, an .env block) is only seen whole, which is why the Node scrubs a file before it splits it. The sentinel is++ * fence, an .env block) is only seen whole, which is why Link scrubs a file before it splits it. The sentinel is* resolved to [redacted] before the per-item pass, which catches anything only visible once the text is on its own.*/function redactDocument(text) {@@@ -2,7 +2,7 @@OpenVibe.Exchange, the buyer and the seller side of the model exchange (the plan of 2026-11-06, "For builders",item 7). The buyer side speaks the Exchange's v1 API with a Network developer key; the seller side registers an--offer in the Network's user-owned AI offers, the same registry OpenVibe.Node's "Offer to the network" uses.++offer in the Network's user-owned AI offers, the same registry OpenVibe Link's "Offer to the network" uses.```jsconst { createExchangeClient } = require('openvibe-sdk/exchange');@@@ -59,8 +59,8 @@ adult gate. A request names one as `model: '@preset/<name>'`.## The seller side--An offer has two forms. A **Node machine** reports its offer to the Network's user-owned AI offers — the same registry--OpenVibe.Node's "Offer to the network" writes to (`POST /internal/ai/offers/report`, the capability++An offer has two forms. A **Link machine** reports its offer to the Network's user-owned AI offers — the same registry++OpenVibe Link's "Offer to the network" writes to (`POST /internal/ai/offers/report`, the capability`network.resource.report`). A **standalone server** registers itself on the Exchange's public seller API with thedeveloper key, so it needs no Node at all. The SDK validates both before anything is sent.@@@ -88,7 +88,7 @@ the OpenAI API is caught before the offer is registered (`probe: false` turns th### A standalone server: the endpoint form--A server that speaks the OpenAI Chat Completions API can sell without an OpenVibe Node. Give `endpoint` and no++A server that speaks the OpenAI Chat Completions API can sell without an OpenVibe Link. Give `endpoint` and no`machineId`; the offer is registered on the Exchange's public seller API(`POST /api/v1/exchange/seller/offers`) with the same developer key as the buyer side.@@@ -113,7 +113,7 @@ The owner is the person the developer key acts for (a developer app's project owthe address only through its egress guard: https, a public DNS answer checked inside the connection (never an IPliteral), redirects judged again, a timeout, a response byte cap, and no buyer identity in any header. No bearer tokenis sent, so the endpoint must accept the POST as it is (`endpointKey` only authenticates the SDK's own probe). The--endpoint is verified exactly as a Node machine (the canaries, the model fingerprint, about 1% spot re-runs), serves the++endpoint is verified exactly as a machine running Link (the canaries, the model fingerprint, about 1% spot re-runs), serves thefree tier only until verified, and earns 0 on the owner's own requests. An answer from it is labelled a communityendpoint on the receipt (`provider: community-endpoint:<id>`, `provider_access: COMMUNITY_ENDPOINT`). It sees theprompt: only requests that opt in to community capacity reach it.@@@ -77,7 +77,7 @@ five-class preference order.**A streaming machine.** When an offer says its machine is streaming, a step that asks for a GPU or 2 or more coresis excluded with `it is streaming`. The contract does not carry that field yet; `place()` reads--`offer.health.streaming === true` (and tolerates a top-level `offer.streaming`), so OpenVibe.Node can start sending++`offer.health.streaming === true` (and tolerates a top-level `offer.streaming`), so OpenVibe Link can start sendingit without another SDK change. Node's status already reports `screen.state`, but Bot's `platform.resource-offer@1`does not project it today.@@@ -1,7 +1,7 @@# openvibe-sdk/redact--The shared secret scrubber for memory (Memory M2). It is the JavaScript port of OpenVibe.Node's--`internal/memory/redact.go` (Node #87), so the Node and OpenVibe.AI run one shape list against one corpus.++The shared secret scrubber for memory (Memory M2). It is the JavaScript port of OpenVibe Link's++`internal/memory/redact.go` (Node #87), so Link and OpenVibe.AI run one shape list against one corpus.```jsconst { redact, onlySecret } = require('openvibe-sdk/redact');@@@ -32,13 +32,13 @@ A text that was already redacted is left alone. The count is the number of secre## Why the whole-text pass exists--The Node scrubs a whole file with a sentinel before it splits the file into items, then scrubs each item. A key block++Link scrubs a whole file with a sentinel before it splits the file into items, then scrubs each item. A key blockor an `.env` block that spans two items is only seen whole. `redact()` runs the same two passes on one string: thesentinel keeps the place of each secret until the text is resolved to `[redacted]`. This is also why AI runs the--scrubber again before sealing an item: the Node is not the only writer.++scrubber again before sealing an item: Link is not the only writer.## Corpus`test/redact-corpus.json` is the shared list: every shape, the split and unterminated key blocks, the `.env` block and--the harmless texts that must not change. `test/redact.test.js` runs it here; OpenVibe.Node copies the file into++the harmless texts that must not change. `test/redact.test.js` runs it here; OpenVibe Link copies the file into`internal/memory/testdata/` and runs the same cases against its Go `Redact`.@@@ -21,7 +21,7 @@ const { stream, publish } = await streams.startAgentStream({## The desk's loop--The desk (OpenVibe.Node) polls Live every ten seconds and keeps publishing while Live wants video:++The desk (OpenVibe Link) polls Live every ten seconds and keeps publishing while Live wants video:```jsconst s = await streams.state(stream.id, { ticket: publish.ticket });@@@ -65,7 +65,7 @@ await streams.end(stream.id); // the card stays, with t## Who may call--The person's own session token, or a first-party service token acting for that person: a Node principal acting for its++The person's own session token, or a first-party service token acting for that person: a Link principal acting for itsowner (Network issues the node token with `live.agent_stream.publish` for the audience `openvibe.live` and names itsowner in `on_behalf_of`), Codes or Actor. Never a developer-app token, never another person's stream.@@@ -5,7 +5,7 @@** OPENVIBE_TOKEN=… node examples/cli-agent-stream.js --title "Fix the login page" -- npm test*-- * The same path a Node desk uses: startAgentStream gives the stream and its publish ticket, every line the command++ * The same path a Link desk uses: startAgentStream gives the stream and its publish ticket, every line the command* writes is a step, and the stream ends with the command's result. Private by default; pass --public to list it.*/const { spawn } = require('node:child_process');@@@ -15,13 +15,13 @@* and the call is never retried (a code is one-time, so a lost answer cannot be safely repeated).* Every other POST carries an Idempotency-Key (the client generates one when retries are on).*-- * Machines (the OpenVibe Node's computer plugin): `machines.list`, `machines.computer`, `machines.command` (bot.command@1's++ * Machines (the OpenVibe Link's computer plugin): `machines.list`, `machines.computer`, `machines.command` (bot.command@1's* computer kinds) and `machines.deskTurn`; `machines.desk(id, { name?, kind?, profile? })` is a desk of your own on a* machine (the Desks plan: its own screen or browser, files and sign-ins), with start/stop/status and browser, screen,* input and exec on it. A desk command that the machine does not do throws an OpenVibeError with code `bot.desk_nack`* (the machine's own fault code is `fault`), so a desk reads as plain awaits.*-- * A machine's own models (AI on your own machines, contracts 0.219.0, OpenVibe.Node v0.27.0's ai plugin):++ * A machine's own models (AI on your own machines, contracts 0.219.0, OpenVibe Link v0.27.0's ai plugin):* `machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (kind ai) and answers* `{ model, message: { role, content }, usage?, ms? }`; a machine that does not answer throws `bot.ai_nack`.* `machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".@@@ -116,7 +116,7 @@ function createBotClient(client, defaults = {}) {},};-- // ── Machines (the OpenVibe Node's computer plugin) and desks of your own on them ──++ // ── Machines (the OpenVibe Link's computer plugin) and desks of your own on them ──const machines = {/** { machines } — the person's machines with their computer capabilities (a service sees the ones opened to it). */list: (o = {}) => call({ path: '/api/v1/machines' }, o),@@@ -12,12 +12,12 @@* // { id: 'chatcmpl-...', object: 'chat.completion', choices, usage, x_openvibe: { run_id, cost_usd, ... } }* for await (const chunk of exchange.chat({ model: 'openvibe', messages, stream: true })) { ... }*-- * The seller side registers an offer in one of two forms. With `machineId`, it reports a Node machine's offer to the++ * The seller side registers an offer in one of two forms. With `machineId`, it reports a Link machine's offer to the* Network's user-owned AI offers (POST /internal/ai/offers/report, the capability network.resource.report). With* `endpoint` and no machine, it registers a standalone OpenAI-compatible server on the Exchange's public seller API* (POST /api/v1/exchange/seller/offers) with the same developer key the buyer side uses; AI reaches that address only* through its egress guard (https, a public DNS answer, never an IP literal, a timeout, a response cap, no buyer-- * identity in a header) and verifies it exactly as a Node machine (canaries, fingerprint, spot re-runs, free tier only++ * identity in a header) and verifies it exactly as a machine running Link (canaries, fingerprint, spot re-runs, free tier only* until verified, self-dealing earns 0).** const offer = await exchange.seller.offer({@@@ -378,7 +378,7 @@ function createExchangeClient({/*** Register a community offer: what is served, at what price, when, and how many answers at once.*-- * With `machineId`, the offer is reported to the Network's user-owned AI offers - the registry OpenVibe.Node's++ * With `machineId`, the offer is reported to the Network's user-owned AI offers - the registry OpenVibe Link's* "Offer to the network" uses - with the document that registry validates.* offer({ model, price, hours, concurrency, endpoint, machineId, owner, audience, group, ready, health })*@@@ -1,6 +1,6 @@'use strict';/**-- * openvibe-sdk/node — platform.node-capabilities@1, plus nodeOffers() that maps a Node descriptor to a++ * openvibe-sdk/node — platform.node-capabilities@1, plus nodeOffers() that maps a node descriptor to a* platform.resource-offer@1 (kind "node") and a platform.runtime-offer@1.* Pure construction and validation, no I/O and no dependency at require time.*@@@ -32,7 +32,7 @@ function nodeCapabilities(fields) {}/**-- * Map a Node descriptor (platform.node-capabilities@1) to two offers:++ * Map a node descriptor (platform.node-capabilities@1) to two offers:* - resource: a platform.resource-offer@1 with kind "node", capabilities including every advertised* worker capability prefixed "worker:<name>", trust from opts.trust or "first-party";* - runtime: a platform.runtime-offer@1 with kind "container" (the default runtime a node offers),@@@ -183,7 +183,7 @@ function subscribe(topics, onEvent, opts = {}) {* for await (const { event, data, id } of parseSSE(res.body)) { … }** `body` is a ReadableStream<Uint8Array> (fetch), or any async iterable of Uint8Array/string chunks-- * (a Node stream). `event` defaults to 'message'; `id` is the id field of that event (undefined when++ * (a Link stream). `event` defaults to 'message'; `id` is the id field of that event (undefined when* it had none); comments are skipped; `retry: <ms>` is passed to onRetry. A line split across chunks,* including a \r\n pair, is reassembled. Breaking out of the loop releases the stream.*/@@@ -1,12 +1,12 @@'use strict';/**-- * openvibe-sdk/redact: the shared secret scrubber (Memory M2). It is the JavaScript port of OpenVibe.Node's-- * internal/memory/redact.go (Node #87), so the Node and OpenVibe.AI run one list of shapes against one corpus-- * (test/redact-corpus.json). The Node scrubs a memory file before it uploads it; AI scrubs every item again before it++ * openvibe-sdk/redact: the shared secret scrubber (Memory M2). It is the JavaScript port of OpenVibe Link's++ * internal/memory/redact.go (Node #87), so Link and OpenVibe.AI run one list of shapes against one corpus++ * (test/redact-corpus.json). Link scrubs a memory file before it uploads it; AI scrubs every item again before it* seals it, and refuses an item that was nothing but a secret.** - redact(text) -> { text, count }: every secret shape is replaced with [redacted], and the count says how many were-- * removed. The whole-text pass uses an internal sentinel first, the way the Node scrubs a file before it splits it++ * removed. The whole-text pass uses an internal sentinel first, the way Link scrubs a file before it splits it* into items; the sentinel is resolved to [redacted] before the result is returned. A text that is already redacted* is left alone.* - onlySecret(text): true when nothing but redacted secrets is left ("the item was only a secret").@@@ -218,7 +218,7 @@ function redactWith(text, marker) {/*** The whole-text pass with the sentinel, then the per-item pass. A secret that spans two items (a key block in a-- * fence, an .env block) is only seen whole, which is why the Node scrubs a file before it splits it. The sentinel is++ * fence, an .env block) is only seen whole, which is why Link scrubs a file before it splits it. The sentinel is* resolved to [redacted] before the per-item pass, which catches anything only visible once the text is on its own.*/function redactDocument(text) {@@@ -3,7 +3,7 @@* openvibe-sdk/streams — the Agent Streams plan (S1): an agent's own stream on OpenVibe.Live.** An agent stream is a Live stream of kind 'agent' (id ags_…): it has no stream key and takes no slot, and it is-- * published with a ticket (wpt_…) instead of one. The desk (OpenVibe.Node) or a person's browser (a Codes task page)++ * published with a ticket (wpt_…) instead of one. The desk (OpenVibe Link) or a person's browser (a Codes task page)* creates it, keeps the ticket and publishes through it; the owner watches the stream on the page Live answers with.** const streams = createStreamsClient(createClient({ tokenProvider }));@@@ -1,7 +1,7 @@'use strict';/**-- * openvibe-sdk/redact: the shared corpus of secret shapes (Memory M2). It is the same file OpenVibe.Node copies into-- * its internal/memory/testdata, so the Node's Go scrubber and this port cannot drift: every case says what must be++ * openvibe-sdk/redact: the shared corpus of secret shapes (Memory M2). It is the same file OpenVibe Link copies into++ * its internal/memory/testdata, so Link's Go scrubber and this port cannot drift: every case says what must be* gone and what must stay.*/const assert = require('node:assert/strict');@@@ -82,7 +82,7 @@ export interface PairInput {name?: string;}export interface MachineCommandInput { id?: string; kind: 'exec' | 'fs' | 'screen' | 'input' | 'browser' | 'probe' | 'desk' | 'ai'; value: Record<string, unknown>; ms?: number; }--/** A machine's own models (OpenVibe.Node's ai plugin): what it offers and how fast its last model answered there. */++/** A machine's own models (OpenVibe Link's ai plugin): what it offers and how fast its last model answered there. */export interface MachineAi { models: string[]; ready: boolean; max_tokens?: number; }export interface MachineAiSpeed { model: string; tokens_per_s: number; }export interface Machine {@@@ -353,7 +353,7 @@ export type ExchangeClient = {/** The receipt of one answered request. */receipt(id: string, opts?: { signal?: AbortSignal }): Promise<ExchangeReceipt>;seller: {-- /** Register a Node machine's offer in the Network's user-owned AI offers, or an endpoint with the Exchange. */++ /** Register a Link machine's offer in the Network's user-owned AI offers, or an endpoint with the Exchange. */offer(input: ExchangeOfferInput, opts?: { signal?: AbortSignal }): Promise<ExchangeOfferResult>;offer(input: ExchangeEndpointOfferInput, opts?: { signal?: AbortSignal }): Promise<ExchangeEndpointOfferResult>;/** The caller's own endpoint offers, with their verification. */@@@ -20,7 +20,7 @@ export interface Machine {os?: string;/** When the machine was last seen. */last_seen_at?: string;-- /** The OpenVibe Node version the machine reports. */++ /** The OpenVibe Link version the machine reports. */node_version?: string;}
@@ -91,19 +91,19 @@
@@ -91,19 +91,19 @@
idempotent per source and task ref) and `read()` gets one the caller may see. The Report service redacts every text
idempotent per source and task ref) and `read()` gets one the caller may see. The Report service redacts every text
field; the client never sends a raw transcript.
field; the client never sends a raw transcript.
- Add `examples/cli-agent-stream.js`: about 40 lines that start a stream, spawn any CLI command, post its steps and end
- Add `examples/cli-agent-stream.js`: about 40 lines that start a stream, spawn any CLI command, post its steps and end
- the stream with the command's result, using the same ticket path a Node desk uses.
+ the stream with the command's result, using the same ticket path a Link desk uses.
## 0.93.0 (2026-11-06)
## 0.93.0 (2026-11-06)
- `openvibe-sdk/exchange`: the seller side gains the **endpoint form**, so any OpenAI-compatible server can sell, not
- `openvibe-sdk/exchange`: the seller side gains the **endpoint form**, so any OpenAI-compatible server can sell, not
- only an OpenVibe Node machine. `seller.offer({ endpoint, models, price, hours, concurrency, audience, name })` — with
+ only an OpenVibe Link machine. `seller.offer({ endpoint, models, price, hours, concurrency, audience, name })` — with
no `machineId` — registers the server on OpenVibe.Exchange's public seller API
no `machineId` — registers the server on OpenVibe.Exchange's public seller API
(`POST /api/v1/exchange/seller/offers`) with the same developer key the buyer side uses. The owner is the person the
(`POST /api/v1/exchange/seller/offers`) with the same developer key the buyer side uses. The owner is the person the
key acts for (a developer app's project owner), never a body field, and the SDK refuses an endpoint that is not https
key acts for (a developer app's project owner), never a body field, and the SDK refuses an endpoint that is not https
before anything is sent.
before anything is sent.
- The platform reaches that address only through its egress guard (https, a public DNS answer checked inside the
- The platform reaches that address only through its egress guard (https, a public DNS answer checked inside the
connection, never an IP literal, redirects judged again, a timeout and a response byte cap, no buyer identity in a
connection, never an IP literal, redirects judged again, a timeout and a response byte cap, no buyer identity in a
- header) and verifies it exactly as a Node machine (canaries, the model fingerprint, about 1% spot re-runs, the free
+ header) and verifies it exactly as a machine running Link (canaries, the model fingerprint, about 1% spot re-runs, the free
tier until verified, self-dealing earns 0). Its answers are labelled `community-endpoint:<id>` on the receipt.
tier until verified, self-dealing earns 0). Its answers are labelled `community-endpoint:<id>` on the receipt.
- `seller.endpointOffers()` lists the caller's endpoint offers with their verification;
- `seller.endpointOffers()` lists the caller's endpoint offers with their verification;
`seller.removeEndpointOffer(id)` removes one. `machineId` still selects the Network registry form.
`seller.removeEndpointOffer(id)` removes one. `machineId` still selects the Network registry form.
@@ -137,7 +137,7 @@
@@ -137,7 +137,7 @@
- `state(id, { ticket })` is what the desk polls every ten seconds: `{ viewers, encode, curtain, ends_at }`. While `encode` is false the desk stops encoding and sends a snapshot every five seconds with `postFrame(id, bytes, { ticket, contentType })` (JPEG or WebP, at most 200 KB).
- `state(id, { ticket })` is what the desk polls every ten seconds: `{ viewers, encode, curtain, ends_at }`. While `encode` is false the desk stops encoding and sends a snapshot every five seconds with `postFrame(id, bytes, { ticket, contentType })` (JPEG or WebP, at most 200 KB).
- `setCurtain(id, up, reason)` raises the desk's privacy screen (while it is up Live pauses every viewer's consumer, so a viewer sees "Private screen" and never why), `setVisibility(id, v)` switches who may watch, `end(id)` ends the stream (the card stays, with the last frame and the VOD) and `viewTicket(id)` mints the player's view ticket (ten minutes).
- `setCurtain(id, up, reason)` raises the desk's privacy screen (while it is up Live pauses every viewer's consumer, so a viewer sees "Private screen" and never why), `setVisibility(id, v)` switches who may watch, `end(id)` ends the stream (the card stays, with the last frame and the VOD) and `viewTicket(id)` mints the player's view ticket (ten minutes).
- The publish ticket is a bearer of its own: on the desk's routes (`state`, `postFrame`) the desk sends the ticket, never the person's token. Every body is checked in the module before it leaves, and a bad id, visibility, curtain reason or an oversized frame is refused with a TypeError.
- The publish ticket is a bearer of its own: on the desk's routes (`state`, `postFrame`) the desk sends the ticket, never the person's token. Every body is checked in the module before it leaves, and a bad id, visibility, curtain reason or an oversized frame is refused with a TypeError.
- - The capability behind it is `live.agent_stream.publish` for the audience `openvibe.live` (Contracts 0.298.0): a Node principal acting for its owner, Codes or Actor. Never a developer-app token, never another person's stream.
+ - The capability behind it is `live.agent_stream.publish` for the audience `openvibe.live` (Contracts 0.298.0): a Link principal acting for its owner, Codes or Actor. Never a developer-app token, never another person's stream.
## 0.90.0 (2026-11-06)
## 0.90.0 (2026-11-06)
@@ -158,7 +158,7 @@
@@ -158,7 +158,7 @@
- Add `openvibe-sdk/exchange`: OpenVibe.Exchange, the buyer and the seller side of the model exchange (the plan of 2026-11-06, "For builders", item 7).
- Add `openvibe-sdk/exchange`: OpenVibe.Exchange, the buyer and the seller side of the model exchange (the plan of 2026-11-06, "For builders", item 7).
- The buyer side speaks the Exchange’s v1 API with a Network developer key: `chat()` (streaming and not, the OpenAI shape), `models()`, `offers(model)`, `catalog()`, `presets()` (list, create, update, delete), `usage()` and `receipt(id)`. Every call takes an `AbortSignal`, and a 429 or a 5xx is retried with a jittered backoff: a safe method always, a write only when it is idempotent, so a completion is never run twice.
- The buyer side speaks the Exchange’s v1 API with a Network developer key: `chat()` (streaming and not, the OpenAI shape), `models()`, `offers(model)`, `catalog()`, `presets()` (list, create, update, delete), `usage()` and `receipt(id)`. Every call takes an `AbortSignal`, and a 429 or a 5xx is retried with a jittered backoff: a safe method always, a write only when it is idempotent, so a completion is never run twice.
- - The seller side registers an offer in the Network’s user-owned AI offers — the same registry OpenVibe.Node’s "Offer to the network" writes to (`POST /internal/ai/offers/report`, the capability `network.resource.report`), so any server that speaks the OpenAI API can be a provider and not only a Node. The SDK sends the document that registry validates, field for field, and refuses an offer the registry would refuse before anything is sent; `earnings()` and `verification()` read the seller dashboard (the tokens served, the credits earned, the canaries, the spot re-runs, the reputation, the uptime).
+ - The seller side registers an offer in the Network’s user-owned AI offers — the same registry OpenVibe Link’s "Offer to the network" writes to (`POST /internal/ai/offers/report`, the capability `network.resource.report`), so any server that speaks the OpenAI API can be a provider and not only a Node. The SDK sends the document that registry validates, field for field, and refuses an offer the registry would refuse before anything is sent; `earnings()` and `verification()` read the seller dashboard (the tokens served, the credits earned, the canaries, the spot re-runs, the reputation, the uptime).
- The webhooks (usage, earnings, an offer going down) carry `X-OpenVibe-Signature-V2`; `webhooks.verify` checks the raw body in constant time and refuses a timestamp more than 300 s either way.
- The webhooks (usage, earnings, an offer going down) carry `X-OpenVibe-Signature-V2`; `webhooks.verify` checks the raw body in constant time and refuses a timestamp more than 300 s either way.
## 0.88.0 (2026-11-06)
## 0.88.0 (2026-11-06)
@@ -381,8 +381,8 @@
@@ -381,8 +381,8 @@
of the step.
of the step.
- **Streaming machines.** A step that asks for a GPU or 2 or more cores excludes an offer whose
- **Streaming machines.** A step that asks for a GPU or 2 or more cores excludes an offer whose
`health.streaming` is `true`, with the reason "it is streaming". The field is not in
`health.streaming` is `true`, with the reason "it is streaming". The field is not in
- `platform.resource-offer@1` yet (Node reports `screen.state` in its own status, Bot's offer does not carry it),
- so this release fixes the name the Node can start sending; a top-level `offer.streaming` is tolerated too.
+ `platform.resource-offer@1` yet (Link reports `screen.state` in its own status, Bot's offer does not carry it),
+ so this release fixes the name Link can start sending; a top-level `offer.streaming` is tolerated too.
- README and [docs/placement.md](docs/placement.md): the API, the rules, the estimate, escalation and the line
- README and [docs/placement.md](docs/placement.md): the API, the rules, the estimate, escalation and the line
templates.
templates.
@@ -391,8 +391,8 @@ release may change an API and says so here.
@@ -391,8 +391,8 @@ release may change an API and says so here.
## 0.75.0 (2026-10-30)
## 0.75.0 (2026-10-30)
-- **`openvibe-sdk/redact`: one secret scrubber for the Node and AI (Memory M2).**
- - `redact(text)` → `{ text, count }` ports OpenVibe.Node's `internal/memory/redact.go` (Node #87): the same shapes,
+- **`openvibe-sdk/redact`: one secret scrubber for Link and AI (Memory M2).**
+ - `redact(text)` → `{ text, count }` ports OpenVibe Link's `internal/memory/redact.go` (Node #87): the same shapes,
the whole-text pass with a sentinel before the per-item pass, the unterminated private key block, Luhn card
the whole-text pass with a sentinel before the per-item pass, the unterminated private key block, Luhn card
numbers, and the deliberately wide environment-assignment rule. `onlySecret(text)` says an item was nothing but a
numbers, and the deliberately wide environment-assignment rule. `onlySecret(text)` says an item was nothing but a
secret, so AI refuses it.
secret, so AI refuses it.
@@ -485,13 +485,13 @@ release may change an API and says so here.
@@ -485,13 +485,13 @@ release may change an API and says so here.
## 0.67.0 (2026-10-27)
## 0.67.0 (2026-10-27)
-- **`openvibe-sdk/bot`: a machine's own models** (AI on your own machines; OpenVibe.Node v0.27.0's ai plugin).
+- **`openvibe-sdk/bot`: a machine's own models** (AI on your own machines; OpenVibe Link v0.27.0's ai plugin).
- `bot.machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (bot.command@1
- `bot.machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (bot.command@1
kind `ai`) and answers `{ model, message, usage?, ms? }`. A machine that does not answer throws an `OpenVibeError`
kind `ai`) and answers `{ model, message, usage?, ms? }`. A machine that does not answer throws an `OpenVibeError`
with code `bot.ai_nack`.
with code `bot.ai_nack`.
- `bot.machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".
- `bot.machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".
- Types: `Machine` for `machines.list()`, with `computer.ai` (the models and whether a server answers) and
- Types: `Machine` for `machines.list()`, with `computer.ai` (the models and whether a server answers) and
- `computer.ai_speed` (how fast the last model answered there, Node v0.29.0); `MachineChatInput` and `MachineChatResult`;
+ `computer.ai_speed` (how fast the last model answered there, Link v0.29.0); `MachineChatInput` and `MachineChatResult`;
`MachineCommandInput.kind` takes `ai`.
`MachineCommandInput.kind` takes `ai`.
## 0.66.0 (2026-10-27)
## 0.66.0 (2026-10-27)
@@ -499,7 +499,7 @@ release may change an API and says so here.
@@ -499,7 +499,7 @@ release may change an API and says so here.
- **`openvibe-sdk/bot`: machines and desks** (the Desks plan; the roadmap's SDK section). `bot.machines.list()`,
- **`openvibe-sdk/bot`: machines and desks** (the Desks plan; the roadmap's SDK section). `bot.machines.list()`,
`.computer(id)`, `.command(id, { kind, value })` (bot.command@1's computer kinds) and `.deskTurn(id, desk)`.
`.computer(id)`, `.command(id, { kind, value })` (bot.command@1's computer kinds) and `.deskTurn(id, desk)`.
- **`bot.machines.desk(id, { name?, kind?, profile? })`** is a desk of your own on a machine: a browser alone or a whole
- **`bot.machines.desk(id, { name?, kind?, profile? })`** is a desk of your own on a machine: a browser alone or a whole
- screen, with its own files and sign-ins (OpenVibe.Node v0.19.0 to v0.26.0).
+ screen, with its own files and sign-ins (OpenVibe Link v0.19.0 to v0.26.0).
- `start`, `stop` and `status`.
- `start`, `stop` and `status`.
- `browser`, `screen`, `input` and `exec` on that desk only, never on the person's own screen. `exec` takes
- `browser`, `screen`, `input` and `exec` on that desk only, never on the person's own screen. `exec` takes
`detach` for an app that keeps running until the desk stops.
`detach` for an app that keeps running until the desk stops.
@@ -206,7 +206,7 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
@@ -206,7 +206,7 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
| `openvibe-sdk/trade` | server | `createTradeClient(client)`: OpenVibe.Trade. `instruments.resolve(q)/list/get/observations(symbol, {metric, before, limit})/documents`, `sources()`; the person's `watchlists.list/get/create/rename/add/removeItem/remove` and `alerts.list/deliveries/create/remove`. Public capabilities since openvibe-contracts 0.208.0. Audience `openvibe.trade` |
| `openvibe-sdk/trade` | server | `createTradeClient(client)`: OpenVibe.Trade. `instruments.resolve(q)/list/get/observations(symbol, {metric, before, limit})/documents`, `sources()`; the person's `watchlists.list/get/create/rename/add/removeItem/remove` and `alerts.list/deliveries/create/remove`. Public capabilities since openvibe-contracts 0.208.0. Audience `openvibe.trade` |
| `openvibe-sdk/space` | server | `createSpaceClient(client)`: OpenVibe.Space, static sites for apps and CI. `deploys.uploadArchive(site, tarGz, {activate, preview, root})`, `uploadFiles(site, [{path, data}], …)`, `uploadSourceBuild(site, archive, {ref, commit_sha})`, `list/get/log/activate/rollback(site, {deploy_id})/remove`; `projects.list/create/get/remove/quota/sites/members.set(p, principal, role)/remove`; `sites.create/get/remove`, `sites.config.get/set/reset` (`{ headers, redirects, spa }`), `sites.source.get/set/remove`; `domains.list/add/verify/remove`. An app reaches only the projects a person added it to (deployer, maintainer or owner). Public capabilities since openvibe-contracts 0.209.0. Audience `openvibe.space` |
| `openvibe-sdk/space` | server | `createSpaceClient(client)`: OpenVibe.Space, static sites for apps and CI. `deploys.uploadArchive(site, tarGz, {activate, preview, root})`, `uploadFiles(site, [{path, data}], …)`, `uploadSourceBuild(site, archive, {ref, commit_sha})`, `list/get/log/activate/rollback(site, {deploy_id})/remove`; `projects.list/create/get/remove/quota/sites/members.set(p, principal, role)/remove`; `sites.create/get/remove`, `sites.config.get/set/reset` (`{ headers, redirects, spa }`), `sites.source.get/set/remove`; `domains.list/add/verify/remove`. An app reaches only the projects a person added it to (deployer, maintainer or owner). Public capabilities since openvibe-contracts 0.209.0. Audience `openvibe.space` |
| `openvibe-sdk/host` | server | `createHostClient(client)`: OpenVibe.Host's public hosting catalog, sent without a token. `providers.list/get(slug)`, `plans.list({kind, min_vcpu, min_ram_mb, min_disk_gb, max_price, currency, location, provider, sort, cursor})/get(id)`, `promos.list`, `offers.list({sort, currency, max_monthly, source, priced, cursor})`, `domains.list/tld('dev')/check('example.dev')`, `benchmarks.list/get`. Any other filter is refused (422). Editing the catalog is first-party |
| `openvibe-sdk/host` | server | `createHostClient(client)`: OpenVibe.Host's public hosting catalog, sent without a token. `providers.list/get(slug)`, `plans.list({kind, min_vcpu, min_ram_mb, min_disk_gb, max_price, currency, location, provider, sort, cursor})/get(id)`, `promos.list`, `offers.list({sort, currency, max_monthly, source, priced, cursor})`, `domains.list/tld('dev')/check('example.dev')`, `benchmarks.list/get`. Any other filter is refused (422). Editing the catalog is first-party |
-| `openvibe-sdk/redact` | any | The shared secret scrubber (Memory M2): `redact(text)` → `{ text, count }` removes the tool and service keys, PEM private key blocks (including one with no END line), authorization headers, bearer tokens, chat webhooks, connection-string passwords, credential-named assignments and Luhn card numbers, keeping the words around them (`password=[redacted]`); `onlySecret(text)` says an item was nothing but a secret. The same shapes and corpus as OpenVibe.Node's `internal/memory/redact.go`, so the Node and AI cannot drift |
+| `openvibe-sdk/redact` | any | The shared secret scrubber (Memory M2): `redact(text)` → `{ text, count }` removes the tool and service keys, PEM private key blocks (including one with no END line), authorization headers, bearer tokens, chat webhooks, connection-string passwords, credential-named assignments and Luhn card numbers, keeping the words around them (`password=[redacted]`); `onlySecret(text)` says an item was nothing but a secret. The same shapes and corpus as OpenVibe Link's `internal/memory/redact.go`, so Link and AI cannot drift |
| `openvibe-sdk/explicit` | any | `isExplicit(...texts)`: true when AI text says a scene shows sexual content (body parts, sex acts, pornography, nudity); such a moment, clip or paste is not published |
| `openvibe-sdk/explicit` | any | `isExplicit(...texts)`: true when AI text says a scene shows sexual content (body parts, sex acts, pornography, nudity); such a moment, clip or paste is not published |
| `openvibe-sdk/no-looks` | any | **Deprecated (2026-10-31):** AI text may describe people. `noLooks`, `noLooksTags` and `hasLooks` return what they are given; `isExplicit` is re-exported from `openvibe-sdk/explicit` |
| `openvibe-sdk/no-looks` | any | **Deprecated (2026-10-31):** AI text may describe people. `noLooks`, `noLooksTags` and `hasLooks` return what they are given; `isExplicit` is re-exported from `openvibe-sdk/explicit` |
| `openvibe-sdk/front-page` | any | What may lead a front page (a latest or most viewed list, a quote strip, a feed): `frontPageOk(text)` is false for a built-in slur category (OpenVibe.Chat's, spaced out or leet-spelled included), strong profanity in English, Portuguese, Spanish, French or German, or a hate topic out of context; `containsCoreSlur(text)` is the slur check alone. Leave such an item off the front page and show the next one; it stays on its own page |
| `openvibe-sdk/front-page` | any | What may lead a front page (a latest or most viewed list, a quote strip, a feed): `frontPageOk(text)` is false for a built-in slur category (OpenVibe.Chat's, spaced out or leet-spelled included), strong profanity in English, Portuguese, Spanish, French or German, or a hate topic out of context; `containsCoreSlur(text)` is the slur check alone. Leave such an item off the front page and show the next one; it stays on its own page |
@@ -223,7 +223,7 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
@@ -223,7 +223,7 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
| `openvibe-sdk/usage` | server | `usageSample(fields)` builds a `platform.usage-sample@1` record (required `id`, `idempotency_key`, `service`, `operation`, `quantity`, `unit`; `at` defaults to now ISO, `source` to `openvibe-sdk/usage`; null fields stripped; money fields never invented); `usageKey(service, ...parts)` joins an idempotency key with `:`; `validateUsageSample(record)` → `{ok, errors}` (openvibe-contracts, required lazily). `createUsageReporter({ db, service, source, table, billingUrl, tokenClient, audience, requireProject, ... })` owns one service's readings: `key(...parts)`/`sample(fields)`, and `record(t, reading)` queues the reading idempotency-keyed in the outbox inside the caller's transaction, relayed to Billing's `billing.usage.record` with backoff (a retry is never dropped; only Billing refusing the reading itself — any 4xx but 401/403/404/408/425/429, which stay pending — marks a row rejected; a 401 drops the cached token). The reporter bills whatever reading it is given: never record first-party or sandbox traffic (pass `requireProject: true` to refuse a reading without a `prj_…` project). `openvibe-sdk/govern` builds its `onUsage` record with it |
| `openvibe-sdk/usage` | server | `usageSample(fields)` builds a `platform.usage-sample@1` record (required `id`, `idempotency_key`, `service`, `operation`, `quantity`, `unit`; `at` defaults to now ISO, `source` to `openvibe-sdk/usage`; null fields stripped; money fields never invented); `usageKey(service, ...parts)` joins an idempotency key with `:`; `validateUsageSample(record)` → `{ok, errors}` (openvibe-contracts, required lazily). `createUsageReporter({ db, service, source, table, billingUrl, tokenClient, audience, requireProject, ... })` owns one service's readings: `key(...parts)`/`sample(fields)`, and `record(t, reading)` queues the reading idempotency-keyed in the outbox inside the caller's transaction, relayed to Billing's `billing.usage.record` with backoff (a retry is never dropped; only Billing refusing the reading itself — any 4xx but 401/403/404/408/425/429, which stay pending — marks a row rejected; a 401 drops the cached token). The reporter bills whatever reading it is given: never record first-party or sandbox traffic (pass `requireProject: true` to refuse a reading without a `prj_…` project). `openvibe-sdk/govern` builds its `onUsage` record with it |
| `openvibe-sdk/commerce` | server | `createCommerceClient(config, {caps, fetchImpl?, tokenClients?, timeoutMs?})`: the Billing client Tips and VIP share. `config` = `{billing: {url, audience, timeoutMs = 10000}, network: {internalUrl}, oauth: {clientId, clientSecret}}`; `caps` maps `intent`, `transfer`, `subscription`, `entitlement` (and optional `rates`, default `entitlement`) to capability strings, one lazily created `serviceAuth` token client per distinct string (`scope` = the string) unless `tokenClients` supplies one, keyed by logical key or capability. Methods: `createIntent` (`kind` purchase with `bits`, or subscription with `creator`/`tier`/`autoRenew`), `createTransfer`, `refundTransfer`, `subscribeWithCredit` (`tier`), `cancelSubscription`, `getSubscription`, `listSubscriptions`, `entitlement`, `rates`, `listTiers(creator)` and `setTier({creator, tier, priceCents, status?, key})` (a creator's tier prices, kept by Billing; optional `caps.tier` = `billing.tier.manage`, which `setTier` needs), `baseUrl`. `Idempotency-Key` is sent verbatim; a 401 invalidates the token and retries once with the same key; problem+json becomes `CommerceError` (`status`, `code`, `body`, `retryable`). `intentKey(prefix, parts)` joins the parts that are set; `receipt(kind, fields)` → `{kind, at, ...fields}` (null stripped, never posted). `openvibe-contracts` is required lazily |
| `openvibe-sdk/commerce` | server | `createCommerceClient(config, {caps, fetchImpl?, tokenClients?, timeoutMs?})`: the Billing client Tips and VIP share. `config` = `{billing: {url, audience, timeoutMs = 10000}, network: {internalUrl}, oauth: {clientId, clientSecret}}`; `caps` maps `intent`, `transfer`, `subscription`, `entitlement` (and optional `rates`, default `entitlement`) to capability strings, one lazily created `serviceAuth` token client per distinct string (`scope` = the string) unless `tokenClients` supplies one, keyed by logical key or capability. Methods: `createIntent` (`kind` purchase with `bits`, or subscription with `creator`/`tier`/`autoRenew`), `createTransfer`, `refundTransfer`, `subscribeWithCredit` (`tier`), `cancelSubscription`, `getSubscription`, `listSubscriptions`, `entitlement`, `rates`, `listTiers(creator)` and `setTier({creator, tier, priceCents, status?, key})` (a creator's tier prices, kept by Billing; optional `caps.tier` = `billing.tier.manage`, which `setTier` needs), `baseUrl`. `Idempotency-Key` is sent verbatim; a 401 invalidates the token and retries once with the same key; problem+json becomes `CommerceError` (`status`, `code`, `body`, `retryable`). `intentKey(prefix, parts)` joins the parts that are set; `receipt(kind, fields)` → `{kind, at, ...fields}` (null stripped, never posted). `openvibe-contracts` is required lazily |
| `openvibe-sdk/vibes` | server | `createVibesClient({appToken \| tokenProvider, billingUrl, fetchImpl?, timeoutMs?})`: Vibes Pay for developer apps (D12). `charges.create({amount, description, reference, return_url})` sends a whole number of Vibes with the reference as the `Idempotency-Key`, → `{id, pay_url, status, expires_at}`; `charges.get(id)`, `charges.refund(id, {reason?})`. `webhooks.verify(rawBody, signatureHeader, secret, {toleranceSec = 300, now?})` checks `X-OpenVibe-Signature-V2` (`t=<unix>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">`, constant time, replay window) and the legacy `sha256=<hex>` body-only form; also exported as `verifyWebhook`. A float, zero or negative amount is refused before any request; problem+json becomes `VibesError` (`status`, `code`, `body`, `retryable`), a 401 invalidates a token provider and retries once with the same key |
| `openvibe-sdk/vibes` | server | `createVibesClient({appToken \| tokenProvider, billingUrl, fetchImpl?, timeoutMs?})`: Vibes Pay for developer apps (D12). `charges.create({amount, description, reference, return_url})` sends a whole number of Vibes with the reference as the `Idempotency-Key`, → `{id, pay_url, status, expires_at}`; `charges.get(id)`, `charges.refund(id, {reason?})`. `webhooks.verify(rawBody, signatureHeader, secret, {toleranceSec = 300, now?})` checks `X-OpenVibe-Signature-V2` (`t=<unix>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">`, constant time, replay window) and the legacy `sha256=<hex>` body-only form; also exported as `verifyWebhook`. A float, zero or negative amount is refused before any request; problem+json becomes `VibesError` (`status`, `code`, `body`, `retryable`), a 401 invalidates a token provider and retries once with the same key |
-| `openvibe-sdk/exchange` | server | `createExchangeClient({developerKey, baseUrl?, networkUrl?, fetchImpl?, timeoutMs?, maxRetries?, owner?, paths?})`: OpenVibe.Exchange, the buyer and the seller side of the model exchange (the plan of 2026-11-06, item 7). Buyer: `chat(body, {signal})` → the OpenAI shape (`stream: true` answers an async iterable of chunks), `models()`, `offers(model)` (the offers that serve one model, with the price, the speed and the uptime), `catalog()`, `presets.list/create/update/remove` (a preset is stored under its name; there is no PATCH), `usage({days})` (the public aggregate; with `from`/`to`/`requester` the platform usage for that requester) and `receipt(id)`. Seller: `seller.offer({model, price, hours, concurrency, endpoint, machineId, owner})` registers the offer in the Network’s user-owned AI offers (the same registry OpenVibe.Node’s "Offer to the network" writes to: `POST /internal/ai/offers/report`, the capability `network.resource.report`), `seller.earnings()`, `seller.verification()` (canaries, spot re-runs, reputation, uptime) and `seller.offers()`. A 429 or a 5xx is retried with a jittered backoff (a write only when it is idempotent); every call takes an AbortSignal. `webhooks.verify(rawBody, signatureHeader, secret, {toleranceSec = 300, now?})` checks `X-OpenVibe-Signature-V2` (`t=<unix>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">`, constant time, replay window) and the legacy `sha256=<hex>` body-only form; also exported as `verifyWebhook` |
+| `openvibe-sdk/exchange` | server | `createExchangeClient({developerKey, baseUrl?, networkUrl?, fetchImpl?, timeoutMs?, maxRetries?, owner?, paths?})`: OpenVibe.Exchange, the buyer and the seller side of the model exchange (the plan of 2026-11-06, item 7). Buyer: `chat(body, {signal})` → the OpenAI shape (`stream: true` answers an async iterable of chunks), `models()`, `offers(model)` (the offers that serve one model, with the price, the speed and the uptime), `catalog()`, `presets.list/create/update/remove` (a preset is stored under its name; there is no PATCH), `usage({days})` (the public aggregate; with `from`/`to`/`requester` the platform usage for that requester) and `receipt(id)`. Seller: `seller.offer({model, price, hours, concurrency, endpoint, machineId, owner})` registers the offer in the Network’s user-owned AI offers (the same registry OpenVibe Link’s "Offer to the network" writes to: `POST /internal/ai/offers/report`, the capability `network.resource.report`), `seller.earnings()`, `seller.verification()` (canaries, spot re-runs, reputation, uptime) and `seller.offers()`. A 429 or a 5xx is retried with a jittered backoff (a write only when it is idempotent); every call takes an AbortSignal. `webhooks.verify(rawBody, signatureHeader, secret, {toleranceSec = 300, now?})` checks `X-OpenVibe-Signature-V2` (`t=<unix>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">`, constant time, replay window) and the legacy `sha256=<hex>` body-only form; also exported as `verifyWebhook` |
| `openvibe-sdk/telemetry` | server | `telemetrySample(fields)` / `validateTelemetrySample(record)` for `platform.telemetry-sample@1`; `createTelemetry({service, instance?, sink, intervalMs = 15000, now?, log?, maxBuffered = 1000})` → `{record(name, value, labels), gauge(name, value, labels), count(name, labels), flush(), stop()}`: buffered samples flushed to `await sink(samples)` on an unref'd interval and on `stop()` (a `gracefulStop` stop step); `record` builds `{service, instance, operation, at, latency_ms, ...labels}` (a non-number value goes to `extra[name]`); a rejecting sink is logged once and its samples kept; past `maxBuffered` the oldest sample is dropped, with a warning at most once a minute; the per-service PostgreSQL sink: `telemetrySchema(table = 'telemetry_samples')` (migration DDL), `createPgTelemetrySink({db, table?, log?})` (one multi-row INSERT per flush, malformed samples dropped with a warning), `pruneTelemetry({db, table?, olderThanDays = 30})`, `startTelemetryRetention({db, table?, olderThanDays = 30, everyMs = 24 h, delayMs = 60 s, log?})` → `{run(), stop()}` (prunes soon after start and then daily on unref'd timers, so a service redeployed more than daily still prunes) |
| `openvibe-sdk/telemetry` | server | `telemetrySample(fields)` / `validateTelemetrySample(record)` for `platform.telemetry-sample@1`; `createTelemetry({service, instance?, sink, intervalMs = 15000, now?, log?, maxBuffered = 1000})` → `{record(name, value, labels), gauge(name, value, labels), count(name, labels), flush(), stop()}`: buffered samples flushed to `await sink(samples)` on an unref'd interval and on `stop()` (a `gracefulStop` stop step); `record` builds `{service, instance, operation, at, latency_ms, ...labels}` (a non-number value goes to `extra[name]`); a rejecting sink is logged once and its samples kept; past `maxBuffered` the oldest sample is dropped, with a warning at most once a minute; the per-service PostgreSQL sink: `telemetrySchema(table = 'telemetry_samples')` (migration DDL), `createPgTelemetrySink({db, table?, log?})` (one multi-row INSERT per flush, malformed samples dropped with a warning), `pruneTelemetry({db, table?, olderThanDays = 30})`, `startTelemetryRetention({db, table?, olderThanDays = 30, everyMs = 24 h, delayMs = 60 s, log?})` → `{run(), stop()}` (prunes soon after start and then daily on unref'd timers, so a service redeployed more than daily still prunes) |
| `openvibe-sdk/cost` | server | Build and validate `platform.rate-card@1` and `platform.cost-snapshot@1` records with `rateCard()`, `costSnapshot()`, `validateRateCard()`, and `validateCostSnapshot()`; `estimate()` calculates a charge from a rate card; the AI Exchange money math in integer nano-USD with `nanoUsd()`, `chargeTokens()`, `expectedCostNano()`, `costPerSuccessNano()`, `formatNanoUsd()` |
| `openvibe-sdk/cost` | server | Build and validate `platform.rate-card@1` and `platform.cost-snapshot@1` records with `rateCard()`, `costSnapshot()`, `validateRateCard()`, and `validateCostSnapshot()`; `estimate()` calculates a charge from a rate card; the AI Exchange money math in integer nano-USD with `nanoUsd()`, `chargeTokens()`, `expectedCostNano()`, `costPerSuccessNano()`, `formatNanoUsd()` |
| `openvibe-sdk/runtime` | server | Build and validate `platform.runtime-offer@1` records with `runtimeOffer()` and `validateRuntimeOffer()`; `runtimeClass()`, `RUNTIME_CLASSES`, and `validateRuntimeClass()` handle known runtime classes |
| `openvibe-sdk/runtime` | server | Build and validate `platform.runtime-offer@1` records with `runtimeOffer()` and `validateRuntimeOffer()`; `runtimeClass()`, `RUNTIME_CLASSES`, and `validateRuntimeClass()` handle known runtime classes |
@@ -14,11 +14,11 @@
@@ -14,11 +14,11 @@
"streams (openvibe-sdk/streams, S7): schedule(), listSchedules(), cancelSchedule() and remindSchedule() for scheduled public agent streams; postStep() and steps.post() from any harness; digest.read() and poll.open()/poll.result() for a Co-pilot service token; onControl() for pause, curtain, mute and end",
"streams (openvibe-sdk/streams, S7): schedule(), listSchedules(), cancelSchedule() and remindSchedule() for scheduled public agent streams; postStep() and steps.post() from any harness; digest.read() and poll.open()/poll.result() for a Co-pilot service token; onControl() for pause, curtain, mute and end",
"reports (openvibe-sdk/reports, S7): write() a session report with the person's own token, private by default and idempotent per source and task ref, and read() one the caller may see; examples/cli-agent-stream.js streams any CLI command's run with steps",
"reports (openvibe-sdk/reports, S7): write() a session report with the person's own token, private by default and idempotent per source and task ref, and read() one the caller may see; examples/cli-agent-stream.js streams any CLI command's run with steps",
"chat-apps (openvibe-sdk/chat-apps): the three chat apps (Telegram, Discord, Slack) in one place — the delivery checks (Telegram's secret token, Slack's v0 signing, Discord's Ed25519), the button callback (signed once, used once, expiring), each app's payload as the same shape and splitText() for a long answer",
"chat-apps (openvibe-sdk/chat-apps): the three chat apps (Telegram, Discord, Slack) in one place — the delivery checks (Telegram's secret token, Slack's v0 signing, Discord's Ed25519), the button callback (signed once, used once, expiring), each app's payload as the same shape and splitText() for a long answer",
- "exchange (openvibe-sdk/exchange): the buyer side (chat, models, offers, presets, usage, receipts) and the seller side (offer, endpointOffers, removeEndpointOffer, earnings, verification) of OpenVibe.Exchange, with the platform webhook signature check; an offer is a Node machine's Network report or a standalone OpenAI-compatible endpoint registered on the public seller API",
+ "exchange (openvibe-sdk/exchange): the buyer side (chat, models, offers, presets, usage, receipts) and the seller side (offer, endpointOffers, removeEndpointOffer, earnings, verification) of OpenVibe.Exchange, with the platform webhook signature check; an offer is a Link machine's Network report or a standalone OpenAI-compatible endpoint registered on the public seller API",
"locker (openvibe-sdk/locker, openvibe-sdk/locker/server): the Locker envelope (locker.envelope@1) for end-to-end encryption, WebCrypto only, in Node and the browser: seal/open for text and bytes, sealStream/openStream, the three locks (people, password, link), person key pairs (ECDH P-256, ECDSA P-256), the eight-word fingerprint, wrapFor and rewrap, recoveryWords for the phrase to write down, opening a people lock by key, inspect for what an envelope says about itself, and the header checks a service runs without a key",
"locker (openvibe-sdk/locker, openvibe-sdk/locker/server): the Locker envelope (locker.envelope@1) for end-to-end encryption, WebCrypto only, in Node and the browser: seal/open for text and bytes, sealStream/openStream, the three locks (people, password, link), person key pairs (ECDH P-256, ECDSA P-256), the eight-word fingerprint, wrapFor and rewrap, recoveryWords for the phrase to write down, opening a people lock by key, inspect for what an envelope says about itself, and the header checks a service runs without a key",
"the 1.0 core surface is frozen now, before 1.0 (core, auth, events, db, media): docs/one-point-oh.json gives every export its tier, test/surface.test.js pins the exact names, and removing or renaming one needs a major release; createCodeVerifier and pkceChallenge left in 0.86.0",
"the 1.0 core surface is frozen now, before 1.0 (core, auth, events, db, media): docs/one-point-oh.json gives every export its tier, test/surface.test.js pins the exact names, and removing or renaming one needs a major release; createCodeVerifier and pkceChallenge left in 0.86.0",
"share (openvibe-sdk/share): transactional local sharing, audience and membership checks, exact-name resolution, recipient-bound ownership transfers, audit, durable events/notifications, uniform API and agent client; locked items (Locker): the lock kind, per-member key slots checked against the key directory, a re-seal on removal through onReseal, and the transfer offer slot",
"share (openvibe-sdk/share): transactional local sharing, audience and membership checks, exact-name resolution, recipient-bound ownership transfers, audit, durable events/notifications, uniform API and agent client; locked items (Locker): the lock kind, per-member key slots checked against the key directory, a re-seal on removal through onReseal, and the transfer offer slot",
- "redact (openvibe-sdk/redact): the shared secret scrubber for memory, the same shapes and corpus as OpenVibe.Node's internal/memory/redact.go (redact, onlySecret)",
+ "redact (openvibe-sdk/redact): the shared secret scrubber for memory, the same shapes and corpus as OpenVibe Link's internal/memory/redact.go (redact, onlySecret)",
"core client: retries, timeouts, problem+json errors, trace propagation",
"core client: retries, timeouts, problem+json errors, trace propagation",
"auth: service and app tokens, PKCE, offline verification, the Network signing keys (createNetworkKeys), revocation cutoffs",
"auth: service and app tokens, PKCE, offline verification, the Network signing keys (createNetworkKeys), revocation cutoffs",
"registry discovery and contracts version checks",
"registry discovery and contracts version checks",
@@ -2663,7 +2663,7 @@ function subscribe(topics, onEvent, opts = {}) {
@@ -2663,7 +2663,7 @@ function subscribe(topics, onEvent, opts = {}) {
* for await (const { event, data, id } of parseSSE(res.body)) { … }
* for await (const { event, data, id } of parseSSE(res.body)) { … }
*
*
* `body` is a ReadableStream<Uint8Array> (fetch), or any async iterable of Uint8Array/string chunks
* `body` is a ReadableStream<Uint8Array> (fetch), or any async iterable of Uint8Array/string chunks
- * (a Node stream). `event` defaults to 'message'; `id` is the id field of that event (undefined when
+ * (a Link stream). `event` defaults to 'message'; `id` is the id field of that event (undefined when
* it had none); comments are skipped; `retry: <ms>` is passed to onRetry. A line split across chunks,
* it had none); comments are skipped; `retry: <ms>` is passed to onRetry. A line split across chunks,
* including a \r\n pair, is reassembled. Breaking out of the loop releases the stream.
* including a \r\n pair, is reassembled. Breaking out of the loop releases the stream.
*/
*/
@@ -3300,13 +3300,13 @@ function (module, exports) {
@@ -3300,13 +3300,13 @@ function (module, exports) {
* and the call is never retried (a code is one-time, so a lost answer cannot be safely repeated).
* and the call is never retried (a code is one-time, so a lost answer cannot be safely repeated).
* Every other POST carries an Idempotency-Key (the client generates one when retries are on).
* Every other POST carries an Idempotency-Key (the client generates one when retries are on).
*
*
- * Machines (the OpenVibe Node's computer plugin): `machines.list`, `machines.computer`, `machines.command` (bot.command@1's
+ * Machines (the OpenVibe Link's computer plugin): `machines.list`, `machines.computer`, `machines.command` (bot.command@1's
* computer kinds) and `machines.deskTurn`; `machines.desk(id, { name?, kind?, profile? })` is a desk of your own on a
* computer kinds) and `machines.deskTurn`; `machines.desk(id, { name?, kind?, profile? })` is a desk of your own on a
* machine (the Desks plan: its own screen or browser, files and sign-ins), with start/stop/status and browser, screen,
* machine (the Desks plan: its own screen or browser, files and sign-ins), with start/stop/status and browser, screen,
* input and exec on it. A desk command that the machine does not do throws an OpenVibeError with code `bot.desk_nack`
* input and exec on it. A desk command that the machine does not do throws an OpenVibeError with code `bot.desk_nack`
* (the machine's own fault code is `fault`), so a desk reads as plain awaits.
* (the machine's own fault code is `fault`), so a desk reads as plain awaits.
*
*
- * A machine's own models (AI on your own machines, contracts 0.219.0, OpenVibe.Node v0.27.0's ai plugin):
+ * A machine's own models (AI on your own machines, contracts 0.219.0, OpenVibe Link v0.27.0's ai plugin):
* `machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (kind ai) and answers
* `machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (kind ai) and answers
* `{ model, message: { role, content }, usage?, ms? }`; a machine that does not answer throws `bot.ai_nack`.
* `{ model, message: { role, content }, usage?, ms? }`; a machine that does not answer throws `bot.ai_nack`.
* `machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".
* `machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".
@@ -3401,7 +3401,7 @@ function createBotClient(client, defaults = {}) {
@@ -3401,7 +3401,7 @@ function createBotClient(client, defaults = {}) {
},
},
};
};
- // ── Machines (the OpenVibe Node's computer plugin) and desks of your own on them ──
+ // ── Machines (the OpenVibe Link's computer plugin) and desks of your own on them ──
const machines = {
const machines = {
/** { machines } — the person's machines with their computer capabilities (a service sees the ones opened to it). */
/** { machines } — the person's machines with their computer capabilities (a service sees the ones opened to it). */
list: (o = {}) => call({ path: '/api/v1/machines' }, o),
list: (o = {}) => call({ path: '/api/v1/machines' }, o),
@@ -4097,13 +4097,13 @@ module.exports = { createThemesClient, followTheme, readTheme, applyTheme, seaso
@@ -4097,13 +4097,13 @@ module.exports = { createThemesClient, followTheme, readTheme, applyTheme, seaso
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
- * openvibe-sdk/redact: the shared secret scrubber (Memory M2). It is the JavaScript port of OpenVibe.Node's
- * internal/memory/redact.go (Node #87), so the Node and OpenVibe.AI run one list of shapes against one corpus
- * (test/redact-corpus.json). The Node scrubs a memory file before it uploads it; AI scrubs every item again before it
+ * openvibe-sdk/redact: the shared secret scrubber (Memory M2). It is the JavaScript port of OpenVibe Link's
+ * internal/memory/redact.go (Node #87), so Link and OpenVibe.AI run one list of shapes against one corpus
+ * (test/redact-corpus.json). Link scrubs a memory file before it uploads it; AI scrubs every item again before it
* seals it, and refuses an item that was nothing but a secret.
* seals it, and refuses an item that was nothing but a secret.
*
*
* - redact(text) -> { text, count }: every secret shape is replaced with [redacted], and the count says how many were
* - redact(text) -> { text, count }: every secret shape is replaced with [redacted], and the count says how many were
- * removed. The whole-text pass uses an internal sentinel first, the way the Node scrubs a file before it splits it
+ * removed. The whole-text pass uses an internal sentinel first, the way Link scrubs a file before it splits it
* into items; the sentinel is resolved to [redacted] before the result is returned. A text that is already redacted
* into items; the sentinel is resolved to [redacted] before the result is returned. A text that is already redacted
* is left alone.
* is left alone.
* - onlySecret(text): true when nothing but redacted secrets is left ("the item was only a secret").
* - onlySecret(text): true when nothing but redacted secrets is left ("the item was only a secret").
@@ -4315,7 +4315,7 @@ function redactWith(text, marker) {
@@ -4315,7 +4315,7 @@ function redactWith(text, marker) {
/**
/**
* The whole-text pass with the sentinel, then the per-item pass. A secret that spans two items (a key block in a
* The whole-text pass with the sentinel, then the per-item pass. A secret that spans two items (a key block in a
- * fence, an .env block) is only seen whole, which is why the Node scrubs a file before it splits it. The sentinel is
+ * fence, an .env block) is only seen whole, which is why Link scrubs a file before it splits it. The sentinel is
* resolved to [redacted] before the per-item pass, which catches anything only visible once the text is on its own.
* resolved to [redacted] before the per-item pass, which catches anything only visible once the text is on its own.
*/
*/
function redactDocument(text) {
function redactDocument(text) {
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
OpenVibe.Exchange, the buyer and the seller side of the model exchange (the plan of 2026-11-06, "For builders",
OpenVibe.Exchange, the buyer and the seller side of the model exchange (the plan of 2026-11-06, "For builders",
item 7). The buyer side speaks the Exchange's v1 API with a Network developer key; the seller side registers an
item 7). The buyer side speaks the Exchange's v1 API with a Network developer key; the seller side registers an
-offer in the Network's user-owned AI offers, the same registry OpenVibe.Node's "Offer to the network" uses.
+offer in the Network's user-owned AI offers, the same registry OpenVibe Link's "Offer to the network" uses.
```js
```js
const { createExchangeClient } = require('openvibe-sdk/exchange');
const { createExchangeClient } = require('openvibe-sdk/exchange');
@@ -59,8 +59,8 @@ adult gate. A request names one as `model: '@preset/<name>'`.
@@ -59,8 +59,8 @@ adult gate. A request names one as `model: '@preset/<name>'`.
## The seller side
## The seller side
-An offer has two forms. A **Node machine** reports its offer to the Network's user-owned AI offers — the same registry
-OpenVibe.Node's "Offer to the network" writes to (`POST /internal/ai/offers/report`, the capability
+An offer has two forms. A **Link machine** reports its offer to the Network's user-owned AI offers — the same registry
+OpenVibe Link's "Offer to the network" writes to (`POST /internal/ai/offers/report`, the capability
`network.resource.report`). A **standalone server** registers itself on the Exchange's public seller API with the
`network.resource.report`). A **standalone server** registers itself on the Exchange's public seller API with the
developer key, so it needs no Node at all. The SDK validates both before anything is sent.
developer key, so it needs no Node at all. The SDK validates both before anything is sent.
@@ -88,7 +88,7 @@ the OpenAI API is caught before the offer is registered (`probe: false` turns th
@@ -88,7 +88,7 @@ the OpenAI API is caught before the offer is registered (`probe: false` turns th
### A standalone server: the endpoint form
### A standalone server: the endpoint form
-A server that speaks the OpenAI Chat Completions API can sell without an OpenVibe Node. Give `endpoint` and no
+A server that speaks the OpenAI Chat Completions API can sell without an OpenVibe Link. Give `endpoint` and no
`machineId`; the offer is registered on the Exchange's public seller API
`machineId`; the offer is registered on the Exchange's public seller API
(`POST /api/v1/exchange/seller/offers`) with the same developer key as the buyer side.
(`POST /api/v1/exchange/seller/offers`) with the same developer key as the buyer side.
@@ -113,7 +113,7 @@ The owner is the person the developer key acts for (a developer app's project ow
@@ -113,7 +113,7 @@ The owner is the person the developer key acts for (a developer app's project ow
the address only through its egress guard: https, a public DNS answer checked inside the connection (never an IP
the address only through its egress guard: https, a public DNS answer checked inside the connection (never an IP
literal), redirects judged again, a timeout, a response byte cap, and no buyer identity in any header. No bearer token
literal), redirects judged again, a timeout, a response byte cap, and no buyer identity in any header. No bearer token
is sent, so the endpoint must accept the POST as it is (`endpointKey` only authenticates the SDK's own probe). The
is sent, so the endpoint must accept the POST as it is (`endpointKey` only authenticates the SDK's own probe). The
-endpoint is verified exactly as a Node machine (the canaries, the model fingerprint, about 1% spot re-runs), serves the
+endpoint is verified exactly as a machine running Link (the canaries, the model fingerprint, about 1% spot re-runs), serves the
free tier only until verified, and earns 0 on the owner's own requests. An answer from it is labelled a community
free tier only until verified, and earns 0 on the owner's own requests. An answer from it is labelled a community
endpoint on the receipt (`provider: community-endpoint:<id>`, `provider_access: COMMUNITY_ENDPOINT`). It sees the
endpoint on the receipt (`provider: community-endpoint:<id>`, `provider_access: COMMUNITY_ENDPOINT`). It sees the
prompt: only requests that opt in to community capacity reach it.
prompt: only requests that opt in to community capacity reach it.
@@ -77,7 +77,7 @@ five-class preference order.
@@ -77,7 +77,7 @@ five-class preference order.
**A streaming machine.** When an offer says its machine is streaming, a step that asks for a GPU or 2 or more cores
**A streaming machine.** When an offer says its machine is streaming, a step that asks for a GPU or 2 or more cores
is excluded with `it is streaming`. The contract does not carry that field yet; `place()` reads
is excluded with `it is streaming`. The contract does not carry that field yet; `place()` reads
-`offer.health.streaming === true` (and tolerates a top-level `offer.streaming`), so OpenVibe.Node can start sending
+`offer.health.streaming === true` (and tolerates a top-level `offer.streaming`), so OpenVibe Link can start sending
it without another SDK change. Node's status already reports `screen.state`, but Bot's `platform.resource-offer@1`
it without another SDK change. Node's status already reports `screen.state`, but Bot's `platform.resource-offer@1`
does not project it today.
does not project it today.
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
# openvibe-sdk/redact
# openvibe-sdk/redact
-The shared secret scrubber for memory (Memory M2). It is the JavaScript port of OpenVibe.Node's
-`internal/memory/redact.go` (Node #87), so the Node and OpenVibe.AI run one shape list against one corpus.
+The shared secret scrubber for memory (Memory M2). It is the JavaScript port of OpenVibe Link's
+`internal/memory/redact.go` (Node #87), so Link and OpenVibe.AI run one shape list against one corpus.
```js
```js
const { redact, onlySecret } = require('openvibe-sdk/redact');
const { redact, onlySecret } = require('openvibe-sdk/redact');
@@ -32,13 +32,13 @@ A text that was already redacted is left alone. The count is the number of secre
@@ -32,13 +32,13 @@ A text that was already redacted is left alone. The count is the number of secre
## Why the whole-text pass exists
## Why the whole-text pass exists
-The Node scrubs a whole file with a sentinel before it splits the file into items, then scrubs each item. A key block
+Link scrubs a whole file with a sentinel before it splits the file into items, then scrubs each item. A key block
or an `.env` block that spans two items is only seen whole. `redact()` runs the same two passes on one string: the
or an `.env` block that spans two items is only seen whole. `redact()` runs the same two passes on one string: the
sentinel keeps the place of each secret until the text is resolved to `[redacted]`. This is also why AI runs the
sentinel keeps the place of each secret until the text is resolved to `[redacted]`. This is also why AI runs the
-scrubber again before sealing an item: the Node is not the only writer.
+scrubber again before sealing an item: Link is not the only writer.
## Corpus
## Corpus
`test/redact-corpus.json` is the shared list: every shape, the split and unterminated key blocks, the `.env` block and
`test/redact-corpus.json` is the shared list: every shape, the split and unterminated key blocks, the `.env` block and
-the harmless texts that must not change. `test/redact.test.js` runs it here; OpenVibe.Node copies the file into
+the harmless texts that must not change. `test/redact.test.js` runs it here; OpenVibe Link copies the file into
`internal/memory/testdata/` and runs the same cases against its Go `Redact`.
`internal/memory/testdata/` and runs the same cases against its Go `Redact`.
@@ -21,7 +21,7 @@ const { stream, publish } = await streams.startAgentStream({
@@ -21,7 +21,7 @@ const { stream, publish } = await streams.startAgentStream({
## The desk's loop
## The desk's loop
-The desk (OpenVibe.Node) polls Live every ten seconds and keeps publishing while Live wants video:
+The desk (OpenVibe Link) polls Live every ten seconds and keeps publishing while Live wants video:
```js
```js
const s = await streams.state(stream.id, { ticket: publish.ticket });
const s = await streams.state(stream.id, { ticket: publish.ticket });
@@ -65,7 +65,7 @@ await streams.end(stream.id); // the card stays, with t
@@ -65,7 +65,7 @@ await streams.end(stream.id); // the card stays, with t
## Who may call
## Who may call
-The person's own session token, or a first-party service token acting for that person: a Node principal acting for its
+The person's own session token, or a first-party service token acting for that person: a Link principal acting for its
owner (Network issues the node token with `live.agent_stream.publish` for the audience `openvibe.live` and names its
owner (Network issues the node token with `live.agent_stream.publish` for the audience `openvibe.live` and names its
owner in `on_behalf_of`), Codes or Actor. Never a developer-app token, never another person's stream.
owner in `on_behalf_of`), Codes or Actor. Never a developer-app token, never another person's stream.
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
*
*
* OPENVIBE_TOKEN=… node examples/cli-agent-stream.js --title "Fix the login page" -- npm test
* OPENVIBE_TOKEN=… node examples/cli-agent-stream.js --title "Fix the login page" -- npm test
*
*
- * The same path a Node desk uses: startAgentStream gives the stream and its publish ticket, every line the command
+ * The same path a Link desk uses: startAgentStream gives the stream and its publish ticket, every line the command
* writes is a step, and the stream ends with the command's result. Private by default; pass --public to list it.
* writes is a step, and the stream ends with the command's result. Private by default; pass --public to list it.
*/
*/
const { spawn } = require('node:child_process');
const { spawn } = require('node:child_process');
@@ -15,13 +15,13 @@
@@ -15,13 +15,13 @@
* and the call is never retried (a code is one-time, so a lost answer cannot be safely repeated).
* and the call is never retried (a code is one-time, so a lost answer cannot be safely repeated).
* Every other POST carries an Idempotency-Key (the client generates one when retries are on).
* Every other POST carries an Idempotency-Key (the client generates one when retries are on).
*
*
- * Machines (the OpenVibe Node's computer plugin): `machines.list`, `machines.computer`, `machines.command` (bot.command@1's
+ * Machines (the OpenVibe Link's computer plugin): `machines.list`, `machines.computer`, `machines.command` (bot.command@1's
* computer kinds) and `machines.deskTurn`; `machines.desk(id, { name?, kind?, profile? })` is a desk of your own on a
* computer kinds) and `machines.deskTurn`; `machines.desk(id, { name?, kind?, profile? })` is a desk of your own on a
* machine (the Desks plan: its own screen or browser, files and sign-ins), with start/stop/status and browser, screen,
* machine (the Desks plan: its own screen or browser, files and sign-ins), with start/stop/status and browser, screen,
* input and exec on it. A desk command that the machine does not do throws an OpenVibeError with code `bot.desk_nack`
* input and exec on it. A desk command that the machine does not do throws an OpenVibeError with code `bot.desk_nack`
* (the machine's own fault code is `fault`), so a desk reads as plain awaits.
* (the machine's own fault code is `fault`), so a desk reads as plain awaits.
*
*
- * A machine's own models (AI on your own machines, contracts 0.219.0, OpenVibe.Node v0.27.0's ai plugin):
+ * A machine's own models (AI on your own machines, contracts 0.219.0, OpenVibe Link v0.27.0's ai plugin):
* `machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (kind ai) and answers
* `machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (kind ai) and answers
* `{ model, message: { role, content }, usage?, ms? }`; a machine that does not answer throws `bot.ai_nack`.
* `{ model, message: { role, content }, usage?, ms? }`; a machine that does not answer throws `bot.ai_nack`.
* `machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".
* `machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".
@@ -116,7 +116,7 @@ function createBotClient(client, defaults = {}) {
@@ -116,7 +116,7 @@ function createBotClient(client, defaults = {}) {
},
},
};
};
- // ── Machines (the OpenVibe Node's computer plugin) and desks of your own on them ──
+ // ── Machines (the OpenVibe Link's computer plugin) and desks of your own on them ──
const machines = {
const machines = {
/** { machines } — the person's machines with their computer capabilities (a service sees the ones opened to it). */
/** { machines } — the person's machines with their computer capabilities (a service sees the ones opened to it). */
list: (o = {}) => call({ path: '/api/v1/machines' }, o),
list: (o = {}) => call({ path: '/api/v1/machines' }, o),
@@ -12,12 +12,12 @@
@@ -12,12 +12,12 @@
* // { id: 'chatcmpl-...', object: 'chat.completion', choices, usage, x_openvibe: { run_id, cost_usd, ... } }
* // { id: 'chatcmpl-...', object: 'chat.completion', choices, usage, x_openvibe: { run_id, cost_usd, ... } }
* for await (const chunk of exchange.chat({ model: 'openvibe', messages, stream: true })) { ... }
* for await (const chunk of exchange.chat({ model: 'openvibe', messages, stream: true })) { ... }
*
*
- * The seller side registers an offer in one of two forms. With `machineId`, it reports a Node machine's offer to the
+ * The seller side registers an offer in one of two forms. With `machineId`, it reports a Link machine's offer to the
* Network's user-owned AI offers (POST /internal/ai/offers/report, the capability network.resource.report). With
* Network's user-owned AI offers (POST /internal/ai/offers/report, the capability network.resource.report). With
* `endpoint` and no machine, it registers a standalone OpenAI-compatible server on the Exchange's public seller API
* `endpoint` and no machine, it registers a standalone OpenAI-compatible server on the Exchange's public seller API
* (POST /api/v1/exchange/seller/offers) with the same developer key the buyer side uses; AI reaches that address only
* (POST /api/v1/exchange/seller/offers) with the same developer key the buyer side uses; AI reaches that address only
* through its egress guard (https, a public DNS answer, never an IP literal, a timeout, a response cap, no buyer
* through its egress guard (https, a public DNS answer, never an IP literal, a timeout, a response cap, no buyer
- * identity in a header) and verifies it exactly as a Node machine (canaries, fingerprint, spot re-runs, free tier only
+ * identity in a header) and verifies it exactly as a machine running Link (canaries, fingerprint, spot re-runs, free tier only
* until verified, self-dealing earns 0).
* until verified, self-dealing earns 0).
*
*
* const offer = await exchange.seller.offer({
* const offer = await exchange.seller.offer({
@@ -378,7 +378,7 @@ function createExchangeClient({
@@ -378,7 +378,7 @@ function createExchangeClient({
/**
/**
* Register a community offer: what is served, at what price, when, and how many answers at once.
* Register a community offer: what is served, at what price, when, and how many answers at once.
*
*
- * With `machineId`, the offer is reported to the Network's user-owned AI offers - the registry OpenVibe.Node's
+ * With `machineId`, the offer is reported to the Network's user-owned AI offers - the registry OpenVibe Link's
* "Offer to the network" uses - with the document that registry validates.
* "Offer to the network" uses - with the document that registry validates.
* offer({ model, price, hours, concurrency, endpoint, machineId, owner, audience, group, ready, health })
* offer({ model, price, hours, concurrency, endpoint, machineId, owner, audience, group, ready, health })
*
*
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
'use strict';
'use strict';
/**
/**
- * openvibe-sdk/node — platform.node-capabilities@1, plus nodeOffers() that maps a Node descriptor to a
+ * openvibe-sdk/node — platform.node-capabilities@1, plus nodeOffers() that maps a node descriptor to a
* platform.resource-offer@1 (kind "node") and a platform.runtime-offer@1.
* platform.resource-offer@1 (kind "node") and a platform.runtime-offer@1.
* Pure construction and validation, no I/O and no dependency at require time.
* Pure construction and validation, no I/O and no dependency at require time.
*
*
@@ -32,7 +32,7 @@ function nodeCapabilities(fields) {
@@ -32,7 +32,7 @@ function nodeCapabilities(fields) {
}
}
/**
/**
- * Map a Node descriptor (platform.node-capabilities@1) to two offers:
+ * Map a node descriptor (platform.node-capabilities@1) to two offers:
* - resource: a platform.resource-offer@1 with kind "node", capabilities including every advertised
* - resource: a platform.resource-offer@1 with kind "node", capabilities including every advertised
* worker capability prefixed "worker:<name>", trust from opts.trust or "first-party";
* worker capability prefixed "worker:<name>", trust from opts.trust or "first-party";
* - runtime: a platform.runtime-offer@1 with kind "container" (the default runtime a node offers),
* - runtime: a platform.runtime-offer@1 with kind "container" (the default runtime a node offers),
@@ -183,7 +183,7 @@ function subscribe(topics, onEvent, opts = {}) {
@@ -183,7 +183,7 @@ function subscribe(topics, onEvent, opts = {}) {
* for await (const { event, data, id } of parseSSE(res.body)) { … }
* for await (const { event, data, id } of parseSSE(res.body)) { … }
*
*
* `body` is a ReadableStream<Uint8Array> (fetch), or any async iterable of Uint8Array/string chunks
* `body` is a ReadableStream<Uint8Array> (fetch), or any async iterable of Uint8Array/string chunks
- * (a Node stream). `event` defaults to 'message'; `id` is the id field of that event (undefined when
+ * (a Link stream). `event` defaults to 'message'; `id` is the id field of that event (undefined when
* it had none); comments are skipped; `retry: <ms>` is passed to onRetry. A line split across chunks,
* it had none); comments are skipped; `retry: <ms>` is passed to onRetry. A line split across chunks,
* including a \r\n pair, is reassembled. Breaking out of the loop releases the stream.
* including a \r\n pair, is reassembled. Breaking out of the loop releases the stream.
*/
*/
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
'use strict';
'use strict';
/**
/**
- * openvibe-sdk/redact: the shared secret scrubber (Memory M2). It is the JavaScript port of OpenVibe.Node's
- * internal/memory/redact.go (Node #87), so the Node and OpenVibe.AI run one list of shapes against one corpus
- * (test/redact-corpus.json). The Node scrubs a memory file before it uploads it; AI scrubs every item again before it
+ * openvibe-sdk/redact: the shared secret scrubber (Memory M2). It is the JavaScript port of OpenVibe Link's
+ * internal/memory/redact.go (Node #87), so Link and OpenVibe.AI run one list of shapes against one corpus
+ * (test/redact-corpus.json). Link scrubs a memory file before it uploads it; AI scrubs every item again before it
* seals it, and refuses an item that was nothing but a secret.
* seals it, and refuses an item that was nothing but a secret.
*
*
* - redact(text) -> { text, count }: every secret shape is replaced with [redacted], and the count says how many were
* - redact(text) -> { text, count }: every secret shape is replaced with [redacted], and the count says how many were
- * removed. The whole-text pass uses an internal sentinel first, the way the Node scrubs a file before it splits it
+ * removed. The whole-text pass uses an internal sentinel first, the way Link scrubs a file before it splits it
* into items; the sentinel is resolved to [redacted] before the result is returned. A text that is already redacted
* into items; the sentinel is resolved to [redacted] before the result is returned. A text that is already redacted
* is left alone.
* is left alone.
* - onlySecret(text): true when nothing but redacted secrets is left ("the item was only a secret").
* - onlySecret(text): true when nothing but redacted secrets is left ("the item was only a secret").
@@ -218,7 +218,7 @@ function redactWith(text, marker) {
@@ -218,7 +218,7 @@ function redactWith(text, marker) {
/**
/**
* The whole-text pass with the sentinel, then the per-item pass. A secret that spans two items (a key block in a
* The whole-text pass with the sentinel, then the per-item pass. A secret that spans two items (a key block in a
- * fence, an .env block) is only seen whole, which is why the Node scrubs a file before it splits it. The sentinel is
+ * fence, an .env block) is only seen whole, which is why Link scrubs a file before it splits it. The sentinel is
* resolved to [redacted] before the per-item pass, which catches anything only visible once the text is on its own.
* resolved to [redacted] before the per-item pass, which catches anything only visible once the text is on its own.
*/
*/
function redactDocument(text) {
function redactDocument(text) {
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
* openvibe-sdk/streams — the Agent Streams plan (S1): an agent's own stream on OpenVibe.Live.
* openvibe-sdk/streams — the Agent Streams plan (S1): an agent's own stream on OpenVibe.Live.
*
*
* An agent stream is a Live stream of kind 'agent' (id ags_…): it has no stream key and takes no slot, and it is
* An agent stream is a Live stream of kind 'agent' (id ags_…): it has no stream key and takes no slot, and it is
- * published with a ticket (wpt_…) instead of one. The desk (OpenVibe.Node) or a person's browser (a Codes task page)
+ * published with a ticket (wpt_…) instead of one. The desk (OpenVibe Link) or a person's browser (a Codes task page)
* creates it, keeps the ticket and publishes through it; the owner watches the stream on the page Live answers with.
* creates it, keeps the ticket and publishes through it; the owner watches the stream on the page Live answers with.
*
*
* const streams = createStreamsClient(createClient({ tokenProvider }));
* const streams = createStreamsClient(createClient({ tokenProvider }));
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
/**
/**
- * openvibe-sdk/redact: the shared corpus of secret shapes (Memory M2). It is the same file OpenVibe.Node copies into
- * its internal/memory/testdata, so the Node's Go scrubber and this port cannot drift: every case says what must be
+ * openvibe-sdk/redact: the shared corpus of secret shapes (Memory M2). It is the same file OpenVibe Link copies into
+ * its internal/memory/testdata, so Link's Go scrubber and this port cannot drift: every case says what must be
* gone and what must stay.
* gone and what must stay.
*/
*/
const assert = require('node:assert/strict');
const assert = require('node:assert/strict');
@@ -82,7 +82,7 @@ export interface PairInput {
@@ -82,7 +82,7 @@ export interface PairInput {
name?: string;
name?: string;
}
}
export interface MachineCommandInput { id?: string; kind: 'exec' | 'fs' | 'screen' | 'input' | 'browser' | 'probe' | 'desk' | 'ai'; value: Record<string, unknown>; ms?: number; }
export interface MachineCommandInput { id?: string; kind: 'exec' | 'fs' | 'screen' | 'input' | 'browser' | 'probe' | 'desk' | 'ai'; value: Record<string, unknown>; ms?: number; }
-/** A machine's own models (OpenVibe.Node's ai plugin): what it offers and how fast its last model answered there. */
+/** A machine's own models (OpenVibe Link's ai plugin): what it offers and how fast its last model answered there. */
export interface MachineAi { models: string[]; ready: boolean; max_tokens?: number; }
export interface MachineAi { models: string[]; ready: boolean; max_tokens?: number; }
export interface MachineAiSpeed { model: string; tokens_per_s: number; }
export interface MachineAiSpeed { model: string; tokens_per_s: number; }
export interface Machine {
export interface Machine {
@@ -353,7 +353,7 @@ export type ExchangeClient = {
@@ -353,7 +353,7 @@ export type ExchangeClient = {
/** The receipt of one answered request. */
/** The receipt of one answered request. */
receipt(id: string, opts?: { signal?: AbortSignal }): Promise<ExchangeReceipt>;
receipt(id: string, opts?: { signal?: AbortSignal }): Promise<ExchangeReceipt>;
seller: {
seller: {
- /** Register a Node machine's offer in the Network's user-owned AI offers, or an endpoint with the Exchange. */
+ /** Register a Link machine's offer in the Network's user-owned AI offers, or an endpoint with the Exchange. */
offer(input: ExchangeOfferInput, opts?: { signal?: AbortSignal }): Promise<ExchangeOfferResult>;
offer(input: ExchangeOfferInput, opts?: { signal?: AbortSignal }): Promise<ExchangeOfferResult>;
offer(input: ExchangeEndpointOfferInput, opts?: { signal?: AbortSignal }): Promise<ExchangeEndpointOfferResult>;
offer(input: ExchangeEndpointOfferInput, opts?: { signal?: AbortSignal }): Promise<ExchangeEndpointOfferResult>;
/** The caller's own endpoint offers, with their verification. */
/** The caller's own endpoint offers, with their verification. */
@@ -20,7 +20,7 @@ export interface Machine {
@@ -20,7 +20,7 @@ export interface Machine {
os?: string;
os?: string;
/** When the machine was last seen. */
/** When the machine was last seen. */
last_seen_at?: string;
last_seen_at?: string;
- /** The OpenVibe Node version the machine reports. */
+ /** The OpenVibe Link version the machine reports. */
node_version?: string;
node_version?: string;
}
}