openvibers/openvibe.contracts Commits

0.372.0: OpenVibe Link renames the machine contracts, principals and AI provider (#386)

OP OpenVibers committed 4 days ago · b4d8cb367a · 235 files, +2830 −2307

* OpenVibe Link rename, in progress (saved as it was left)

* Merge remote-tracking branch 'origin/main' into rename/link-1109-Contracts

* Release 0.366.0: the machine product is OpenVibe Link

The network registry contracts become network.link@1, network.link-report-request@1
and network.link-list-result@1, the event network.link.revoked, and the platform
platform.link-capabilities@1 plus link.compute@1. Capability ids network.node.* become
network.link.* with their internal routes and manifests.

A paired machine is a link principal: identity.service-token-claims actor_type is link
(sub link:lnk_…), ids.principalSub mints link:lnk_…, and the AI provider node is link in
ai.credential-put@1/@2 and ai.credential@1/@2. The old contract ids are deprecated with
their Link replacements and the schema corrections are recorded for v0.352.0 and v0.365.0.

* Merge main and release the Link rename as 0.369.0

Main advanced to 0.368.0 while the rename waited, so the release takes the next
free tag, 0.369.0, and the schema corrections cover v0.368.0 (the pull-request
merge base), v0.365.0 and v0.352.0. Generated output is regenerated.

* Merge main and take the free tag 0.372.0

Main reached 0.371.0 while the rename waited; the release takes 0.372.0, its
schema corrections cover v0.371.0 as the merge base, and generated output is
regenerated.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex eba6e891..a6dab960 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,17 @@
# Changelog
++## 0.372.0 — 2026-11-09
++
++- The machine product Node became OpenVibe Link. The network registry contracts are `network.link@1`,
++ `network.link-report-request@1` and `network.link-list-result@1`, the event is `network.link.revoked`, and the platform
++ carries `platform.link-capabilities@1` and `link.compute@1`. The capabilities `network.node.manage`,
++ `network.node.report` and `network.node.self.manage` are `network.link.manage`, `network.link.report` and
++ `network.link.self.manage`; their internal routes and the manifests follow.
++- A paired machine is a `link` principal: identity.service-token-claims actor_type `node` is `link` with sub `link:lnk_…`,
++ and `ids.principalSub` mints `link:lnk_…` (the old prefix and ids are gone, no alias). The AI provider `node` is `link`
++ in ai.credential-put@1/@2 and ai.credential@1/@2. The old contract ids are deprecated with their Link replacements; the
++ schema corrections are recorded against the current release and the ones before it.
++
## 0.371.0 — 2026-11-09
- Exchange API platform and search: exchange.key@1, exchange.key-created@1, exchange.usage@1, exchange.deployments@1, exchange.search-result@1, exchange.image-result@1, and the Exchange manifest's routes
@@@ -851,7 +863,7 @@
## 0.261.0 — 2026-10-31
- **The machines hub** (plan L34, steps M2 and M4). A person's computers are the network's: Bot stays the device
-- registry underneath, and Network fronts it on openvibe.network/machines for the person and for apps the person
++ registry underneath, and Network fronts it on openvibe.link/machines for the person and for apps the person
consented to.
- **`network.machine@1`:** one machine as the hub shows it — its id (Bot's `dev_…`), name, kind, os, whether it is
online, when it was last seen, its `roles` and what it does right now (`running`: its Codes jobs, its desks, its
@@@ -3119,7 +3131,7 @@ contract changes.
**The node-revoked event contract** (plan T2 follow-up; the deferred item in OpenVibe.Network's
`docs/t2-cells-and-node-principal.md` section 10, "Revocation latency outside Bot"). Adds
--`network.node.revoked@1` (owner `network`, first-party, active): a node principal (`nod_…`), a person's paired
++`network.link.revoked@1` (owner `network`, first-party, active): a node principal (`lnk_…`), a person's paired
machine, was revoked through `POST /internal/node-principals/:id/revoke` (`network.node.manage`, the service that
paired it) or `POST /api/v1/me/nodes/:id/revoke` (the owner's session), or with the owner's account. The payload is
`{ node_id, principal_id, owner, reason?, at }` (`extra` fields refused): `owner` is the person, project or platform
@@@ -3523,9 +3535,9 @@ Network grants for the usage producers follow this release. Additive.
**Node principals, node capabilities and per-kind resource offers** (plan T1 lane A, the contracts brief of
OpenVibe.Network `docs/t2-cells-and-node-principal.md` §9.1):
--- `lib/ids.js`: `PREFIX.node = 'nod'`; `principalSub({ type: 'node', id })` gives `node:nod_<ULID>`. A node is a
++- `lib/ids.js`: `PREFIX.node = 'nod'`; `principalSub({ type: 'link', id })` gives `link:lnk_<ULID>`. A node is a
principal only: `node` is not a `SUBJECT_TYPES` entry nor an `identity.subject-ref@1` type.
--- `identity.service-token-claims`: `sub` also accepts `node:nod_<ULID>` and `actor_type` gains `node`, so
++- `identity.service-token-claims`: `sub` also accepts `link:lnk_<ULID>` and `actor_type` gains `link`, so
`verifyServiceToken` accepts a node token and still rejects a malformed node id. Fixtures `node-token` (valid) and
`node-bad-id` (invalid).
- New capabilities `network.node.manage` (a service mints pairing codes and reads or revokes the node principals it
ddiff --git a/README.md b/README.md
iindex e23553e6..48fe298d 100644
---- a/README.md
++++ b/README.md
@@@ -242,7 +242,7 @@ A job moves `queued → placed → running` and ends `succeeded`, `failed`, `can
`run.job.queued|started|succeeded|failed|cancelled|expired`. Run mints the job id (`job_<ULID>`, `ids.newId('job')`)
and passes it unchanged as the `platform.job@1` id, so every metering reading is `run:<job id>:<n>` (service `run`,
operation `function.invoke`, unit `s`) and `usage.seconds` is their sum. Inputs are Media objects pinned by sha256
--(never a URL); `egress` takes Node's `worker.egress` values (`none` by default, `public`, `openvibe-only`).
++(never a URL); `egress` takes Link's `worker.egress` values (`none` by default, `public`, `openvibe-only`).
## Versioning and compatibility
ddiff --git a/compatibility/capability-schema-gaps.json b/compatibility/capability-schema-gaps.json
iindex 46524559..75b1d7c4 100644
---- a/compatibility/capability-schema-gaps.json
++++ b/compatibility/capability-schema-gaps.json
@@@ -2,14 +2,14 @@
"_about": "Active capabilities still without an input or output schema (roadmap WS-C task 4). test/capability-schemas.test.js fails on any active capability missing a schema that is not listed here, and on a listed one that has since gained it (remove it from this list). The list only shrinks: fill a capability's schemas, then delete its row. A request with no body names common.no-body@1; a response of bytes names common.binary@1.",
"gaps": [
{
-- "id": "network.node.manage",
++ "id": "network.link.manage",
"missing": [
"inputSchema",
"outputSchema"
]
},
{
-- "id": "network.node.self.manage",
++ "id": "network.link.self.manage",
"missing": [
"inputSchema",
"outputSchema"
ddiff --git a/compatibility/corrections.json b/compatibility/corrections.json
iindex 6c153c63..8ec9610b 100644
---- a/compatibility/corrections.json
++++ b/compatibility/corrections.json
@@@ -18,6 +18,78 @@
"base": "v0.289.0",
"fixedIn": "0.289.1",
"reason": "0.289.0 made `project_id` optional on billing.credit-hold-create-request@1 for a personal hold, but billing.credit-hold@1 still required a string, so the answer Billing's presentHold sends for a personal hold (`project_id: null`) did not validate. The string-only rule never described a personal hold's answer; project holds still send a string, and `project_id` widens to optional [string, null]."
++ },
++ {
++ "id": "ai.credential-put",
++ "base": "v0.352.0",
++ "fixedIn": "0.372.0",
++ "reason": "The machine product Node became OpenVibe Link, and the person's own-machine provider is `link`: the `node` value of ai.credential-put@1 and @2 is the same provider under its new name. Only our own AI service ever produced it, and it moves to `link` in the same release, so the enum is corrected in place rather than carried as an alias."
++ },
++ {
++ "id": "ai.credential",
++ "base": "v0.352.0",
++ "fixedIn": "0.372.0",
++ "reason": "The machine product Node became OpenVibe Link, and the person's own-machine provider is `link`: the `node` value of ai.credential@1 and @2 is the same provider under its new name, produced and read only by our own AI service, which moves to `link` in the same release."
++ },
++ {
++ "id": "identity.service-token-claims",
++ "base": "v0.352.0",
++ "fixedIn": "0.372.0",
++ "reason": "The machine product Node became OpenVibe Link: a paired machine's principal actor_type is `link` (sub `link:lnk_…`) instead of `node`. Network mints these tokens and every receiver is ours, updated in the same release; no third party consumed the old value."
++ },
++ {
++ "id": "ai.credential-put",
++ "base": "v0.365.0",
++ "fixedIn": "0.372.0",
++ "reason": "The machine product Node became OpenVibe Link, and the person's own-machine provider is `link`: the `node` value of ai.credential-put@1 and @2 is the same provider under its new name. Only our own AI service ever produced it, and it moves to `link` in the same release, so the enum is corrected in place rather than carried as an alias."
++ },
++ {
++ "id": "ai.credential",
++ "base": "v0.365.0",
++ "fixedIn": "0.372.0",
++ "reason": "The machine product Node became OpenVibe Link, and the person's own-machine provider is `link`: the `node` value of ai.credential@1 and @2 is the same provider under its new name, produced and read only by our own AI service, which moves to `link` in the same release."
++ },
++ {
++ "id": "identity.service-token-claims",
++ "base": "v0.365.0",
++ "fixedIn": "0.372.0",
++ "reason": "The machine product Node became OpenVibe Link: a paired machine's principal actor_type is `link` (sub `link:lnk_…`) instead of `node`. Network mints these tokens and every receiver is ours, updated in the same release; no third party consumed the old value."
++ },
++ {
++ "id": "ai.credential-put",
++ "base": "v0.368.0",
++ "fixedIn": "0.372.0",
++ "reason": "The machine product Node became OpenVibe Link, and the person's own-machine provider is `link`: the `node` value of ai.credential-put@1 and @2 is the same provider under its new name. Only our own AI service ever produced it, and it moves to `link` in the same release, so the enum is corrected in place rather than carried as an alias."
++ },
++ {
++ "id": "ai.credential",
++ "base": "v0.368.0",
++ "fixedIn": "0.372.0",
++ "reason": "The machine product Node became OpenVibe Link, and the person's own-machine provider is `link`: the `node` value of ai.credential@1 and @2 is the same provider under its new name, produced and read only by our own AI service, which moves to `link` in the same release."
++ },
++ {
++ "id": "identity.service-token-claims",
++ "base": "v0.368.0",
++ "fixedIn": "0.372.0",
++ "reason": "The machine product Node became OpenVibe Link: a paired machine's principal actor_type is `link` (sub `link:lnk_…`) instead of `node`. Network mints these tokens and every receiver is ours, updated in the same release; no third party consumed the old value."
++ },
++ {
++ "id": "ai.credential-put",
++ "base": "v0.371.0",
++ "fixedIn": "0.372.0",
++ "reason": "The machine product Node became OpenVibe Link, and the person's own-machine provider is `link`: the `node` value of ai.credential-put@1 and @2 is the same provider under its new name. Only our own AI service ever produced it, and it moves to `link` in the same release, so the enum is corrected in place rather than carried as an alias."
++ },
++ {
++ "id": "ai.credential",
++ "base": "v0.371.0",
++ "fixedIn": "0.372.0",
++ "reason": "The machine product Node became OpenVibe Link, and the person's own-machine provider is `link`: the `node` value of ai.credential@1 and @2 is the same provider under its new name, produced and read only by our own AI service, which moves to `link` in the same release."
++ },
++ {
++ "id": "identity.service-token-claims",
++ "base": "v0.371.0",
++ "fixedIn": "0.372.0",
++ "reason": "The machine product Node became OpenVibe Link: a paired machine's principal actor_type is `link` (sub `link:lnk_…`) instead of `node`. Network mints these tokens and every receiver is ours, updated in the same release; no third party consumed the old value."
}
]
}
ddiff --git a/compatibility/deprecations.json b/compatibility/deprecations.json
iindex b50dc037..69a43c70 100644
---- a/compatibility/deprecations.json
++++ b/compatibility/deprecations.json
@@@ -231,6 +231,41 @@
"since": "0.263.0",
"removeAfter": "1.0.0",
"reason": "Report's personal AI usage answers rows, totals, by_paid_by, insights and source (plan L41 R1); v1's usage array and its field names never matched the route, so the shape moved to a new major (2026-10-31)"
++ },
++ {
++ "id": "network.node",
++ "replacement": "network.link@1",
++ "since": "0.372.0",
++ "removeAfter": "1.0.0",
++ "reason": "The machine product Node became OpenVibe Link: network.node@1 became network.link@1 in the rename (2026-11-09)"
++ },
++ {
++ "id": "network.node-report-request",
++ "replacement": "network.link-report-request@1",
++ "since": "0.372.0",
++ "removeAfter": "1.0.0",
++ "reason": "The machine product Node became OpenVibe Link: network.node-report-request@1 became network.link-report-request@1 in the rename (2026-11-09)"
++ },
++ {
++ "id": "network.node-list-result",
++ "replacement": "network.link-list-result@1",
++ "since": "0.372.0",
++ "removeAfter": "1.0.0",
++ "reason": "The machine product Node became OpenVibe Link: network.node-list-result@1 became network.link-list-result@1 in the rename (2026-11-09)"
++ },
++ {
++ "id": "platform.node-capabilities",
++ "replacement": "platform.link-capabilities@1",
++ "since": "0.372.0",
++ "removeAfter": "1.0.0",
++ "reason": "The machine product Node became OpenVibe Link: platform.node-capabilities@1 became platform.link-capabilities@1, and link.compute@1 carries the Link's offer in the platform fabric (2026-11-09)"
++ },
++ {
++ "id": "network.node.revoked",
++ "replacement": "network.link.revoked@1",
++ "since": "0.372.0",
++ "removeAfter": "1.0.0",
++ "reason": "The machine product Node became OpenVibe Link: the network.node.revoked event became network.link.revoked and its principal ids move to lnk_ (2026-11-09)"
}
]
}
ddiff --git a/contracts/ai/credential-put.v1.json b/contracts/ai/credential-put.v1.json
iindex dddb2415..36e1ed16 100644
---- a/contracts/ai/credential-put.v1.json
++++ b/contracts/ai/credential-put.v1.json
@@@ -13,14 +13,14 @@
"enum": [
"openai",
"anthropic",
-- "node"
++ "link"
],
"description": "openai = any OpenAI-compatible endpoint. node = the person's own machine: a model on their own model server, through their OpenVibe Link and OpenVibe.Bot (no key, no base_url, no cost)."
},
"machine": {
"type": "string",
"pattern": "^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$",
-- "description": "provider node: the person's own machine (openvibe.bot/machines) whose OpenVibe Link runs the model (the ai plugin, contracts 0.219.0), or `any` (contracts 0.221.0): whichever of their machines is on, lets their AI use its models and offers the model, the fastest first, the next one when it is busy or away. Never anyone else's machine."
++ "description": "provider link: the person's own machine (openvibe.link/machines) whose OpenVibe Link runs the model (the ai plugin, contracts 0.219.0), or `any` (contracts 0.221.0): whichever of their machines is on, lets their AI use its models and offers the model, the fastest first, the next one when it is busy or away. Never anyone else's machine."
},
"base_url": {
"type": "string",
@@@ -78,7 +78,7 @@
"if": {
"properties": {
"provider": {
-- "const": "node"
++ "const": "link"
}
},
"required": [
ddiff --git a/contracts/ai/credential-put.v2.json b/contracts/ai/credential-put.v2.json
iindex 3134fdba..e8064e59 100644
---- a/contracts/ai/credential-put.v2.json
++++ b/contracts/ai/credential-put.v2.json
@@@ -17,7 +17,7 @@
"minimax",
"openrouter",
"custom",
-- "node"
++ "link"
],
"description": "anthropic = Anthropic's Messages API; openai = any OpenAI-compatible endpoint; deepseek, minimax and openrouter = those providers; custom = the endpoint in base_url (a URL that speaks the OpenAI or the Anthropic format, by the route it is used on); node = the person's own machine (no key, no base_url, no cost)."
},
@@@ -42,7 +42,7 @@
"machine": {
"type": "string",
"pattern": "^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$",
-- "description": "provider node: the person's own machine (openvibe.bot/machines) whose OpenVibe Link runs the model, or `any` of their own."
++ "description": "provider link: the person's own machine (openvibe.link/machines) whose OpenVibe Link runs the model, or `any` of their own."
},
"models": {
"type": "object",
@@@ -89,7 +89,7 @@
"if": {
"properties": {
"provider": {
-- "const": "node"
++ "const": "link"
}
},
"required": [
ddiff --git a/contracts/ai/credential.v1.json b/contracts/ai/credential.v1.json
iindex 0ed6e3c5..ed79289f 100644
---- a/contracts/ai/credential.v1.json
++++ b/contracts/ai/credential.v1.json
@@@ -26,7 +26,7 @@
"enum": [
"openai",
"anthropic",
-- "node"
++ "link"
]
},
"machine": {
@@@ -35,7 +35,7 @@
"null"
],
"pattern": "^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$",
-- "description": "provider node: the machine it runs on, or `any` of the person's own (contracts 0.221.0)"
++ "description": "provider link: the machine it runs on, or `any` of the person's own (contracts 0.221.0)"
},
"base_url": {
"type": [
ddiff --git a/contracts/ai/credential.v2.json b/contracts/ai/credential.v2.json
iindex 223f0b6f..2a597ac6 100644
---- a/contracts/ai/credential.v2.json
++++ b/contracts/ai/credential.v2.json
@@@ -30,7 +30,7 @@
"minimax",
"openrouter",
"custom",
-- "node"
++ "link"
]
},
"machine": {
@@@ -39,7 +39,7 @@
"null"
],
"pattern": "^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$",
-- "description": "provider node: the machine it runs on, or `any` of the person's own"
++ "description": "provider link: the machine it runs on, or `any` of the person's own"
},
"base_url": {
"type": [
@@@ -118,7 +118,7 @@
"properties": {
"provider": {
"not": {
-- "const": "node"
++ "const": "link"
}
}
},
ddiff --git a/contracts/ai/inference-deployment.v1.json b/contracts/ai/inference-deployment.v1.json
iindex 37b9ad15..38f48147 100644
---- a/contracts/ai/inference-deployment.v1.json
++++ b/contracts/ai/inference-deployment.v1.json
@@@ -39,7 +39,7 @@
},
"operator": {
"type": "string",
-- "description": "Operator slug or `owner:<subject_id>` for a private node."
++ "description": "Operator slug or `owner:<subject_id>` for a private machine."
},
"host": {
"type": [
ddiff --git a/contracts/bench/run.v1.json b/contracts/bench/run.v1.json
iindex 8a47572c..22a80aaf 100644
---- a/contracts/bench/run.v1.json
++++ b/contracts/bench/run.v1.json
@@@ -158,7 +158,7 @@
"node",
"submitted"
],
-- "description": "codes: an openvibe.codes task. run: a Run worker. node: the person's own machine. submitted: nobody ran it."
++ "description": "codes: an openvibe.codes task. run: a Run worker. link: the person's own machine. submitted: nobody ran it."
},
"id": {
"type": "string",
ddiff --git a/contracts/bench/run.v2.json b/contracts/bench/run.v2.json
iindex a1d7c33a..48a29684 100644
---- a/contracts/bench/run.v2.json
++++ b/contracts/bench/run.v2.json
@@@ -158,7 +158,7 @@
"node",
"submitted"
],
-- "description": "codes: an openvibe.codes task. run: a Run worker. node: the person's own machine. submitted: nobody ran it."
++ "description": "codes: an openvibe.codes task. run: a Run worker. link: the person's own machine. submitted: nobody ran it."
},
"id": {
"type": "string",
ddiff --git a/contracts/bot/job-dispatch-result.v1.json b/contracts/bot/job-dispatch-result.v1.json
iindex 01c5fd4d..8447643a 100644
---- a/contracts/bot/job-dispatch-result.v1.json
++++ b/contracts/bot/job-dispatch-result.v1.json
@@@ -79,7 +79,7 @@
"null"
],
"maxLength": 128,
-- "description": "The offer's provider, as Run sent it; null for a first-party node."
++ "description": "The offer's provider, as Run sent it; null for a first-party machine."
},
"cancel_requested": {
"type": "boolean",
ddiff --git a/contracts/bot/machine-pool.v1.json b/contracts/bot/machine-pool.v1.json
iindex b744c456..284b4328 100644
---- a/contracts/bot/machine-pool.v1.json
++++ b/contracts/bot/machine-pool.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/bot/machine-pool.v1.json",
"title": "BotMachinePool",
-- "description": "bot.machine-pool@1 (the pooled machines design, 2026-11-06): several of one owner's machines on the same local network that serve one model together through llama.cpp RPC, `rpc-server` on the members and `llama-server --rpc` on the head. OpenVibe.Network holds the record: the owner proposes it on /machines (POST /api/v1/machine-pools, two to eight of their own machines and an optional head; the default head has the most free VRAM), and each machine reads it with its own node token (GET /api/v1/node/self/pools/:id, network.node.self.manage) and approves it locally (POST /api/v1/node/self/pools/:id/approve with { certificate }). A machine's `certificate` is null until it approves on that machine: the Link generates an Ed25519 key there, keeps the private key, and uploads only its self-signed public certificate, which the other members pin for the pool's private link. A pool with any null certificate never serves. The record never carries a private key or a prompt. What a schema cannot say, Network and Node check at run time: the members are distinct and the head is one of them; every member has the pool's owner, today, through Bot; every member was seen from the same public address with a private address in the same subnet; the head's direct link probe to each member passes; each machine's certificate is its own; and any change of membership, owner, head or key revokes the pool, so all machines approve a new one. A machine that leaves (removed, signed out, a different owner) drops its link and its key at once.",
++ "description": "bot.machine-pool@1 (the pooled machines design, 2026-11-06): several of one owner's machines on the same local network that serve one model together through llama.cpp RPC, `rpc-server` on the members and `llama-server --rpc` on the head. OpenVibe.Network holds the record: the owner proposes it on /machines (POST /api/v1/machine-pools, two to eight of their own machines and an optional head; the default head has the most free VRAM), and each machine reads it with its own Link token (GET /api/v1/link/self/pools/:id, network.link.self.manage) and approves it locally (POST /api/v1/link/self/pools/:id/approve with { certificate }). A machine's `certificate` is null until it approves on that machine: the Link generates an Ed25519 key there, keeps the private key, and uploads only its self-signed public certificate, which the other members pin for the pool's private link. A pool with any null certificate never serves. The record never carries a private key or a prompt. What a schema cannot say, Network and Link check at run time: the members are distinct and the head is one of them; every member has the pool's owner, today, through Bot; every member was seen from the same public address with a private address in the same subnet; the head's direct link probe to each member passes; each machine's certificate is its own; and any change of membership, owner, head or key revokes the pool, so all machines approve a new one. A machine that leaves (removed, signed out, a different owner) drops its link and its key at once.",
"type": "object",
"additionalProperties": false,
"required": [
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 3cd48a7b..d2b71028 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -6436,30 +6436,30 @@
"status": "retired"
},
{
-- "id": "network.node",
++ "id": "network.link",
"version": "1.0.0",
"owner": "network",
-- "schema": "network/node.v1.json",
++ "schema": "network/link.v1.json",
"visibility": "public",
"compatibility": "backward",
"status": "active",
"adr": "ADR-034"
},
{
-- "id": "network.node-report-request",
++ "id": "network.link-report-request",
"version": "1.0.0",
"owner": "network",
-- "schema": "network/node-report-request.v1.json",
++ "schema": "network/link-report-request.v1.json",
"visibility": "first-party",
"compatibility": "backward",
"status": "active",
"adr": "ADR-034"
},
{
-- "id": "network.node-list-result",
++ "id": "network.link-list-result",
"version": "1.0.0",
"owner": "network",
-- "schema": "network/node-list-result.v1.json",
++ "schema": "network/link-list-result.v1.json",
"visibility": "public",
"compatibility": "backward",
"status": "active",
@@@ -6728,10 +6728,19 @@
"adr": "ADR-034"
},
{
-- "id": "platform.node-capabilities",
++ "id": "link.compute",
"version": "1.0.0",
"owner": "network",
-- "schema": "platform/node-capabilities.v1.json",
++ "schema": "link/compute.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "platform.link-capabilities",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "platform/link-capabilities.v1.json",
"visibility": "public",
"compatibility": "backward",
"status": "active",
@@@ -7391,10 +7400,10 @@
"adr": "ADR-048"
},
{
-- "id": "network.node.revoked",
++ "id": "network.link.revoked",
"version": "1.0.0",
"owner": "network",
-- "schema": "events/payloads/network.node.revoked.v1.json",
++ "schema": "events/payloads/network.link.revoked.v1.json",
"visibility": "first-party",
"compatibility": "backward",
"status": "active",
ddiff --git a/contracts/connect/device.v1.json b/contracts/connect/device.v1.json
iindex a3fd4ced..2beae237 100644
---- a/contracts/connect/device.v1.json
++++ b/contracts/connect/device.v1.json
@@@ -83,7 +83,7 @@
"string",
"null"
],
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
"description": "The Network node principal the device presents; null for an agent or server that joins through a pre-auth key."
},
"name": {
ddiff --git a/contracts/events/payloads/network.link.revoked.v1.json b/contracts/events/payloads/network.link.revoked.v1.json
nnew file mode 100644
iindex 00000000..c5432e09
---- /dev/null
++++ b/contracts/events/payloads/network.link.revoked.v1.json
@@@ -0,0 +1,68 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.link.revoked.v1.json",
++ "title": "NetworkLinkRevokedPayload",
++ "description": "network.link.revoked v1 (OpenVibe.Network; plan T2, docs/t2-cells-and-node-principal.md section 10, migrations/0014_node_pairing.sql). A node principal (lnk_…), a person's paired machine, was revoked: through POST /internal/link-principals/:id/revoke (network.link.manage, the service that paired it), through POST /api/v1/me/nodes/:id/revoke (the owner's session), or with the owner's account. Network revokes the row and clears the credential and its grace window; a machine holding a live node token is refused at its next use (≤ 300 s), and until this event only learned of the revoke at its next reauth (≤ 330 s). A consumer stops the machine at once; a second revoke changes nothing and emits nothing. The credential and its hash are never carried. Envelope: subject { type: node_principal, id: <lnk_…> }, visibility internal, actor the service (svc:<service>) or the owner ({ type: user }) that revoked.",
++ "type": "object",
++ "required": [
++ "node_id",
++ "principal_id",
++ "owner",
++ "at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "node_id": {
++ "type": "string",
++ "pattern": "^n-[a-z0-9]{26}$",
++ "description": "The machine's node id (platform_node_principals.node_id), minted with the principal and unique among principals."
++ },
++ "principal_id": {
++ "type": "string",
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The revoked node principal. Also the envelope subject id."
++ },
++ "owner": {
++ "description": "Who the machine belongs to, as GET /api/v1/me/nodes and the operator list show it: the person who paired it, the project it is paired into, or the platform for a Host-reported machine.",
++ "oneOf": [
++ {
++ "type": "object",
++ "required": ["kind", "subject"],
++ "additionalProperties": false,
++ "properties": {
++ "kind": { "const": "user" },
++ "subject": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" }
++ }
++ },
++ {
++ "type": "object",
++ "required": ["kind", "project_id"],
++ "additionalProperties": false,
++ "properties": {
++ "kind": { "const": "project" },
++ "project_id": { "type": "string", "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$" }
++ }
++ },
++ {
++ "type": "object",
++ "required": ["kind"],
++ "additionalProperties": false,
++ "properties": {
++ "kind": { "const": "platform" }
++ }
++ }
++ ]
++ },
++ "reason": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 500,
++ "description": "Why it was revoked, when the caller gave one (the owner's text or the service's); absent otherwise."
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the revoke was applied (platform_node_principals.revoked_at)."
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/network.machine.offline.v1.json b/contracts/events/payloads/network.machine.offline.v1.json
iindex d91502d8..d243cfa1 100644
---- a/contracts/events/payloads/network.machine.offline.v1.json
++++ b/contracts/events/payloads/network.machine.offline.v1.json
@@@ -8,7 +8,7 @@
"additionalProperties": false,
"properties": {
"machine_id": { "type": "string", "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$" },
-- "principal_id": { "type": "string", "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The machine's node principal on Network, when it has one." },
++ "principal_id": { "type": "string", "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The machine's node principal on Network, when it has one." },
"owner": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The person the machine belongs to." },
"at": { "type": "string", "format": "date-time" }
}
ddiff --git a/contracts/events/payloads/network.machine.online.v1.json b/contracts/events/payloads/network.machine.online.v1.json
iindex 4c0e26f6..8d5d7be1 100644
---- a/contracts/events/payloads/network.machine.online.v1.json
++++ b/contracts/events/payloads/network.machine.online.v1.json
@@@ -8,7 +8,7 @@
"additionalProperties": false,
"properties": {
"machine_id": { "type": "string", "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$" },
-- "principal_id": { "type": "string", "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The machine's node principal on Network, when it has one." },
++ "principal_id": { "type": "string", "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The machine's node principal on Network, when it has one." },
"owner": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The person the machine belongs to." },
"at": { "type": "string", "format": "date-time" }
}
ddiff --git a/contracts/events/payloads/network.machine.paired.v1.json b/contracts/events/payloads/network.machine.paired.v1.json
iindex 5ee45de7..bf38fb3d 100644
---- a/contracts/events/payloads/network.machine.paired.v1.json
++++ b/contracts/events/payloads/network.machine.paired.v1.json
@@@ -2,14 +2,14 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/network.machine.paired.v1.json",
"title": "NetworkMachinePairedPayload",
-- "description": "network.machine.paired v1 (OpenVibe.Network, plan L34: the machines hub). A machine was paired with a person's account: it redeemed the one-time code and became theirs. It holds a credential from then on; the code is spent. The machine's device id in Bot is absent until the machine checks in for the first time. Envelope: subject { type: machine, id: <nod_…> }, visibility internal, actor service:network.",
++ "description": "network.machine.paired v1 (OpenVibe.Network, plan L34: the machines hub). A machine was paired with a person's account: it redeemed the one-time code and became theirs. It holds a credential from then on; the code is spent. The machine's device id in Bot is absent until the machine checks in for the first time. Envelope: subject { type: machine, id: <lnk_…> }, visibility internal, actor service:network.",
"type": "object",
"required": ["principal_id", "owner", "at"],
"additionalProperties": false,
"properties": {
"machine_id": { "type": "string", "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The machine's device id in Bot's registry, when Bot already knows the device." },
"node_id": { "type": "string", "pattern": "^n-[a-z0-9]{26}$", "description": "The machine's node id (platform_node_principals.node_id)." },
-- "principal_id": { "type": "string", "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The node principal the pairing created. Also the envelope subject id." },
++ "principal_id": { "type": "string", "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The node principal the pairing created. Also the envelope subject id." },
"owner": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The person the machine belongs to." },
"at": { "type": "string", "format": "date-time" }
}
ddiff --git a/contracts/events/payloads/network.machine.revoked.v1.json b/contracts/events/payloads/network.machine.revoked.v1.json
iindex f0577af2..c007daa1 100644
---- a/contracts/events/payloads/network.machine.revoked.v1.json
++++ b/contracts/events/payloads/network.machine.revoked.v1.json
@@@ -8,7 +8,7 @@
"additionalProperties": false,
"properties": {
"machine_id": { "type": "string", "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$" },
-- "principal_id": { "type": "string", "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The machine's node principal on Network, when it had one." },
++ "principal_id": { "type": "string", "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The machine's node principal on Network, when it had one." },
"owner": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The person the machine belonged to." },
"reason": { "type": "string", "minLength": 1, "maxLength": 500, "description": "Why it was revoked, when the caller gave one. Absent otherwise." },
"at": { "type": "string", "format": "date-time" }
ddiff --git a/contracts/events/payloads/network.node.revoked.v1.json b/contracts/events/payloads/network.node.revoked.v1.json
ddeleted file mode 100644
iindex df69420a..00000000
---- a/contracts/events/payloads/network.node.revoked.v1.json
++++ /dev/null
@@@ -1,68 +0,0 @@
--{
-- "$schema": "https://json-schema.org/draft/2020-12/schema",
-- "$id": "https://openvibe.network/contracts/events/payloads/network.node.revoked.v1.json",
-- "title": "NetworkNodeRevokedPayload",
-- "description": "network.node.revoked v1 (OpenVibe.Network; plan T2, docs/t2-cells-and-node-principal.md section 10, migrations/0014_node_pairing.sql). A node principal (nod_…), a person's paired machine, was revoked: through POST /internal/node-principals/:id/revoke (network.node.manage, the service that paired it), through POST /api/v1/me/nodes/:id/revoke (the owner's session), or with the owner's account. Network revokes the row and clears the credential and its grace window; a machine holding a live node token is refused at its next use (≤ 300 s), and until this event only learned of the revoke at its next reauth (≤ 330 s). A consumer stops the machine at once; a second revoke changes nothing and emits nothing. The credential and its hash are never carried. Envelope: subject { type: node_principal, id: <nod_…> }, visibility internal, actor the service (svc:<service>) or the owner ({ type: user }) that revoked.",
-- "type": "object",
-- "required": [
-- "node_id",
-- "principal_id",
-- "owner",
-- "at"
-- ],
-- "additionalProperties": false,
-- "properties": {
-- "node_id": {
-- "type": "string",
-- "pattern": "^n-[a-z0-9]{26}$",
-- "description": "The machine's node id (platform_node_principals.node_id), minted with the principal and unique among principals."
-- },
-- "principal_id": {
-- "type": "string",
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
-- "description": "The revoked node principal. Also the envelope subject id."
-- },
-- "owner": {
-- "description": "Who the machine belongs to, as GET /api/v1/me/nodes and the operator list show it: the person who paired it, the project it is paired into, or the platform for a Host-reported machine.",
-- "oneOf": [
-- {
-- "type": "object",
-- "required": ["kind", "subject"],
-- "additionalProperties": false,
-- "properties": {
-- "kind": { "const": "user" },
-- "subject": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" }
-- }
-- },
-- {
-- "type": "object",
-- "required": ["kind", "project_id"],
-- "additionalProperties": false,
-- "properties": {
-- "kind": { "const": "project" },
-- "project_id": { "type": "string", "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$" }
-- }
-- },
-- {
-- "type": "object",
-- "required": ["kind"],
-- "additionalProperties": false,
-- "properties": {
-- "kind": { "const": "platform" }
-- }
-- }
-- ]
-- },
-- "reason": {
-- "type": "string",
-- "minLength": 1,
-- "maxLength": 500,
-- "description": "Why it was revoked, when the caller gave one (the owner's text or the service's); absent otherwise."
-- },
-- "at": {
-- "type": "string",
-- "format": "date-time",
-- "description": "When the revoke was applied (platform_node_principals.revoked_at)."
-- }
-- }
--}
ddiff --git a/contracts/identity/service-token-claims.v1.json b/contracts/identity/service-token-claims.v1.json
iindex bdc64316..9e5aeada 100644
---- a/contracts/identity/service-token-claims.v1.json
++++ b/contracts/identity/service-token-claims.v1.json
@@@ -23,14 +23,14 @@
},
"sub": {
"type": "string",
-- "pattern": "^(svc:[a-z][a-z0-9-]{1,39}|app:app_[0-9A-HJKMNP-TV-Z]{26}|mod:mod_[0-9A-HJKMNP-TV-Z]{26}|node:nod_[0-9A-HJKMNP-TV-Z]{26}|agent:agt_[0-9A-HJKMNP-TV-Z]{26})$"
++ "pattern": "^(svc:[a-z][a-z0-9-]{1,39}|app:app_[0-9A-HJKMNP-TV-Z]{26}|mod:mod_[0-9A-HJKMNP-TV-Z]{26}|link:lnk_[0-9A-HJKMNP-TV-Z]{26}|agent:agt_[0-9A-HJKMNP-TV-Z]{26})$"
},
"actor_type": {
"enum": [
"service",
"app",
"mod",
-- "node",
++ "link",
"agent"
]
},
ddiff --git a/contracts/link/compute.v1.json b/contracts/link/compute.v1.json
nnew file mode 100644
iindex 00000000..66534457
---- /dev/null
++++ b/contracts/link/compute.v1.json
@@@ -0,0 +1,69 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/link/compute.v1.json",
++ "title": "LinkCompute",
++ "description": "Where a person's work can run. Only ready candidates may be picked; a missing rental quote is never a price.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["sources", "candidates"],
++ "properties": {
++ "sources": {
++ "type": "array",
++ "items": { "$ref": "#/$defs/source" }
++ },
++ "candidates": {
++ "type": "array",
++ "items": { "$ref": "#/$defs/candidate" }
++ }
++ },
++ "$defs": {
++ "kind": { "enum": ["personal", "cloud", "exchange", "community"] },
++ "status": { "enum": ["ready", "unavailable", "coming_soon"] },
++ "price": {
++ "type": ["object", "null"],
++ "additionalProperties": false,
++ "required": ["amount_usd", "unit"],
++ "properties": {
++ "amount_usd": { "type": "number", "minimum": 0 },
++ "unit": { "enum": ["hour"] }
++ }
++ },
++ "source": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["kind", "name", "status", "reason"],
++ "properties": {
++ "id": { "type": "string", "minLength": 1 },
++ "principal_id": { "type": "string", "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$" },
++ "kind": { "$ref": "#/$defs/kind" },
++ "name": { "type": "string", "minLength": 1 },
++ "status": { "$ref": "#/$defs/status" },
++ "reason": { "type": ["string", "null"] },
++ "price": { "$ref": "#/$defs/price" },
++ "quoted_at": { "type": "string", "format": "date-time" },
++ "expires_at": { "type": "string", "format": "date-time" }
++ },
++ "if": { "properties": { "kind": { "const": "exchange" }, "status": { "const": "ready" } }, "required": ["kind", "status"] },
++ "then": { "required": ["price", "quoted_at", "expires_at"], "properties": { "price": { "type": "object" } } }
++ },
++ "candidate": {
++ "allOf": [
++ { "$ref": "#/$defs/source" },
++ { "type": "object", "required": ["id", "price"], "properties": { "id": { "type": "string", "minLength": 1 } } }
++ ]
++ }
++ },
++ "examples": [
++ {
++ "sources": [
++ { "kind": "personal", "name": "Your Link machines", "status": "ready", "reason": null },
++ { "kind": "cloud", "name": "OpenVibe cloud machines", "status": "coming_soon", "reason": "No cloud pool is accepting jobs yet." },
++ { "kind": "exchange", "name": "Exchange GPU rentals", "status": "coming_soon", "reason": "A live quote is required.", "price": null },
++ { "kind": "community", "name": "Community machines", "status": "coming_soon", "reason": "Shared placement is being connected." }
++ ],
++ "candidates": [
++ { "id": "dev_01J0000000000000000000000", "kind": "personal", "name": "Studio", "status": "ready", "reason": null, "price": { "amount_usd": 0, "unit": "hour" } }
++ ]
++ }
++ ]
++}
ddiff --git a/contracts/network/link-list-result.v1.json b/contracts/network/link-list-result.v1.json
nnew file mode 100644
iindex 00000000..83f2a969
---- /dev/null
++++ b/contracts/network/link-list-result.v1.json
@@@ -0,0 +1,13 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/link-list-result.v1.json",
++ "title": "NetworkLinkListResult",
++ "description": "network.link-list-result@1: GET /api/v1/links on OpenVibe.Network (public, cacheable): every node of the platform, filterable by ?role= and ?region=. The input of openvibe-sdk/geo's nearest().",
++ "type": "object",
++ "required": ["nodes", "generated_at"],
++ "additionalProperties": false,
++ "properties": {
++ "nodes": { "type": "array", "items": { "$ref": "link.v1.json" } },
++ "generated_at": { "type": "string", "format": "date-time" }
++ }
++}
ddiff --git a/contracts/network/link-report-request.v1.json b/contracts/network/link-report-request.v1.json
nnew file mode 100644
iindex 00000000..d89ebda1
---- /dev/null
++++ b/contracts/network/link-report-request.v1.json
@@@ -0,0 +1,13 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/link-report-request.v1.json",
++ "title": "NetworkLinkReportRequest",
++ "description": "network.link-report-request@1: POST /internal/links/report on OpenVibe.Network (capability network.link.report, held by the Host principal). The complete set of nodes one Host inventory knows, with their health now: a node absent from a later report of the same source is marked down, never deleted silently.",
++ "type": "object",
++ "required": ["source", "nodes"],
++ "additionalProperties": false,
++ "properties": {
++ "source": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,39}$", "description": "The reporting inventory (e.g. the host id)." },
++ "nodes": { "type": "array", "maxItems": 500, "items": { "$ref": "link.v1.json" } }
++ }
++}
ddiff --git a/contracts/network/link.v1.json b/contracts/network/link.v1.json
nnew file mode 100644
iindex 00000000..ef0579ee
---- /dev/null
++++ b/contracts/network/link.v1.json
@@@ -0,0 +1,42 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/link.v1.json",
++ "title": "NetworkLink",
++ "description": "network.link@1: one machine of the OpenVibe platform as the node registry lists it (ADR-034 section 12; roadmap WS-X1). Roles are what it serves; location is region-level; the beacon is an HTTPS URL a client times to measure its own round trip. No addresses, capacity or secrets are public: a node is found by role and location, and reached through the service that uses it.",
++ "type": "object",
++ "required": ["id", "roles", "location", "health", "updated_at"],
++ "additionalProperties": false,
++ "properties": {
++ "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,39}$" },
++ "name": { "type": "string", "maxLength": 80 },
++ "roles": {
++ "type": "array", "minItems": 1, "uniqueItems": true,
++ "items": { "enum": ["web", "app", "data", "media-worker", "ingest", "edge-probe", "edge-relay", "edge-cache", "gpu", "staging", "ci"] }
++ },
++ "location": {
++ "type": "object",
++ "required": ["region", "country"],
++ "additionalProperties": false,
++ "properties": {
++ "region": { "type": "string", "pattern": "^[a-z]{2}-[a-z]+(-[0-9])?$", "description": "e.g. us-west, eu-central" },
++ "country": { "type": "string", "pattern": "^[A-Z]{2}$" },
++ "city": { "type": "string", "maxLength": 80 },
++ "lat": { "type": "number", "minimum": -90, "maximum": 90 },
++ "lon": { "type": "number", "minimum": -180, "maximum": 180 }
++ }
++ },
++ "provider": { "type": "string", "maxLength": 40 },
++ "beacon": { "type": "string", "format": "uri", "pattern": "^https://", "description": "Timed by clients (GET, tiny body, no caching) to measure round-trip time." },
++ "health": {
++ "type": "object",
++ "required": ["status", "checked_at"],
++ "additionalProperties": false,
++ "properties": {
++ "status": { "enum": ["up", "degraded", "down", "unknown"] },
++ "checked_at": { "type": "string", "format": "date-time" }
++ }
++ },
++ "updated_at": { "type": "string", "format": "date-time" },
++ "cell": { "type": "string", "pattern": "^[a-z]{2,8}-[0-9]{1,3}$", "description": "Cell the node belongs to, e.g. wnam-1." }
++ }
++}
ddiff --git a/contracts/network/machine-pairing.v1.json b/contracts/network/machine-pairing.v1.json
iindex 575cbf24..d2132bdf 100644
---- a/contracts/network/machine-pairing.v1.json
++++ b/contracts/network/machine-pairing.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/network/machine-pairing.v1.json",
"title": "NetworkMachinePairing",
-- "description": "network.machine-pairing@1: a one-time code that pairs a machine with the person's account (POST /api/v1/machines/pairings, capability machines.manage). The code is shown once, expires, and works once: the machine redeems it and becomes the person's. The command is what the owner runs on the machine (openvibe.bot/install). The code is never stored in the clear and never returned again.",
++ "description": "network.machine-pairing@1: a one-time code that pairs a machine with the person's account (POST /api/v1/machines/pairings, capability machines.manage). The code is shown once, expires, and works once: the machine redeems it and becomes the person's. The command is what the owner runs on the machine (openvibe.link/install). The code is never stored in the clear and never returned again.",
"type": "object",
"required": ["code", "expires_at", "installer"],
"additionalProperties": false,
@@@ -21,7 +21,7 @@
"installer": {
"type": "string",
"maxLength": 500,
-- "description": "The command the owner runs on the machine (openvibe.bot/install, with the network, the pairing id and the code)."
++ "description": "The command the owner runs on the machine (openvibe.link/install, with the network, the pairing id and the code)."
}
}
}
ddiff --git a/contracts/network/machine.v1.json b/contracts/network/machine.v1.json
iindex 687a0c19..fa20f5fe 100644
---- a/contracts/network/machine.v1.json
++++ b/contracts/network/machine.v1.json
@@@ -14,8 +14,8 @@
},
"principal_id": {
"type": "string",
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
-- "description": "The machine's node principal on Network (nod_…): the credential it holds. Absent for a machine Bot knows but Network has no principal for."
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's node principal on Network (lnk_…): the credential it holds. Absent for a machine Bot knows but Network has no principal for."
},
"name": { "type": "string", "maxLength": 80, "description": "The name the owner gave it. Absent when they gave none." },
"kind": {
ddiff --git a/contracts/network/node-list-result.v1.json b/contracts/network/node-list-result.v1.json
ddeleted file mode 100644
iindex 07979988..00000000
---- a/contracts/network/node-list-result.v1.json
++++ /dev/null
@@@ -1,13 +0,0 @@
--{
-- "$schema": "https://json-schema.org/draft/2020-12/schema",
-- "$id": "https://openvibe.network/contracts/network/node-list-result.v1.json",
-- "title": "NetworkNodeListResult",
-- "description": "network.node-list-result@1: GET /api/v1/nodes on OpenVibe.Network (public, cacheable): every node of the platform, filterable by ?role= and ?region=. The input of openvibe-sdk/geo's nearest().",
-- "type": "object",
-- "required": ["nodes", "generated_at"],
-- "additionalProperties": false,
-- "properties": {
-- "nodes": { "type": "array", "items": { "$ref": "node.v1.json" } },
-- "generated_at": { "type": "string", "format": "date-time" }
-- }
--}
ddiff --git a/contracts/network/node-report-request.v1.json b/contracts/network/node-report-request.v1.json
ddeleted file mode 100644
iindex 09f77940..00000000
---- a/contracts/network/node-report-request.v1.json
++++ /dev/null
@@@ -1,13 +0,0 @@
--{
-- "$schema": "https://json-schema.org/draft/2020-12/schema",
-- "$id": "https://openvibe.network/contracts/network/node-report-request.v1.json",
-- "title": "NetworkNodeReportRequest",
-- "description": "network.node-report-request@1: POST /internal/nodes/report on OpenVibe.Network (capability network.node.report, held by the Host principal). The complete set of nodes one Host inventory knows, with their health now: a node absent from a later report of the same source is marked down, never deleted silently.",
-- "type": "object",
-- "required": ["source", "nodes"],
-- "additionalProperties": false,
-- "properties": {
-- "source": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,39}$", "description": "The reporting inventory (e.g. the host id)." },
-- "nodes": { "type": "array", "maxItems": 500, "items": { "$ref": "node.v1.json" } }
-- }
--}
ddiff --git a/contracts/network/node.v1.json b/contracts/network/node.v1.json
ddeleted file mode 100644
iindex 784ac0e3..00000000
---- a/contracts/network/node.v1.json
++++ /dev/null
@@@ -1,42 +0,0 @@
--{
-- "$schema": "https://json-schema.org/draft/2020-12/schema",
-- "$id": "https://openvibe.network/contracts/network/node.v1.json",
-- "title": "NetworkNode",
-- "description": "network.node@1: one machine of the OpenVibe platform as the node registry lists it (ADR-034 section 12; roadmap WS-X1). Roles are what it serves; location is region-level; the beacon is an HTTPS URL a client times to measure its own round trip. No addresses, capacity or secrets are public: a node is found by role and location, and reached through the service that uses it.",
-- "type": "object",
-- "required": ["id", "roles", "location", "health", "updated_at"],
-- "additionalProperties": false,
-- "properties": {
-- "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,39}$" },
-- "name": { "type": "string", "maxLength": 80 },
-- "roles": {
-- "type": "array", "minItems": 1, "uniqueItems": true,
-- "items": { "enum": ["web", "app", "data", "media-worker", "ingest", "edge-probe", "edge-relay", "edge-cache", "gpu", "staging", "ci"] }
-- },
-- "location": {
-- "type": "object",
-- "required": ["region", "country"],
-- "additionalProperties": false,
-- "properties": {
-- "region": { "type": "string", "pattern": "^[a-z]{2}-[a-z]+(-[0-9])?$", "description": "e.g. us-west, eu-central" },
-- "country": { "type": "string", "pattern": "^[A-Z]{2}$" },
-- "city": { "type": "string", "maxLength": 80 },
-- "lat": { "type": "number", "minimum": -90, "maximum": 90 },
-- "lon": { "type": "number", "minimum": -180, "maximum": 180 }
-- }
-- },
-- "provider": { "type": "string", "maxLength": 40 },
-- "beacon": { "type": "string", "format": "uri", "pattern": "^https://", "description": "Timed by clients (GET, tiny body, no caching) to measure round-trip time." },
-- "health": {
-- "type": "object",
-- "required": ["status", "checked_at"],
-- "additionalProperties": false,
-- "properties": {
-- "status": { "enum": ["up", "degraded", "down", "unknown"] },
-- "checked_at": { "type": "string", "format": "date-time" }
-- }
-- },
-- "updated_at": { "type": "string", "format": "date-time" },
-- "cell": { "type": "string", "pattern": "^[a-z]{2,8}-[0-9]{1,3}$", "description": "Cell the node belongs to, e.g. wnam-1." }
-- }
--}
ddiff --git a/contracts/platform/job-frame.v1.json b/contracts/platform/job-frame.v1.json
iindex 2e789e51..551c1a61 100644
---- a/contracts/platform/job-frame.v1.json
++++ b/contracts/platform/job-frame.v1.json
@@@ -117,7 +117,7 @@
{
"type": "object",
"additionalProperties": false,
-- "description": "Device to server: wall-clock second `second` of the job has fully elapsed. Metering (platform.usage-sample@1, written through billing.usage.record by whoever terminates the link: OpenVibe.Bot now, Fabric later): second n is [started_ms + n*1000, started_ms + (n+1)*1000) on the worker's monotonic clock. A worker sends `job_usage` for n only once that second has fully elapsed, so every `job_usage` stands for quantity 1; the partial last second is never sent here, only in `job_exit`. Each second is exactly one reading: `id` and `idempotency_key` `run:<job id>:<n>`, `service` `run`, `operation` `function.invoke`, `unit` `s`, `quantity` 1 (the partial last second: (wall_ms mod 1000)/1000, never 0), `at` new Date(started_ms + n*1000).toISOString(), `resource` the job id, `node` the worker's device id, `source` `openvibe-node.worker`, and `project`, `subject` and `provider` from the dispatcher's own record of the job. No field depends on when or how often a frame was sent, and no rating fields (`cost_estimate`, `free_allowance_used`, `vibes_charged`) are set, because Billing keeps the first reading under a key, replays an identical one and refuses a different one (409): a resend after a reconnect, a `job_usage` and the `job_exit` backfill of the same second, and two writers of one job all land on one record.",
++ "description": "Device to server: wall-clock second `second` of the job has fully elapsed. Metering (platform.usage-sample@1, written through billing.usage.record by whoever terminates the link: OpenVibe.Bot now, Fabric later): second n is [started_ms + n*1000, started_ms + (n+1)*1000) on the worker's monotonic clock. A worker sends `job_usage` for n only once that second has fully elapsed, so every `job_usage` stands for quantity 1; the partial last second is never sent here, only in `job_exit`. Each second is exactly one reading: `id` and `idempotency_key` `run:<job id>:<n>`, `service` `run`, `operation` `function.invoke`, `unit` `s`, `quantity` 1 (the partial last second: (wall_ms mod 1000)/1000, never 0), `at` new Date(started_ms + n*1000).toISOString(), `resource` the job id, `node` the worker's device id, `source` `openvibe-link.worker`, and `project`, `subject` and `provider` from the dispatcher's own record of the job. No field depends on when or how often a frame was sent, and no rating fields (`cost_estimate`, `free_allowance_used`, `vibes_charged`) are set, because Billing keeps the first reading under a key, replays an identical one and refuses a different one (409): a resend after a reconnect, a `job_usage` and the `job_exit` backfill of the same second, and two writers of one job all land on one record.",
"required": [
"v",
"seq",
ddiff --git a/contracts/platform/link-capabilities.v1.json b/contracts/platform/link-capabilities.v1.json
nnew file mode 100644
iindex 00000000..78314374
---- /dev/null
++++ b/contracts/platform/link-capabilities.v1.json
@@@ -0,0 +1,194 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/platform/link-capabilities.v1.json",
++ "title": "LinkCapabilities",
++ "description": "Link machine hardware, placement regions, tags, and operating costs (T1 Universal Fabric; ADR-034 proposed).",
++ "$comment": "Additive optional fields may extend this v1 contract.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "node_id",
++ "cpu",
++ "arch",
++ "memory_mb",
++ "storage",
++ "network",
++ "regions",
++ "tags",
++ "costs"
++ ],
++ "properties": {
++ "node_id": {
++ "type": "string"
++ },
++ "cpu": {
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "cores": {
++ "type": "number",
++ "minimum": 0
++ },
++ "threads": {
++ "type": "integer",
++ "minimum": 0
++ }
++ },
++ "required": [
++ "cores"
++ ]
++ },
++ "arch": {
++ "type": "string",
++ "description": "CPU architecture"
++ },
++ "memory_mb": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "gpu": {
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "model": {
++ "type": "string"
++ },
++ "count": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "vram_mb": {
++ "type": "integer",
++ "minimum": 0
++ }
++ },
++ "required": [
++ "model",
++ "count"
++ ]
++ },
++ "storage": {
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "capacity_gb": {
++ "type": "number",
++ "minimum": 0
++ },
++ "available_gb": {
++ "type": "number",
++ "minimum": 0
++ },
++ "kind": {
++ "type": "string"
++ }
++ },
++ "required": [
++ "capacity_gb"
++ ]
++ },
++ "network": {
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "ingress_mbps": {
++ "type": "number",
++ "minimum": 0
++ },
++ "egress_mbps": {
++ "type": "number",
++ "minimum": 0
++ }
++ },
++ "required": [
++ "ingress_mbps",
++ "egress_mbps"
++ ]
++ },
++ "regions": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ },
++ "minItems": 1
++ },
++ "tags": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "costs": {
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "per_hour_usd": {
++ "type": "number",
++ "minimum": 0,
++ "description": "Estimated USD"
++ },
++ "egress_per_gb_usd": {
++ "type": "number",
++ "minimum": 0,
++ "description": "Estimated USD"
++ }
++ },
++ "required": [
++ "per_hour_usd"
++ ]
++ },
++ "capabilities": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*:[a-z0-9.-]+$"
++ },
++ "description": "What the machine offers, in the platform.resource-offer@1 capability form (e.g. node:http, worker:ffmpeg)."
++ },
++ "agent_version": {
++ "type": "string",
++ "maxLength": 40,
++ "description": "Version of the node agent that reported."
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ },
++ "examples": [
++ {
++ "node_id": "node-1",
++ "cpu": {
++ "cores": 8,
++ "threads": 16
++ },
++ "arch": "x86_64",
++ "memory_mb": 16384,
++ "gpu": {
++ "model": "L4",
++ "count": 1,
++ "vram_mb": 24576
++ },
++ "storage": {
++ "capacity_gb": 500,
++ "available_gb": 300,
++ "kind": "ssd"
++ },
++ "network": {
++ "ingress_mbps": 1000,
++ "egress_mbps": 1000
++ },
++ "regions": [
++ "us-west"
++ ],
++ "tags": [
++ "gpu",
++ "edge"
++ ],
++ "costs": {
++ "per_hour_usd": 0.2,
++ "egress_per_gb_usd": 0.01
++ }
++ }
++ ]
++}
ddiff --git a/contracts/platform/node-capabilities.v1.json b/contracts/platform/node-capabilities.v1.json
ddeleted file mode 100644
iindex 783da4da..00000000
---- a/contracts/platform/node-capabilities.v1.json
++++ /dev/null
@@@ -1,194 +0,0 @@
--{
-- "$schema": "https://json-schema.org/draft/2020-12/schema",
-- "$id": "https://openvibe.network/contracts/platform/node-capabilities.v1.json",
-- "title": "NodeCapabilities",
-- "description": "Node hardware, placement regions, tags, and operating costs (T1 Universal Fabric; ADR-034 proposed).",
-- "$comment": "Additive optional fields may extend this v1 contract.",
-- "type": "object",
-- "additionalProperties": false,
-- "required": [
-- "node_id",
-- "cpu",
-- "arch",
-- "memory_mb",
-- "storage",
-- "network",
-- "regions",
-- "tags",
-- "costs"
-- ],
-- "properties": {
-- "node_id": {
-- "type": "string"
-- },
-- "cpu": {
-- "type": "object",
-- "additionalProperties": false,
-- "properties": {
-- "cores": {
-- "type": "number",
-- "minimum": 0
-- },
-- "threads": {
-- "type": "integer",
-- "minimum": 0
-- }
-- },
-- "required": [
-- "cores"
-- ]
-- },
-- "arch": {
-- "type": "string",
-- "description": "CPU architecture"
-- },
-- "memory_mb": {
-- "type": "integer",
-- "minimum": 0
-- },
-- "gpu": {
-- "type": "object",
-- "additionalProperties": false,
-- "properties": {
-- "model": {
-- "type": "string"
-- },
-- "count": {
-- "type": "integer",
-- "minimum": 0
-- },
-- "vram_mb": {
-- "type": "integer",
-- "minimum": 0
-- }
-- },
-- "required": [
-- "model",
-- "count"
-- ]
-- },
-- "storage": {
-- "type": "object",
-- "additionalProperties": false,
-- "properties": {
-- "capacity_gb": {
-- "type": "number",
-- "minimum": 0
-- },
-- "available_gb": {
-- "type": "number",
-- "minimum": 0
-- },
-- "kind": {
-- "type": "string"
-- }
-- },
-- "required": [
-- "capacity_gb"
-- ]
-- },
-- "network": {
-- "type": "object",
-- "additionalProperties": false,
-- "properties": {
-- "ingress_mbps": {
-- "type": "number",
-- "minimum": 0
-- },
-- "egress_mbps": {
-- "type": "number",
-- "minimum": 0
-- }
-- },
-- "required": [
-- "ingress_mbps",
-- "egress_mbps"
-- ]
-- },
-- "regions": {
-- "type": "array",
-- "items": {
-- "type": "string"
-- },
-- "minItems": 1
-- },
-- "tags": {
-- "type": "array",
-- "items": {
-- "type": "string"
-- }
-- },
-- "costs": {
-- "type": "object",
-- "additionalProperties": false,
-- "properties": {
-- "per_hour_usd": {
-- "type": "number",
-- "minimum": 0,
-- "description": "Estimated USD"
-- },
-- "egress_per_gb_usd": {
-- "type": "number",
-- "minimum": 0,
-- "description": "Estimated USD"
-- }
-- },
-- "required": [
-- "per_hour_usd"
-- ]
-- },
-- "capabilities": {
-- "type": "array",
-- "items": {
-- "type": "string",
-- "pattern": "^[a-z][a-z0-9-]*:[a-z0-9.-]+$"
-- },
-- "description": "What the machine offers, in the platform.resource-offer@1 capability form (e.g. node:http, worker:ffmpeg)."
-- },
-- "agent_version": {
-- "type": "string",
-- "maxLength": 40,
-- "description": "Version of the node agent that reported."
-- },
-- "updated_at": {
-- "type": "string",
-- "format": "date-time"
-- }
-- },
-- "examples": [
-- {
-- "node_id": "node-1",
-- "cpu": {
-- "cores": 8,
-- "threads": 16
-- },
-- "arch": "x86_64",
-- "memory_mb": 16384,
-- "gpu": {
-- "model": "L4",
-- "count": 1,
-- "vram_mb": 24576
-- },
-- "storage": {
-- "capacity_gb": 500,
-- "available_gb": 300,
-- "kind": "ssd"
-- },
-- "network": {
-- "ingress_mbps": 1000,
-- "egress_mbps": 1000
-- },
-- "regions": [
-- "us-west"
-- ],
-- "tags": [
-- "gpu",
-- "edge"
-- ],
-- "costs": {
-- "per_hour_usd": 0.2,
-- "egress_per_gb_usd": 0.01
-- }
-- }
-- ]
--}
ddiff --git a/contracts/platform/resource-offer.v1.json b/contracts/platform/resource-offer.v1.json
iindex 764cc3a9..5104d15e 100644
---- a/contracts/platform/resource-offer.v1.json
++++ b/contracts/platform/resource-offer.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/platform/resource-offer.v1.json",
"title": "ResourceOffer",
-- "description": "What one node or external provider can run right now (roadmap WS-Z9, decision 43): capabilities, multidimensional capacity, measured latencies, health and pricing. First-party only: it carries capacity the public node registry (network.node@1) deliberately leaves out.",
++ "description": "What one node or external provider can run right now (roadmap WS-Z9, decision 43): capabilities, multidimensional capacity, measured latencies, health and pricing. First-party only: it carries capacity the public node registry (network.link@1) deliberately leaves out.",
"type": "object",
"additionalProperties": false,
"required": [
@@@ -33,7 +33,7 @@
},
"node_id": {
"type": "string",
-- "description": "network.node@1 id, for kind node"
++ "description": "network.link@1 id, for kind node"
},
"provider": {
"type": "string",
@@@ -71,7 +71,7 @@
"type": "string",
"pattern": "^[a-z][a-z0-9-]*:[a-z0-9.-]+$"
},
-- "description": "e.g. node:http, worker:browser, worker:ffmpeg, worker:ai-gpu, events:gateway, events:durable, object:r2. Kind harness: task:edit, task:review, task:browse, task:run, task:test and task:plan for the task kinds it takes (its detail's task_capabilities), and harness:mcp, harness:resume, harness:host-access and harness:long-autonomy for its detail's capabilities that are true. The `worker:<class>` names that map to platform.runtime-class@1 values are reserved for OpenVibe Link's job worker, one per class: worker:function, worker:code, worker:browser, worker:linux, worker:desktop and worker:gpu ($defs.reservedWorkerCapabilities). A node advertises the name of each class it runs (Node's status.capabilities.worker); every other `worker:` name (worker:ffmpeg, worker:ai-gpu) is an ordinary capability name. Beside them, a kind harness names `runtime:<class>` for each platform.runtime-class@1 class it can run (runtime:function, runtime:code, runtime:browser, runtime:linux, runtime:desktop, runtime:gpu); these are ordinary capability names, not reserved, and OpenVibe.Codes already advertises the classes it offers."
++ "description": "e.g. node:http, worker:browser, worker:ffmpeg, worker:ai-gpu, events:gateway, events:durable, object:r2. Kind harness: task:edit, task:review, task:browse, task:run, task:test and task:plan for the task kinds it takes (its detail's task_capabilities), and harness:mcp, harness:resume, harness:host-access and harness:long-autonomy for its detail's capabilities that are true. The `worker:<class>` names that map to platform.runtime-class@1 values are reserved for OpenVibe Link's job worker, one per class: worker:function, worker:code, worker:browser, worker:linux, worker:desktop and worker:gpu ($defs.reservedWorkerCapabilities). A Link advertises the name of each class it runs (Link's status.capabilities.worker); every other `worker:` name (worker:ffmpeg, worker:ai-gpu) is an ordinary capability name. Beside them, a kind harness names `runtime:<class>` for each platform.runtime-class@1 class it can run (runtime:function, runtime:code, runtime:browser, runtime:linux, runtime:desktop, runtime:gpu); these are ordinary capability names, not reserved, and OpenVibe.Codes already advertises the classes it offers."
},
"capacity": {
"type": "object",
ddiff --git a/contracts/platform/usage-sample.v1.json b/contracts/platform/usage-sample.v1.json
iindex ebf7ba40..2c8752ef 100644
---- a/contracts/platform/usage-sample.v1.json
++++ b/contracts/platform/usage-sample.v1.json
@@@ -146,7 +146,7 @@
"quantity": 1,
"unit": "s",
"at": "2026-10-02T10:00:00.250Z",
-- "source": "openvibe-node.worker"
++ "source": "openvibe-link.worker"
}
]
}
ddiff --git a/contracts/run/job-read-result.v1.json b/contracts/run/job-read-result.v1.json
iindex d2e4ac43..60a5fb16 100644
---- a/contracts/run/job-read-result.v1.json
++++ b/contracts/run/job-read-result.v1.json
@@@ -132,7 +132,7 @@
"required": ["node", "provider", "region"],
"properties": {
"node": { "type": "string", "minLength": 1, "maxLength": 128, "description": "The worker's device id (the usage reading's `node`)." },
-- "provider": { "type": ["string", "null"], "maxLength": 128, "description": "The offer's provider (the usage reading's `provider`); null for a first-party node." },
++ "provider": { "type": ["string", "null"], "maxLength": 128, "description": "The offer's provider (the usage reading's `provider`); null for a first-party machine." },
"region": { "type": ["string", "null"], "maxLength": 64 }
}
},
ddiff --git a/contracts/watch/watch.v1.json b/contracts/watch/watch.v1.json
iindex d5b7b263..81b9be0f 100644
---- a/contracts/watch/watch.v1.json
++++ b/contracts/watch/watch.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/watch/watch.v1.json",
"title": "Watch",
-- "description": "watch.watch@1: a user-defined persistent observation (plan T18). It names a source (a push event, a webhook, a conditional HTTP URL, a feed or API, a local Node probe, or an expensive Run check), how a value is extracted, how it is compared with the last observation, the condition that fires, the actions it wakes, its budget and its retention. Carrier preference is binding: an event or webhook source is used when one exists, then ETag/Last-Modified, then a feed, then an API, and browser polling through Run only when none works. A price or stock is 'as of' the observation, null when it stated none.",
++ "description": "watch.watch@1: a user-defined persistent observation (plan T18). It names a source (a push event, a webhook, a conditional HTTP URL, a feed or API, a local Link probe, or an expensive Run check), how a value is extracted, how it is compared with the last observation, the condition that fires, the actions it wakes, its budget and its retention. Carrier preference is binding: an event or webhook source is used when one exists, then ETag/Last-Modified, then a feed, then an API, and browser polling through Run only when none works. A price or stock is 'as of' the observation, null when it stated none.",
"type": "object",
"additionalProperties": false,
"required": [
ddiff --git a/contracts/website/share-connect-request.v1.json b/contracts/website/share-connect-request.v1.json
iindex e0b352ed..fa7a6542 100644
---- a/contracts/website/share-connect-request.v1.json
++++ b/contracts/website/share-connect-request.v1.json
@@@ -17,7 +17,7 @@
},
"node_id": {
"type": "string",
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
"description": "Must equal the node principal in the Network token; a mismatch is refused and audited."
},
"tunnel_token": {
ddiff --git a/contracts/website/share-create-request.v1.json b/contracts/website/share-create-request.v1.json
iindex fd2190e0..26439f45 100644
---- a/contracts/website/share-create-request.v1.json
++++ b/contracts/website/share-create-request.v1.json
@@@ -52,8 +52,8 @@
},
"node_id": {
"type": "string",
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
-- "description": "Optional: bind the share to this Node principal now. Omit to bind it when the first tunnel connects."
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Optional: bind the share to this Link principal now. Omit to bind it when the first tunnel connects."
},
"region": {
"type": "string",
ddiff --git a/contracts/website/share.v1.json b/contracts/website/share.v1.json
iindex a44aafd5..4bf56ca1 100644
---- a/contracts/website/share.v1.json
++++ b/contracts/website/share.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/website/share.v1.json",
"title": "WebsiteShare",
-- "description": "website.share@1: one Share on OpenVibe.Website Connect, a local port published at a stable hostname through the edge relay. The record says who owns it, which project scopes it, which Node tunnel serves it, which local target it may reach, which policy version the edge enforces and where it is in its life (draft, review, active, paused, revoked). The tunnel token, the request bodies the inspector captures and the policy's password hash are never part of this document.",
++ "description": "website.share@1: one Share on OpenVibe.Website Connect, a local port published at a stable hostname through the edge relay. The record says who owns it, which project scopes it, which Link tunnel serves it, which local target it may reach, which policy version the edge enforces and where it is in its life (draft, review, active, paused, revoked). The tunnel token, the request bodies the inspector captures and the policy's password hash are never part of this document.",
"type": "object",
"additionalProperties": false,
"required": [
@@@ -44,7 +44,7 @@
"string",
"null"
],
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
"description": "The Network node principal expected to serve the tunnel; null until one is bound."
},
"name": {
@@@ -122,7 +122,7 @@
"null"
],
"format": "date-time",
-- "description": "The last successful Node liveness check; null until the first tunnel."
++ "description": "The last successful Link liveness check; null until the first tunnel."
},
"created_at": {
"type": "string",
ddiff --git a/docs/ESTATE.md b/docs/ESTATE.md
iindex 1f2637a9..c60843a3 100644
---- a/docs/ESTATE.md
++++ b/docs/ESTATE.md
@@@ -37,7 +37,7 @@ service id); **Runtime** is `STATUS.json.runtime`; **Database** is inferred from
| OpenVibe.MediaHub | Video | video.*, pics.*, download.*, mediahub.* | node 22 | PostgreSQL | openvibe.video, openvibe.download, openvibe.pics, openvibe.network | contracts v0.222.0, sdk v0.67.0, shared v4.17.0 | deployed | live | W20 |
| OpenVibe.Network | Network | identity.*, network.*, theme.*, notification.* | node 22 | PostgreSQL | openvibe.network, auth.openvibe.network, api.openvibe.network, admin.openvibe.network, my.openvibe.network, themes.openvibe.network | contracts v0.222.0, sdk v0.67.0, shared v4.17.0 | deployed | live | — |
| OpenVibe.News | News | news.* | node 22 | PostgreSQL | openvibe.news | contracts v0.222.0, sdk v0.67.0, shared v4.17.0, publishing v1.4.1 | deployed | live | W17 |
--| OpenVibe.Node | — | — | go 1.24 (one static binary per platform) | none | — | — | not deployed | — | T15 |
++| OpenVibe.Link | — | — | go 1.24 (one static binary per platform) | none | — | — | not deployed | — | T15 |
| OpenVibe.Play | Play | play.* | node 22 | PostgreSQL | play.openvibe.network | contracts v0.222.0, sdk v0.67.0, shared v4.17.0 | deployed | live | T8 |
| OpenVibe.Publishing | OpenVibe.Publishing | publishing | node >=20 | none | — | contracts v0.222.0, sdk v0.67.0, shared v4.17.0 | not deployed | library | W15 |
| OpenVibe.Quest | Quest | quest.* | node 22 | PostgreSQL | openvibe.quest | contracts v0.222.0, sdk v0.67.0, shared v4.17.0, publishing v1.4.0 | deployed | live | W20 |
ddiff --git a/docs/adr/ADR-027-tools-platform-api.md b/docs/adr/ADR-027-tools-platform-api.md
iindex f8a9848e..911f4301 100644
---- a/docs/adr/ADR-027-tools-platform-api.md
++++ b/docs/adr/ADR-027-tools-platform-api.md
@@@ -66,7 +66,7 @@ OpenVibe.Tools that day found:
### Browser-only tools may get server engines
--A pure transform whose engine also runs in Node gets an opt-in server engine for the API: JSON,
++A pure transform whose engine also runs in Link gets an opt-in server engine for the API: JSON,
minify and beautify, case, slug, hash, base64 and the like. Its descriptor then says
`execution: client` and `api: true`. The page stays browser-only, so "nothing leaves your browser"
stays true for people using the page. A tool without such an engine says `api: false`.
ddiff --git a/docs/adr/ADR-034-platform-north-star.md b/docs/adr/ADR-034-platform-north-star.md
iindex b206456f..681dd0ba 100644
---- a/docs/adr/ADR-034-platform-north-star.md
++++ b/docs/adr/ADR-034-platform-north-star.md
@@@ -19,7 +19,7 @@ What exists already maps onto the primitives of a cloud:
| Developer surface | Codes (portal, apps, playgrounds, docs), openvibe-sdk v0.13, Contracts v0.75 | ADR-002, ADR-008 |
| Extensions | Mods with their own principals, budgets and grants; Tools platform API | ADR-013, ADR-027 |
--Everything runs on one host: about 25 Node services, one SQLite database each, as systemd units. Measured load is about 1% of what SQLite handles (ADR-007 amendment, 2026-09-26). Deploys are manual `ovhost deploy` runs.
++Everything runs on one host: about 25 Link services, one SQLite database each, as systemd units. Measured load is about 1% of what SQLite handles (ADR-007 amendment, 2026-09-26). Deploys are manual `ovhost deploy` runs.
At a much larger size, the database engine and the orchestrator are not what is expensive to change. The expensive things are **boundaries**: how tenants are separated, how resources are named, how access is decided, what the API contract is, and how usage is counted. Each of those touches every row, every route and every event, so changing it late means migrating everything. Today they are cheap to set.
ddiff --git a/docs/adr/ADR-036-run-execution-authority.md b/docs/adr/ADR-036-run-execution-authority.md
iindex 6e3484ce..4750801b 100644
---- a/docs/adr/ADR-036-run-execution-authority.md
++++ b/docs/adr/ADR-036-run-execution-authority.md
@@@ -9,7 +9,7 @@ isolation belongs to Stage C) and ADR-046 (offers, requirements, placement, sign
- **Plan T14** (lines 559–583): *"Finish line: any workload — a function, code, a browser session, a Linux or desktop
machine, a GPU job — runs in a sandbox with metering, limits, cancellation and streaming, placed by the Fabric on
-- the cheapest capability that works; the Node runtime runs the same workloads on official servers and on users' own
++ the cheapest capability that works; the Link runtime runs the same workloads on official servers and on users' own
machines under their trust policy."* And: *"**Run is not a Host detail.** Host deploys persistent applications; Run
executes isolated jobs/environments. They can share machines; they are separate authorities (ADR-036 rewritten
around this expanded purpose)."*
@@@ -77,16 +77,16 @@ Run states `platform.workload-requirements@1` and reads the `platform.placement-
Run writes no placer of its own (ADR-046: "A service states requirements and reads results; it does not rank
offers."). The plan's §2.1.11 cross-reference is the Fabric section as written in ADR-046/T1, not a separate scheme.
--**Node descriptor → offers.** When Run turns an OpenVibe Link into Fabric offers it applies the mapping the Node
++**Link descriptor → offers.** When Run turns an OpenVibe Link into Fabric offers it applies the mapping the Link
protocol already fixes (OpenVibe Link `docs/protocol.md:310–321`):
--| Node descriptor field | Fabric offer | Example |
++| Link descriptor field | Fabric offer | Example |
| --- | --- | --- |
| `capabilities.worker.runtime_classes` | `resource-offer.capabilities` item `worker:<class>` | `worker:function`, `worker:code` |
| `capabilities.worker.max_jobs` | `runtime-offer.limits.concurrency` | 1 |
| `capabilities.worker.max_ttl_ms` | `runtime-offer.limits.duration_seconds` | 600 |
| device `node_id` / cell | `resource-offer.node_id` / `resource-offer.cell` | `node_01H…` / `wnam-1` |
--| worker trust (local policy) | `resource-offer.trust` | `user-owned` for a user's own Node |
++| worker trust (local policy) | `resource-offer.trust` | `user-owned` for a user's own Link |
### 4. The cost ladder
@@@ -130,7 +130,7 @@ Every job, on every adapter, is held to the same guarantees (plan T14, lines 572
1. **Isolation mechanism.** The plan requires more than a container for hostile arbitrary code but names no
mechanism. Micro-VM (Firecracker/Kata), a userspace kernel (gVisor) and namespace/seccomp sandboxes are all open;
the Link worker's current `function` isolation is namespaces plus a seccomp allowlist.
--2. **Snapshot/resume backend.** Whether a resumed environment's state lives in Media, a Node-local disk, or a
++2. **Snapshot/resume backend.** Whether a resumed environment's state lives in Media, a Link-local disk, or a
provider's own snapshot is open.
3. **Idle-suspend policy.** Who decides to suspend — Run, the node's local policy, or a placement objective — is
open.
@@@ -147,6 +147,6 @@ Every job, on every adapter, is held to the same guarantees (plan T14, lines 572
`linux`, `desktop` and `gpu` are reserved names with no worker.
- Cells are a single host (`cell = wnam-1`) today; O24's second physical cell, the WireGuard mesh and geo placement
are not built (plan T14, lines 580–581).
--- Per-second metering is proven only for Node jobs; no Billing consumer reads Run usage on `origin/main` yet.
++- Per-second metering is proven only for Link jobs; no Billing consumer reads Run usage on `origin/main` yet.
- The provider adapters above are the plan's list, not deployed adapters; no managed-browser, external-sandbox or
external-GPU offer producer exists.
ddiff --git a/docs/adr/ADR-043-bot-devices-and-control.md b/docs/adr/ADR-043-bot-devices-and-control.md
iindex e024476e..1fe8d6ea 100644
---- a/docs/adr/ADR-043-bot-devices-and-control.md
++++ b/docs/adr/ADR-043-bot-devices-and-control.md
@@@ -39,7 +39,7 @@ a Cozmo-based robot).
robot and reaches it over the robot's own link (Cozmo first through PyCozmo; later Vector, Tello, Sphero, LEGO); a
bridge joins the robot's network on one interface and reaches OpenVibe on another. **Server-side**: Bot itself
connects to ONVIF/RTSP cameras (credentials by secret reference, never in the profile). The agent is a Python 3 package
-- (the Pi, PyCozmo and the kit libraries are Python), installed by `curl -fsSL https://openvibe.bot/install | sh` into a
++ (the Pi, PyCozmo and the kit libraries are Python), installed by `curl -fsSL https://openvibe.link/install | sh` into a
virtual environment with a systemd unit and the hardware watchdog.
4. **Control runs over one outbound WebSocket per device; video over WHIP.** The device dials Bot
(`wss://openvibe.bot/device`), authenticates with its credential, and keeps one connection for commands, telemetry and
ddiff --git a/docs/adr/ADR-044-actor-runtime.md b/docs/adr/ADR-044-actor-runtime.md
iindex 83bfab14..4716728e 100644
---- a/docs/adr/ADR-044-actor-runtime.md
++++ b/docs/adr/ADR-044-actor-runtime.md
@@@ -6,7 +6,7 @@ the first new adapters (OpenCode, Antigravity CLI), capability-aware routing, vi
(headless browser + virtual desktop, screenshots)" ahead of "ADR-044 (task contract, adapter interface, modes,
signals, verification, trust)", and the engine's harness work is paused where it stands. Contract: `platform.task@1`.
Builds on ADR-034 §2 (projects and resource names), §5 (control and data plane) and §9 (metering), on ADR-046 (the
--placer, its objectives and the five trust classes), on ADR-012 (one Billing authority) and on ADR-043 (Node control
++placer, its objectives and the five trust classes), on ADR-012 (one Billing authority) and on ADR-043 (Link control
levels and the local kill switch). For the owner's review.
## Context and current evidence
@@@ -112,7 +112,7 @@ ADR-046 §4's five classes apply unchanged: `first-party`, `user-owned`, `partne
data on OpenVibe — `first-party`, and a `user-owned` node only when the deployment allows it — which the plan
describes as "only agents that keep your data on OpenVibe: its own runtime and open models on its hardware". Local
execution on a person's own machine goes through OpenVibe Link under ADR-043's control levels (Observe / Ask /
--Trusted / Full control), a local indicator and a kill switch the owner holds; Node's local policy always overrides a
++Trusted / Full control), a local indicator and a kill switch the owner holds; Link's local policy always overrides a
cloud command (plan T14).
### 7. Progress, cancel and webhooks
@@@ -146,7 +146,7 @@ Actor plans and routes; it does not become an authority. The plan's own list of
| Coding work | Codes (the one coding-harness router) |
| Persistent conditions | Watch |
| Cloud execution | Run (`platform.job@1` / `platform.job-frame@1`) |
--| Local execution and computer control | Node (ADR-043 control levels, kill switch) |
++| Local execution and computer control | Link (ADR-043 control levels, kill switch) |
| Resource control | Services (`common.resource-control-request@1`, ADR-048) |
| Artifacts | Media |
| Robots | Bot |
ddiff --git a/docs/adr/ADR-045-node-universal-runtime.md b/docs/adr/ADR-045-node-universal-runtime.md
iindex 654aa371..6c4875ed 100644
---- a/docs/adr/ADR-045-node-universal-runtime.md
++++ b/docs/adr/ADR-045-node-universal-runtime.md
@@@ -1,26 +1,26 @@
# ADR-045: The Link is OpenVibe's universal runtime
--**Status:** Proposed 2026-10-05 (plan §8: *"ADR-045 (Node; **revise: Node is the universal runtime, not a
++**Status:** Proposed 2026-10-05 (plan §8: *"ADR-045 (Link; **revise: Link is the universal runtime, not a
Bot/Actor helper**)"*; plan T14, lines 575–579). For the owner's review. Builds on ADR-043 (Bot — devices, pairing,
control and safety), ADR-046 §5 (the `worker:<class>` capability names) and ADR-048 (OVRN and authority boundaries).
--ADR-043 stays the robot/device half of the same Node; this record is the runtime half, and amends ADR-043's credential
++ADR-043 stays the robot/device half of the same Link; this record is the runtime half, and amends ADR-043's credential
paragraph: Network owns the node/device principal and the pairing credential, Bot keeps the robot binding and control
link.
## Context and current evidence
--- **The plan revises ADR-045.** Plan §8 (line 986) calls ADR-045 written but to be revised: *"Node is the universal
++- **The plan revises ADR-045.** Plan §8 (line 986) calls ADR-045 written but to be revised: *"Link is the universal
runtime, not a Bot/Actor helper"*. `origin/main` has no ADR-045 file in any repository, so the revision is unstated
in the contract repository.
--- **What the plan says the Link is.** Plan T14, lines 575–579: *"**Node runtime** (ADR-045 revised: Node is the
++- **What the plan says the Link is.** Plan T14, lines 575–579: *"**Link runtime** (ADR-045 revised: Link is the
universal runtime, not a Bot/Actor helper). **Core built on `origin/main`:** one signed binary, pairing, control
link, WHIP publisher, plugin runtime, the Adeept and Cozmo plugins, installer and CI. Still to add for its other
roles: computer control (terminal, files, screen, input, browser) under control levels, local kill switch and
recording (Actor's need, T17); execution-worker capability (Run's need); cache/storage/delivery capability (Media's
need, T4). **Local policy always overrides cloud commands.**"*
--- **Products bind capabilities, not device code.** Plan lines 321–324: *"Node identity is a Network principal.
-- Network owns the node/device principal and pairing credentials. Services manages the resource; Node presents
-- capabilities (T14); Bot binds robot resources to Node capabilities (T15); Actor binds computer control (T17); Run
++- **Products bind capabilities, not device code.** Plan lines 321–324: *"Link identity is a Network principal.
++ Network owns the node/device principal and pairing credentials. Services manages the resource; Link presents
++ capabilities (T14); Bot binds robot resources to Link capabilities (T15); Actor binds computer control (T17); Run
binds execution workers (T14); Media binds cache/storage (T4). Bot keeps robots, robot profiles, operators, command
leases and physical safety — not universal device identity."*
- **What the code already does** (OpenVibe Link, `origin/main`): one Go static binary per platform — pairing
@@@ -35,20 +35,20 @@ link.
- **The capability names are already agreed.** ADR-046 §5 reserves `worker:function`, `worker:code`,
`worker:browser`, `worker:linux`, `worker:desktop` and `worker:gpu` for the Link job worker, one per
`platform.runtime-class@1` class, in `platform.resource-offer@1.$defs.reservedWorkerCapabilities`.
--- **The control levels are plan wording.** Plan lines 745–747: *"each Node has a control level set on the device
++- **The control levels are plan wording.** Plan lines 745–747: *"each Link has a control level set on the device
(Observe / Ask / Trusted / Full control), a local indicator, a local kill switch, optional container/VM
confinement, replayable transcripts and file changes, and local secrets used through handles."* Of that, the local
kill switch (the persisted stop latch) exists; the four named levels and recording do not.
--- **Media's need is stated too.** Plan lines 366–368: *"Node is a storage/delivery candidate too
++- **Media's need is stated too.** Plan lines 366–368: *"Link is a storage/delivery candidate too
(`media.cache`, `storage.hot`, prepaid disk and bandwidth), so the placement engine can use unused capacity on
-- already-paid OpenVibe nodes before buying CDN traffic."*
++ already-paid OpenVibe Links before buying CDN traffic."*
## Decision
--### 1. One Node, the universal runtime
++### 1. One Link, the universal runtime
The Link is OpenVibe's universal runtime — one agent for a person's computers, servers, Raspberry Pis, phones and
--robots. It is **not** a Bot helper or an Actor helper: Bot, Actor, Run and Media are all clients of the same Node,
++robots. It is **not** a Bot helper or an Actor helper: Bot, Actor, Run and Media are all clients of the same Link,
and each binds the capabilities it needs. ADR-043 remains the robot/device half (robots, robot profiles, operators,
command leases and physical safety); this ADR is the runtime half. The one split this record draws inside ADR-043 is
the credential: **Network owns the node/device principal and issues the pairing credential; Bot keeps the robot
@@@ -61,7 +61,7 @@ stands.
A role is **a capability the Link advertises plus local configuration**, never a second agent or a second install.
- The Link is a Network principal (its node/device principal and pairing credential are Network's), and its resource
is named by OVRN (ADR-048). Network owns identity and pairing; Bot keeps the robot binding and the control link
-- (amending ADR-043's credential paragraph); Node presents capabilities.
++ (amending ADR-043's credential paragraph); Link presents capabilities.
### 3. Execution worker (Run's need)
@@@ -102,7 +102,7 @@ stands.
## Alternatives considered
--- **Node as a Bot-only helper (the plan's earlier framing):** rejected by the revision. Bot, Actor, Run and Media
++- **Link as a Bot-only helper (the plan's earlier framing):** rejected by the revision. Bot, Actor, Run and Media
would each grow a device agent, and an owner would install and pair several.
- **A separate agent per product (an Actor agent, a Run worker, a Media cache daemon):** rejected. One binary, one
control link and one local policy are what make a single device safe to reason about; capabilities distinguish the
@@@ -128,7 +128,7 @@ stands.
2. **One principal or many.** Whether a Link serving several products/projects is one Network principal with scoped
grants or one principal per project is Network's decision and is open (ADR-048 OVRN).
3. **Cache/storage contracts.** Which offer contracts carry the Link's cache/storage role — the existing
-- `platform.storage-offer@1` / `platform.delivery-offer@1` or a Node-specific one — and whether placement policy
++ `platform.storage-offer@1` / `platform.delivery-offer@1` or a Link-specific one — and whether placement policy
stays Media's, is open.
4. **Other users' workloads.** Whether a Link may run other users' jobs or serve other users' computer control, or
only its owner's, mirrors ADR-046's `user-owned` question and is open.
@@@ -140,7 +140,7 @@ stands.
- The release binary is not code-signed and has no self-update: releases publish `SHA256SUMS` only.
- The four control-level names and the local indicator are plan wording; the code today has a local stop latch, not
the four levels.
--- No Actor, Run or Media consumer binds these Node capabilities on `origin/main`; Run is a placeholder service
++- No Actor, Run or Media consumer binds these Link capabilities on `origin/main`; Run is a placeholder service
(ADR-036).
- The Link worker is reached over the Bot control link today; it is not yet an offer producer, because the Run that
would convert its descriptor is not built.
ddiff --git a/docs/adr/ADR-046-universal-adaptive-fabric.md b/docs/adr/ADR-046-universal-adaptive-fabric.md
iindex 590a31f4..817ba2d0 100644
---- a/docs/adr/ADR-046-universal-adaptive-fabric.md
++++ b/docs/adr/ADR-046-universal-adaptive-fabric.md
@@@ -77,7 +77,7 @@ no existing caller's eligible set, and the SDK must not add it to its default li
An OpenVibe Link that runs jobs advertises one `worker:<class>` capability in its `resource-offer.capabilities` for
each `platform.runtime-class@1` class it runs: `worker:function` (a declared function artifact) and `worker:code`
--(submitted code in a sandbox) today, and `worker:browser`, `worker:linux`, `worker:desktop` and `worker:gpu` as Node
++(submitted code in a sandbox) today, and `worker:browser`, `worker:linux`, `worker:desktop` and `worker:gpu` as Link
gains them. These six names are reserved for the Link job worker (`$defs.reservedWorkerCapabilities` in
`platform.resource-offer@1`); every other `worker:` name (`worker:ffmpeg`, `worker:ai-gpu`) is an ordinary capability.
A requirement asks for a class by listing the same name in its `capabilities`.
ddiff --git a/docs/adr/ADR-057-website-connect.md b/docs/adr/ADR-057-website-connect.md
iindex 1ae8caba..6c7a95db 100644
---- a/docs/adr/ADR-057-website-connect.md
++++ b/docs/adr/ADR-057-website-connect.md
@@@ -18,12 +18,12 @@ authority per resource) and ADR-056 (project secrets: write-only, used by name).
verdict is one path per pillar: build a focused relay on `quic-go` for Share, adopt headscale + `tsnet` with a
tenant boundary for Connect, build the policy/abuse layer on top of Cloudflare and Network identity, and extend
Events for hooks. §3.3 and §3.4 name what to build and what not to reuse.
--- **The data plane is Go; the control plane is Node.** The Link agent is Go (ADR-045). QUIC, HTTP/3, datagrams,
-- half-close and backpressure are native there and awkward in Node.js. The control API can stay Node/Express, like
++- **The data plane is Go; the control plane is Link.** The Link agent is Go (ADR-045). QUIC, HTTP/3, datagrams,
++ half-close and backpressure are native there and awkward in Node.js. The control API can stay Link/Express, like
every other network service. The edge is a separate, exposed, bandwidth-heavy tier and must not share a host with
the control plane (research §4.1).
--- **Node already has a principal.** Network owns node principals (`nod_…`, `ids.newId('node')`) and pairs machines;
-- `network.node.revoked` already exists in Network's manifest. Connect must reuse that principal, not mint a second
++- **Link already has a principal.** Network owns node principals (`lnk_…`, `ids.newId('link')`) and pairs machines;
++ `network.link.revoked` already exists in Network's manifest. Connect must reuse that principal, not mint a second
machine identity. Network also owns OIDC, project membership and grants; Domains owns DNS proposals; Events owns
durable delivery and v2 signatures (ADR-042); Report and Billing consume usage.
- **Space already uses `<name>.openvibe.website`.** A single-label name is in use for tenant sites. A new share cannot
@@@ -46,19 +46,19 @@ authority per resource) and ADR-056 (project secrets: write-only, used by name).
## Decision
1. **Share is a focused Go edge relay, `openvibe-edge`, plus a Link `connect` core in OpenVibe Link.** The relay is
-- built on `quic-go` (MIT): one QUIC connection per Node, ALPN `ovconnect/1`, TLS 1.3, streams for HTTP/WebSocket/TCP,
++ built on `quic-go` (MIT): one QUIC connection per Link, ALPN `ovconnect/1`, TLS 1.3, streams for HTTP/WebSocket/TCP,
datagrams for UDP, and an HTTP/2 + WebSocket fallback on 443 for networks that block UDP. It routes by SNI/Host to a
`share_id` from Connect's name registry, never from a user-controlled header, checks the signed policy, and then
-- opens a stream to the Node. It does not own users, shares, policies or audit. No frp, sish, zrok, Pangolin or
++ opens a stream to the Link. It does not own users, shares, policies or audit. No frp, sish, zrok, Pangolin or
cloudflared code is forked or embedded.
--2. **The control API is a new Node service, OpenVibe.Connect.** It owns shares, share policies, devices, mesh policies,
++2. **The control API is a new Link service, OpenVibe.Connect.** It owns shares, share policies, devices, mesh policies,
hooks, the name registry, abuse holds, audit and quota counters in `ov_connect` PostgreSQL; it mints tunnel tokens
and signs the short-lived policy cache the edge reads. It is a peer of Domains, Events, Network, Report and Billing,
not a module of Bot. Bot keeps robots, the device link and command safety (ADR-043); Connect keeps shares, the mesh,
policies and hooks.
3. **Connect mesh is headscale + embedded `tsnet`, with a tenant boundary that must be proven before consumer
multi-tenancy.** v1 maps one workspace to one headscale tenant: a person's own network for a personal account, a
-- project's network for a team. Node embeds `tsnet` (userspace, no root, no TUN) for the private mesh; the public
++ project's network for a team. Link embeds `tsnet` (userspace, no root, no TUN) for the private mesh; the public
ingress stays Share. headscale does not implement Funnel or Serve and Connect must not promise Funnel-compatible
behaviour. The tenant-isolation proof must cover: no cross-tenant MagicDNS resolution, no cross-tenant route or
exit-node leak, no shared pre-auth key, and revocation in one tenant leaving the others untouched. If the proof
@@@ -68,10 +68,10 @@ authority per resource) and ADR-056 (project secrets: write-only, used by name).
is for PCs, NAS and servers. DERP is self-hosted (`derper`) so relay traffic stays on OpenVibe infrastructure;
Live's TURN is for WebRTC and is not a WireGuard relay.
4. **Identity and the tunnel handshake use two credentials, both checked.** The Link presents a Network
-- client-credentials token whose subject is its node principal (`node:nod_…`) and whose audience is
++ client-credentials token whose subject is its node principal (`link:lnk_…`) and whose audience is
`openvibe.website`; the request body is `website.share-connect-request@1` with the `share_id`, the same `node_id`
and a short-lived, single-use, share-scoped `tunnel_token` minted by Connect. The edge validates the token against
-- Network's JWKS, requires the subject to equal `node:<node_id>` (for example `node:nod_…`), redeems the
++ Network's JWKS, requires the subject to equal `link:<node_id>` (for example `link:lnk_…`), redeems the
tunnel token with Connect exactly once and binds the connection to that share. One credential is never
enough. The tunnel token is sent once over TLS and is
never stored, logged, audited or published in an event. OIDC at the share host uses Authorization Code + PKCE
@@@ -79,10 +79,10 @@ authority per resource) and ADR-056 (project secrets: write-only, used by name).
`share_id`, `sub` and `policy_version`.
`POST /api/v1/shares` returns only `website.share@1`; its response has no tunnel token. The Link obtains the
short-lived, single-use token through a separate authenticated credential exchange before connecting. CON-5
-- defines that exchange's endpoint, request and response schemas, authorization, and delivery to the Node.
++ defines that exchange's endpoint, request and response schemas, authorization, and delivery to the Link.
5. **Revocation is immediate and has one owner per trigger.** `DELETE /api/v1/shares/:id` writes
`website.share.revoked`, closes the edge route and tunnel, removes the DNS route if Connect added one and makes the
-- public host return 410 within the 5-second budget. `network.node.revoked` closes every share bound to that node.
++ public host return 410 within the 5-second budget. `network.link.revoked` closes every share bound to that node.
A policy change bumps `policy_version`, so the edge's cached signed policy is stale on the next request. Removing a
device revokes its mesh key at the control plane. No capability, route or cached policy survives a revoked share.
6. **Policy is enforced at the edge, authored and audited by Connect, and fronted by Cloudflare.** Cloudflare
@@@ -106,8 +106,8 @@ authority per resource) and ADR-056 (project secrets: write-only, used by name).
Events to retry. Events produces `events.hook.received|delivered|dead`; Connect writes none of them. No smee or
Hookdeck is adopted.
8. **The SDK stays MIT and thin.** `openvibe-sdk` (MIT) gets at most a thin `openvibe-sdk/connect` client that talks
-- to the local Node daemon over HTTP/Unix socket and to the control API over HTTP. The QUIC data plane, the mesh
-- (`tsnet`) and the policy engine stay in the AGPL Node/Connect/edge code. No AGPL/GPL tunnel code is copied into the
++ to the local Link daemon over HTTP/Unix socket and to the control API over HTTP. The QUIC data plane, the mesh
++ (`tsnet`) and the policy engine stay in the AGPL Link/Connect/edge code. No AGPL/GPL tunnel code is copied into the
SDK or `openvibe-shared` (MIT). This keeps the permissive client boundary and the copyleft in the agent.
9. **Naming and the first proof use one label.** The first proof is the owner's agent portal at
`https://agents-<owner>.openvibe.website` (single label), behind OpenVibe sign-in, opened on a phone over 5G with a
@@@ -127,9 +127,9 @@ authority per resource) and ADR-056 (project secrets: write-only, used by name).
| Concern | Owner | Not this service |
|---|---|---|
--| Share record, name registry, policy, tunnel token, inspector metadata, audit | OpenVibe.Connect (Node control API, `ov_connect`) | the edge keeps active session state and a signed policy cache only |
++| Share record, name registry, policy, tunnel token, inspector metadata, audit | OpenVibe.Connect (Link control API, `ov_connect`) | the edge keeps active session state and a signed policy cache only |
| Public QUIC/HTTP/WS/SSE/TCP/UDP ingress, routing, edge policy check, rate limits | `openvibe-edge` (Go, `quic-go`) | it does not own users, shares, policies, audit or hooks |
--| Node tunnel, local forwarding, `ov share`, `tsnet` | OpenVibe Link (`internal/connect`) | it does not decide policy or open arbitrary hosts |
++| Link tunnel, local forwarding, `ov share`, `tsnet` | OpenVibe Link (`internal/connect`) | it does not decide policy or open arbitrary hosts |
| Account, node principal, OIDC, grants, revocation event | OpenVibe.Network | Connect never mints identity |
| DNS proposal, TXT/CNAME, certificate path | OpenVibe.Domains | Connect never writes DNS or holds a provider credential |
| Durable event store, v2 signature, retries, DLQ, replay | OpenVibe.Events | Connect creates the subscription; the edge does not queue |
@@@ -212,7 +212,7 @@ capability id is concrete; `website.abuse.review` stays the moderator queue. Non
- `research-2026-10-29/website-connect.md` §1.1–1.7, §3.2–3.4, §4.1–4.13 (checked 2026-10-29).
- `OpenVibe_New_Domains_Plan_2026-10-28.md` §5.9b and the CON-1…CON-21 ledger.
--- ADR-053 (Website/Host boundary), ADR-045 (Node runtime), ADR-042 (events fabric), ADR-043 (Bot devices), ADR-048
++- ADR-053 (Website/Host boundary), ADR-045 (Link runtime), ADR-042 (events fabric), ADR-043 (Bot devices), ADR-048
(control plane), ADR-056 (project secrets), ADR-028 (expand/migrate/contract).
- `manifests/products/openvibe.website.json` (the distinct product/redirect manifest, preserved), ADR-005 (realtime),
`manifests/services/events.json` and `manifests/services/network.json` (node revocation and hook events).
ddiff --git a/docs/adr/ADR-060-skills-run-sandboxed.md b/docs/adr/ADR-060-skills-run-sandboxed.md
iindex 03f722a9..8e638305 100644
---- a/docs/adr/ADR-060-skills-run-sandboxed.md
++++ b/docs/adr/ADR-060-skills-run-sandboxed.md
@@@ -39,11 +39,11 @@
bytes that were scanned, and its scan findings may name the `tool` they are about. `actor.skill@1` and
`actor.skill-listing@1` are unchanged.
3. **The Link runner** (`[email protected]`, a declared function like `[email protected]`):
-- - off unless the owner turns it on (`worker.functions`, `openvibe node skills on`);
++ - off unless the owner turns it on (`worker.functions`, `openvibe-link skills on`);
- takes `{ tool, entry, input }` and the code as job `inputs` (Media objects pinned by sha256; a mismatch ends the
job before anything runs);
- runs `node --permission --allow-fs-read=<the job's input dir>` with no child process, no worker, no addons and
-- `--disallow-code-generation-from-strings`, inside the function sandbox Node already has (its own root, limits,
++ `--disallow-code-generation-from-strings`, inside the function sandbox Link already has (its own root, limits,
no credential, network per `net`);
- the input arrives on stdin as JSON; the tool writes one JSON value (≤ 64 KiB; more fails the job `limit`);
stderr is kept for the person (≤ 16 KiB).
@@@ -74,7 +74,7 @@
- **Run the code on Actor's server.** Rejected: Actor holds every person's agent, token and registry record; one
skill's mistake would be a registry-wide incident, and the design rule forbids it outright.
- **A container on the host that publishes the skill.** Rejected: the host is someone's machine, and a container
-- boundary we do not control is not a sandbox promise we can make to every site. Node already has the sandbox, the
++ boundary we do not control is not a sandbox promise we can make to every site. Link already has the sandbox, the
limits, the egress policy and the placement; running there needs no second runtime.
- **Python and npm in v1.** Deferred: both widen the runtime surface (packages, install recipes, dependency
resolution) before the scan and the sandbox have a release behind them. Each needs its own review, as does
@@@ -82,7 +82,7 @@
## Migration and rollback
--Both new contracts are additive majors; nothing parses `actor.skill@2` until Actor's registry and Node's runner
++Both new contracts are additive majors; nothing parses `actor.skill@2` until Actor's registry and Link's runner
ship, and @1 keeps working for every existing prompt skill. The only data a sandbox listing adds is the Media
objects that hold the code; they are private to Actor's tenant and can be deleted with the listing. Rollback is
retiring the @2 listing and turning the runner off: no prompt skill is affected, no job can be submitted without
@@@ -103,5 +103,5 @@ the `skill.run` kind, and nothing ever ran on our servers, so there is no server
`secret`, `token`, `env` or `key`;
- a @2 listing pins each code resource's Media id and sha256, the pinned sha256 is the document's own bytes, and a
sandbox listing without code is invalid.
--Node's own tests (in OpenVibe Link) prove the runner: a tool cannot read env, spawn, write a file, open a socket
++Link's own tests (in OpenVibe Link) prove the runner: a tool cannot read env, spawn, write a file, open a socket
under `deny`, or exceed its output.
ddiff --git a/docs/adr/README.md b/docs/adr/README.md
iindex e0ddd549..17aecbee 100644
---- a/docs/adr/README.md
++++ b/docs/adr/README.md
@@@ -42,9 +42,9 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each
| [ADR-036](ADR-036-run-execution-authority.md) | Run is an execution authority, not a Host detail: Host deploys persistent applications, Run executes isolated jobs/environments (`function`, `code`, `browser`, `linux`, `desktop`, `gpu`), placed by the Fabric on the cheapest capable offer; provider adapters are offers, not code paths; per-second metering, TTL, idle suspend, cancel, snapshot/resume, stdout streaming, artifacts via Media, limits, network policy and strong isolation | Proposed 2026-10-05 (plan T14; plan §8 lists it to write before its code, and Run's code is owner-blocked), for the owner's review |
| [ADR-042](ADR-042-events-fabric.md) | The events fabric: the delivery-policy class is the one vocabulary and TOPIC/QUEUE/STREAM a derived carrier class; PostgreSQL stays the record, carriers are transports that fail open to `pg-v1`; a delivery lease before any second worker; per-key ordering; the SDK planner places deliveries and explains them; JetStream for STREAM only; opaque cursors replace the global seq after one overlap release; hot/replay/archive tiers; amends ADR-004's NATS rejection | Accepted 2026-09-29 (plan T7) |
| [ADR-043](ADR-043-bot-devices-and-control.md) | Bot — devices, pairing, control and safety: robots, devices and credentials are separate (one hashed, rotatable, revocable device credential); pairing is a one-time code; one outbound WebSocket per device with absolute deadlines, device-side stops and a latched e-stop; robot profiles rather than rows; owner/operators/viewers with a timed public queue; everything audited; OpenVibe.Bot is its own service | Accepted 2026-09-29 (plan track T15, "Bot complete"; owner direction the same evening: openvibe.bot is an open control panel for robots, streaming optional; the first robots are the owner's Adeept 4WD Smart Car for Raspberry Pi and a Cozmo-based robot) |
--| [ADR-044](ADR-044-actor-runtime.md) | The Actor runtime: one `platform.task@1` task (`POST /v1/tasks`) with a mode, a per-task and per-day budget, result, cost and explanation (a chain of `platform.placement-result@1`); adapters declare capabilities, limits, a reviewed rate card, a trust class and BYO keys; modes are `openvibe-sdk/placement` objectives; live signals auto-balance with hysteresis and immediate failover; a `verifying` state holds cheap-first cross-family checks and the task's own tests; the five ADR-046 trust classes, `private` narrowing to OpenVibe-only, Node control levels underneath; progress over SSE, cancel and webhooks; owner `network` (no Actor service manifest exists yet) | Proposed 2026-10-05 (plan T17), for the owner's review |
++| [ADR-044](ADR-044-actor-runtime.md) | The Actor runtime: one `platform.task@1` task (`POST /v1/tasks`) with a mode, a per-task and per-day budget, result, cost and explanation (a chain of `platform.placement-result@1`); adapters declare capabilities, limits, a reviewed rate card, a trust class and BYO keys; modes are `openvibe-sdk/placement` objectives; live signals auto-balance with hysteresis and immediate failover; a `verifying` state holds cheap-first cross-family checks and the task's own tests; the five ADR-046 trust classes, `private` narrowing to OpenVibe-only, Link control levels underneath; progress over SSE, cancel and webhooks; owner `network` (no Actor service manifest exists yet) | Proposed 2026-10-05 (plan T17), for the owner's review |
| [ADR-045](ADR-045-node-universal-runtime.md) | The Link is OpenVibe's universal runtime, not a Bot/Actor helper: one checksummed release binary and one control link serving the execution worker (`worker:<class>`, per ADR-046 §5), computer control under device-set control levels with a local kill switch and recording, and cache/storage/delivery; local policy always overrides cloud commands; ADR-043 stays the robot/device half | Proposed 2026-10-05 (plan T14 and §8 revision; consistent with ADR-046 and ADR-048, amends ADR-043's credential paragraph), for the owner's review |
--| [ADR-046](ADR-046-universal-adaptive-fabric.md) | The universal adaptive fabric: offers, requirements, placement results and signed plans through one placer (`openvibe-sdk/placement`); health, cost, telemetry and usage samples; five trust classes, with `user-owned` eligible only when a workload names it; `worker:<class>` Node capabilities; the data plane keeps running on the last valid signed plan; T6 AI the reference consumer | Proposed 2026-10-03 (plan T1 step 3), for the owner's review |
++| [ADR-046](ADR-046-universal-adaptive-fabric.md) | The universal adaptive fabric: offers, requirements, placement results and signed plans through one placer (`openvibe-sdk/placement`); health, cost, telemetry and usage samples; five trust classes, with `user-owned` eligible only when a workload names it; `worker:<class>` Link capabilities; the data plane keeps running on the last valid signed plan; T6 AI the reference consumer | Proposed 2026-10-03 (plan T1 step 3), for the owner's review |
| [ADR-048](ADR-048-services-control-plane.md) | The Services control plane: Services aggregates authorities and owns no other service's rows; every console action is a `common.resource-control-request@1` to the owning authority, answered with `common.resource-control-result@1` (idempotency keys, `confirmation_id` for sensitive actions); OVRN `ovrn:<service>:<project_id>:<type>/<id>` (`common.resource-name@1`) is the one cross-service resource name | Accepted 2026-10-04 (plan T13) |
| [ADR-053](ADR-053-website-host-boundary.md) | The Website/Host boundary: OpenVibe.Website owns authoring (projects, pages, revisions, builds, Media asset pointers and intended domains) and OpenVibe.Host owns site, deploy, activation, domain binding/verification/TLS and serving; Website keeps no duplicate `host_sites`/`host_deploys` rows and calls Host's routes; a Website build is not a Host deploy; Website composes Host + Codes + Actor + Run + Media and is not OpenVibe.Sites | Proposed 2026-10-05 (plan T19), for the owner's review |
| [ADR-054](ADR-054-inventory-authority.md) | The OpenVibe inventory: a new authority, OpenVibe.Inventory (`inventory`, `ov_inventory`), owns item kinds (contract + renderer per surface), definitions (`itd_`), instances (`inv_`), the equipped set and an append-only ledger; issuers grant within their namespace; no selling, buying, trading or conversion until a later money ADR (ADR-012/ADR-025); Live's cosmetics are the first kinds, migrated convert-verify-delete | Accepted 2026-10-09 (plan T21) |
ddiff --git a/docs/locker.md b/docs/locker.md
iindex 3f68e4cc..403fd5da 100644
---- a/docs/locker.md
++++ b/docs/locker.md
@@@ -1,7 +1,7 @@
# Locker: end-to-end encryption for anything on OpenVibe
`locker.envelope@1` (`contracts/locker/envelope.v1.json`) is the format. The library that reads and writes it is
--`openvibe-sdk/locker` (Node and the browser); `openvibe-sdk/locker/server` checks an envelope's header without
++`openvibe-sdk/locker` (Link and the browser); `openvibe-sdk/locker/server` checks an envelope's header without
decrypting it.
Locker sits on top of sharing: sharing decides **who may fetch** a thing, Locker decides **who can read** it. With a
ddiff --git a/fixtures/ai.credential-put/invalid/link-machine-anyone.json b/fixtures/ai.credential-put/invalid/link-machine-anyone.json
nnew file mode 100644
iindex 00000000..2555e71b
---- /dev/null
++++ b/fixtures/ai.credential-put/invalid/link-machine-anyone.json
@@@ -0,0 +1,7 @@
++{
++ "provider": "link",
++ "machine": "anyone",
++ "models": {
++ "chat": "llama3.1:8b"
++ }
++}
ddiff --git a/fixtures/ai.credential-put/invalid/link-with-key.json b/fixtures/ai.credential-put/invalid/link-with-key.json
nnew file mode 100644
iindex 00000000..e993cf2c
---- /dev/null
++++ b/fixtures/ai.credential-put/invalid/link-with-key.json
@@@ -0,0 +1,5 @@
++{
++ "provider": "link",
++ "machine": "dev_01M5YGM39VC2RRGDQPRD6D4SHD",
++ "api_key": "sk-not-for-a-machine"
++}
ddiff --git a/fixtures/ai.credential-put/invalid/link-without-machine.json b/fixtures/ai.credential-put/invalid/link-without-machine.json
nnew file mode 100644
iindex 00000000..68b09f1f
---- /dev/null
++++ b/fixtures/ai.credential-put/invalid/link-without-machine.json
@@@ -0,0 +1,6 @@
++{
++ "provider": "link",
++ "models": {
++ "chat": "llama3.1:8b"
++ }
++}
ddiff --git a/fixtures/ai.credential-put/invalid/node-machine-anyone.json b/fixtures/ai.credential-put/invalid/node-machine-anyone.json
ddeleted file mode 100644
iindex a3a39e9f..00000000
---- a/fixtures/ai.credential-put/invalid/node-machine-anyone.json
++++ /dev/null
@@@ -1,7 +0,0 @@
--{
-- "provider": "node",
-- "machine": "anyone",
-- "models": {
-- "chat": "llama3.1:8b"
-- }
--}
ddiff --git a/fixtures/ai.credential-put/invalid/node-with-key.json b/fixtures/ai.credential-put/invalid/node-with-key.json
ddeleted file mode 100644
iindex 6073890d..00000000
---- a/fixtures/ai.credential-put/invalid/node-with-key.json
++++ /dev/null
@@@ -1,5 +0,0 @@
--{
-- "provider": "node",
-- "machine": "dev_01M5YGM39VC2RRGDQPRD6D4SHD",
-- "api_key": "sk-not-for-a-machine"
--}
ddiff --git a/fixtures/ai.credential-put/invalid/node-without-machine.json b/fixtures/ai.credential-put/invalid/node-without-machine.json
ddeleted file mode 100644
iindex 59d4ef93..00000000
---- a/fixtures/ai.credential-put/invalid/node-without-machine.json
++++ /dev/null
@@@ -1,6 +0,0 @@
--{
-- "provider": "node",
-- "models": {
-- "chat": "llama3.1:8b"
-- }
--}
ddiff --git a/fixtures/ai.credential-put/valid/link-any.json b/fixtures/ai.credential-put/valid/link-any.json
nnew file mode 100644
iindex 00000000..201dfcd9
---- /dev/null
++++ b/fixtures/ai.credential-put/valid/link-any.json
@@@ -0,0 +1,7 @@
++{
++ "provider": "link",
++ "machine": "any",
++ "models": {
++ "chat": "llama3.1:8b"
++ }
++}
ddiff --git a/fixtures/ai.credential-put/valid/link.json b/fixtures/ai.credential-put/valid/link.json
nnew file mode 100644
iindex 00000000..f8a1dee9
---- /dev/null
++++ b/fixtures/ai.credential-put/valid/link.json
@@@ -0,0 +1,7 @@
++{
++ "provider": "link",
++ "machine": "dev_01M5YGM39VC2RRGDQPRD6D4SHD",
++ "models": {
++ "chat": "llama3.1:8b"
++ }
++}
ddiff --git a/fixtures/ai.credential-put/valid/node-any.json b/fixtures/ai.credential-put/valid/node-any.json
ddeleted file mode 100644
iindex 41c15f99..00000000
---- a/fixtures/ai.credential-put/valid/node-any.json
++++ /dev/null
@@@ -1,7 +0,0 @@
--{
-- "provider": "node",
-- "machine": "any",
-- "models": {
-- "chat": "llama3.1:8b"
-- }
--}
ddiff --git a/fixtures/ai.credential-put/valid/node.json b/fixtures/ai.credential-put/valid/node.json
ddeleted file mode 100644
iindex 342f87eb..00000000
---- a/fixtures/ai.credential-put/valid/node.json
++++ /dev/null
@@@ -1,7 +0,0 @@
--{
-- "provider": "node",
-- "machine": "dev_01M5YGM39VC2RRGDQPRD6D4SHD",
-- "models": {
-- "chat": "llama3.1:8b"
-- }
--}
ddiff --git a/fixtures/ai.credential/valid/link.json b/fixtures/ai.credential/valid/link.json
nnew file mode 100644
iindex 00000000..23f0fe9c
---- /dev/null
++++ b/fixtures/ai.credential/valid/link.json
@@@ -0,0 +1,15 @@
++{
++ "subject": "usr_01M4W5JBFEG3QG9ER8W1A2K0RX",
++ "owner": "chat",
++ "provider": "link",
++ "machine": "dev_01M5YGM39VC2RRGDQPRD6D4SHD",
++ "base_url": null,
++ "models": {
++ "chat": "llama3.1:8b"
++ },
++ "budget_usd_per_day": null,
++ "spent_usd_today": 0,
++ "created_at": "2026-10-27T13:00:00.000Z",
++ "updated_at": "2026-10-27T13:00:00.000Z",
++ "last_used_at": null
++}
ddiff --git a/fixtures/ai.credential/valid/node.json b/fixtures/ai.credential/valid/node.json
ddeleted file mode 100644
iindex 547d3799..00000000
---- a/fixtures/ai.credential/valid/node.json
++++ /dev/null
@@@ -1,15 +0,0 @@
--{
-- "subject": "usr_01M4W5JBFEG3QG9ER8W1A2K0RX",
-- "owner": "chat",
-- "provider": "node",
-- "machine": "dev_01M5YGM39VC2RRGDQPRD6D4SHD",
-- "base_url": null,
-- "models": {
-- "chat": "llama3.1:8b"
-- },
-- "budget_usd_per_day": null,
-- "spent_usd_today": 0,
-- "created_at": "2026-10-27T13:00:00.000Z",
-- "updated_at": "2026-10-27T13:00:00.000Z",
-- "last_used_at": null
--}
ddiff --git a/fixtures/ai.memory-item/valid/item.json b/fixtures/ai.memory-item/valid/item.json
iindex c056db79..9f74cccd 100644
---- a/fixtures/ai.memory-item/valid/item.json
++++ b/fixtures/ai.memory-item/valid/item.json
@@@ -1,7 +1,7 @@
{
"id": "mem_01JZ0000000000000000000001",
"owner": "user:usr_01JZ0000000000000000000AAA",
-- "scope": "project:openvibe-node",
++ "scope": "project:openvibe-link",
"kind": "project_rule",
"text": "Run the Go tests with go test ./... before a commit.",
"source": "claude-code",
ddiff --git a/fixtures/ai.memory-write-request/valid/batch.json b/fixtures/ai.memory-write-request/valid/batch.json
iindex fe80c38c..63225ad0 100644
---- a/fixtures/ai.memory-write-request/valid/batch.json
++++ b/fixtures/ai.memory-write-request/valid/batch.json
@@@ -3,7 +3,7 @@
{
"text": "Use the repo's formatter before a commit.",
"kind": "procedure",
-- "scope": "project:openvibe-node",
++ "scope": "project:openvibe-link",
"source": "claude-code",
"source_ref": {
"path": ".claude/CLAUDE.md",
ddiff --git a/fixtures/bot.robot-manage-result/valid/created.json b/fixtures/bot.robot-manage-result/valid/created.json
iindex 1bc4722a..5a3f9dbf 100644
---- a/fixtures/bot.robot-manage-result/valid/created.json
++++ b/fixtures/bot.robot-manage-result/valid/created.json
@@@ -19,6 +19,6 @@
"pairing": {
"code": "ABCD-EFGH",
"expires_at": "2026-09-29T19:20:00.000Z",
-- "installer": "curl -fsSL https://openvibe.bot/install.sh | sh -s ABCD-EFGH"
++ "installer": "curl -fsSL https://openvibe.link/install.sh | sh -s ABCD-EFGH"
}
}
ddiff --git a/fixtures/connect.device/invalid/leading-hyphen.json b/fixtures/connect.device/invalid/leading-hyphen.json
iindex de09d92a..0c62f06d 100644
---- a/fixtures/connect.device/invalid/leading-hyphen.json
++++ b/fixtures/connect.device/invalid/leading-hyphen.json
@@@ -9,7 +9,7 @@
"type": "user",
"id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
},
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"name": "-ab",
"status": "online",
"os": "linux",
ddiff --git a/fixtures/connect.device/invalid/too-long.json b/fixtures/connect.device/invalid/too-long.json
iindex bb28ce0e..6041191b 100644
---- a/fixtures/connect.device/invalid/too-long.json
++++ b/fixtures/connect.device/invalid/too-long.json
@@@ -9,7 +9,7 @@
"type": "user",
"id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
},
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"status": "online",
"os": "linux",
ddiff --git a/fixtures/connect.device/invalid/trailing-hyphen.json b/fixtures/connect.device/invalid/trailing-hyphen.json
iindex e35b6dbb..9089a806 100644
---- a/fixtures/connect.device/invalid/trailing-hyphen.json
++++ b/fixtures/connect.device/invalid/trailing-hyphen.json
@@@ -9,7 +9,7 @@
"type": "user",
"id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
},
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"name": "ab-",
"status": "online",
"os": "linux",
ddiff --git a/fixtures/connect.device/valid/max-label.json b/fixtures/connect.device/valid/max-label.json
iindex ddd8f9af..c7c2c5f1 100644
---- a/fixtures/connect.device/valid/max-label.json
++++ b/fixtures/connect.device/valid/max-label.json
@@@ -9,7 +9,7 @@
"type": "user",
"id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
},
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"name": "a-------------------------------------------------------------z",
"status": "online",
"os": "linux",
ddiff --git a/fixtures/connect.device/valid/one-char.json b/fixtures/connect.device/valid/one-char.json
iindex 374e9145..6dd4c7ae 100644
---- a/fixtures/connect.device/valid/one-char.json
++++ b/fixtures/connect.device/valid/one-char.json
@@@ -9,7 +9,7 @@
"type": "user",
"id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
},
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"name": "a",
"status": "online",
"os": "linux",
ddiff --git a/fixtures/connect.device/valid/two-char.json b/fixtures/connect.device/valid/two-char.json
iindex 3bfcbf0b..fbc62749 100644
---- a/fixtures/connect.device/valid/two-char.json
++++ b/fixtures/connect.device/valid/two-char.json
@@@ -9,7 +9,7 @@
"type": "user",
"id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
},
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"name": "ab",
"status": "online",
"os": "linux",
ddiff --git a/fixtures/connect.device/valid/workstation.json b/fixtures/connect.device/valid/workstation.json
iindex 03938786..8035ba41 100644
---- a/fixtures/connect.device/valid/workstation.json
++++ b/fixtures/connect.device/valid/workstation.json
@@@ -9,7 +9,7 @@
"type": "user",
"id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
},
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"name": "workstation",
"status": "online",
"os": "linux",
ddiff --git a/fixtures/identity.service-token-claims/invalid/node-bad-id.json b/fixtures/identity.service-token-claims/invalid/node-bad-id.json
iindex d2d27825..693319fe 100644
---- a/fixtures/identity.service-token-claims/invalid/node-bad-id.json
++++ b/fixtures/identity.service-token-claims/invalid/node-bad-id.json
@@@ -1,12 +1,12 @@
{
"iss": "https://openvibe.network",
-- "sub": "node:nod_42",
-- "actor_type": "node",
++ "sub": "link:lnk_42",
++ "actor_type": "link",
"aud": [
"openvibe.bot"
],
"cap": [
-- "network.node.self.manage"
++ "network.link.self.manage"
],
"iat": 1790000000,
"exp": 1790000300,
ddiff --git a/fixtures/identity.service-token-claims/valid/node-token.json b/fixtures/identity.service-token-claims/valid/node-token.json
iindex 95c82bd7..51fedf97 100644
---- a/fixtures/identity.service-token-claims/valid/node-token.json
++++ b/fixtures/identity.service-token-claims/valid/node-token.json
@@@ -1,12 +1,12 @@
{
"iss": "https://openvibe.network",
-- "sub": "node:nod_01JAB2C3D4E5F6G7H8J9K0MNPQ",
-- "actor_type": "node",
++ "sub": "link:lnk_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "actor_type": "link",
"aud": [
"openvibe.bot"
],
"cap": [
-- "network.node.self.manage"
++ "network.link.self.manage"
],
"iat": 1790000000,
"exp": 1790000300,
ddiff --git a/fixtures/link.compute/invalid/unknown-source.json b/fixtures/link.compute/invalid/unknown-source.json
nnew file mode 100644
iindex 00000000..3737d4b9
---- /dev/null
++++ b/fixtures/link.compute/invalid/unknown-source.json
@@@ -0,0 +1,11 @@
++{
++ "sources": [
++ {
++ "kind": "invalid",
++ "name": "Unknown",
++ "status": "ready",
++ "reason": null
++ }
++ ],
++ "candidates": []
++}
ddiff --git a/fixtures/link.compute/valid/personal-and-coming-soon.json b/fixtures/link.compute/valid/personal-and-coming-soon.json
nnew file mode 100644
iindex 00000000..d43fbfaf
---- /dev/null
++++ b/fixtures/link.compute/valid/personal-and-coming-soon.json
@@@ -0,0 +1,42 @@
++{
++ "sources": [
++ {
++ "kind": "personal",
++ "name": "Your Link machines",
++ "status": "ready",
++ "reason": null
++ },
++ {
++ "kind": "cloud",
++ "name": "OpenVibe cloud machines",
++ "status": "coming_soon",
++ "reason": "No cloud pool is accepting jobs yet."
++ },
++ {
++ "kind": "exchange",
++ "name": "Exchange GPU rentals",
++ "status": "coming_soon",
++ "reason": "A live quote is required.",
++ "price": null
++ },
++ {
++ "kind": "community",
++ "name": "Community machines",
++ "status": "coming_soon",
++ "reason": "Shared placement is being connected."
++ }
++ ],
++ "candidates": [
++ {
++ "id": "dev_01J0000000000000000000000",
++ "kind": "personal",
++ "name": "Studio",
++ "status": "ready",
++ "reason": null,
++ "price": {
++ "amount_usd": 0,
++ "unit": "hour"
++ }
++ }
++ ]
++}
ddiff --git a/fixtures/network.link-list-result/invalid/no-generated-at.json b/fixtures/network.link-list-result/invalid/no-generated-at.json
nnew file mode 100644
iindex 00000000..fa38e6f3
---- /dev/null
++++ b/fixtures/network.link-list-result/invalid/no-generated-at.json
@@@ -0,0 +1 @@
++{ "nodes": [ { "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"], "location": { "region": "us-west", "country": "US", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "provider": "ovh", "beacon": "https://openvibe.network/api/v1/links/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" } ] }
ddiff --git a/fixtures/network.link-list-result/valid/one.json b/fixtures/network.link-list-result/valid/one.json
nnew file mode 100644
iindex 00000000..0f0162f9
---- /dev/null
++++ b/fixtures/network.link-list-result/valid/one.json
@@@ -0,0 +1 @@
++{ "nodes": [ { "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"], "location": { "region": "us-west", "country": "US", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "provider": "ovh", "beacon": "https://openvibe.network/api/v1/links/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" } ], "generated_at": "2026-09-28T12:00:05Z" }
ddiff --git a/fixtures/network.link-report-request/invalid/no-source.json b/fixtures/network.link-report-request/invalid/no-source.json
nnew file mode 100644
iindex 00000000..fa38e6f3
---- /dev/null
++++ b/fixtures/network.link-report-request/invalid/no-source.json
@@@ -0,0 +1 @@
++{ "nodes": [ { "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"], "location": { "region": "us-west", "country": "US", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "provider": "ovh", "beacon": "https://openvibe.network/api/v1/links/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" } ] }
ddiff --git a/fixtures/network.link-report-request/valid/empty.json b/fixtures/network.link-report-request/valid/empty.json
nnew file mode 100644
iindex 00000000..879066da
---- /dev/null
++++ b/fixtures/network.link-report-request/valid/empty.json
@@@ -0,0 +1 @@
++{ "source": "openvibe-oregon", "nodes": [] }
ddiff --git a/fixtures/network.link-report-request/valid/one-host.json b/fixtures/network.link-report-request/valid/one-host.json
nnew file mode 100644
iindex 00000000..6ab6870e
---- /dev/null
++++ b/fixtures/network.link-report-request/valid/one-host.json
@@@ -0,0 +1 @@
++{ "source": "openvibe-oregon", "nodes": [ { "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"], "location": { "region": "us-west", "country": "US", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "provider": "ovh", "beacon": "https://openvibe.network/api/v1/links/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" } ] }
ddiff --git a/fixtures/network.link.revoked/invalid/bad-node-id.json b/fixtures/network.link.revoked/invalid/bad-node-id.json
nnew file mode 100644
iindex 00000000..7701af4d
---- /dev/null
++++ b/fixtures/network.link.revoked/invalid/bad-node-id.json
@@@ -0,0 +1,6 @@
++{
++ "node_id": "node-1",
++ "principal_id": "lnk_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "owner": { "kind": "user", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" },
++ "at": "2026-10-04T18:00:00.000Z"
++}
ddiff --git a/fixtures/network.link.revoked/invalid/missing-principal.json b/fixtures/network.link.revoked/invalid/missing-principal.json
nnew file mode 100644
iindex 00000000..af39109f
---- /dev/null
++++ b/fixtures/network.link.revoked/invalid/missing-principal.json
@@@ -0,0 +1,5 @@
++{
++ "node_id": "n-01jab2c3d4e5f6g7h8j9k0mnps",
++ "owner": { "kind": "user", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" },
++ "at": "2026-10-04T18:00:00.000Z"
++}
ddiff --git a/fixtures/network.link.revoked/invalid/owner-mixed.json b/fixtures/network.link.revoked/invalid/owner-mixed.json
nnew file mode 100644
iindex 00000000..0082790e
---- /dev/null
++++ b/fixtures/network.link.revoked/invalid/owner-mixed.json
@@@ -0,0 +1,6 @@
++{
++ "node_id": "n-01jab2c3d4e5f6g7h8j9k0mnps",
++ "principal_id": "lnk_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "owner": { "kind": "user", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ" },
++ "at": "2026-10-04T18:00:00.000Z"
++}
ddiff --git a/fixtures/network.link.revoked/valid/project-owned.json b/fixtures/network.link.revoked/valid/project-owned.json
nnew file mode 100644
iindex 00000000..f7f15f0a
---- /dev/null
++++ b/fixtures/network.link.revoked/valid/project-owned.json
@@@ -0,0 +1,6 @@
++{
++ "node_id": "n-01jab2c3d4e5f6g7h8j9k0mnpt",
++ "principal_id": "lnk_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "owner": { "kind": "project", "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ" },
++ "at": "2026-10-04T18:00:00.000Z"
++}
ddiff --git a/fixtures/network.link.revoked/valid/revoked-by-owner.json b/fixtures/network.link.revoked/valid/revoked-by-owner.json
nnew file mode 100644
iindex 00000000..53ceb97a
---- /dev/null
++++ b/fixtures/network.link.revoked/valid/revoked-by-owner.json
@@@ -0,0 +1,6 @@
++{
++ "node_id": "n-01jab2c3d4e5f6g7h8j9k0mnps",
++ "principal_id": "lnk_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "owner": { "kind": "user", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" },
++ "at": "2026-10-04T18:00:00.000Z"
++}
ddiff --git a/fixtures/network.link.revoked/valid/revoked-by-service.json b/fixtures/network.link.revoked/valid/revoked-by-service.json
nnew file mode 100644
iindex 00000000..166c8cd0
---- /dev/null
++++ b/fixtures/network.link.revoked/valid/revoked-by-service.json
@@@ -0,0 +1,7 @@
++{
++ "node_id": "n-01jab2c3d4e5f6g7h8j9k0mnps",
++ "principal_id": "lnk_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "owner": { "kind": "user", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" },
++ "reason": "the owner disconnected the machine from Bot",
++ "at": "2026-10-04T18:00:00.000Z"
++}
ddiff --git a/fixtures/network.link/invalid/bad-cell.json b/fixtures/network.link/invalid/bad-cell.json
nnew file mode 100644
iindex 00000000..c85e45ce
---- /dev/null
++++ b/fixtures/network.link/invalid/bad-cell.json
@@@ -0,0 +1,25 @@
++{
++ "id": "oregon-1",
++ "name": "Oregon 1",
++ "roles": [
++ "web",
++ "app",
++ "data",
++ "ingest"
++ ],
++ "location": {
++ "region": "us-west",
++ "country": "US",
++ "city": "Hillsboro",
++ "lat": 45.52,
++ "lon": -122.99
++ },
++ "provider": "ovh",
++ "beacon": "https://openvibe.network/api/v1/links/oregon-1/beacon",
++ "health": {
++ "status": "up",
++ "checked_at": "2026-09-28T12:00:00Z"
++ },
++ "updated_at": "2026-09-28T12:00:00Z",
++ "cell": "WNAM_1"
++}
ddiff --git a/fixtures/network.link/invalid/bad-country.json b/fixtures/network.link/invalid/bad-country.json
nnew file mode 100644
iindex 00000000..635cf02d
---- /dev/null
++++ b/fixtures/network.link/invalid/bad-country.json
@@@ -0,0 +1 @@
++{ "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"], "location": { "region": "us-west", "country": "usa", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "provider": "ovh", "beacon": "https://openvibe.network/api/v1/links/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" }
ddiff --git a/fixtures/network.link/invalid/plain-http-beacon.json b/fixtures/network.link/invalid/plain-http-beacon.json
nnew file mode 100644
iindex 00000000..f543ad2d
---- /dev/null
++++ b/fixtures/network.link/invalid/plain-http-beacon.json
@@@ -0,0 +1 @@
++{ "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"], "location": { "region": "us-west", "country": "US", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "provider": "ovh", "beacon": "http://openvibe.network/api/v1/links/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" }
ddiff --git a/fixtures/network.link/invalid/public-address.json b/fixtures/network.link/invalid/public-address.json
nnew file mode 100644
iindex 00000000..c27e2604
---- /dev/null
++++ b/fixtures/network.link/invalid/public-address.json
@@@ -0,0 +1 @@
++{ "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"], "location": { "region": "us-west", "country": "US", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "address": "15.204.79.215", "beacon": "https://openvibe.network/api/v1/links/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" }
ddiff --git a/fixtures/network.link/invalid/unknown-role.json b/fixtures/network.link/invalid/unknown-role.json
nnew file mode 100644
iindex 00000000..d693ecd5
---- /dev/null
++++ b/fixtures/network.link/invalid/unknown-role.json
@@@ -0,0 +1 @@
++{ "id": "oregon-1", "name": "Oregon 1", "roles": ["mainframe", "app", "data", "ingest"], "location": { "region": "us-west", "country": "US", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "provider": "ovh", "beacon": "https://openvibe.network/api/v1/links/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" }
ddiff --git a/fixtures/network.link/valid/minimal-edge.json b/fixtures/network.link/valid/minimal-edge.json
nnew file mode 100644
iindex 00000000..934f34b6
---- /dev/null
++++ b/fixtures/network.link/valid/minimal-edge.json
@@@ -0,0 +1 @@
++{ "id": "probe-fra", "roles": ["edge-probe", "edge-relay"], "location": { "region": "eu-central", "country": "DE" }, "health": { "status": "unknown", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" }
ddiff --git a/fixtures/network.link/valid/oregon.json b/fixtures/network.link/valid/oregon.json
nnew file mode 100644
iindex 00000000..d74a3fab
---- /dev/null
++++ b/fixtures/network.link/valid/oregon.json
@@@ -0,0 +1 @@
++{ "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"], "location": { "region": "us-west", "country": "US", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "provider": "ovh", "beacon": "https://openvibe.network/api/v1/links/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" }
ddiff --git a/fixtures/network.link/valid/with-cell.json b/fixtures/network.link/valid/with-cell.json
nnew file mode 100644
iindex 00000000..0d6e1029
---- /dev/null
++++ b/fixtures/network.link/valid/with-cell.json
@@@ -0,0 +1,25 @@
++{
++ "id": "oregon-1",
++ "name": "Oregon 1",
++ "roles": [
++ "web",
++ "app",
++ "data",
++ "ingest"
++ ],
++ "location": {
++ "region": "us-west",
++ "country": "US",
++ "city": "Hillsboro",
++ "lat": 45.52,
++ "lon": -122.99
++ },
++ "provider": "ovh",
++ "beacon": "https://openvibe.network/api/v1/links/oregon-1/beacon",
++ "health": {
++ "status": "up",
++ "checked_at": "2026-09-28T12:00:00Z"
++ },
++ "updated_at": "2026-09-28T12:00:00Z",
++ "cell": "wnam-1"
++}
ddiff --git a/fixtures/network.machine-pairing/valid/code.json b/fixtures/network.machine-pairing/valid/code.json
iindex e72c019d..67313b05 100644
---- a/fixtures/network.machine-pairing/valid/code.json
++++ b/fixtures/network.machine-pairing/valid/code.json
@@@ -2,5 +2,5 @@
"code": "ABCD-1234",
"pairing_id": "pair_01JAB2C3D4E5F6G7H8J9K0MNPS",
"expires_at": "2026-10-31T09:00:00.000Z",
-- "installer": "curl -fsSL https://openvibe.bot/install | sh -s -- --network https://openvibe.network --pairing pair_01JAB2C3D4E5F6G7H8J9K0MNPS --code ABCD-1234"
++ "installer": "curl -fsSL https://openvibe.link/install | sh -s -- --network https://openvibe.network --pairing pair_01JAB2C3D4E5F6G7H8J9K0MNPS --code ABCD-1234"
}
ddiff --git a/fixtures/network.machine.offline/invalid/bad-owner.json b/fixtures/network.machine.offline/invalid/bad-owner.json
iindex 5e49fc71..d2d799de 100644
---- a/fixtures/network.machine.offline/invalid/bad-owner.json
++++ b/fixtures/network.machine.offline/invalid/bad-owner.json
@@@ -1,6 +1,6 @@
{
"machine_id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
-- "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "principal_id": "lnk_01JAB2C3D4E5F6G7H8J9K0MNPS",
"owner": "usr_x",
"at": "2026-10-31T09:00:00.000Z"
}
ddiff --git a/fixtures/network.machine.offline/valid/machine.json b/fixtures/network.machine.offline/valid/machine.json
iindex a20faf5f..d0d29b8f 100644
---- a/fixtures/network.machine.offline/valid/machine.json
++++ b/fixtures/network.machine.offline/valid/machine.json
@@@ -1,6 +1,6 @@
{
"machine_id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
-- "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "principal_id": "lnk_01JAB2C3D4E5F6G7H8J9K0MNPS",
"owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS",
"at": "2026-10-31T09:00:00.000Z"
}
ddiff --git a/fixtures/network.machine.online/invalid/bad-machine-id.json b/fixtures/network.machine.online/invalid/bad-machine-id.json
iindex 1c535e79..3e8a55db 100644
---- a/fixtures/network.machine.online/invalid/bad-machine-id.json
++++ b/fixtures/network.machine.online/invalid/bad-machine-id.json
@@@ -1,6 +1,6 @@
{
"machine_id": "dev_x",
-- "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "principal_id": "lnk_01JAB2C3D4E5F6G7H8J9K0MNPS",
"owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS",
"at": "2026-10-31T09:00:00.000Z"
}
ddiff --git a/fixtures/network.machine.online/invalid/missing-at.json b/fixtures/network.machine.online/invalid/missing-at.json
iindex 15e4a859..8f882638 100644
---- a/fixtures/network.machine.online/invalid/missing-at.json
++++ b/fixtures/network.machine.online/invalid/missing-at.json
@@@ -1,5 +1,5 @@
{
"machine_id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
-- "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "principal_id": "lnk_01JAB2C3D4E5F6G7H8J9K0MNPS",
"owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS"
}
ddiff --git a/fixtures/network.machine.online/valid/machine.json b/fixtures/network.machine.online/valid/machine.json
iindex a20faf5f..d0d29b8f 100644
---- a/fixtures/network.machine.online/valid/machine.json
++++ b/fixtures/network.machine.online/valid/machine.json
@@@ -1,6 +1,6 @@
{
"machine_id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
-- "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "principal_id": "lnk_01JAB2C3D4E5F6G7H8J9K0MNPS",
"owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS",
"at": "2026-10-31T09:00:00.000Z"
}
ddiff --git a/fixtures/network.machine.paired/valid/paired.json b/fixtures/network.machine.paired/valid/paired.json
iindex 540ee237..2de84d4e 100644
---- a/fixtures/network.machine.paired/valid/paired.json
++++ b/fixtures/network.machine.paired/valid/paired.json
@@@ -1,6 +1,6 @@
{
"node_id": "n-01jab2c3d4e5f6g7h8j9k0mnps",
-- "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "principal_id": "lnk_01JAB2C3D4E5F6G7H8J9K0MNPS",
"owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS",
"at": "2026-10-31T09:00:00.000Z"
}
ddiff --git a/fixtures/network.machine.revoked/valid/revoked.json b/fixtures/network.machine.revoked/valid/revoked.json
iindex 7460a2eb..a5921d80 100644
---- a/fixtures/network.machine.revoked/valid/revoked.json
++++ b/fixtures/network.machine.revoked/valid/revoked.json
@@@ -1,6 +1,6 @@
{
"machine_id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
-- "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "principal_id": "lnk_01JAB2C3D4E5F6G7H8J9K0MNPS",
"owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS",
"reason": "the owner removed it",
"at": "2026-10-31T09:00:00.000Z"
ddiff --git a/fixtures/network.machine/valid/worker.json b/fixtures/network.machine/valid/worker.json
iindex 596297bc..92f56600 100644
---- a/fixtures/network.machine/valid/worker.json
++++ b/fixtures/network.machine/valid/worker.json
@@@ -1,6 +1,6 @@
{
"id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
-- "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "principal_id": "lnk_01JAB2C3D4E5F6G7H8J9K0MNPS",
"name": "Kitchen server",
"kind": "server",
"os": "linux",
ddiff --git a/fixtures/network.node-list-result/invalid/no-generated-at.json b/fixtures/network.node-list-result/invalid/no-generated-at.json
ddeleted file mode 100644
iindex 30e3da1f..00000000
---- a/fixtures/network.node-list-result/invalid/no-generated-at.json
++++ /dev/null
@@@ -1 +0,0 @@
--{ "nodes": [ { "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"], "location": { "region": "us-west", "country": "US", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "provider": "ovh", "beacon": "https://openvibe.network/api/v1/nodes/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" } ] }
ddiff --git a/fixtures/network.node-list-result/valid/one.json b/fixtures/network.node-list-result/valid/one.json
ddeleted file mode 100644
iindex 8aa5657d..00000000
---- a/fixtures/network.node-list-result/valid/one.json
++++ /dev/null
@@@ -1 +0,0 @@
--{ "nodes": [ { "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"], "location": { "region": "us-west", "country": "US", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "provider": "ovh", "beacon": "https://openvibe.network/api/v1/nodes/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" } ], "generated_at": "2026-09-28T12:00:05Z" }
ddiff --git a/fixtures/network.node-report-request/invalid/no-source.json b/fixtures/network.node-report-request/invalid/no-source.json
ddeleted file mode 100644
iindex 30e3da1f..00000000
---- a/fixtures/network.node-report-request/invalid/no-source.json
++++ /dev/null
@@@ -1 +0,0 @@
--{ "nodes": [ { "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"], "location": { "region": "us-west", "country": "US", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "provider": "ovh", "beacon": "https://openvibe.network/api/v1/nodes/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" } ] }
ddiff --git a/fixtures/network.node-report-request/valid/empty.json b/fixtures/network.node-report-request/valid/empty.json
ddeleted file mode 100644
iindex 879066da..00000000
---- a/fixtures/network.node-report-request/valid/empty.json
++++ /dev/null
@@@ -1 +0,0 @@
--{ "source": "openvibe-oregon", "nodes": [] }
ddiff --git a/fixtures/network.node-report-request/valid/one-host.json b/fixtures/network.node-report-request/valid/one-host.json
ddeleted file mode 100644
iindex 6410e63b..00000000
---- a/fixtures/network.node-report-request/valid/one-host.json
++++ /dev/null
@@@ -1 +0,0 @@
--{ "source": "openvibe-oregon", "nodes": [ { "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"], "location": { "region": "us-west", "country": "US", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "provider": "ovh", "beacon": "https://openvibe.network/api/v1/nodes/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" } ] }
ddiff --git a/fixtures/network.node.revoked/invalid/bad-node-id.json b/fixtures/network.node.revoked/invalid/bad-node-id.json
ddeleted file mode 100644
iindex 6d5fe57a..00000000
---- a/fixtures/network.node.revoked/invalid/bad-node-id.json
++++ /dev/null
@@@ -1,6 +0,0 @@
--{
-- "node_id": "node-1",
-- "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
-- "owner": { "kind": "user", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" },
-- "at": "2026-10-04T18:00:00.000Z"
--}
ddiff --git a/fixtures/network.node.revoked/invalid/missing-principal.json b/fixtures/network.node.revoked/invalid/missing-principal.json
ddeleted file mode 100644
iindex af39109f..00000000
---- a/fixtures/network.node.revoked/invalid/missing-principal.json
++++ /dev/null
@@@ -1,5 +0,0 @@
--{
-- "node_id": "n-01jab2c3d4e5f6g7h8j9k0mnps",
-- "owner": { "kind": "user", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" },
-- "at": "2026-10-04T18:00:00.000Z"
--}
ddiff --git a/fixtures/network.node.revoked/invalid/owner-mixed.json b/fixtures/network.node.revoked/invalid/owner-mixed.json
ddeleted file mode 100644
iindex 85b05c0f..00000000
---- a/fixtures/network.node.revoked/invalid/owner-mixed.json
++++ /dev/null
@@@ -1,6 +0,0 @@
--{
-- "node_id": "n-01jab2c3d4e5f6g7h8j9k0mnps",
-- "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
-- "owner": { "kind": "user", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ" },
-- "at": "2026-10-04T18:00:00.000Z"
--}
ddiff --git a/fixtures/network.node.revoked/valid/project-owned.json b/fixtures/network.node.revoked/valid/project-owned.json
ddeleted file mode 100644
iindex 4da6f211..00000000
---- a/fixtures/network.node.revoked/valid/project-owned.json
++++ /dev/null
@@@ -1,6 +0,0 @@
--{
-- "node_id": "n-01jab2c3d4e5f6g7h8j9k0mnpt",
-- "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPT",
-- "owner": { "kind": "project", "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ" },
-- "at": "2026-10-04T18:00:00.000Z"
--}
ddiff --git a/fixtures/network.node.revoked/valid/revoked-by-owner.json b/fixtures/network.node.revoked/valid/revoked-by-owner.json
ddeleted file mode 100644
iindex 65feccf6..00000000
---- a/fixtures/network.node.revoked/valid/revoked-by-owner.json
++++ /dev/null
@@@ -1,6 +0,0 @@
--{
-- "node_id": "n-01jab2c3d4e5f6g7h8j9k0mnps",
-- "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
-- "owner": { "kind": "user", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" },
-- "at": "2026-10-04T18:00:00.000Z"
--}
ddiff --git a/fixtures/network.node.revoked/valid/revoked-by-service.json b/fixtures/network.node.revoked/valid/revoked-by-service.json
ddeleted file mode 100644
iindex 862e2e50..00000000
---- a/fixtures/network.node.revoked/valid/revoked-by-service.json
++++ /dev/null
@@@ -1,7 +0,0 @@
--{
-- "node_id": "n-01jab2c3d4e5f6g7h8j9k0mnps",
-- "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
-- "owner": { "kind": "user", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" },
-- "reason": "the owner disconnected the machine from Bot",
-- "at": "2026-10-04T18:00:00.000Z"
--}
ddiff --git a/fixtures/network.node/invalid/bad-cell.json b/fixtures/network.node/invalid/bad-cell.json
ddeleted file mode 100644
iindex 3e23149f..00000000
---- a/fixtures/network.node/invalid/bad-cell.json
++++ /dev/null
@@@ -1,25 +0,0 @@
--{
-- "id": "oregon-1",
-- "name": "Oregon 1",
-- "roles": [
-- "web",
-- "app",
-- "data",
-- "ingest"
-- ],
-- "location": {
-- "region": "us-west",
-- "country": "US",
-- "city": "Hillsboro",
-- "lat": 45.52,
-- "lon": -122.99
-- },
-- "provider": "ovh",
-- "beacon": "https://openvibe.network/api/v1/nodes/oregon-1/beacon",
-- "health": {
-- "status": "up",
-- "checked_at": "2026-09-28T12:00:00Z"
-- },
-- "updated_at": "2026-09-28T12:00:00Z",
-- "cell": "WNAM_1"
--}
ddiff --git a/fixtures/network.node/invalid/bad-country.json b/fixtures/network.node/invalid/bad-country.json
ddeleted file mode 100644
iindex f1d5c2d1..00000000
---- a/fixtures/network.node/invalid/bad-country.json
++++ /dev/null
@@@ -1 +0,0 @@
--{ "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"], "location": { "region": "us-west", "country": "usa", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "provider": "ovh", "beacon": "https://openvibe.network/api/v1/nodes/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" }
ddiff --git a/fixtures/network.node/invalid/plain-http-beacon.json b/fixtures/network.node/invalid/plain-http-beacon.json
ddeleted file mode 100644
iindex 4b62094a..00000000
---- a/fixtures/network.node/invalid/plain-http-beacon.json
++++ /dev/null
@@@ -1 +0,0 @@
--{ "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"], "location": { "region": "us-west", "country": "US", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "provider": "ovh", "beacon": "http://openvibe.network/api/v1/nodes/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" }
ddiff --git a/fixtures/network.node/invalid/public-address.json b/fixtures/network.node/invalid/public-address.json
ddeleted file mode 100644
iindex d4719f60..00000000
---- a/fixtures/network.node/invalid/public-address.json
++++ /dev/null
@@@ -1 +0,0 @@
--{ "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"], "location": { "region": "us-west", "country": "US", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "address": "15.204.79.215", "beacon": "https://openvibe.network/api/v1/nodes/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" }
ddiff --git a/fixtures/network.node/invalid/unknown-role.json b/fixtures/network.node/invalid/unknown-role.json
ddeleted file mode 100644
iindex eb2700f7..00000000
---- a/fixtures/network.node/invalid/unknown-role.json
++++ /dev/null
@@@ -1 +0,0 @@
--{ "id": "oregon-1", "name": "Oregon 1", "roles": ["mainframe", "app", "data", "ingest"], "location": { "region": "us-west", "country": "US", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "provider": "ovh", "beacon": "https://openvibe.network/api/v1/nodes/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" }
ddiff --git a/fixtures/network.node/valid/minimal-edge.json b/fixtures/network.node/valid/minimal-edge.json
ddeleted file mode 100644
iindex 934f34b6..00000000
---- a/fixtures/network.node/valid/minimal-edge.json
++++ /dev/null
@@@ -1 +0,0 @@
--{ "id": "probe-fra", "roles": ["edge-probe", "edge-relay"], "location": { "region": "eu-central", "country": "DE" }, "health": { "status": "unknown", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" }
ddiff --git a/fixtures/network.node/valid/oregon.json b/fixtures/network.node/valid/oregon.json
ddeleted file mode 100644
iindex c0f7b2b7..00000000
---- a/fixtures/network.node/valid/oregon.json
++++ /dev/null
@@@ -1 +0,0 @@
--{ "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"], "location": { "region": "us-west", "country": "US", "city": "Hillsboro", "lat": 45.52, "lon": -122.99 }, "provider": "ovh", "beacon": "https://openvibe.network/api/v1/nodes/oregon-1/beacon", "health": { "status": "up", "checked_at": "2026-09-28T12:00:00Z" }, "updated_at": "2026-09-28T12:00:00Z" }
ddiff --git a/fixtures/network.node/valid/with-cell.json b/fixtures/network.node/valid/with-cell.json
ddeleted file mode 100644
iindex 348a4a55..00000000
---- a/fixtures/network.node/valid/with-cell.json
++++ /dev/null
@@@ -1,25 +0,0 @@
--{
-- "id": "oregon-1",
-- "name": "Oregon 1",
-- "roles": [
-- "web",
-- "app",
-- "data",
-- "ingest"
-- ],
-- "location": {
-- "region": "us-west",
-- "country": "US",
-- "city": "Hillsboro",
-- "lat": 45.52,
-- "lon": -122.99
-- },
-- "provider": "ovh",
-- "beacon": "https://openvibe.network/api/v1/nodes/oregon-1/beacon",
-- "health": {
-- "status": "up",
-- "checked_at": "2026-09-28T12:00:00Z"
-- },
-- "updated_at": "2026-09-28T12:00:00Z",
-- "cell": "wnam-1"
--}
ddiff --git a/fixtures/platform.link-capabilities/invalid/bad-capability.json b/fixtures/platform.link-capabilities/invalid/bad-capability.json
nnew file mode 100644
iindex 00000000..bc9963b9
---- /dev/null
++++ b/fixtures/platform.link-capabilities/invalid/bad-capability.json
@@@ -0,0 +1,37 @@
++{
++ "node_id": "node-1",
++ "cpu": {
++ "cores": 8,
++ "threads": 16
++ },
++ "arch": "x86_64",
++ "memory_mb": 16384,
++ "gpu": {
++ "model": "L4",
++ "count": 1,
++ "vram_mb": 24576
++ },
++ "storage": {
++ "capacity_gb": 500,
++ "available_gb": 300,
++ "kind": "ssd"
++ },
++ "network": {
++ "ingress_mbps": 1000,
++ "egress_mbps": 1000
++ },
++ "regions": [
++ "us-west"
++ ],
++ "tags": [
++ "gpu",
++ "edge"
++ ],
++ "costs": {
++ "per_hour_usd": 0.2,
++ "egress_per_gb_usd": 0.01
++ },
++ "capabilities": [
++ "ffmpeg"
++ ]
++}
ddiff --git a/fixtures/platform.link-capabilities/invalid/missing-node_id.json b/fixtures/platform.link-capabilities/invalid/missing-node_id.json
nnew file mode 100644
iindex 00000000..c12590d6
---- /dev/null
++++ b/fixtures/platform.link-capabilities/invalid/missing-node_id.json
@@@ -0,0 +1,33 @@
++{
++ "cpu": {
++ "cores": 8,
++ "threads": 16
++ },
++ "arch": "x86_64",
++ "memory_mb": 16384,
++ "gpu": {
++ "model": "L4",
++ "count": 1,
++ "vram_mb": 24576
++ },
++ "storage": {
++ "capacity_gb": 500,
++ "available_gb": 300,
++ "kind": "ssd"
++ },
++ "network": {
++ "ingress_mbps": 1000,
++ "egress_mbps": 1000
++ },
++ "regions": [
++ "us-west"
++ ],
++ "tags": [
++ "gpu",
++ "edge"
++ ],
++ "costs": {
++ "per_hour_usd": 0.2,
++ "egress_per_gb_usd": 0.01
++ }
++}
ddiff --git a/fixtures/platform.link-capabilities/valid/agent-report.json b/fixtures/platform.link-capabilities/valid/agent-report.json
nnew file mode 100644
iindex 00000000..78aeef60
---- /dev/null
++++ b/fixtures/platform.link-capabilities/valid/agent-report.json
@@@ -0,0 +1,40 @@
++{
++ "node_id": "node-1",
++ "cpu": {
++ "cores": 8,
++ "threads": 16
++ },
++ "arch": "x86_64",
++ "memory_mb": 16384,
++ "gpu": {
++ "model": "L4",
++ "count": 1,
++ "vram_mb": 24576
++ },
++ "storage": {
++ "capacity_gb": 500,
++ "available_gb": 300,
++ "kind": "ssd"
++ },
++ "network": {
++ "ingress_mbps": 1000,
++ "egress_mbps": 1000
++ },
++ "regions": [
++ "us-west"
++ ],
++ "tags": [
++ "gpu",
++ "edge"
++ ],
++ "costs": {
++ "per_hour_usd": 0.2,
++ "egress_per_gb_usd": 0.01
++ },
++ "capabilities": [
++ "node:http",
++ "worker:ffmpeg"
++ ],
++ "agent_version": "0.9.0",
++ "updated_at": "2026-10-02T00:00:00Z"
++}
ddiff --git a/fixtures/platform.link-capabilities/valid/example.json b/fixtures/platform.link-capabilities/valid/example.json
nnew file mode 100644
iindex 00000000..626d0fb2
---- /dev/null
++++ b/fixtures/platform.link-capabilities/valid/example.json
@@@ -0,0 +1,34 @@
++{
++ "node_id": "node-1",
++ "cpu": {
++ "cores": 8,
++ "threads": 16
++ },
++ "arch": "x86_64",
++ "memory_mb": 16384,
++ "gpu": {
++ "model": "L4",
++ "count": 1,
++ "vram_mb": 24576
++ },
++ "storage": {
++ "capacity_gb": 500,
++ "available_gb": 300,
++ "kind": "ssd"
++ },
++ "network": {
++ "ingress_mbps": 1000,
++ "egress_mbps": 1000
++ },
++ "regions": [
++ "us-west"
++ ],
++ "tags": [
++ "gpu",
++ "edge"
++ ],
++ "costs": {
++ "per_hour_usd": 0.2,
++ "egress_per_gb_usd": 0.01
++ }
++}
ddiff --git a/fixtures/platform.node-capabilities/invalid/bad-capability.json b/fixtures/platform.node-capabilities/invalid/bad-capability.json
ddeleted file mode 100644
iindex bc9963b9..00000000
---- a/fixtures/platform.node-capabilities/invalid/bad-capability.json
++++ /dev/null
@@@ -1,37 +0,0 @@
--{
-- "node_id": "node-1",
-- "cpu": {
-- "cores": 8,
-- "threads": 16
-- },
-- "arch": "x86_64",
-- "memory_mb": 16384,
-- "gpu": {
-- "model": "L4",
-- "count": 1,
-- "vram_mb": 24576
-- },
-- "storage": {
-- "capacity_gb": 500,
-- "available_gb": 300,
-- "kind": "ssd"
-- },
-- "network": {
-- "ingress_mbps": 1000,
-- "egress_mbps": 1000
-- },
-- "regions": [
-- "us-west"
-- ],
-- "tags": [
-- "gpu",
-- "edge"
-- ],
-- "costs": {
-- "per_hour_usd": 0.2,
-- "egress_per_gb_usd": 0.01
-- },
-- "capabilities": [
-- "ffmpeg"
-- ]
--}
ddiff --git a/fixtures/platform.node-capabilities/invalid/missing-node_id.json b/fixtures/platform.node-capabilities/invalid/missing-node_id.json
ddeleted file mode 100644
iindex c12590d6..00000000
---- a/fixtures/platform.node-capabilities/invalid/missing-node_id.json
++++ /dev/null
@@@ -1,33 +0,0 @@
--{
-- "cpu": {
-- "cores": 8,
-- "threads": 16
-- },
-- "arch": "x86_64",
-- "memory_mb": 16384,
-- "gpu": {
-- "model": "L4",
-- "count": 1,
-- "vram_mb": 24576
-- },
-- "storage": {
-- "capacity_gb": 500,
-- "available_gb": 300,
-- "kind": "ssd"
-- },
-- "network": {
-- "ingress_mbps": 1000,
-- "egress_mbps": 1000
-- },
-- "regions": [
-- "us-west"
-- ],
-- "tags": [
-- "gpu",
-- "edge"
-- ],
-- "costs": {
-- "per_hour_usd": 0.2,
-- "egress_per_gb_usd": 0.01
-- }
--}
ddiff --git a/fixtures/platform.node-capabilities/valid/agent-report.json b/fixtures/platform.node-capabilities/valid/agent-report.json
ddeleted file mode 100644
iindex 78aeef60..00000000
---- a/fixtures/platform.node-capabilities/valid/agent-report.json
++++ /dev/null
@@@ -1,40 +0,0 @@
--{
-- "node_id": "node-1",
-- "cpu": {
-- "cores": 8,
-- "threads": 16
-- },
-- "arch": "x86_64",
-- "memory_mb": 16384,
-- "gpu": {
-- "model": "L4",
-- "count": 1,
-- "vram_mb": 24576
-- },
-- "storage": {
-- "capacity_gb": 500,
-- "available_gb": 300,
-- "kind": "ssd"
-- },
-- "network": {
-- "ingress_mbps": 1000,
-- "egress_mbps": 1000
-- },
-- "regions": [
-- "us-west"
-- ],
-- "tags": [
-- "gpu",
-- "edge"
-- ],
-- "costs": {
-- "per_hour_usd": 0.2,
-- "egress_per_gb_usd": 0.01
-- },
-- "capabilities": [
-- "node:http",
-- "worker:ffmpeg"
-- ],
-- "agent_version": "0.9.0",
-- "updated_at": "2026-10-02T00:00:00Z"
--}
ddiff --git a/fixtures/platform.node-capabilities/valid/example.json b/fixtures/platform.node-capabilities/valid/example.json
ddeleted file mode 100644
iindex 626d0fb2..00000000
---- a/fixtures/platform.node-capabilities/valid/example.json
++++ /dev/null
@@@ -1,34 +0,0 @@
--{
-- "node_id": "node-1",
-- "cpu": {
-- "cores": 8,
-- "threads": 16
-- },
-- "arch": "x86_64",
-- "memory_mb": 16384,
-- "gpu": {
-- "model": "L4",
-- "count": 1,
-- "vram_mb": 24576
-- },
-- "storage": {
-- "capacity_gb": 500,
-- "available_gb": 300,
-- "kind": "ssd"
-- },
-- "network": {
-- "ingress_mbps": 1000,
-- "egress_mbps": 1000
-- },
-- "regions": [
-- "us-west"
-- ],
-- "tags": [
-- "gpu",
-- "edge"
-- ],
-- "costs": {
-- "per_hour_usd": 0.2,
-- "egress_per_gb_usd": 0.01
-- }
--}
ddiff --git a/fixtures/platform.usage-sample/valid/run-function-last-partial-second.json b/fixtures/platform.usage-sample/valid/run-function-last-partial-second.json
iindex 9c09d04d..e1cfbc08 100644
---- a/fixtures/platform.usage-sample/valid/run-function-last-partial-second.json
++++ b/fixtures/platform.usage-sample/valid/run-function-last-partial-second.json
@@@ -10,5 +10,5 @@
"quantity": 0.35,
"unit": "s",
"at": "2026-10-02T10:00:02.250Z",
-- "source": "openvibe-node.worker"
++ "source": "openvibe-link.worker"
}
ddiff --git a/fixtures/platform.usage-sample/valid/run-function-second.json b/fixtures/platform.usage-sample/valid/run-function-second.json
iindex 50f802e9..f07fd91e 100644
---- a/fixtures/platform.usage-sample/valid/run-function-second.json
++++ b/fixtures/platform.usage-sample/valid/run-function-second.json
@@@ -10,5 +10,5 @@
"quantity": 1,
"unit": "s",
"at": "2026-10-02T10:00:00.250Z",
-- "source": "openvibe-node.worker"
++ "source": "openvibe-link.worker"
}
ddiff --git a/fixtures/website.share-connect-request/invalid/bad-transport.json b/fixtures/website.share-connect-request/invalid/bad-transport.json
iindex 93394eff..15393db5 100644
---- a/fixtures/website.share-connect-request/invalid/bad-transport.json
++++ b/fixtures/website.share-connect-request/invalid/bad-transport.json
@@@ -1,6 +1,6 @@
{
"share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"tunnel_token": "ovt_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9_one_time",
"transport": "websocket"
}
ddiff --git a/fixtures/website.share-connect-request/invalid/missing-token.json b/fixtures/website.share-connect-request/invalid/missing-token.json
iindex d35c6565..50ae7846 100644
---- a/fixtures/website.share-connect-request/invalid/missing-token.json
++++ b/fixtures/website.share-connect-request/invalid/missing-token.json
@@@ -1,4 +1,4 @@
{
"share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5"
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5"
}
ddiff --git a/fixtures/website.share-connect-request/valid/quic.json b/fixtures/website.share-connect-request/valid/quic.json
iindex 70e82797..d4e667a5 100644
---- a/fixtures/website.share-connect-request/valid/quic.json
++++ b/fixtures/website.share-connect-request/valid/quic.json
@@@ -1,6 +1,6 @@
{
"share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"tunnel_token": "ovt_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9_one_time",
"transport": "quic",
"node_version": "0.42.0",
ddiff --git a/fixtures/website.share-create-request/invalid/leading-hyphen.json b/fixtures/website.share-create-request/invalid/leading-hyphen.json
iindex ef0ddd0b..76b0d197 100644
---- a/fixtures/website.share-create-request/invalid/leading-hyphen.json
++++ b/fixtures/website.share-create-request/invalid/leading-hyphen.json
@@@ -2,7 +2,7 @@
"name": "-ab",
"protocol": "http",
"local_target": "127.0.0.1:3000",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"policy": {
"mode": "signin",
"allowed_projects": [
ddiff --git a/fixtures/website.share-create-request/invalid/policy-created-at.json b/fixtures/website.share-create-request/invalid/policy-created-at.json
iindex acfb3698..2d85b61f 100644
---- a/fixtures/website.share-create-request/invalid/policy-created-at.json
++++ b/fixtures/website.share-create-request/invalid/policy-created-at.json
@@@ -2,7 +2,7 @@
"name": "agents-alice",
"protocol": "http",
"local_target": "127.0.0.1:3000",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"policy": {
"mode": "signin",
"allowed_projects": [
ddiff --git a/fixtures/website.share-create-request/invalid/policy-owner.json b/fixtures/website.share-create-request/invalid/policy-owner.json
iindex 2f370260..52fee09b 100644
---- a/fixtures/website.share-create-request/invalid/policy-owner.json
++++ b/fixtures/website.share-create-request/invalid/policy-owner.json
@@@ -2,7 +2,7 @@
"name": "agents-alice",
"protocol": "http",
"local_target": "127.0.0.1:3000",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"policy": {
"mode": "signin",
"allowed_projects": [
ddiff --git a/fixtures/website.share-create-request/invalid/policy-password-set.json b/fixtures/website.share-create-request/invalid/policy-password-set.json
iindex 8dcbb62c..5dab5b1e 100644
---- a/fixtures/website.share-create-request/invalid/policy-password-set.json
++++ b/fixtures/website.share-create-request/invalid/policy-password-set.json
@@@ -2,7 +2,7 @@
"name": "agents-alice",
"protocol": "http",
"local_target": "127.0.0.1:3000",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"policy": {
"mode": "signin",
"allowed_projects": [
ddiff --git a/fixtures/website.share-create-request/invalid/policy-project-id.json b/fixtures/website.share-create-request/invalid/policy-project-id.json
iindex 2fcba219..35e0c9bd 100644
---- a/fixtures/website.share-create-request/invalid/policy-project-id.json
++++ b/fixtures/website.share-create-request/invalid/policy-project-id.json
@@@ -2,7 +2,7 @@
"name": "agents-alice",
"protocol": "http",
"local_target": "127.0.0.1:3000",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"policy": {
"mode": "signin",
"allowed_projects": [
ddiff --git a/fixtures/website.share-create-request/invalid/policy-updated-at.json b/fixtures/website.share-create-request/invalid/policy-updated-at.json
iindex d2905ec5..369aaf01 100644
---- a/fixtures/website.share-create-request/invalid/policy-updated-at.json
++++ b/fixtures/website.share-create-request/invalid/policy-updated-at.json
@@@ -2,7 +2,7 @@
"name": "agents-alice",
"protocol": "http",
"local_target": "127.0.0.1:3000",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"policy": {
"mode": "signin",
"allowed_projects": [
ddiff --git a/fixtures/website.share-create-request/invalid/policy-version.json b/fixtures/website.share-create-request/invalid/policy-version.json
iindex 664b6489..64c15d48 100644
---- a/fixtures/website.share-create-request/invalid/policy-version.json
++++ b/fixtures/website.share-create-request/invalid/policy-version.json
@@@ -2,7 +2,7 @@
"name": "agents-alice",
"protocol": "http",
"local_target": "127.0.0.1:3000",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"policy": {
"mode": "signin",
"allowed_projects": [
ddiff --git a/fixtures/website.share-create-request/invalid/too-long.json b/fixtures/website.share-create-request/invalid/too-long.json
iindex cc18fa0b..05d322b9 100644
---- a/fixtures/website.share-create-request/invalid/too-long.json
++++ b/fixtures/website.share-create-request/invalid/too-long.json
@@@ -2,7 +2,7 @@
"name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"protocol": "http",
"local_target": "127.0.0.1:3000",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"policy": {
"mode": "signin",
"allowed_projects": [
ddiff --git a/fixtures/website.share-create-request/invalid/trailing-hyphen.json b/fixtures/website.share-create-request/invalid/trailing-hyphen.json
iindex 25ed9ef8..a56c7c2f 100644
---- a/fixtures/website.share-create-request/invalid/trailing-hyphen.json
++++ b/fixtures/website.share-create-request/invalid/trailing-hyphen.json
@@@ -2,7 +2,7 @@
"name": "ab-",
"protocol": "http",
"local_target": "127.0.0.1:3000",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"policy": {
"mode": "signin",
"allowed_projects": [
ddiff --git a/fixtures/website.share-create-request/valid/agents-portal.json b/fixtures/website.share-create-request/valid/agents-portal.json
iindex b0ff0e7f..8776bf96 100644
---- a/fixtures/website.share-create-request/valid/agents-portal.json
++++ b/fixtures/website.share-create-request/valid/agents-portal.json
@@@ -2,7 +2,7 @@
"name": "agents-alice",
"protocol": "http",
"local_target": "127.0.0.1:3000",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"policy": {
"mode": "signin",
"allowed_projects": [
ddiff --git a/fixtures/website.share-create-request/valid/existing-policy-id.json b/fixtures/website.share-create-request/valid/existing-policy-id.json
iindex 347e0e7b..fb617ea8 100644
---- a/fixtures/website.share-create-request/valid/existing-policy-id.json
++++ b/fixtures/website.share-create-request/valid/existing-policy-id.json
@@@ -2,7 +2,7 @@
"name": "agents-alice",
"protocol": "http",
"local_target": "127.0.0.1:3000",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"policy": {
"id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
"mode": "signin"
ddiff --git a/fixtures/website.share-create-request/valid/max-label.json b/fixtures/website.share-create-request/valid/max-label.json
iindex 49d81f33..c44e6c10 100644
---- a/fixtures/website.share-create-request/valid/max-label.json
++++ b/fixtures/website.share-create-request/valid/max-label.json
@@@ -2,7 +2,7 @@
"name": "a-------------------------------------------------------------z",
"protocol": "http",
"local_target": "127.0.0.1:3000",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"policy": {
"mode": "signin",
"allowed_projects": [
ddiff --git a/fixtures/website.share-create-request/valid/one-char.json b/fixtures/website.share-create-request/valid/one-char.json
iindex ce6fa4ed..a77df9ed 100644
---- a/fixtures/website.share-create-request/valid/one-char.json
++++ b/fixtures/website.share-create-request/valid/one-char.json
@@@ -2,7 +2,7 @@
"name": "a",
"protocol": "http",
"local_target": "127.0.0.1:3000",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"policy": {
"mode": "signin",
"allowed_projects": [
ddiff --git a/fixtures/website.share-create-request/valid/two-char.json b/fixtures/website.share-create-request/valid/two-char.json
iindex 142b5c34..8c25e7fc 100644
---- a/fixtures/website.share-create-request/valid/two-char.json
++++ b/fixtures/website.share-create-request/valid/two-char.json
@@@ -2,7 +2,7 @@
"name": "ab",
"protocol": "http",
"local_target": "127.0.0.1:3000",
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"policy": {
"mode": "signin",
"allowed_projects": [
ddiff --git a/fixtures/website.share/invalid/leading-hyphen.json b/fixtures/website.share/invalid/leading-hyphen.json
iindex 6f96339f..8e2211bc 100644
---- a/fixtures/website.share/invalid/leading-hyphen.json
++++ b/fixtures/website.share/invalid/leading-hyphen.json
@@@ -5,7 +5,7 @@
"id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
},
"project_id": null,
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"name": "-ab",
"hostname": "-ab.openvibe.website",
"protocol": "http",
ddiff --git a/fixtures/website.share/invalid/too-long.json b/fixtures/website.share/invalid/too-long.json
iindex 86d2d208..21e7629e 100644
---- a/fixtures/website.share/invalid/too-long.json
++++ b/fixtures/website.share/invalid/too-long.json
@@@ -5,7 +5,7 @@
"id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
},
"project_id": null,
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"hostname": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.openvibe.website",
"protocol": "http",
ddiff --git a/fixtures/website.share/invalid/trailing-hyphen.json b/fixtures/website.share/invalid/trailing-hyphen.json
iindex 30068c39..b86d96c0 100644
---- a/fixtures/website.share/invalid/trailing-hyphen.json
++++ b/fixtures/website.share/invalid/trailing-hyphen.json
@@@ -5,7 +5,7 @@
"id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
},
"project_id": null,
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"name": "ab-",
"hostname": "ab-.openvibe.website",
"protocol": "http",
ddiff --git a/fixtures/website.share/valid/agents-portal.json b/fixtures/website.share/valid/agents-portal.json
iindex 2c8f7a28..82f65cd9 100644
---- a/fixtures/website.share/valid/agents-portal.json
++++ b/fixtures/website.share/valid/agents-portal.json
@@@ -5,7 +5,7 @@
"id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
},
"project_id": null,
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"name": "agents-alice",
"hostname": "agents-alice.openvibe.website",
"protocol": "http",
ddiff --git a/fixtures/website.share/valid/max-label.json b/fixtures/website.share/valid/max-label.json
iindex f6497e4d..dc09ebb9 100644
---- a/fixtures/website.share/valid/max-label.json
++++ b/fixtures/website.share/valid/max-label.json
@@@ -5,7 +5,7 @@
"id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
},
"project_id": null,
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"name": "a-------------------------------------------------------------z",
"hostname": "a-------------------------------------------------------------z.openvibe.website",
"protocol": "http",
ddiff --git a/fixtures/website.share/valid/one-char.json b/fixtures/website.share/valid/one-char.json
iindex b7e6aa6b..f5c60fd5 100644
---- a/fixtures/website.share/valid/one-char.json
++++ b/fixtures/website.share/valid/one-char.json
@@@ -5,7 +5,7 @@
"id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
},
"project_id": null,
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"name": "a",
"hostname": "a.openvibe.website",
"protocol": "http",
ddiff --git a/fixtures/website.share/valid/two-char.json b/fixtures/website.share/valid/two-char.json
iindex 0d70a908..d68abcef 100644
---- a/fixtures/website.share/valid/two-char.json
++++ b/fixtures/website.share/valid/two-char.json
@@@ -5,7 +5,7 @@
"id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
},
"project_id": null,
-- "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
++ "node_id": "lnk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
"name": "ab",
"hostname": "ab.openvibe.website",
"protocol": "http",
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex f76f1b78..8c6ffc33 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -264,14 +264,14 @@
},
"sub": {
"type": "string",
-- "pattern": "^(svc:[a-z][a-z0-9-]{1,39}|app:app_[0-9A-HJKMNP-TV-Z]{26}|mod:mod_[0-9A-HJKMNP-TV-Z]{26}|node:nod_[0-9A-HJKMNP-TV-Z]{26}|agent:agt_[0-9A-HJKMNP-TV-Z]{26})$"
++ "pattern": "^(svc:[a-z][a-z0-9-]{1,39}|app:app_[0-9A-HJKMNP-TV-Z]{26}|mod:mod_[0-9A-HJKMNP-TV-Z]{26}|link:lnk_[0-9A-HJKMNP-TV-Z]{26}|agent:agt_[0-9A-HJKMNP-TV-Z]{26})$"
},
"actor_type": {
"enum": [
"service",
"app",
"mod",
-- "node",
++ "link",
"agent"
]
},
@@@ -15739,14 +15739,14 @@
"enum": [
"openai",
"anthropic",
-- "node"
++ "link"
],
"description": "openai = any OpenAI-compatible endpoint. node = the person's own machine: a model on their own model server, through their OpenVibe Link and OpenVibe.Bot (no key, no base_url, no cost)."
},
"machine": {
"type": "string",
"pattern": "^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$",
-- "description": "provider node: the person's own machine (openvibe.bot/machines) whose OpenVibe Link runs the model (the ai plugin, contracts 0.219.0), or `any` (contracts 0.221.0): whichever of their machines is on, lets their AI use its models and offers the model, the fastest first, the next one when it is busy or away. Never anyone else's machine."
++ "description": "provider link: the person's own machine (openvibe.link/machines) whose OpenVibe Link runs the model (the ai plugin, contracts 0.219.0), or `any` (contracts 0.221.0): whichever of their machines is on, lets their AI use its models and offers the model, the fastest first, the next one when it is busy or away. Never anyone else's machine."
},
"base_url": {
"type": "string",
@@@ -15804,7 +15804,7 @@
"if": {
"properties": {
"provider": {
-- "const": "node"
++ "const": "link"
}
},
"required": [
@@@ -15857,7 +15857,7 @@
"minimax",
"openrouter",
"custom",
-- "node"
++ "link"
],
"description": "anthropic = Anthropic's Messages API; openai = any OpenAI-compatible endpoint; deepseek, minimax and openrouter = those providers; custom = the endpoint in base_url (a URL that speaks the OpenAI or the Anthropic format, by the route it is used on); node = the person's own machine (no key, no base_url, no cost)."
},
@@@ -15882,7 +15882,7 @@
"machine": {
"type": "string",
"pattern": "^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$",
-- "description": "provider node: the person's own machine (openvibe.bot/machines) whose OpenVibe Link runs the model, or `any` of their own."
++ "description": "provider link: the person's own machine (openvibe.link/machines) whose OpenVibe Link runs the model, or `any` of their own."
},
"models": {
"type": "object",
@@@ -15929,7 +15929,7 @@
"if": {
"properties": {
"provider": {
-- "const": "node"
++ "const": "link"
}
},
"required": [
@@@ -16013,7 +16013,7 @@
"enum": [
"openai",
"anthropic",
-- "node"
++ "link"
]
},
"machine": {
@@@ -16022,7 +16022,7 @@
"null"
],
"pattern": "^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$",
-- "description": "provider node: the machine it runs on, or `any` of the person's own (contracts 0.221.0)"
++ "description": "provider link: the machine it runs on, or `any` of the person's own (contracts 0.221.0)"
},
"base_url": {
"type": [
@@@ -16155,7 +16155,7 @@
"minimax",
"openrouter",
"custom",
-- "node"
++ "link"
]
},
"machine": {
@@@ -16164,7 +16164,7 @@
"null"
],
"pattern": "^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$",
-- "description": "provider node: the machine it runs on, or `any` of the person's own"
++ "description": "provider link: the machine it runs on, or `any` of the person's own"
},
"base_url": {
"type": [
@@@ -16243,7 +16243,7 @@
"properties": {
"provider": {
"not": {
-- "const": "node"
++ "const": "link"
}
}
},
@@@ -18773,7 +18773,7 @@
},
"operator": {
"type": "string",
-- "description": "Operator slug or `owner:<subject_id>` for a private node."
++ "description": "Operator slug or `owner:<subject_id>` for a private machine."
},
"host": {
"type": [
@@@ -54015,7 +54015,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/watch/watch.v1.json",
"title": "Watch",
-- "description": "watch.watch@1: a user-defined persistent observation (plan T18). It names a source (a push event, a webhook, a conditional HTTP URL, a feed or API, a local Node probe, or an expensive Run check), how a value is extracted, how it is compared with the last observation, the condition that fires, the actions it wakes, its budget and its retention. Carrier preference is binding: an event or webhook source is used when one exists, then ETag/Last-Modified, then a feed, then an API, and browser polling through Run only when none works. A price or stock is 'as of' the observation, null when it stated none.",
++ "description": "watch.watch@1: a user-defined persistent observation (plan T18). It names a source (a push event, a webhook, a conditional HTTP URL, a feed or API, a local Link probe, or an expensive Run check), how a value is extracted, how it is compared with the last observation, the condition that fires, the actions it wakes, its budget and its retention. Carrier preference is binding: an event or webhook source is used when one exists, then ETag/Last-Modified, then a feed, then an API, and browser polling through Run only when none works. A price or stock is 'as of' the observation, null when it stated none.",
"type": "object",
"additionalProperties": false,
"required": [
@@@ -72957,15 +72957,15 @@
}
}
},
-- "network.node": {
++ "network.link": {
"version": "1.0.0",
"owner": "network",
"status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
-- "$id": "https://openvibe.network/contracts/network/node.v1.json",
-- "title": "NetworkNode",
-- "description": "network.node@1: one machine of the OpenVibe platform as the node registry lists it (ADR-034 section 12; roadmap WS-X1). Roles are what it serves; location is region-level; the beacon is an HTTPS URL a client times to measure its own round trip. No addresses, capacity or secrets are public: a node is found by role and location, and reached through the service that uses it.",
++ "$id": "https://openvibe.network/contracts/network/link.v1.json",
++ "title": "NetworkLink",
++ "description": "network.link@1: one machine of the OpenVibe platform as the node registry lists it (ADR-034 section 12; roadmap WS-X1). Roles are what it serves; location is region-level; the beacon is an HTTPS URL a client times to measure its own round trip. No addresses, capacity or secrets are public: a node is found by role and location, and reached through the service that uses it.",
"type": "object",
"required": [
"id",
@@@ -73081,15 +73081,15 @@
}
}
},
-- "network.node-report-request": {
++ "network.link-report-request": {
"version": "1.0.0",
"owner": "network",
"status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
-- "$id": "https://openvibe.network/contracts/network/node-report-request.v1.json",
-- "title": "NetworkNodeReportRequest",
-- "description": "network.node-report-request@1: POST /internal/nodes/report on OpenVibe.Network (capability network.node.report, held by the Host principal). The complete set of nodes one Host inventory knows, with their health now: a node absent from a later report of the same source is marked down, never deleted silently.",
++ "$id": "https://openvibe.network/contracts/network/link-report-request.v1.json",
++ "title": "NetworkLinkReportRequest",
++ "description": "network.link-report-request@1: POST /internal/links/report on OpenVibe.Network (capability network.link.report, held by the Host principal). The complete set of nodes one Host inventory knows, with their health now: a node absent from a later report of the same source is marked down, never deleted silently.",
"type": "object",
"required": [
"source",
@@@ -73106,21 +73106,21 @@
"type": "array",
"maxItems": 500,
"items": {
-- "$ref": "node.v1.json"
++ "$ref": "link.v1.json"
}
}
}
}
},
-- "network.node-list-result": {
++ "network.link-list-result": {
"version": "1.0.0",
"owner": "network",
"status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
-- "$id": "https://openvibe.network/contracts/network/node-list-result.v1.json",
-- "title": "NetworkNodeListResult",
-- "description": "network.node-list-result@1: GET /api/v1/nodes on OpenVibe.Network (public, cacheable): every node of the platform, filterable by ?role= and ?region=. The input of openvibe-sdk/geo's nearest().",
++ "$id": "https://openvibe.network/contracts/network/link-list-result.v1.json",
++ "title": "NetworkLinkListResult",
++ "description": "network.link-list-result@1: GET /api/v1/links on OpenVibe.Network (public, cacheable): every node of the platform, filterable by ?role= and ?region=. The input of openvibe-sdk/geo's nearest().",
"type": "object",
"required": [
"nodes",
@@@ -73131,7 +73131,7 @@
"nodes": {
"type": "array",
"items": {
-- "$ref": "node.v1.json"
++ "$ref": "link.v1.json"
}
},
"generated_at": {
@@@ -73167,8 +73167,8 @@
},
"principal_id": {
"type": "string",
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
-- "description": "The machine's node principal on Network (nod_…): the credential it holds. Absent for a machine Bot knows but Network has no principal for."
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's node principal on Network (lnk_…): the credential it holds. Absent for a machine Bot knows but Network has no principal for."
},
"name": {
"type": "string",
@@@ -73457,7 +73457,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/network/machine-pairing.v1.json",
"title": "NetworkMachinePairing",
-- "description": "network.machine-pairing@1: a one-time code that pairs a machine with the person's account (POST /api/v1/machines/pairings, capability machines.manage). The code is shown once, expires, and works once: the machine redeems it and becomes the person's. The command is what the owner runs on the machine (openvibe.bot/install). The code is never stored in the clear and never returned again.",
++ "description": "network.machine-pairing@1: a one-time code that pairs a machine with the person's account (POST /api/v1/machines/pairings, capability machines.manage). The code is shown once, expires, and works once: the machine redeems it and becomes the person's. The command is what the owner runs on the machine (openvibe.link/install). The code is never stored in the clear and never returned again.",
"type": "object",
"required": [
"code",
@@@ -73484,7 +73484,7 @@
"installer": {
"type": "string",
"maxLength": 500,
-- "description": "The command the owner runs on the machine (openvibe.bot/install, with the network, the pairing id and the code)."
++ "description": "The command the owner runs on the machine (openvibe.link/install, with the network, the pairing id and the code)."
}
}
}
@@@ -73512,7 +73512,7 @@
},
"principal_id": {
"type": "string",
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
"description": "The machine's node principal on Network, when it has one."
},
"owner": {
@@@ -73550,7 +73550,7 @@
},
"principal_id": {
"type": "string",
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
"description": "The machine's node principal on Network, when it has one."
},
"owner": {
@@@ -73573,7 +73573,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/network.machine.paired.v1.json",
"title": "NetworkMachinePairedPayload",
-- "description": "network.machine.paired v1 (OpenVibe.Network, plan L34: the machines hub). A machine was paired with a person's account: it redeemed the one-time code and became theirs. It holds a credential from then on; the code is spent. The machine's device id in Bot is absent until the machine checks in for the first time. Envelope: subject { type: machine, id: <nod_…> }, visibility internal, actor service:network.",
++ "description": "network.machine.paired v1 (OpenVibe.Network, plan L34: the machines hub). A machine was paired with a person's account: it redeemed the one-time code and became theirs. It holds a credential from then on; the code is spent. The machine's device id in Bot is absent until the machine checks in for the first time. Envelope: subject { type: machine, id: <lnk_…> }, visibility internal, actor service:network.",
"type": "object",
"required": [
"principal_id",
@@@ -73594,7 +73594,7 @@
},
"principal_id": {
"type": "string",
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
"description": "The node principal the pairing created. Also the envelope subject id."
},
"owner": {
@@@ -73680,7 +73680,7 @@
},
"principal_id": {
"type": "string",
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
"description": "The machine's node principal on Network, when it had one."
},
"owner": {
@@@ -73879,7 +73879,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/platform/resource-offer.v1.json",
"title": "ResourceOffer",
-- "description": "What one node or external provider can run right now (roadmap WS-Z9, decision 43): capabilities, multidimensional capacity, measured latencies, health and pricing. First-party only: it carries capacity the public node registry (network.node@1) deliberately leaves out.",
++ "description": "What one node or external provider can run right now (roadmap WS-Z9, decision 43): capabilities, multidimensional capacity, measured latencies, health and pricing. First-party only: it carries capacity the public node registry (network.link@1) deliberately leaves out.",
"type": "object",
"additionalProperties": false,
"required": [
@@@ -73910,7 +73910,7 @@
},
"node_id": {
"type": "string",
-- "description": "network.node@1 id, for kind node"
++ "description": "network.link@1 id, for kind node"
},
"provider": {
"type": "string",
@@@ -73948,7 +73948,7 @@
"type": "string",
"pattern": "^[a-z][a-z0-9-]*:[a-z0-9.-]+$"
},
-- "description": "e.g. node:http, worker:browser, worker:ffmpeg, worker:ai-gpu, events:gateway, events:durable, object:r2. Kind harness: task:edit, task:review, task:browse, task:run, task:test and task:plan for the task kinds it takes (its detail's task_capabilities), and harness:mcp, harness:resume, harness:host-access and harness:long-autonomy for its detail's capabilities that are true. The `worker:<class>` names that map to platform.runtime-class@1 values are reserved for OpenVibe Link's job worker, one per class: worker:function, worker:code, worker:browser, worker:linux, worker:desktop and worker:gpu ($defs.reservedWorkerCapabilities). A node advertises the name of each class it runs (Node's status.capabilities.worker); every other `worker:` name (worker:ffmpeg, worker:ai-gpu) is an ordinary capability name. Beside them, a kind harness names `runtime:<class>` for each platform.runtime-class@1 class it can run (runtime:function, runtime:code, runtime:browser, runtime:linux, runtime:desktop, runtime:gpu); these are ordinary capability names, not reserved, and OpenVibe.Codes already advertises the classes it offers."
++ "description": "e.g. node:http, worker:browser, worker:ffmpeg, worker:ai-gpu, events:gateway, events:durable, object:r2. Kind harness: task:edit, task:review, task:browse, task:run, task:test and task:plan for the task kinds it takes (its detail's task_capabilities), and harness:mcp, harness:resume, harness:host-access and harness:long-autonomy for its detail's capabilities that are true. The `worker:<class>` names that map to platform.runtime-class@1 values are reserved for OpenVibe Link's job worker, one per class: worker:function, worker:code, worker:browser, worker:linux, worker:desktop and worker:gpu ($defs.reservedWorkerCapabilities). A Link advertises the name of each class it runs (Link's status.capabilities.worker); every other `worker:` name (worker:ffmpeg, worker:ai-gpu) is an ordinary capability name. Beside them, a kind harness names `runtime:<class>` for each platform.runtime-class@1 class it can run (runtime:function, runtime:code, runtime:browser, runtime:linux, runtime:desktop, runtime:gpu); these are ordinary capability names, not reserved, and OpenVibe.Codes already advertises the classes it offers."
},
"capacity": {
"type": "object",
@@@ -75221,7 +75221,7 @@
"quantity": 1,
"unit": "s",
"at": "2026-10-02T10:00:00.250Z",
-- "source": "openvibe-node.worker"
++ "source": "openvibe-link.worker"
}
]
}
@@@ -75357,15 +75357,222 @@
]
}
},
-- "platform.node-capabilities": {
++ "link.compute": {
"version": "1.0.0",
"owner": "network",
"status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
-- "$id": "https://openvibe.network/contracts/platform/node-capabilities.v1.json",
-- "title": "NodeCapabilities",
-- "description": "Node hardware, placement regions, tags, and operating costs (T1 Universal Fabric; ADR-034 proposed).",
++ "$id": "https://openvibe.network/contracts/link/compute.v1.json",
++ "title": "LinkCompute",
++ "description": "Where a person's work can run. Only ready candidates may be picked; a missing rental quote is never a price.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "sources",
++ "candidates"
++ ],
++ "properties": {
++ "sources": {
++ "type": "array",
++ "items": {
++ "$ref": "#/$defs/source"
++ }
++ },
++ "candidates": {
++ "type": "array",
++ "items": {
++ "$ref": "#/$defs/candidate"
++ }
++ }
++ },
++ "$defs": {
++ "kind": {
++ "enum": [
++ "personal",
++ "cloud",
++ "exchange",
++ "community"
++ ]
++ },
++ "status": {
++ "enum": [
++ "ready",
++ "unavailable",
++ "coming_soon"
++ ]
++ },
++ "price": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "additionalProperties": false,
++ "required": [
++ "amount_usd",
++ "unit"
++ ],
++ "properties": {
++ "amount_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "unit": {
++ "enum": [
++ "hour"
++ ]
++ }
++ }
++ },
++ "source": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind",
++ "name",
++ "status",
++ "reason"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "principal_id": {
++ "type": "string",
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "kind": {
++ "$ref": "#/$defs/kind"
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1
++ },
++ "status": {
++ "$ref": "#/$defs/status"
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "price": {
++ "$ref": "#/$defs/price"
++ },
++ "quoted_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ },
++ "if": {
++ "properties": {
++ "kind": {
++ "const": "exchange"
++ },
++ "status": {
++ "const": "ready"
++ }
++ },
++ "required": [
++ "kind",
++ "status"
++ ]
++ },
++ "then": {
++ "required": [
++ "price",
++ "quoted_at",
++ "expires_at"
++ ],
++ "properties": {
++ "price": {
++ "type": "object"
++ }
++ }
++ }
++ },
++ "candidate": {
++ "allOf": [
++ {
++ "$ref": "#/$defs/source"
++ },
++ {
++ "type": "object",
++ "required": [
++ "id",
++ "price"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "minLength": 1
++ }
++ }
++ }
++ ]
++ }
++ },
++ "examples": [
++ {
++ "sources": [
++ {
++ "kind": "personal",
++ "name": "Your Link machines",
++ "status": "ready",
++ "reason": null
++ },
++ {
++ "kind": "cloud",
++ "name": "OpenVibe cloud machines",
++ "status": "coming_soon",
++ "reason": "No cloud pool is accepting jobs yet."
++ },
++ {
++ "kind": "exchange",
++ "name": "Exchange GPU rentals",
++ "status": "coming_soon",
++ "reason": "A live quote is required.",
++ "price": null
++ },
++ {
++ "kind": "community",
++ "name": "Community machines",
++ "status": "coming_soon",
++ "reason": "Shared placement is being connected."
++ }
++ ],
++ "candidates": [
++ {
++ "id": "dev_01J0000000000000000000000",
++ "kind": "personal",
++ "name": "Studio",
++ "status": "ready",
++ "reason": null,
++ "price": {
++ "amount_usd": 0,
++ "unit": "hour"
++ }
++ }
++ ]
++ }
++ ]
++ }
++ },
++ "platform.link-capabilities": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/platform/link-capabilities.v1.json",
++ "title": "LinkCapabilities",
++ "description": "Link machine hardware, placement regions, tags, and operating costs (T1 Universal Fabric; ADR-034 proposed).",
"$comment": "Additive optional fields may extend this v1 contract.",
"type": "object",
"additionalProperties": false,
@@@ -76923,7 +77130,7 @@
{
"type": "object",
"additionalProperties": false,
-- "description": "Device to server: wall-clock second `second` of the job has fully elapsed. Metering (platform.usage-sample@1, written through billing.usage.record by whoever terminates the link: OpenVibe.Bot now, Fabric later): second n is [started_ms + n*1000, started_ms + (n+1)*1000) on the worker's monotonic clock. A worker sends `job_usage` for n only once that second has fully elapsed, so every `job_usage` stands for quantity 1; the partial last second is never sent here, only in `job_exit`. Each second is exactly one reading: `id` and `idempotency_key` `run:<job id>:<n>`, `service` `run`, `operation` `function.invoke`, `unit` `s`, `quantity` 1 (the partial last second: (wall_ms mod 1000)/1000, never 0), `at` new Date(started_ms + n*1000).toISOString(), `resource` the job id, `node` the worker's device id, `source` `openvibe-node.worker`, and `project`, `subject` and `provider` from the dispatcher's own record of the job. No field depends on when or how often a frame was sent, and no rating fields (`cost_estimate`, `free_allowance_used`, `vibes_charged`) are set, because Billing keeps the first reading under a key, replays an identical one and refuses a different one (409): a resend after a reconnect, a `job_usage` and the `job_exit` backfill of the same second, and two writers of one job all land on one record.",
++ "description": "Device to server: wall-clock second `second` of the job has fully elapsed. Metering (platform.usage-sample@1, written through billing.usage.record by whoever terminates the link: OpenVibe.Bot now, Fabric later): second n is [started_ms + n*1000, started_ms + (n+1)*1000) on the worker's monotonic clock. A worker sends `job_usage` for n only once that second has fully elapsed, so every `job_usage` stands for quantity 1; the partial last second is never sent here, only in `job_exit`. Each second is exactly one reading: `id` and `idempotency_key` `run:<job id>:<n>`, `service` `run`, `operation` `function.invoke`, `unit` `s`, `quantity` 1 (the partial last second: (wall_ms mod 1000)/1000, never 0), `at` new Date(started_ms + n*1000).toISOString(), `resource` the job id, `node` the worker's device id, `source` `openvibe-link.worker`, and `project`, `subject` and `provider` from the dispatcher's own record of the job. No field depends on when or how often a frame was sent, and no rating fields (`cost_estimate`, `free_allowance_used`, `vibes_charged`) are set, because Billing keeps the first reading under a key, replays an identical one and refuses a different one (409): a resend after a reconnect, a `job_usage` and the `job_exit` backfill of the same second, and two writers of one job all land on one record.",
"required": [
"v",
"seq",
@@@ -82245,7 +82452,7 @@
"null"
],
"maxLength": 128,
-- "description": "The offer's provider, as Run sent it; null for a first-party node."
++ "description": "The offer's provider, as Run sent it; null for a first-party machine."
},
"cancel_requested": {
"type": "boolean",
@@@ -83789,7 +83996,7 @@
"null"
],
"maxLength": 128,
-- "description": "The offer's provider (the usage reading's `provider`); null for a first-party node."
++ "description": "The offer's provider (the usage reading's `provider`); null for a first-party machine."
},
"region": {
"type": [
@@@ -85328,15 +85535,15 @@
}
}
},
-- "network.node.revoked": {
++ "network.link.revoked": {
"version": "1.0.0",
"owner": "network",
"status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
-- "$id": "https://openvibe.network/contracts/events/payloads/network.node.revoked.v1.json",
-- "title": "NetworkNodeRevokedPayload",
-- "description": "network.node.revoked v1 (OpenVibe.Network; plan T2, docs/t2-cells-and-node-principal.md section 10, migrations/0014_node_pairing.sql). A node principal (nod_…), a person's paired machine, was revoked: through POST /internal/node-principals/:id/revoke (network.node.manage, the service that paired it), through POST /api/v1/me/nodes/:id/revoke (the owner's session), or with the owner's account. Network revokes the row and clears the credential and its grace window; a machine holding a live node token is refused at its next use (≤ 300 s), and until this event only learned of the revoke at its next reauth (≤ 330 s). A consumer stops the machine at once; a second revoke changes nothing and emits nothing. The credential and its hash are never carried. Envelope: subject { type: node_principal, id: <nod_…> }, visibility internal, actor the service (svc:<service>) or the owner ({ type: user }) that revoked.",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.link.revoked.v1.json",
++ "title": "NetworkLinkRevokedPayload",
++ "description": "network.link.revoked v1 (OpenVibe.Network; plan T2, docs/t2-cells-and-node-principal.md section 10, migrations/0014_node_pairing.sql). A node principal (lnk_…), a person's paired machine, was revoked: through POST /internal/link-principals/:id/revoke (network.link.manage, the service that paired it), through POST /api/v1/me/nodes/:id/revoke (the owner's session), or with the owner's account. Network revokes the row and clears the credential and its grace window; a machine holding a live node token is refused at its next use (≤ 300 s), and until this event only learned of the revoke at its next reauth (≤ 330 s). A consumer stops the machine at once; a second revoke changes nothing and emits nothing. The credential and its hash are never carried. Envelope: subject { type: node_principal, id: <lnk_…> }, visibility internal, actor the service (svc:<service>) or the owner ({ type: user }) that revoked.",
"type": "object",
"required": [
"node_id",
@@@ -85353,7 +85560,7 @@
},
"principal_id": {
"type": "string",
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
"description": "The revoked node principal. Also the envelope subject id."
},
"owner": {
@@@ -103862,7 +104069,7 @@
"node",
"submitted"
],
-- "description": "codes: an openvibe.codes task. run: a Run worker. node: the person's own machine. submitted: nobody ran it."
++ "description": "codes: an openvibe.codes task. run: a Run worker. link: the person's own machine. submitted: nobody ran it."
},
"id": {
"type": "string",
@@@ -104111,7 +104318,7 @@
"node",
"submitted"
],
-- "description": "codes: an openvibe.codes task. run: a Run worker. node: the person's own machine. submitted: nobody ran it."
++ "description": "codes: an openvibe.codes task. run: a Run worker. link: the person's own machine. submitted: nobody ran it."
},
"id": {
"type": "string",
@@@ -105252,7 +105459,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/website/share.v1.json",
"title": "WebsiteShare",
-- "description": "website.share@1: one Share on OpenVibe.Website Connect, a local port published at a stable hostname through the edge relay. The record says who owns it, which project scopes it, which Node tunnel serves it, which local target it may reach, which policy version the edge enforces and where it is in its life (draft, review, active, paused, revoked). The tunnel token, the request bodies the inspector captures and the policy's password hash are never part of this document.",
++ "description": "website.share@1: one Share on OpenVibe.Website Connect, a local port published at a stable hostname through the edge relay. The record says who owns it, which project scopes it, which Link tunnel serves it, which local target it may reach, which policy version the edge enforces and where it is in its life (draft, review, active, paused, revoked). The tunnel token, the request bodies the inspector captures and the policy's password hash are never part of this document.",
"type": "object",
"additionalProperties": false,
"required": [
@@@ -105294,7 +105501,7 @@
"string",
"null"
],
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
"description": "The Network node principal expected to serve the tunnel; null until one is bound."
},
"name": {
@@@ -105372,7 +105579,7 @@
"null"
],
"format": "date-time",
-- "description": "The last successful Node liveness check; null until the first tunnel."
++ "description": "The last successful Link liveness check; null until the first tunnel."
},
"created_at": {
"type": "string",
@@@ -105455,8 +105662,8 @@
},
"node_id": {
"type": "string",
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
-- "description": "Optional: bind the share to this Node principal now. Omit to bind it when the first tunnel connects."
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Optional: bind the share to this Link principal now. Omit to bind it when the first tunnel connects."
},
"region": {
"type": "string",
@@@ -105489,7 +105696,7 @@
},
"node_id": {
"type": "string",
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
"description": "Must equal the node principal in the Network token; a mismatch is refused and audited."
},
"tunnel_token": {
@@@ -105995,7 +106202,7 @@
"string",
"null"
],
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
"description": "The Network node principal the device presents; null for an agent or server that joins through a pre-auth key."
},
"name": {
@@@ -111902,7 +112109,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/bot/machine-pool.v1.json",
"title": "BotMachinePool",
-- "description": "bot.machine-pool@1 (the pooled machines design, 2026-11-06): several of one owner's machines on the same local network that serve one model together through llama.cpp RPC, `rpc-server` on the members and `llama-server --rpc` on the head. OpenVibe.Network holds the record: the owner proposes it on /machines (POST /api/v1/machine-pools, two to eight of their own machines and an optional head; the default head has the most free VRAM), and each machine reads it with its own node token (GET /api/v1/node/self/pools/:id, network.node.self.manage) and approves it locally (POST /api/v1/node/self/pools/:id/approve with { certificate }). A machine's `certificate` is null until it approves on that machine: the Link generates an Ed25519 key there, keeps the private key, and uploads only its self-signed public certificate, which the other members pin for the pool's private link. A pool with any null certificate never serves. The record never carries a private key or a prompt. What a schema cannot say, Network and Node check at run time: the members are distinct and the head is one of them; every member has the pool's owner, today, through Bot; every member was seen from the same public address with a private address in the same subnet; the head's direct link probe to each member passes; each machine's certificate is its own; and any change of membership, owner, head or key revokes the pool, so all machines approve a new one. A machine that leaves (removed, signed out, a different owner) drops its link and its key at once.",
++ "description": "bot.machine-pool@1 (the pooled machines design, 2026-11-06): several of one owner's machines on the same local network that serve one model together through llama.cpp RPC, `rpc-server` on the members and `llama-server --rpc` on the head. OpenVibe.Network holds the record: the owner proposes it on /machines (POST /api/v1/machine-pools, two to eight of their own machines and an optional head; the default head has the most free VRAM), and each machine reads it with its own Link token (GET /api/v1/link/self/pools/:id, network.link.self.manage) and approves it locally (POST /api/v1/link/self/pools/:id/approve with { certificate }). A machine's `certificate` is null until it approves on that machine: the Link generates an Ed25519 key there, keeps the private key, and uploads only its self-signed public certificate, which the other members pin for the pool's private link. A pool with any null certificate never serves. The record never carries a private key or a prompt. What a schema cannot say, Network and Link check at run time: the members are distinct and the head is one of them; every member has the pool's owner, today, through Bot; every member was seen from the same public address with a private address in the same subnet; the head's direct link probe to each member passes; each machine's certificate is its own; and any change of membership, owner, head or key revokes the pool, so all machines approve a new one. A machine that leaves (removed, signed out, a different owner) drops its link and its key at once.",
"type": "object",
"additionalProperties": false,
"required": [
ddiff --git a/generated/openapi/actor.json b/generated/openapi/actor.json
iindex 2afb486d..68b0d337 100644
---- a/generated/openapi/actor.json
++++ b/generated/openapi/actor.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Actor API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex e434097e..97060005 100644
---- a/generated/openapi/ai.json
++++ b/generated/openapi/ai.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.AI API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -773,7 +773,7 @@
"enum": [
"openai",
"anthropic",
-- "node"
++ "link"
],
"description": "openai = any OpenAI-compatible endpoint. node = the person's own machine: a model on their own model server, through their OpenVibe Link and OpenVibe.Bot (no key, no base_url, no cost)."
},
@@@ -786,9 +786,9 @@
"schema": {
"type": "string",
"pattern": "^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$",
-- "description": "provider node: the person's own machine (openvibe.bot/machines) whose OpenVibe Link runs the model (the ai plugin, contracts 0.219.0), or `any` (contracts 0.221.0): whichever of their machines is on, lets their AI use its models and offers the model, the fastest first, the next one when it is busy or away. Never anyone else's machine."
++ "description": "provider link: the person's own machine (openvibe.link/machines) whose OpenVibe Link runs the model (the ai plugin, contracts 0.219.0), or `any` (contracts 0.221.0): whichever of their machines is on, lets their AI use its models and offers the model, the fastest first, the next one when it is busy or away. Never anyone else's machine."
},
-- "description": "provider node: the person's own machine (openvibe.bot/machines) whose OpenVibe Link runs the model (the ai plugin, contracts 0.219.0), or `any` (contracts 0.221.0): whichever of their machines is on, lets their AI use its models and offers the model, the fastest first, the next one when it is busy or away. Never anyone else's machine."
++ "description": "provider link: the person's own machine (openvibe.link/machines) whose OpenVibe Link runs the model (the ai plugin, contracts 0.219.0), or `any` (contracts 0.221.0): whichever of their machines is on, lets their AI use its models and offers the model, the fastest first, the next one when it is busy or away. Never anyone else's machine."
},
{
"name": "base_url",
@@@ -928,7 +928,7 @@
"enum": [
"openai",
"anthropic",
-- "node"
++ "link"
],
"description": "openai = any OpenAI-compatible endpoint. node = the person's own machine: a model on their own model server, through their OpenVibe Link and OpenVibe.Bot (no key, no base_url, no cost)."
},
@@@ -941,9 +941,9 @@
"schema": {
"type": "string",
"pattern": "^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$",
-- "description": "provider node: the person's own machine (openvibe.bot/machines) whose OpenVibe Link runs the model (the ai plugin, contracts 0.219.0), or `any` (contracts 0.221.0): whichever of their machines is on, lets their AI use its models and offers the model, the fastest first, the next one when it is busy or away. Never anyone else's machine."
++ "description": "provider link: the person's own machine (openvibe.link/machines) whose OpenVibe Link runs the model (the ai plugin, contracts 0.219.0), or `any` (contracts 0.221.0): whichever of their machines is on, lets their AI use its models and offers the model, the fastest first, the next one when it is busy or away. Never anyone else's machine."
},
-- "description": "provider node: the person's own machine (openvibe.bot/machines) whose OpenVibe Link runs the model (the ai plugin, contracts 0.219.0), or `any` (contracts 0.221.0): whichever of their machines is on, lets their AI use its models and offers the model, the fastest first, the next one when it is busy or away. Never anyone else's machine."
++ "description": "provider link: the person's own machine (openvibe.link/machines) whose OpenVibe Link runs the model (the ai plugin, contracts 0.219.0), or `any` (contracts 0.221.0): whichever of their machines is on, lets their AI use its models and offers the model, the fastest first, the next one when it is busy or away. Never anyone else's machine."
},
{
"name": "base_url",
@@@ -5296,14 +5296,14 @@
"enum": [
"openai",
"anthropic",
-- "node"
++ "link"
],
"description": "openai = any OpenAI-compatible endpoint. node = the person's own machine: a model on their own model server, through their OpenVibe Link and OpenVibe.Bot (no key, no base_url, no cost)."
},
"machine": {
"type": "string",
"pattern": "^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$",
-- "description": "provider node: the person's own machine (openvibe.bot/machines) whose OpenVibe Link runs the model (the ai plugin, contracts 0.219.0), or `any` (contracts 0.221.0): whichever of their machines is on, lets their AI use its models and offers the model, the fastest first, the next one when it is busy or away. Never anyone else's machine."
++ "description": "provider link: the person's own machine (openvibe.link/machines) whose OpenVibe Link runs the model (the ai plugin, contracts 0.219.0), or `any` (contracts 0.221.0): whichever of their machines is on, lets their AI use its models and offers the model, the fastest first, the next one when it is busy or away. Never anyone else's machine."
},
"base_url": {
"type": "string",
@@@ -5361,7 +5361,7 @@
"if": {
"properties": {
"provider": {
-- "const": "node"
++ "const": "link"
}
},
"required": [
@@@ -5416,7 +5416,7 @@
"enum": [
"openai",
"anthropic",
-- "node"
++ "link"
]
},
"machine": {
@@@ -5425,7 +5425,7 @@
"null"
],
"pattern": "^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$",
-- "description": "provider node: the machine it runs on, or `any` of the person's own (contracts 0.221.0)"
++ "description": "provider link: the machine it runs on, or `any` of the person's own (contracts 0.221.0)"
},
"base_url": {
"type": [
ddiff --git a/generated/openapi/bench.json b/generated/openapi/bench.json
iindex af6c8692..e25b27c4 100644
---- a/generated/openapi/bench.json
++++ b/generated/openapi/bench.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Bench API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -2462,7 +2462,7 @@
"node",
"submitted"
],
-- "description": "codes: an openvibe.codes task. run: a Run worker. node: the person's own machine. submitted: nobody ran it."
++ "description": "codes: an openvibe.codes task. run: a Run worker. link: the person's own machine. submitted: nobody ran it."
},
"id": {
"type": "string",
ddiff --git a/generated/openapi/billing.json b/generated/openapi/billing.json
iindex 9d0c24d6..88b1d69c 100644
---- a/generated/openapi/billing.json
++++ b/generated/openapi/billing.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Billing API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -6456,7 +6456,7 @@
"quantity": 1,
"unit": "s",
"at": "2026-10-02T10:00:00.250Z",
-- "source": "openvibe-node.worker"
++ "source": "openvibe-link.worker"
}
]
}
ddiff --git a/generated/openapi/blog.json b/generated/openapi/blog.json
iindex fbee8a6f..1f686457 100644
---- a/generated/openapi/blog.json
++++ b/generated/openapi/blog.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Blog API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/bot.json b/generated/openapi/bot.json
iindex a31746f8..1f80efe9 100644
---- a/generated/openapi/bot.json
++++ b/generated/openapi/bot.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Bot API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -476,7 +476,7 @@
"get": {
"operationId": "get_api_v1_machines",
"summary": "A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject).",
-- "description": "**bot.machine.manage** (first-party): A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.network/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
++ "description": "**bot.machine.manage** (first-party): A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.link/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
"tags": [
"bot.machine"
],
@@@ -525,7 +525,7 @@
"get": {
"operationId": "get_api_v1_machines_id",
"summary": "A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject).",
-- "description": "**bot.machine.manage** (first-party): A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.network/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
++ "description": "**bot.machine.manage** (first-party): A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.link/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
"tags": [
"bot.machine"
],
@@@ -582,7 +582,7 @@
"patch": {
"operationId": "patch_api_v1_machines_id",
"summary": "A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject).",
-- "description": "**bot.machine.manage** (first-party): A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.network/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
++ "description": "**bot.machine.manage** (first-party): A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.link/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
"tags": [
"bot.machine"
],
@@@ -992,7 +992,7 @@
"post": {
"operationId": "post_api_v1_machines_id_revoke",
"summary": "A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject).",
-- "description": "**bot.machine.manage** (first-party): A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.network/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
++ "description": "**bot.machine.manage** (first-party): A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.link/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
"tags": [
"bot.machine"
],
@@@ -1058,7 +1058,7 @@
"put": {
"operationId": "put_api_v1_machines_id_roles",
"summary": "A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject).",
-- "description": "**bot.machine.manage** (first-party): A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.network/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
++ "description": "**bot.machine.manage** (first-party): A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.link/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
"tags": [
"bot.machine"
],
@@@ -2563,7 +2563,7 @@
"null"
],
"maxLength": 128,
-- "description": "The offer's provider, as Run sent it; null for a first-party node."
++ "description": "The offer's provider, as Run sent it; null for a first-party machine."
},
"cancel_requested": {
"type": "boolean",
ddiff --git a/generated/openapi/chat.json b/generated/openapi/chat.json
iindex 73152cfb..313596a1 100644
---- a/generated/openapi/chat.json
++++ b/generated/openapi/chat.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Chat API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/codes.json b/generated/openapi/codes.json
iindex adaf56a4..3b79dcdb 100644
---- a/generated/openapi/codes.json
++++ b/generated/openapi/codes.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Codes API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/community.json b/generated/openapi/community.json
iindex 304493b5..35c9efcb 100644
---- a/generated/openapi/community.json
++++ b/generated/openapi/community.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Community API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/connect.json b/generated/openapi/connect.json
iindex 170ddae0..1bc5ef41 100644
---- a/generated/openapi/connect.json
++++ b/generated/openapi/connect.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe Connect API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe Connect as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe Connect as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/coupons.json b/generated/openapi/coupons.json
iindex ff92bf28..b77a708d 100644
---- a/generated/openapi/coupons.json
++++ b/generated/openapi/coupons.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Coupons API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/deals.json b/generated/openapi/deals.json
iindex 1d72eb1e..2122c089 100644
---- a/generated/openapi/deals.json
++++ b/generated/openapi/deals.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Deals API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/design.json b/generated/openapi/design.json
iindex 2e833027..a8c68058 100644
---- a/generated/openapi/design.json
++++ b/generated/openapi/design.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Design API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Design as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Design as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/domains.json b/generated/openapi/domains.json
iindex d8b6f6ba..7f671276 100644
---- a/generated/openapi/domains.json
++++ b/generated/openapi/domains.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Domains API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/events.json b/generated/openapi/events.json
iindex 4bc76160..e5c3c9b0 100644
---- a/generated/openapi/events.json
++++ b/generated/openapi/events.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Events API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/exchange.json b/generated/openapi/exchange.json
iindex 390bc8da..083f6537 100644
---- a/generated/openapi/exchange.json
++++ b/generated/openapi/exchange.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Exchange API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Exchange as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Exchange as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/food.json b/generated/openapi/food.json
iindex 4543b66a..28805e51 100644
---- a/generated/openapi/food.json
++++ b/generated/openapi/food.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Food API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/games.json b/generated/openapi/games.json
iindex 80a61c9d..6c596170 100644
---- a/generated/openapi/games.json
++++ b/generated/openapi/games.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Games API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/host.json b/generated/openapi/host.json
iindex 8db9ec58..3c490a7b 100644
---- a/generated/openapi/host.json
++++ b/generated/openapi/host.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Host API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/inventory.json b/generated/openapi/inventory.json
iindex dd0a1f17..f155a270 100644
---- a/generated/openapi/inventory.json
++++ b/generated/openapi/inventory.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Inventory API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/live.json b/generated/openapi/live.json
iindex dbbf0d67..6026c918 100644
---- a/generated/openapi/live.json
++++ b/generated/openapi/live.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Live API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/mail.json b/generated/openapi/mail.json
iindex 3b3d5104..749224a7 100644
---- a/generated/openapi/mail.json
++++ b/generated/openapi/mail.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Mail API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Mail as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Mail as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/media-hub.json b/generated/openapi/media-hub.json
iindex 5d8b96cc..1d53f5af 100644
---- a/generated/openapi/media-hub.json
++++ b/generated/openapi/media-hub.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.MediaHub API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/media.json b/generated/openapi/media.json
iindex cf44d066..8d29920c 100644
---- a/generated/openapi/media.json
++++ b/generated/openapi/media.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Media API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/network.json b/generated/openapi/network.json
iindex c7b73b9e..563bb236 100644
---- a/generated/openapi/network.json
++++ b/generated/openapi/network.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Network API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -192,38 +192,70 @@
}
}
},
-- "/api/v1/machines": {
-- "get": {
-- "operationId": "get_api_v1_machines",
-- "summary": "Read a person's own machines: the list, one machine, and the catalogue of roles.",
-- "description": "**network.machines.read** (public): Read a person's own machines: the list, one machine, and the catalogue of roles. A person reads their own with their own token; an app needs the person's consent for this scope (it is a public capability, so it can be granted to an app, and the app then acts only on behalf of the person who consented).",
++ "/api/v1/link/self/capabilities": {
++ "put": {
++ "operationId": "put_api_v1_link_self_capabilities",
++ "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
++ "description": "**network.link.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's. It also announces itself for its owner's machine pools (bot.machine-pool@1), reads, approves and renews the pools it is a member of, and leaves them; it never acts for another machine.",
"tags": [
-- "network.machines"
++ "network.link"
],
"x-openvibe-capabilities": [
-- "network.machines.read"
++ "network.link.self.manage"
],
"x-openvibe-visibility": [
-- "public"
++ "internal"
],
-- "x-openvibe-quota-class": "network-read",
++ "x-openvibe-quota-class": "admin",
"security": [
{
"openvibeToken": [
-- "network.machines.read"
++ "network.link.self.manage"
]
}
],
"responses": {
"2XX": {
-- "description": "Success",
++ "description": "Success"
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
"content": {
-- "application/json": {
++ "application/problem+json": {
"schema": {
-- "$ref": "#/components/schemas/network.machine-list-result.v1"
++ "$ref": "#/components/schemas/errors.problem.v1"
}
}
}
++ }
++ }
++ }
++ },
++ "/api/v1/link/self/credential": {
++ "post": {
++ "operationId": "post_api_v1_link_self_credential",
++ "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
++ "description": "**network.link.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's. It also announces itself for its owner's machine pools (bot.machine-pool@1), reads, approves and renews the pools it is a member of, and leaves them; it never acts for another machine.",
++ "tags": [
++ "network.link"
++ ],
++ "x-openvibe-capabilities": [
++ "network.link.self.manage"
++ ],
++ "x-openvibe-visibility": [
++ "internal"
++ ],
++ "x-openvibe-quota-class": "admin",
++ "security": [
++ {
++ "openvibeToken": [
++ "network.link.self.manage"
++ ]
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success"
},
"default": {
"description": "An error (RFC 9457 problem details)",
@@@ -238,25 +270,25 @@
}
}
},
-- "/api/v1/machines/{id}": {
++ "/api/v1/link/self/pools/{id}": {
"get": {
-- "operationId": "get_api_v1_machines_id",
-- "summary": "Read a person's own machines: the list, one machine, and the catalogue of roles.",
-- "description": "**network.machines.read** (public): Read a person's own machines: the list, one machine, and the catalogue of roles. A person reads their own with their own token; an app needs the person's consent for this scope (it is a public capability, so it can be granted to an app, and the app then acts only on behalf of the person who consented).",
++ "operationId": "get_api_v1_link_self_pools_id",
++ "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
++ "description": "**network.link.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's. It also announces itself for its owner's machine pools (bot.machine-pool@1), reads, approves and renews the pools it is a member of, and leaves them; it never acts for another machine.",
"tags": [
-- "network.machines"
++ "network.link"
],
"x-openvibe-capabilities": [
-- "network.machines.read"
++ "network.link.self.manage"
],
"x-openvibe-visibility": [
-- "public"
++ "internal"
],
-- "x-openvibe-quota-class": "network-read",
++ "x-openvibe-quota-class": "admin",
"security": [
{
"openvibeToken": [
-- "network.machines.read"
++ "network.link.self.manage"
]
}
],
@@@ -272,14 +304,7 @@
],
"responses": {
"2XX": {
-- "description": "Success",
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/network.machine-list-result.v1"
-- }
-- }
-- }
++ "description": "Success"
},
"default": {
"description": "An error (RFC 9457 problem details)",
@@@ -292,38 +317,30 @@
}
}
}
-- },
-- "patch": {
-- "operationId": "patch_api_v1_machines_id",
-- "summary": "Pair, rename, give roles to and revoke a person's own machines.",
-- "description": "**network.machines.manage** (public): Pair, rename, give roles to and revoke a person's own machines. Bot stays the registry underneath: Network asks Bot as the person, and Bot checks that the machine is theirs. An app needs the person's consent for this scope, and may then do only what the person could.",
++ }
++ },
++ "/api/v1/link/self/pools/{id}/approve": {
++ "post": {
++ "operationId": "post_api_v1_link_self_pools_id_approve",
++ "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
++ "description": "**network.link.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's. It also announces itself for its owner's machine pools (bot.machine-pool@1), reads, approves and renews the pools it is a member of, and leaves them; it never acts for another machine.",
"tags": [
-- "network.machines"
++ "network.link"
],
"x-openvibe-capabilities": [
-- "network.machines.manage"
++ "network.link.self.manage"
],
"x-openvibe-visibility": [
-- "public"
++ "internal"
],
-- "x-openvibe-quota-class": "network-write",
++ "x-openvibe-quota-class": "admin",
"security": [
{
"openvibeToken": [
-- "network.machines.manage"
++ "network.link.self.manage"
]
}
],
-- "requestBody": {
-- "required": false,
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/bot.machine-manage-request.v1"
-- }
-- }
-- }
-- },
"parameters": [
{
"name": "id",
@@@ -336,14 +353,7 @@
],
"responses": {
"2XX": {
-- "description": "Success",
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/network.machine-manage-result.v1"
-- }
-- }
-- }
++ "description": "Success"
},
"default": {
"description": "An error (RFC 9457 problem details)",
@@@ -358,38 +368,28 @@
}
}
},
-- "/api/v1/machines/{id}/revoke": {
++ "/api/v1/link/self/pools/{id}/leave": {
"post": {
-- "operationId": "post_api_v1_machines_id_revoke",
-- "summary": "Pair, rename, give roles to and revoke a person's own machines.",
-- "description": "**network.machines.manage** (public): Pair, rename, give roles to and revoke a person's own machines. Bot stays the registry underneath: Network asks Bot as the person, and Bot checks that the machine is theirs. An app needs the person's consent for this scope, and may then do only what the person could.",
++ "operationId": "post_api_v1_link_self_pools_id_leave",
++ "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
++ "description": "**network.link.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's. It also announces itself for its owner's machine pools (bot.machine-pool@1), reads, approves and renews the pools it is a member of, and leaves them; it never acts for another machine.",
"tags": [
-- "network.machines"
++ "network.link"
],
"x-openvibe-capabilities": [
-- "network.machines.manage"
++ "network.link.self.manage"
],
"x-openvibe-visibility": [
-- "public"
++ "internal"
],
-- "x-openvibe-quota-class": "network-write",
++ "x-openvibe-quota-class": "admin",
"security": [
{
"openvibeToken": [
-- "network.machines.manage"
++ "network.link.self.manage"
]
}
],
-- "requestBody": {
-- "required": false,
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/bot.machine-manage-request.v1"
-- }
-- }
-- }
-- },
"parameters": [
{
"name": "id",
@@@ -402,14 +402,7 @@
],
"responses": {
"2XX": {
-- "description": "Success",
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/network.machine-manage-result.v1"
-- }
-- }
-- }
++ "description": "Success"
},
"default": {
"description": "An error (RFC 9457 problem details)",
@@@ -424,38 +417,28 @@
}
}
},
-- "/api/v1/machines/{id}/roles": {
-- "put": {
-- "operationId": "put_api_v1_machines_id_roles",
-- "summary": "Pair, rename, give roles to and revoke a person's own machines.",
-- "description": "**network.machines.manage** (public): Pair, rename, give roles to and revoke a person's own machines. Bot stays the registry underneath: Network asks Bot as the person, and Bot checks that the machine is theirs. An app needs the person's consent for this scope, and may then do only what the person could.",
++ "/api/v1/link/self/pools/{id}/status": {
++ "post": {
++ "operationId": "post_api_v1_link_self_pools_id_status",
++ "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
++ "description": "**network.link.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's. It also announces itself for its owner's machine pools (bot.machine-pool@1), reads, approves and renews the pools it is a member of, and leaves them; it never acts for another machine.",
"tags": [
-- "network.machines"
++ "network.link"
],
"x-openvibe-capabilities": [
-- "network.machines.manage"
++ "network.link.self.manage"
],
"x-openvibe-visibility": [
-- "public"
++ "internal"
],
-- "x-openvibe-quota-class": "network-write",
++ "x-openvibe-quota-class": "admin",
"security": [
{
"openvibeToken": [
-- "network.machines.manage"
++ "network.link.self.manage"
]
}
],
-- "requestBody": {
-- "required": false,
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/bot.machine-manage-request.v1"
-- }
-- }
-- }
-- },
"parameters": [
{
"name": "id",
@@@ -468,14 +451,7 @@
],
"responses": {
"2XX": {
-- "description": "Success",
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/network.machine-manage-result.v1"
-- }
-- }
-- }
++ "description": "Success"
},
"default": {
"description": "An error (RFC 9457 problem details)",
@@@ -490,48 +466,31 @@
}
}
},
-- "/api/v1/machines/pairings": {
++ "/api/v1/link/self/pools/announce": {
"post": {
-- "operationId": "post_api_v1_machines_pairings",
-- "summary": "Pair, rename, give roles to and revoke a person's own machines.",
-- "description": "**network.machines.manage** (public): Pair, rename, give roles to and revoke a person's own machines. Bot stays the registry underneath: Network asks Bot as the person, and Bot checks that the machine is theirs. An app needs the person's consent for this scope, and may then do only what the person could.",
++ "operationId": "post_api_v1_link_self_pools_announce",
++ "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
++ "description": "**network.link.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's. It also announces itself for its owner's machine pools (bot.machine-pool@1), reads, approves and renews the pools it is a member of, and leaves them; it never acts for another machine.",
"tags": [
-- "network.machines"
++ "network.link"
],
"x-openvibe-capabilities": [
-- "network.machines.manage"
++ "network.link.self.manage"
],
"x-openvibe-visibility": [
-- "public"
++ "internal"
],
-- "x-openvibe-quota-class": "network-write",
++ "x-openvibe-quota-class": "admin",
"security": [
{
"openvibeToken": [
-- "network.machines.manage"
++ "network.link.self.manage"
]
}
],
-- "requestBody": {
-- "required": false,
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/bot.machine-manage-request.v1"
-- }
-- }
-- }
-- },
"responses": {
"2XX": {
-- "description": "Success",
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/network.machine-manage-result.v1"
-- }
-- }
-- }
++ "description": "Success"
},
"default": {
"description": "An error (RFC 9457 problem details)",
@@@ -546,9 +505,9 @@
}
}
},
-- "/api/v1/machines/roles": {
++ "/api/v1/machines": {
"get": {
-- "operationId": "get_api_v1_machines_roles",
++ "operationId": "get_api_v1_machines",
"summary": "Read a person's own machines: the list, one machine, and the catalogue of roles.",
"description": "**network.machines.read** (public): Read a person's own machines: the list, one machine, and the catalogue of roles. A person reads their own with their own token; an app needs the person's consent for this scope (it is a public capability, so it can be granted to an app, and the app then acts only on behalf of the person who consented).",
"tags": [
@@@ -592,35 +551,45 @@
}
}
},
-- "/api/v1/me/actions": {
++ "/api/v1/machines/{id}": {
"get": {
-- "operationId": "get_api_v1_me_actions",
-- "summary": "The Action Center for the signed-in person (plan 2026-10-28 §3.10): GET /api/v1/me/actions merges Actor, Codes and AI items and Network's own confirmations into running, waiting, scheduled, done and f",
-- "description": "**network.actions.read** (public): The Action Center for the signed-in person (plan 2026-10-28 §3.10): GET /api/v1/me/actions merges Actor, Codes and AI items and Network's own confirmations into running, waiting, scheduled, done and failed (network.me-actions-result@1); POST /api/v1/me/actions/:service/:id/:action approves, declines, cancels or retries through the owning authority. Readable from every OpenVibe site with the person's Network token, like the bell.",
++ "operationId": "get_api_v1_machines_id",
++ "summary": "Read a person's own machines: the list, one machine, and the catalogue of roles.",
++ "description": "**network.machines.read** (public): Read a person's own machines: the list, one machine, and the catalogue of roles. A person reads their own with their own token; an app needs the person's consent for this scope (it is a public capability, so it can be granted to an app, and the app then acts only on behalf of the person who consented).",
"tags": [
-- "network.actions"
++ "network.machines"
],
"x-openvibe-capabilities": [
-- "network.actions.read"
++ "network.machines.read"
],
"x-openvibe-visibility": [
"public"
],
-- "x-openvibe-quota-class": "standard",
++ "x-openvibe-quota-class": "network-read",
"security": [
{
"openvibeToken": [
-- "network.actions.read"
++ "network.machines.read"
]
}
],
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
"responses": {
"2XX": {
"description": "Success",
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/network.me-actions-result.v1"
++ "$ref": "#/components/schemas/network.machine-list-result.v1"
}
}
}
@@@ -636,39 +605,39 @@
}
}
}
-- }
-- },
-- "/api/v1/me/actions/{service}/{id}/{action}": {
-- "post": {
-- "operationId": "post_api_v1_me_actions_service_id_action",
-- "summary": "The Action Center for the signed-in person (plan 2026-10-28 §3.10): GET /api/v1/me/actions merges Actor, Codes and AI items and Network's own confirmations into running, waiting, scheduled, done and f",
-- "description": "**network.actions.read** (public): The Action Center for the signed-in person (plan 2026-10-28 §3.10): GET /api/v1/me/actions merges Actor, Codes and AI items and Network's own confirmations into running, waiting, scheduled, done and failed (network.me-actions-result@1); POST /api/v1/me/actions/:service/:id/:action approves, declines, cancels or retries through the owning authority. Readable from every OpenVibe site with the person's Network token, like the bell.",
++ },
++ "patch": {
++ "operationId": "patch_api_v1_machines_id",
++ "summary": "Pair, rename, give roles to and revoke a person's own machines.",
++ "description": "**network.machines.manage** (public): Pair, rename, give roles to and revoke a person's own machines. Bot stays the registry underneath: Network asks Bot as the person, and Bot checks that the machine is theirs. An app needs the person's consent for this scope, and may then do only what the person could.",
"tags": [
-- "network.actions"
++ "network.machines"
],
"x-openvibe-capabilities": [
-- "network.actions.read"
++ "network.machines.manage"
],
"x-openvibe-visibility": [
"public"
],
-- "x-openvibe-quota-class": "standard",
++ "x-openvibe-quota-class": "network-write",
"security": [
{
"openvibeToken": [
-- "network.actions.read"
++ "network.machines.manage"
]
}
],
-- "parameters": [
-- {
-- "name": "service",
-- "in": "path",
-- "required": true,
-- "schema": {
-- "type": "string"
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.machine-manage-request.v1"
++ }
}
-- },
++ }
++ },
++ "parameters": [
{
"name": "id",
"in": "path",
@@@ -676,14 +645,6 @@
"schema": {
"type": "string"
}
-- },
-- {
-- "name": "action",
-- "in": "path",
-- "required": true,
-- "schema": {
-- "type": "string"
-- }
}
],
"responses": {
@@@ -692,7 +653,7 @@
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/network.me-actions-result.v1"
++ "$ref": "#/components/schemas/network.machine-manage-result.v1"
}
}
}
@@@ -710,25 +671,25 @@
}
}
},
-- "/api/v1/me/actions/skills/{id}/run": {
++ "/api/v1/machines/{id}/revoke": {
"post": {
-- "operationId": "post_api_v1_me_actions_skills_id_run",
-- "summary": "Start one of Actor's catalogue skills as the signed-in person (plan 2026-11-09 §7): POST /api/v1/me/actions/skills/:id/run asks Actor to run that skill for them, with the input and the choice.",
-- "description": "**network.skill.run** (public): Start one of Actor's catalogue skills as the signed-in person (plan 2026-11-09 §7): POST /api/v1/me/actions/skills/:id/run asks Actor to run that skill for them, with the input and the choice. Network never runs the skill itself; Actor does, under the person's own approvals and budget.",
++ "operationId": "post_api_v1_machines_id_revoke",
++ "summary": "Pair, rename, give roles to and revoke a person's own machines.",
++ "description": "**network.machines.manage** (public): Pair, rename, give roles to and revoke a person's own machines. Bot stays the registry underneath: Network asks Bot as the person, and Bot checks that the machine is theirs. An app needs the person's consent for this scope, and may then do only what the person could.",
"tags": [
-- "network.skill"
++ "network.machines"
],
"x-openvibe-capabilities": [
-- "network.skill.run"
++ "network.machines.manage"
],
"x-openvibe-visibility": [
"public"
],
-- "x-openvibe-quota-class": "standard",
++ "x-openvibe-quota-class": "network-write",
"security": [
{
"openvibeToken": [
-- "network.skill.run"
++ "network.machines.manage"
]
}
],
@@@ -737,7 +698,7 @@
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/actor.skill-run-request.v1"
++ "$ref": "#/components/schemas/bot.machine-manage-request.v1"
}
}
}
@@@ -758,7 +719,7 @@
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/actor.skill-run-result.v1"
++ "$ref": "#/components/schemas/network.machine-manage-result.v1"
}
}
}
@@@ -776,112 +737,46 @@
}
}
},
-- "/api/v1/me/consent": {
-- "get": {
-- "operationId": "get_api_v1_me_consent",
-- "summary": "\"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or",
-- "description": "**network.consent.manage** (public): \"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or off (network.consent-choice@1); DELETE /api/v1/me/consent/:purpose/:client turns one app's purpose off. \"Apps you allowed\" (0.310.0): GET /api/v1/me/consent/apps lists the purposes each app was granted or refused at sign-in (network.me-consent-apps-result@1), and DELETE /api/v1/me/consent/:purpose/:client/refusal ends a refusal's 30 quiet days so that app's next sign-in asks again. Off is the default for every purpose. Only the person's own session or token reaches it.",
++ "/api/v1/machines/{id}/roles": {
++ "put": {
++ "operationId": "put_api_v1_machines_id_roles",
++ "summary": "Pair, rename, give roles to and revoke a person's own machines.",
++ "description": "**network.machines.manage** (public): Pair, rename, give roles to and revoke a person's own machines. Bot stays the registry underneath: Network asks Bot as the person, and Bot checks that the machine is theirs. An app needs the person's consent for this scope, and may then do only what the person could.",
"tags": [
-- "network.consent"
++ "network.machines"
],
"x-openvibe-capabilities": [
-- "network.consent.manage"
++ "network.machines.manage"
],
"x-openvibe-visibility": [
"public"
],
-- "x-openvibe-quota-class": "standard",
++ "x-openvibe-quota-class": "network-write",
"security": [
{
"openvibeToken": [
-- "network.consent.manage"
++ "network.machines.manage"
]
}
],
-- "x-openvibe-input": {
-- "$ref": "#/components/schemas/network.consent-choice.v1"
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.machine-manage-request.v1"
++ }
++ }
++ }
},
"parameters": [
{
-- "name": "on",
-- "in": "query",
++ "name": "id",
++ "in": "path",
"required": true,
"schema": {
-- "type": "boolean"
-- }
-- },
-- {
-- "name": "kinds",
-- "in": "query",
-- "required": false,
-- "schema": {
-- "type": "array",
-- "maxItems": 8,
-- "uniqueItems": true,
-- "items": {
-- "enum": [
-- "streams",
-- "vods",
-- "clips",
-- "voice",
-- "chat",
-- "posts",
-- "pictures",
-- "code"
-- ]
-- }
-- }
-- },
-- {
-- "name": "options",
-- "in": "query",
-- "required": false,
-- "schema": {
-- "type": "object",
-- "additionalProperties": false,
-- "properties": {
-- "licence": {
-- "enum": [
-- "cc-by-4.0",
-- "cc0-1.0"
-- ]
-- },
-- "features": {
-- "type": "array",
-- "maxItems": 4,
-- "uniqueItems": true,
-- "items": {
-- "enum": [
-- "verify",
-- "find",
-- "cast",
-- "library"
-- ]
-- }
-- }
-- }
++ "type": "string"
}
-- },
-- {
-- "name": "text_version",
-- "in": "query",
-- "required": false,
-- "schema": {
-- "type": "string",
-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}\\.[0-9]{1,3}$",
-- "description": "The wording the page showed. Required to turn on; a stale one is refused (consent.stale_text) so the page shows the new words first."
-- },
-- "description": "The wording the page showed. Required to turn on; a stale one is refused (consent.stale_text) so the page shows the new words first."
-- },
-- {
-- "name": "adult",
-- "in": "query",
-- "required": false,
-- "schema": {
-- "type": "boolean",
-- "description": "The person confirmed they are 18 or older. Required (true) to turn on face.recognize, ai.looks, ai.train or data.public."
-- },
-- "description": "The person confirmed they are 18 or older. Required (true) to turn on face.recognize, ai.looks, ai.train or data.public."
}
],
"responses": {
@@@ -890,7 +785,7 @@
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/network.me-consent-result.v1"
++ "$ref": "#/components/schemas/network.machine-manage-result.v1"
}
}
}
@@@ -908,25 +803,25 @@
}
}
},
-- "/api/v1/me/consent/{purpose}": {
-- "put": {
-- "operationId": "put_api_v1_me_consent_purpose",
-- "summary": "\"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or",
-- "description": "**network.consent.manage** (public): \"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or off (network.consent-choice@1); DELETE /api/v1/me/consent/:purpose/:client turns one app's purpose off. \"Apps you allowed\" (0.310.0): GET /api/v1/me/consent/apps lists the purposes each app was granted or refused at sign-in (network.me-consent-apps-result@1), and DELETE /api/v1/me/consent/:purpose/:client/refusal ends a refusal's 30 quiet days so that app's next sign-in asks again. Off is the default for every purpose. Only the person's own session or token reaches it.",
++ "/api/v1/machines/pairings": {
++ "post": {
++ "operationId": "post_api_v1_machines_pairings",
++ "summary": "Pair, rename, give roles to and revoke a person's own machines.",
++ "description": "**network.machines.manage** (public): Pair, rename, give roles to and revoke a person's own machines. Bot stays the registry underneath: Network asks Bot as the person, and Bot checks that the machine is theirs. An app needs the person's consent for this scope, and may then do only what the person could.",
"tags": [
-- "network.consent"
++ "network.machines"
],
"x-openvibe-capabilities": [
-- "network.consent.manage"
++ "network.machines.manage"
],
"x-openvibe-visibility": [
"public"
],
-- "x-openvibe-quota-class": "standard",
++ "x-openvibe-quota-class": "network-write",
"security": [
{
"openvibeToken": [
-- "network.consent.manage"
++ "network.machines.manage"
]
}
],
@@@ -935,28 +830,18 @@
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/network.consent-choice.v1"
++ "$ref": "#/components/schemas/bot.machine-manage-request.v1"
}
}
}
},
-- "parameters": [
-- {
-- "name": "purpose",
-- "in": "path",
-- "required": true,
-- "schema": {
-- "type": "string"
-- }
-- }
-- ],
"responses": {
"2XX": {
"description": "Success",
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/network.me-consent-result.v1"
++ "$ref": "#/components/schemas/network.machine-manage-result.v1"
}
}
}
@@@ -974,128 +859,72 @@
}
}
},
-- "/api/v1/me/consent/{purpose}/{client}": {
-- "delete": {
-- "operationId": "delete_api_v1_me_consent_purpose_client",
-- "summary": "\"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or",
-- "description": "**network.consent.manage** (public): \"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or off (network.consent-choice@1); DELETE /api/v1/me/consent/:purpose/:client turns one app's purpose off. \"Apps you allowed\" (0.310.0): GET /api/v1/me/consent/apps lists the purposes each app was granted or refused at sign-in (network.me-consent-apps-result@1), and DELETE /api/v1/me/consent/:purpose/:client/refusal ends a refusal's 30 quiet days so that app's next sign-in asks again. Off is the default for every purpose. Only the person's own session or token reaches it.",
++ "/api/v1/machines/roles": {
++ "get": {
++ "operationId": "get_api_v1_machines_roles",
++ "summary": "Read a person's own machines: the list, one machine, and the catalogue of roles.",
++ "description": "**network.machines.read** (public): Read a person's own machines: the list, one machine, and the catalogue of roles. A person reads their own with their own token; an app needs the person's consent for this scope (it is a public capability, so it can be granted to an app, and the app then acts only on behalf of the person who consented).",
"tags": [
-- "network.consent"
++ "network.machines"
],
"x-openvibe-capabilities": [
-- "network.consent.manage"
++ "network.machines.read"
],
"x-openvibe-visibility": [
"public"
],
-- "x-openvibe-quota-class": "standard",
++ "x-openvibe-quota-class": "network-read",
"security": [
{
"openvibeToken": [
-- "network.consent.manage"
++ "network.machines.read"
]
}
],
-- "x-openvibe-input": {
-- "$ref": "#/components/schemas/network.consent-choice.v1"
-- },
-- "parameters": [
-- {
-- "name": "purpose",
-- "in": "path",
-- "required": true,
-- "schema": {
-- "type": "string"
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.machine-list-result.v1"
++ }
++ }
}
},
-- {
-- "name": "client",
-- "in": "path",
-- "required": true,
-- "schema": {
-- "type": "string"
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
}
-- },
++ }
++ }
++ }
++ },
++ "/api/v1/me/actions": {
++ "get": {
++ "operationId": "get_api_v1_me_actions",
++ "summary": "The Action Center for the signed-in person (plan 2026-10-28 §3.10): GET /api/v1/me/actions merges Actor, Codes and AI items and Network's own confirmations into running, waiting, scheduled, done and f",
++ "description": "**network.actions.read** (public): The Action Center for the signed-in person (plan 2026-10-28 §3.10): GET /api/v1/me/actions merges Actor, Codes and AI items and Network's own confirmations into running, waiting, scheduled, done and failed (network.me-actions-result@1); POST /api/v1/me/actions/:service/:id/:action approves, declines, cancels or retries through the owning authority. Readable from every OpenVibe site with the person's Network token, like the bell.",
++ "tags": [
++ "network.actions"
++ ],
++ "x-openvibe-capabilities": [
++ "network.actions.read"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "standard",
++ "security": [
{
-- "name": "on",
-- "in": "query",
-- "required": true,
-- "schema": {
-- "type": "boolean"
-- }
-- },
-- {
-- "name": "kinds",
-- "in": "query",
-- "required": false,
-- "schema": {
-- "type": "array",
-- "maxItems": 8,
-- "uniqueItems": true,
-- "items": {
-- "enum": [
-- "streams",
-- "vods",
-- "clips",
-- "voice",
-- "chat",
-- "posts",
-- "pictures",
-- "code"
-- ]
-- }
-- }
-- },
-- {
-- "name": "options",
-- "in": "query",
-- "required": false,
-- "schema": {
-- "type": "object",
-- "additionalProperties": false,
-- "properties": {
-- "licence": {
-- "enum": [
-- "cc-by-4.0",
-- "cc0-1.0"
-- ]
-- },
-- "features": {
-- "type": "array",
-- "maxItems": 4,
-- "uniqueItems": true,
-- "items": {
-- "enum": [
-- "verify",
-- "find",
-- "cast",
-- "library"
-- ]
-- }
-- }
-- }
-- }
-- },
-- {
-- "name": "text_version",
-- "in": "query",
-- "required": false,
-- "schema": {
-- "type": "string",
-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}\\.[0-9]{1,3}$",
-- "description": "The wording the page showed. Required to turn on; a stale one is refused (consent.stale_text) so the page shows the new words first."
-- },
-- "description": "The wording the page showed. Required to turn on; a stale one is refused (consent.stale_text) so the page shows the new words first."
-- },
-- {
-- "name": "adult",
-- "in": "query",
-- "required": false,
-- "schema": {
-- "type": "boolean",
-- "description": "The person confirmed they are 18 or older. Required (true) to turn on face.recognize, ai.looks, ai.train or data.public."
-- },
-- "description": "The person confirmed they are 18 or older. Required (true) to turn on face.recognize, ai.looks, ai.train or data.public."
++ "openvibeToken": [
++ "network.actions.read"
++ ]
}
],
"responses": {
@@@ -1104,7 +933,7 @@
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/network.me-consent-result.v1"
++ "$ref": "#/components/schemas/network.me-actions-result.v1"
}
}
}
@@@ -1122,16 +951,16 @@
}
}
},
-- "/api/v1/me/consent/{purpose}/{client}/refusal": {
-- "delete": {
-- "operationId": "delete_api_v1_me_consent_purpose_client_refusal",
-- "summary": "\"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or",
-- "description": "**network.consent.manage** (public): \"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or off (network.consent-choice@1); DELETE /api/v1/me/consent/:purpose/:client turns one app's purpose off. \"Apps you allowed\" (0.310.0): GET /api/v1/me/consent/apps lists the purposes each app was granted or refused at sign-in (network.me-consent-apps-result@1), and DELETE /api/v1/me/consent/:purpose/:client/refusal ends a refusal's 30 quiet days so that app's next sign-in asks again. Off is the default for every purpose. Only the person's own session or token reaches it.",
++ "/api/v1/me/actions/{service}/{id}/{action}": {
++ "post": {
++ "operationId": "post_api_v1_me_actions_service_id_action",
++ "summary": "The Action Center for the signed-in person (plan 2026-10-28 §3.10): GET /api/v1/me/actions merges Actor, Codes and AI items and Network's own confirmations into running, waiting, scheduled, done and f",
++ "description": "**network.actions.read** (public): The Action Center for the signed-in person (plan 2026-10-28 §3.10): GET /api/v1/me/actions merges Actor, Codes and AI items and Network's own confirmations into running, waiting, scheduled, done and failed (network.me-actions-result@1); POST /api/v1/me/actions/:service/:id/:action approves, declines, cancels or retries through the owning authority. Readable from every OpenVibe site with the person's Network token, like the bell.",
"tags": [
-- "network.consent"
++ "network.actions"
],
"x-openvibe-capabilities": [
-- "network.consent.manage"
++ "network.actions.read"
],
"x-openvibe-visibility": [
"public"
@@@ -1140,16 +969,13 @@
"security": [
{
"openvibeToken": [
-- "network.consent.manage"
++ "network.actions.read"
]
}
],
-- "x-openvibe-input": {
-- "$ref": "#/components/schemas/network.consent-choice.v1"
-- },
"parameters": [
{
-- "name": "purpose",
++ "name": "service",
"in": "path",
"required": true,
"schema": {
@@@ -1157,7 +983,7 @@
}
},
{
-- "name": "client",
++ "name": "id",
"in": "path",
"required": true,
"schema": {
@@@ -1165,85 +991,78 @@
}
},
{
-- "name": "on",
-- "in": "query",
++ "name": "action",
++ "in": "path",
"required": true,
"schema": {
-- "type": "boolean"
++ "type": "string"
}
-- },
-- {
-- "name": "kinds",
-- "in": "query",
-- "required": false,
-- "schema": {
-- "type": "array",
-- "maxItems": 8,
-- "uniqueItems": true,
-- "items": {
-- "enum": [
-- "streams",
-- "vods",
-- "clips",
-- "voice",
-- "chat",
-- "posts",
-- "pictures",
-- "code"
-- ]
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.me-actions-result.v1"
++ }
}
}
},
-- {
-- "name": "options",
-- "in": "query",
-- "required": false,
-- "schema": {
-- "type": "object",
-- "additionalProperties": false,
-- "properties": {
-- "licence": {
-- "enum": [
-- "cc-by-4.0",
-- "cc0-1.0"
-- ]
-- },
-- "features": {
-- "type": "array",
-- "maxItems": 4,
-- "uniqueItems": true,
-- "items": {
-- "enum": [
-- "verify",
-- "find",
-- "cast",
-- "library"
-- ]
-- }
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
}
}
}
-- },
++ }
++ }
++ }
++ },
++ "/api/v1/me/actions/skills/{id}/run": {
++ "post": {
++ "operationId": "post_api_v1_me_actions_skills_id_run",
++ "summary": "Start one of Actor's catalogue skills as the signed-in person (plan 2026-11-09 §7): POST /api/v1/me/actions/skills/:id/run asks Actor to run that skill for them, with the input and the choice.",
++ "description": "**network.skill.run** (public): Start one of Actor's catalogue skills as the signed-in person (plan 2026-11-09 §7): POST /api/v1/me/actions/skills/:id/run asks Actor to run that skill for them, with the input and the choice. Network never runs the skill itself; Actor does, under the person's own approvals and budget.",
++ "tags": [
++ "network.skill"
++ ],
++ "x-openvibe-capabilities": [
++ "network.skill.run"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "standard",
++ "security": [
{
-- "name": "text_version",
-- "in": "query",
-- "required": false,
-- "schema": {
-- "type": "string",
-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}\\.[0-9]{1,3}$",
-- "description": "The wording the page showed. Required to turn on; a stale one is refused (consent.stale_text) so the page shows the new words first."
-- },
-- "description": "The wording the page showed. Required to turn on; a stale one is refused (consent.stale_text) so the page shows the new words first."
-- },
++ "openvibeToken": [
++ "network.skill.run"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/actor.skill-run-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
{
-- "name": "adult",
-- "in": "query",
-- "required": false,
++ "name": "id",
++ "in": "path",
++ "required": true,
"schema": {
-- "type": "boolean",
-- "description": "The person confirmed they are 18 or older. Required (true) to turn on face.recognize, ai.looks, ai.train or data.public."
-- },
-- "description": "The person confirmed they are 18 or older. Required (true) to turn on face.recognize, ai.looks, ai.train or data.public."
++ "type": "string"
++ }
}
],
"responses": {
@@@ -1252,7 +1071,7 @@
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/network.me-consent-result.v1"
++ "$ref": "#/components/schemas/actor.skill-run-result.v1"
}
}
}
@@@ -1270,9 +1089,9 @@
}
}
},
-- "/api/v1/me/consent/apps": {
++ "/api/v1/me/consent": {
"get": {
-- "operationId": "get_api_v1_me_consent_apps",
++ "operationId": "get_api_v1_me_consent",
"summary": "\"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or",
"description": "**network.consent.manage** (public): \"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or off (network.consent-choice@1); DELETE /api/v1/me/consent/:purpose/:client turns one app's purpose off. \"Apps you allowed\" (0.310.0): GET /api/v1/me/consent/apps lists the purposes each app was granted or refused at sign-in (network.me-consent-apps-result@1), and DELETE /api/v1/me/consent/:purpose/:client/refusal ends a refusal's 30 quiet days so that app's next sign-in asks again. Off is the default for every purpose. Only the person's own session or token reaches it.",
"tags": [
@@@ -1402,38 +1221,55 @@
}
}
},
-- "/api/v1/me/emails": {
-- "get": {
-- "operationId": "get_api_v1_me_emails",
-- "summary": "Read the one verified email address a person has shared with this app, or check whether one supplied address is verified for a supplied subject.",
-- "description": "**identity.emails.read** (public): Read the one verified email address a person has shared with this app, or check whether one supplied address is verified for a supplied subject. The full address list is only available to the person in their own account hub.",
++ "/api/v1/me/consent/{purpose}": {
++ "put": {
++ "operationId": "put_api_v1_me_consent_purpose",
++ "summary": "\"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or",
++ "description": "**network.consent.manage** (public): \"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or off (network.consent-choice@1); DELETE /api/v1/me/consent/:purpose/:client turns one app's purpose off. \"Apps you allowed\" (0.310.0): GET /api/v1/me/consent/apps lists the purposes each app was granted or refused at sign-in (network.me-consent-apps-result@1), and DELETE /api/v1/me/consent/:purpose/:client/refusal ends a refusal's 30 quiet days so that app's next sign-in asks again. Off is the default for every purpose. Only the person's own session or token reaches it.",
"tags": [
-- "identity.emails"
++ "network.consent"
],
"x-openvibe-capabilities": [
-- "identity.emails.read"
++ "network.consent.manage"
],
"x-openvibe-visibility": [
"public"
],
-- "x-openvibe-quota-class": "identity-read",
++ "x-openvibe-quota-class": "standard",
"security": [
{
"openvibeToken": [
-- "identity.emails.read"
++ "network.consent.manage"
]
}
],
-- "x-openvibe-input": {
-- "$ref": "#/components/schemas/identity.emails-request.v1"
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.consent-choice.v1"
++ }
++ }
++ }
},
++ "parameters": [
++ {
++ "name": "purpose",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
"responses": {
"2XX": {
"description": "Success",
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/identity.emails.v1"
++ "$ref": "#/components/schemas/network.me-consent-result.v1"
}
}
}
@@@ -1451,119 +1287,140 @@
}
}
},
-- "/api/v1/me/trust": {
-- "get": {
-- "operationId": "get_api_v1_me_trust",
-- "summary": "Read privacy-limited trust signals for an account or guest by Network subject.",
-- "description": "**identity.trust.read** (public): Read privacy-limited trust signals for an account or guest by Network subject. The caller receives only age bands, booleans and aggregate counts, never an IP, email, last-seen timestamp, service user id or linked account identities. Services may use the capability; an app needs the owner's explicit capability grant.",
++ "/api/v1/me/consent/{purpose}/{client}": {
++ "delete": {
++ "operationId": "delete_api_v1_me_consent_purpose_client",
++ "summary": "\"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or",
++ "description": "**network.consent.manage** (public): \"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or off (network.consent-choice@1); DELETE /api/v1/me/consent/:purpose/:client turns one app's purpose off. \"Apps you allowed\" (0.310.0): GET /api/v1/me/consent/apps lists the purposes each app was granted or refused at sign-in (network.me-consent-apps-result@1), and DELETE /api/v1/me/consent/:purpose/:client/refusal ends a refusal's 30 quiet days so that app's next sign-in asks again. Off is the default for every purpose. Only the person's own session or token reaches it.",
"tags": [
-- "identity.trust"
++ "network.consent"
],
"x-openvibe-capabilities": [
-- "identity.trust.read"
++ "network.consent.manage"
],
"x-openvibe-visibility": [
"public"
],
-- "x-openvibe-quota-class": "identity-read",
++ "x-openvibe-quota-class": "standard",
"security": [
{
"openvibeToken": [
-- "identity.trust.read"
++ "network.consent.manage"
]
}
],
"x-openvibe-input": {
-- "$ref": "#/components/schemas/identity.trust-request.v1"
++ "$ref": "#/components/schemas/network.consent-choice.v1"
},
-- "responses": {
-- "2XX": {
-- "description": "Success",
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/identity.trust.v1"
-- }
++ "parameters": [
++ {
++ "name": "purpose",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "client",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "on",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "boolean"
++ }
++ },
++ {
++ "name": "kinds",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "array",
++ "maxItems": 8,
++ "uniqueItems": true,
++ "items": {
++ "enum": [
++ "streams",
++ "vods",
++ "clips",
++ "voice",
++ "chat",
++ "posts",
++ "pictures",
++ "code"
++ ]
}
}
},
-- "default": {
-- "description": "An error (RFC 9457 problem details)",
-- "content": {
-- "application/problem+json": {
-- "schema": {
-- "$ref": "#/components/schemas/errors.problem.v1"
++ {
++ "name": "options",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "licence": {
++ "enum": [
++ "cc-by-4.0",
++ "cc0-1.0"
++ ]
++ },
++ "features": {
++ "type": "array",
++ "maxItems": 4,
++ "uniqueItems": true,
++ "items": {
++ "enum": [
++ "verify",
++ "find",
++ "cast",
++ "library"
++ ]
++ }
}
}
}
-- }
-- }
-- }
-- },
-- "/api/v1/node/self/capabilities": {
-- "put": {
-- "operationId": "put_api_v1_node_self_capabilities",
-- "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
-- "description": "**network.node.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's. It also announces itself for its owner's machine pools (bot.machine-pool@1), reads, approves and renews the pools it is a member of, and leaves them; it never acts for another machine.",
-- "tags": [
-- "network.node"
-- ],
-- "x-openvibe-capabilities": [
-- "network.node.self.manage"
-- ],
-- "x-openvibe-visibility": [
-- "internal"
-- ],
-- "x-openvibe-quota-class": "admin",
-- "security": [
++ },
{
-- "openvibeToken": [
-- "network.node.self.manage"
-- ]
++ "name": "text_version",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}\\.[0-9]{1,3}$",
++ "description": "The wording the page showed. Required to turn on; a stale one is refused (consent.stale_text) so the page shows the new words first."
++ },
++ "description": "The wording the page showed. Required to turn on; a stale one is refused (consent.stale_text) so the page shows the new words first."
++ },
++ {
++ "name": "adult",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "boolean",
++ "description": "The person confirmed they are 18 or older. Required (true) to turn on face.recognize, ai.looks, ai.train or data.public."
++ },
++ "description": "The person confirmed they are 18 or older. Required (true) to turn on face.recognize, ai.looks, ai.train or data.public."
}
],
"responses": {
"2XX": {
-- "description": "Success"
-- },
-- "default": {
-- "description": "An error (RFC 9457 problem details)",
++ "description": "Success",
"content": {
-- "application/problem+json": {
++ "application/json": {
"schema": {
-- "$ref": "#/components/schemas/errors.problem.v1"
++ "$ref": "#/components/schemas/network.me-consent-result.v1"
}
}
}
-- }
-- }
-- }
-- },
-- "/api/v1/node/self/credential": {
-- "post": {
-- "operationId": "post_api_v1_node_self_credential",
-- "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
-- "description": "**network.node.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's. It also announces itself for its owner's machine pools (bot.machine-pool@1), reads, approves and renews the pools it is a member of, and leaves them; it never acts for another machine.",
-- "tags": [
-- "network.node"
-- ],
-- "x-openvibe-capabilities": [
-- "network.node.self.manage"
-- ],
-- "x-openvibe-visibility": [
-- "internal"
-- ],
-- "x-openvibe-quota-class": "admin",
-- "security": [
-- {
-- "openvibeToken": [
-- "network.node.self.manage"
-- ]
-- }
-- ],
-- "responses": {
-- "2XX": {
-- "description": "Success"
},
"default": {
"description": "An error (RFC 9457 problem details)",
@@@ -1578,41 +1435,140 @@
}
}
},
-- "/api/v1/node/self/pools/{id}": {
-- "get": {
-- "operationId": "get_api_v1_node_self_pools_id",
-- "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
-- "description": "**network.node.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's. It also announces itself for its owner's machine pools (bot.machine-pool@1), reads, approves and renews the pools it is a member of, and leaves them; it never acts for another machine.",
++ "/api/v1/me/consent/{purpose}/{client}/refusal": {
++ "delete": {
++ "operationId": "delete_api_v1_me_consent_purpose_client_refusal",
++ "summary": "\"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or",
++ "description": "**network.consent.manage** (public): \"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or off (network.consent-choice@1); DELETE /api/v1/me/consent/:purpose/:client turns one app's purpose off. \"Apps you allowed\" (0.310.0): GET /api/v1/me/consent/apps lists the purposes each app was granted or refused at sign-in (network.me-consent-apps-result@1), and DELETE /api/v1/me/consent/:purpose/:client/refusal ends a refusal's 30 quiet days so that app's next sign-in asks again. Off is the default for every purpose. Only the person's own session or token reaches it.",
"tags": [
-- "network.node"
++ "network.consent"
],
"x-openvibe-capabilities": [
-- "network.node.self.manage"
++ "network.consent.manage"
],
"x-openvibe-visibility": [
-- "internal"
++ "public"
],
-- "x-openvibe-quota-class": "admin",
++ "x-openvibe-quota-class": "standard",
"security": [
{
"openvibeToken": [
-- "network.node.self.manage"
++ "network.consent.manage"
]
}
],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/network.consent-choice.v1"
++ },
"parameters": [
{
-- "name": "id",
++ "name": "purpose",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "client",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
++ },
++ {
++ "name": "on",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "boolean"
++ }
++ },
++ {
++ "name": "kinds",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "array",
++ "maxItems": 8,
++ "uniqueItems": true,
++ "items": {
++ "enum": [
++ "streams",
++ "vods",
++ "clips",
++ "voice",
++ "chat",
++ "posts",
++ "pictures",
++ "code"
++ ]
++ }
++ }
++ },
++ {
++ "name": "options",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "licence": {
++ "enum": [
++ "cc-by-4.0",
++ "cc0-1.0"
++ ]
++ },
++ "features": {
++ "type": "array",
++ "maxItems": 4,
++ "uniqueItems": true,
++ "items": {
++ "enum": [
++ "verify",
++ "find",
++ "cast",
++ "library"
++ ]
++ }
++ }
++ }
++ }
++ },
++ {
++ "name": "text_version",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}\\.[0-9]{1,3}$",
++ "description": "The wording the page showed. Required to turn on; a stale one is refused (consent.stale_text) so the page shows the new words first."
++ },
++ "description": "The wording the page showed. Required to turn on; a stale one is refused (consent.stale_text) so the page shows the new words first."
++ },
++ {
++ "name": "adult",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "boolean",
++ "description": "The person confirmed they are 18 or older. Required (true) to turn on face.recognize, ai.looks, ai.train or data.public."
++ },
++ "description": "The person confirmed they are 18 or older. Required (true) to turn on face.recognize, ai.looks, ai.train or data.public."
}
],
"responses": {
"2XX": {
-- "description": "Success"
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.me-consent-result.v1"
++ }
++ }
++ }
},
"default": {
"description": "An error (RFC 9457 problem details)",
@@@ -1627,90 +1583,124 @@
}
}
},
-- "/api/v1/node/self/pools/{id}/approve": {
-- "post": {
-- "operationId": "post_api_v1_node_self_pools_id_approve",
-- "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
-- "description": "**network.node.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's. It also announces itself for its owner's machine pools (bot.machine-pool@1), reads, approves and renews the pools it is a member of, and leaves them; it never acts for another machine.",
++ "/api/v1/me/consent/apps": {
++ "get": {
++ "operationId": "get_api_v1_me_consent_apps",
++ "summary": "\"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or",
++ "description": "**network.consent.manage** (public): \"Your data choices\" for the signed-in person (L24): GET /api/v1/me/consent lists every purpose with its wording, the person's choices and their history; PUT /api/v1/me/consent/:purpose turns one on or off (network.consent-choice@1); DELETE /api/v1/me/consent/:purpose/:client turns one app's purpose off. \"Apps you allowed\" (0.310.0): GET /api/v1/me/consent/apps lists the purposes each app was granted or refused at sign-in (network.me-consent-apps-result@1), and DELETE /api/v1/me/consent/:purpose/:client/refusal ends a refusal's 30 quiet days so that app's next sign-in asks again. Off is the default for every purpose. Only the person's own session or token reaches it.",
"tags": [
-- "network.node"
++ "network.consent"
],
"x-openvibe-capabilities": [
-- "network.node.self.manage"
++ "network.consent.manage"
],
"x-openvibe-visibility": [
-- "internal"
++ "public"
],
-- "x-openvibe-quota-class": "admin",
++ "x-openvibe-quota-class": "standard",
"security": [
{
"openvibeToken": [
-- "network.node.self.manage"
++ "network.consent.manage"
]
}
],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/network.consent-choice.v1"
++ },
"parameters": [
{
-- "name": "id",
-- "in": "path",
++ "name": "on",
++ "in": "query",
"required": true,
"schema": {
-- "type": "string"
++ "type": "boolean"
}
-- }
-- ],
-- "responses": {
-- "2XX": {
-- "description": "Success"
},
-- "default": {
-- "description": "An error (RFC 9457 problem details)",
-- "content": {
-- "application/problem+json": {
-- "schema": {
-- "$ref": "#/components/schemas/errors.problem.v1"
++ {
++ "name": "kinds",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "array",
++ "maxItems": 8,
++ "uniqueItems": true,
++ "items": {
++ "enum": [
++ "streams",
++ "vods",
++ "clips",
++ "voice",
++ "chat",
++ "posts",
++ "pictures",
++ "code"
++ ]
++ }
++ }
++ },
++ {
++ "name": "options",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "licence": {
++ "enum": [
++ "cc-by-4.0",
++ "cc0-1.0"
++ ]
++ },
++ "features": {
++ "type": "array",
++ "maxItems": 4,
++ "uniqueItems": true,
++ "items": {
++ "enum": [
++ "verify",
++ "find",
++ "cast",
++ "library"
++ ]
++ }
}
}
}
-- }
-- }
-- }
-- },
-- "/api/v1/node/self/pools/{id}/leave": {
-- "post": {
-- "operationId": "post_api_v1_node_self_pools_id_leave",
-- "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
-- "description": "**network.node.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's. It also announces itself for its owner's machine pools (bot.machine-pool@1), reads, approves and renews the pools it is a member of, and leaves them; it never acts for another machine.",
-- "tags": [
-- "network.node"
-- ],
-- "x-openvibe-capabilities": [
-- "network.node.self.manage"
-- ],
-- "x-openvibe-visibility": [
-- "internal"
-- ],
-- "x-openvibe-quota-class": "admin",
-- "security": [
++ },
{
-- "openvibeToken": [
-- "network.node.self.manage"
-- ]
-- }
-- ],
-- "parameters": [
++ "name": "text_version",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}\\.[0-9]{1,3}$",
++ "description": "The wording the page showed. Required to turn on; a stale one is refused (consent.stale_text) so the page shows the new words first."
++ },
++ "description": "The wording the page showed. Required to turn on; a stale one is refused (consent.stale_text) so the page shows the new words first."
++ },
{
-- "name": "id",
-- "in": "path",
-- "required": true,
++ "name": "adult",
++ "in": "query",
++ "required": false,
"schema": {
-- "type": "string"
-- }
++ "type": "boolean",
++ "description": "The person confirmed they are 18 or older. Required (true) to turn on face.recognize, ai.looks, ai.train or data.public."
++ },
++ "description": "The person confirmed they are 18 or older. Required (true) to turn on face.recognize, ai.looks, ai.train or data.public."
}
],
"responses": {
"2XX": {
-- "description": "Success"
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.me-consent-result.v1"
++ }
++ }
++ }
},
"default": {
"description": "An error (RFC 9457 problem details)",
@@@ -1725,41 +1715,41 @@
}
}
},
-- "/api/v1/node/self/pools/{id}/status": {
-- "post": {
-- "operationId": "post_api_v1_node_self_pools_id_status",
-- "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
-- "description": "**network.node.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's. It also announces itself for its owner's machine pools (bot.machine-pool@1), reads, approves and renews the pools it is a member of, and leaves them; it never acts for another machine.",
++ "/api/v1/me/emails": {
++ "get": {
++ "operationId": "get_api_v1_me_emails",
++ "summary": "Read the one verified email address a person has shared with this app, or check whether one supplied address is verified for a supplied subject.",
++ "description": "**identity.emails.read** (public): Read the one verified email address a person has shared with this app, or check whether one supplied address is verified for a supplied subject. The full address list is only available to the person in their own account hub.",
"tags": [
-- "network.node"
++ "identity.emails"
],
"x-openvibe-capabilities": [
-- "network.node.self.manage"
++ "identity.emails.read"
],
"x-openvibe-visibility": [
-- "internal"
++ "public"
],
-- "x-openvibe-quota-class": "admin",
++ "x-openvibe-quota-class": "identity-read",
"security": [
{
"openvibeToken": [
-- "network.node.self.manage"
++ "identity.emails.read"
]
}
],
-- "parameters": [
-- {
-- "name": "id",
-- "in": "path",
-- "required": true,
-- "schema": {
-- "type": "string"
-- }
-- }
-- ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/identity.emails-request.v1"
++ },
"responses": {
"2XX": {
-- "description": "Success"
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/identity.emails.v1"
++ }
++ }
++ }
},
"default": {
"description": "An error (RFC 9457 problem details)",
@@@ -1774,31 +1764,41 @@
}
}
},
-- "/api/v1/node/self/pools/announce": {
-- "post": {
-- "operationId": "post_api_v1_node_self_pools_announce",
-- "summary": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
-- "description": "**network.node.self.manage** (internal): A paired machine presents its own capabilities and rotates its own credential, never another machine's. It also announces itself for its owner's machine pools (bot.machine-pool@1), reads, approves and renews the pools it is a member of, and leaves them; it never acts for another machine.",
++ "/api/v1/me/trust": {
++ "get": {
++ "operationId": "get_api_v1_me_trust",
++ "summary": "Read privacy-limited trust signals for an account or guest by Network subject.",
++ "description": "**identity.trust.read** (public): Read privacy-limited trust signals for an account or guest by Network subject. The caller receives only age bands, booleans and aggregate counts, never an IP, email, last-seen timestamp, service user id or linked account identities. Services may use the capability; an app needs the owner's explicit capability grant.",
"tags": [
-- "network.node"
++ "identity.trust"
],
"x-openvibe-capabilities": [
-- "network.node.self.manage"
++ "identity.trust.read"
],
"x-openvibe-visibility": [
-- "internal"
++ "public"
],
-- "x-openvibe-quota-class": "admin",
++ "x-openvibe-quota-class": "identity-read",
"security": [
{
"openvibeToken": [
-- "network.node.self.manage"
++ "identity.trust.read"
]
}
],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/identity.trust-request.v1"
++ },
"responses": {
"2XX": {
-- "description": "Success"
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/identity.trust.v1"
++ }
++ }
++ }
},
"default": {
"description": "An error (RFC 9457 problem details)",
@@@ -2925,35 +2925,228 @@
}
}
},
-- "/internal/integrations/github-token": {
-- "get": {
-- "operationId": "get_internal_integrations_github_token",
-- "summary": "Read the network's GitHub API token (read-only, public repositories), which the owner sets in Network's admin panel or in GITHUB_TOKEN.",
-- "description": "**network.integration.github.read** (first-party): Read the network's GitHub API token (read-only, public repositories), which the owner sets in Network's admin panel or in GITHUB_TOKEN. Used by OpenVibe.Blog's network changelog so its GitHub calls do not share the host's anonymous rate limit. Never granted to apps or mods.",
++ "/internal/integrations/github-token": {
++ "get": {
++ "operationId": "get_internal_integrations_github_token",
++ "summary": "Read the network's GitHub API token (read-only, public repositories), which the owner sets in Network's admin panel or in GITHUB_TOKEN.",
++ "description": "**network.integration.github.read** (first-party): Read the network's GitHub API token (read-only, public repositories), which the owner sets in Network's admin panel or in GITHUB_TOKEN. Used by OpenVibe.Blog's network changelog so its GitHub calls do not share the host's anonymous rate limit. Never granted to apps or mods.",
++ "tags": [
++ "network.integration"
++ ],
++ "x-openvibe-capabilities": [
++ "network.integration.github.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "integration-secret",
++ "security": [
++ {
++ "openvibeToken": [
++ "network.integration.github.read"
++ ]
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.github-token-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/internal/link-pairings": {
++ "post": {
++ "operationId": "post_internal_link_pairings",
++ "summary": "A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired.",
++ "description": "**network.link.manage** (internal): A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired. Never grantable to apps.",
++ "tags": [
++ "network.link"
++ ],
++ "x-openvibe-capabilities": [
++ "network.link.manage"
++ ],
++ "x-openvibe-visibility": [
++ "internal"
++ ],
++ "x-openvibe-quota-class": "admin",
++ "security": [
++ {
++ "openvibeToken": [
++ "network.link.manage"
++ ]
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success"
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/internal/link-principals/{id}": {
++ "get": {
++ "operationId": "get_internal_link_principals_id",
++ "summary": "A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired.",
++ "description": "**network.link.manage** (internal): A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired. Never grantable to apps.",
++ "tags": [
++ "network.link"
++ ],
++ "x-openvibe-capabilities": [
++ "network.link.manage"
++ ],
++ "x-openvibe-visibility": [
++ "internal"
++ ],
++ "x-openvibe-quota-class": "admin",
++ "security": [
++ {
++ "openvibeToken": [
++ "network.link.manage"
++ ]
++ }
++ ],
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success"
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/internal/link-principals/{id}/revoke": {
++ "post": {
++ "operationId": "post_internal_link_principals_id_revoke",
++ "summary": "A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired.",
++ "description": "**network.link.manage** (internal): A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired. Never grantable to apps.",
++ "tags": [
++ "network.link"
++ ],
++ "x-openvibe-capabilities": [
++ "network.link.manage"
++ ],
++ "x-openvibe-visibility": [
++ "internal"
++ ],
++ "x-openvibe-quota-class": "admin",
++ "security": [
++ {
++ "openvibeToken": [
++ "network.link.manage"
++ ]
++ }
++ ],
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success"
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/internal/links/report": {
++ "post": {
++ "operationId": "post_internal_links_report",
++ "summary": "Report the machines of the platform (roles, region-level location, beacon, health) to the node registry, which the geo API and every location-aware product read (ADR-034 section 12).",
++ "description": "**network.link.report** (internal): Report the machines of the platform (roles, region-level location, beacon, health) to the node registry, which the geo API and every location-aware product read (ADR-034 section 12).",
"tags": [
-- "network.integration"
++ "network.link"
],
"x-openvibe-capabilities": [
-- "network.integration.github.read"
++ "network.link.report"
],
"x-openvibe-visibility": [
-- "first-party"
++ "internal"
],
-- "x-openvibe-quota-class": "integration-secret",
++ "x-openvibe-quota-class": "admin",
"security": [
{
"openvibeToken": [
-- "network.integration.github.read"
++ "network.link.report"
]
}
],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.link-report-request.v1"
++ }
++ }
++ }
++ },
"responses": {
"2XX": {
"description": "Success",
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/network.github-token-result.v1"
++ "$ref": "#/components/schemas/network.link-list-result.v1"
}
}
}
@@@ -3299,248 +3492,17 @@
],
"requestBody": {
"required": false,
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/network.mod-install-request.v1"
-- }
-- }
-- }
-- },
-- "parameters": [
-- {
-- "name": "mod_id",
-- "in": "path",
-- "required": true,
-- "schema": {
-- "type": "string"
-- }
-- }
-- ],
-- "responses": {
-- "2XX": {
-- "description": "Success",
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/network.mod-principal.v1"
-- }
-- }
-- }
-- },
-- "default": {
-- "description": "An error (RFC 9457 problem details)",
-- "content": {
-- "application/problem+json": {
-- "schema": {
-- "$ref": "#/components/schemas/errors.problem.v1"
-- }
-- }
-- }
-- }
-- }
-- }
-- },
-- "/internal/modules/{namespace}/{subject}": {
-- "get": {
-- "operationId": "get_internal_modules_namespace_subject",
-- "summary": "Read a subject's user-module record in a granted namespace.",
-- "description": "**network.modules.read** (first-party): Read a subject's user-module record in a granted namespace.",
-- "tags": [
-- "network.modules"
-- ],
-- "x-openvibe-capabilities": [
-- "network.modules.read"
-- ],
-- "x-openvibe-visibility": [
-- "first-party"
-- ],
-- "x-openvibe-quota-class": "modules-read",
-- "security": [
-- {
-- "openvibeToken": [
-- "network.modules.read"
-- ]
-- }
-- ],
-- "parameters": [
-- {
-- "name": "namespace",
-- "in": "path",
-- "required": true,
-- "schema": {
-- "type": "string"
-- }
-- },
-- {
-- "name": "subject",
-- "in": "path",
-- "required": true,
-- "schema": {
-- "type": "string"
-- }
-- }
-- ],
-- "responses": {
-- "2XX": {
-- "description": "Success",
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/modules.module-record.v1"
-- }
-- }
-- }
-- },
-- "default": {
-- "description": "An error (RFC 9457 problem details)",
-- "content": {
-- "application/problem+json": {
-- "schema": {
-- "$ref": "#/components/schemas/errors.problem.v1"
-- }
-- }
-- }
-- }
-- }
-- },
-- "put": {
-- "operationId": "put_internal_modules_namespace_subject",
-- "summary": "Write a subject's record in a namespace the service owns (If-Match revision).",
-- "description": "**network.modules.write** (first-party): Write a subject's record in a namespace the service owns (If-Match revision).",
-- "tags": [
-- "network.modules"
-- ],
-- "x-openvibe-capabilities": [
-- "network.modules.write"
-- ],
-- "x-openvibe-visibility": [
-- "first-party"
-- ],
-- "x-openvibe-quota-class": "modules-write",
-- "security": [
-- {
-- "openvibeToken": [
-- "network.modules.write"
-- ]
-- }
-- ],
-- "requestBody": {
-- "required": false,
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/network.module-write-request.v1"
-- }
-- }
-- }
-- },
-- "parameters": [
-- {
-- "name": "namespace",
-- "in": "path",
-- "required": true,
-- "schema": {
-- "type": "string"
-- }
-- },
-- {
-- "name": "subject",
-- "in": "path",
-- "required": true,
-- "schema": {
-- "type": "string"
-- }
-- }
-- ],
-- "responses": {
-- "2XX": {
-- "description": "Success",
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/modules.module-record.v1"
-- }
-- }
-- }
-- },
-- "default": {
-- "description": "An error (RFC 9457 problem details)",
-- "content": {
-- "application/problem+json": {
-- "schema": {
-- "$ref": "#/components/schemas/errors.problem.v1"
-- }
-- }
-- }
-- }
-- }
-- }
-- },
-- "/internal/node-pairings": {
-- "post": {
-- "operationId": "post_internal_node_pairings",
-- "summary": "A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired.",
-- "description": "**network.node.manage** (internal): A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired. Never grantable to apps.",
-- "tags": [
-- "network.node"
-- ],
-- "x-openvibe-capabilities": [
-- "network.node.manage"
-- ],
-- "x-openvibe-visibility": [
-- "internal"
-- ],
-- "x-openvibe-quota-class": "admin",
-- "security": [
-- {
-- "openvibeToken": [
-- "network.node.manage"
-- ]
-- }
-- ],
-- "responses": {
-- "2XX": {
-- "description": "Success"
-- },
-- "default": {
-- "description": "An error (RFC 9457 problem details)",
-- "content": {
-- "application/problem+json": {
-- "schema": {
-- "$ref": "#/components/schemas/errors.problem.v1"
-- }
-- }
-- }
-- }
-- }
-- }
-- },
-- "/internal/node-principals/{id}": {
-- "get": {
-- "operationId": "get_internal_node_principals_id",
-- "summary": "A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired.",
-- "description": "**network.node.manage** (internal): A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired. Never grantable to apps.",
-- "tags": [
-- "network.node"
-- ],
-- "x-openvibe-capabilities": [
-- "network.node.manage"
-- ],
-- "x-openvibe-visibility": [
-- "internal"
-- ],
-- "x-openvibe-quota-class": "admin",
-- "security": [
-- {
-- "openvibeToken": [
-- "network.node.manage"
-- ]
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.mod-install-request.v1"
++ }
++ }
}
-- ],
++ },
"parameters": [
{
-- "name": "id",
++ "name": "mod_id",
"in": "path",
"required": true,
"schema": {
@@@ -3550,7 +3512,14 @@
],
"responses": {
"2XX": {
-- "description": "Success"
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.mod-principal.v1"
++ }
++ }
++ }
},
"default": {
"description": "An error (RFC 9457 problem details)",
@@@ -3565,31 +3534,39 @@
}
}
},
-- "/internal/node-principals/{id}/revoke": {
-- "post": {
-- "operationId": "post_internal_node_principals_id_revoke",
-- "summary": "A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired.",
-- "description": "**network.node.manage** (internal): A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired. Never grantable to apps.",
++ "/internal/modules/{namespace}/{subject}": {
++ "get": {
++ "operationId": "get_internal_modules_namespace_subject",
++ "summary": "Read a subject's user-module record in a granted namespace.",
++ "description": "**network.modules.read** (first-party): Read a subject's user-module record in a granted namespace.",
"tags": [
-- "network.node"
++ "network.modules"
],
"x-openvibe-capabilities": [
-- "network.node.manage"
++ "network.modules.read"
],
"x-openvibe-visibility": [
-- "internal"
++ "first-party"
],
-- "x-openvibe-quota-class": "admin",
++ "x-openvibe-quota-class": "modules-read",
"security": [
{
"openvibeToken": [
-- "network.node.manage"
++ "network.modules.read"
]
}
],
"parameters": [
{
-- "name": "id",
++ "name": "namespace",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "subject",
"in": "path",
"required": true,
"schema": {
@@@ -3599,7 +3576,14 @@
],
"responses": {
"2XX": {
-- "description": "Success"
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/modules.module-record.v1"
++ }
++ }
++ }
},
"default": {
"description": "An error (RFC 9457 problem details)",
@@@ -3612,27 +3596,25 @@
}
}
}
-- }
-- },
-- "/internal/nodes/report": {
-- "post": {
-- "operationId": "post_internal_nodes_report",
-- "summary": "Report the machines of the platform (roles, region-level location, beacon, health) to the node registry, which the geo API and every location-aware product read (ADR-034 section 12).",
-- "description": "**network.node.report** (internal): Report the machines of the platform (roles, region-level location, beacon, health) to the node registry, which the geo API and every location-aware product read (ADR-034 section 12).",
++ },
++ "put": {
++ "operationId": "put_internal_modules_namespace_subject",
++ "summary": "Write a subject's record in a namespace the service owns (If-Match revision).",
++ "description": "**network.modules.write** (first-party): Write a subject's record in a namespace the service owns (If-Match revision).",
"tags": [
-- "network.node"
++ "network.modules"
],
"x-openvibe-capabilities": [
-- "network.node.report"
++ "network.modules.write"
],
"x-openvibe-visibility": [
-- "internal"
++ "first-party"
],
-- "x-openvibe-quota-class": "admin",
++ "x-openvibe-quota-class": "modules-write",
"security": [
{
"openvibeToken": [
-- "network.node.report"
++ "network.modules.write"
]
}
],
@@@ -3641,18 +3623,36 @@
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/network.node-report-request.v1"
++ "$ref": "#/components/schemas/network.module-write-request.v1"
}
}
}
},
++ "parameters": [
++ {
++ "name": "namespace",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "subject",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
"responses": {
"2XX": {
"description": "Success",
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/network.node-list-result.v1"
++ "$ref": "#/components/schemas/modules.module-record.v1"
}
}
}
@@@ -3952,12 +3952,12 @@
"post": {
"operationId": "post_internal_registry_instances_report",
"summary": "Report the machines of the platform (roles, region-level location, beacon, health) to the node registry, which the geo API and every location-aware product read (ADR-034 section 12).",
-- "description": "**network.node.report** (internal): Report the machines of the platform (roles, region-level location, beacon, health) to the node registry, which the geo API and every location-aware product read (ADR-034 section 12).",
++ "description": "**network.link.report** (internal): Report the machines of the platform (roles, region-level location, beacon, health) to the node registry, which the geo API and every location-aware product read (ADR-034 section 12).",
"tags": [
-- "network.node"
++ "network.link"
],
"x-openvibe-capabilities": [
-- "network.node.report"
++ "network.link.report"
],
"x-openvibe-visibility": [
"internal"
@@@ -3966,7 +3966,7 @@
"security": [
{
"openvibeToken": [
-- "network.node.report"
++ "network.link.report"
]
}
],
@@@ -3975,7 +3975,7 @@
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/network.node-report-request.v1"
++ "$ref": "#/components/schemas/network.link-report-request.v1"
}
}
}
@@@ -3986,7 +3986,7 @@
"content": {
"application/json": {
"schema": {
-- "$ref": "#/components/schemas/network.node-list-result.v1"
++ "$ref": "#/components/schemas/network.link-list-result.v1"
}
}
}
@@@ -6660,73 +6660,236 @@
"channel"
]
},
-- "target_id": {
++ "target_id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "followers": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "following": {
++ "type": "boolean",
++ "description": "Present for a signed-in caller."
++ },
++ "notify_email": {
++ "type": "boolean"
++ },
++ "notify_push": {
++ "type": "boolean"
++ },
++ "since": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the caller started following."
++ }
++ }
++ },
++ "network.follow-write-request.v1": {
++ "title": "NetworkFollowWriteRequest",
++ "description": "network.follow-write-request@1 (ADR-030 step 4; capability network.follows.write): a first-party product records a follow or unfollow on a person's behalf, where they pressed the button. PUT /internal/follows/:type/:target { follower, notify_email?, notify_push? } follows (idempotent; flags change a follow). DELETE /internal/follows/:type/:target?follower=usr_… unfollows (idempotent). Answers network.follow-status-result@1 for the follower. Network emits network.follow.* as for a person's own change.",
++ "type": "object",
++ "required": [
++ "follower"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "follower": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "notify_email": {
++ "type": "boolean"
++ },
++ "notify_push": {
++ "type": "boolean"
++ }
++ }
++ },
++ "network.github-token-result.v1": {
++ "title": "NetworkGithubTokenResult",
++ "description": "network.github-token-result@1: the answer of network.integration.github.read on OpenVibe.Network. GET /internal/integrations/github-token (a service token holding the capability; the shared internal key is refused) → { token, source }: the network's read-only GitHub API token and where it is set (GITHUB_TOKEN in the environment, or the value the owner saved in admin). Answered with Cache-Control: no-store; 404 { error: not_configured, detail } when no token is set.",
++ "type": "object",
++ "required": [
++ "token",
++ "source"
++ ],
++ "properties": {
++ "token": {
++ "type": "string",
++ "minLength": 1,
++ "description": "A GitHub token (ghp_…, github_pat_…). A secret: never log or show it."
++ },
++ "source": {
++ "enum": [
++ "env",
++ "database",
++ "unset"
++ ],
++ "description": "env: GITHUB_TOKEN; database: saved in admin (Settings → GitHub)."
++ }
++ }
++ },
++ "network.link-list-result.v1": {
++ "title": "NetworkLinkListResult",
++ "description": "network.link-list-result@1: GET /api/v1/links on OpenVibe.Network (public, cacheable): every node of the platform, filterable by ?role= and ?region=. The input of openvibe-sdk/geo's nearest().",
++ "type": "object",
++ "required": [
++ "nodes",
++ "generated_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "nodes": {
++ "type": "array",
++ "items": {
++ "$ref": "#/components/schemas/network.link.v1"
++ }
++ },
++ "generated_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "network.link-report-request.v1": {
++ "title": "NetworkLinkReportRequest",
++ "description": "network.link-report-request@1: POST /internal/links/report on OpenVibe.Network (capability network.link.report, held by the Host principal). The complete set of nodes one Host inventory knows, with their health now: a node absent from a later report of the same source is marked down, never deleted silently.",
++ "type": "object",
++ "required": [
++ "source",
++ "nodes"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "source": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{1,39}$",
++ "description": "The reporting inventory (e.g. the host id)."
++ },
++ "nodes": {
++ "type": "array",
++ "maxItems": 500,
++ "items": {
++ "$ref": "#/components/schemas/network.link.v1"
++ }
++ }
++ }
++ },
++ "network.link.v1": {
++ "title": "NetworkLink",
++ "description": "network.link@1: one machine of the OpenVibe platform as the node registry lists it (ADR-034 section 12; roadmap WS-X1). Roles are what it serves; location is region-level; the beacon is an HTTPS URL a client times to measure its own round trip. No addresses, capacity or secrets are public: a node is found by role and location, and reached through the service that uses it.",
++ "type": "object",
++ "required": [
++ "id",
++ "roles",
++ "location",
++ "health",
++ "updated_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{1,39}$"
++ },
++ "name": {
++ "type": "string",
++ "maxLength": 80
++ },
++ "roles": {
++ "type": "array",
++ "minItems": 1,
++ "uniqueItems": true,
++ "items": {
++ "enum": [
++ "web",
++ "app",
++ "data",
++ "media-worker",
++ "ingest",
++ "edge-probe",
++ "edge-relay",
++ "edge-cache",
++ "gpu",
++ "staging",
++ "ci"
++ ]
++ }
++ },
++ "location": {
++ "type": "object",
++ "required": [
++ "region",
++ "country"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "region": {
++ "type": "string",
++ "pattern": "^[a-z]{2}-[a-z]+(-[0-9])?$",
++ "description": "e.g. us-west, eu-central"
++ },
++ "country": {
++ "type": "string",
++ "pattern": "^[A-Z]{2}$"
++ },
++ "city": {
++ "type": "string",
++ "maxLength": 80
++ },
++ "lat": {
++ "type": "number",
++ "minimum": -90,
++ "maximum": 90
++ },
++ "lon": {
++ "type": "number",
++ "minimum": -180,
++ "maximum": 180
++ }
++ }
++ },
++ "provider": {
"type": "string",
-- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
-- },
-- "followers": {
-- "type": "integer",
-- "minimum": 0
-- },
-- "following": {
-- "type": "boolean",
-- "description": "Present for a signed-in caller."
-- },
-- "notify_email": {
-- "type": "boolean"
-- },
-- "notify_push": {
-- "type": "boolean"
++ "maxLength": 40
},
-- "since": {
-- "type": "string",
-- "format": "date-time",
-- "description": "When the caller started following."
-- }
-- }
-- },
-- "network.follow-write-request.v1": {
-- "title": "NetworkFollowWriteRequest",
-- "description": "network.follow-write-request@1 (ADR-030 step 4; capability network.follows.write): a first-party product records a follow or unfollow on a person's behalf, where they pressed the button. PUT /internal/follows/:type/:target { follower, notify_email?, notify_push? } follows (idempotent; flags change a follow). DELETE /internal/follows/:type/:target?follower=usr_… unfollows (idempotent). Answers network.follow-status-result@1 for the follower. Network emits network.follow.* as for a person's own change.",
-- "type": "object",
-- "required": [
-- "follower"
-- ],
-- "additionalProperties": false,
-- "properties": {
-- "follower": {
++ "beacon": {
"type": "string",
-- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ "format": "uri",
++ "pattern": "^https://",
++ "description": "Timed by clients (GET, tiny body, no caching) to measure round-trip time."
},
-- "notify_email": {
-- "type": "boolean"
++ "health": {
++ "type": "object",
++ "required": [
++ "status",
++ "checked_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "status": {
++ "enum": [
++ "up",
++ "degraded",
++ "down",
++ "unknown"
++ ]
++ },
++ "checked_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
},
-- "notify_push": {
-- "type": "boolean"
-- }
-- }
-- },
-- "network.github-token-result.v1": {
-- "title": "NetworkGithubTokenResult",
-- "description": "network.github-token-result@1: the answer of network.integration.github.read on OpenVibe.Network. GET /internal/integrations/github-token (a service token holding the capability; the shared internal key is refused) → { token, source }: the network's read-only GitHub API token and where it is set (GITHUB_TOKEN in the environment, or the value the owner saved in admin). Answered with Cache-Control: no-store; 404 { error: not_configured, detail } when no token is set.",
-- "type": "object",
-- "required": [
-- "token",
-- "source"
-- ],
-- "properties": {
-- "token": {
++ "updated_at": {
"type": "string",
-- "minLength": 1,
-- "description": "A GitHub token (ghp_…, github_pat_…). A secret: never log or show it."
++ "format": "date-time"
},
-- "source": {
-- "enum": [
-- "env",
-- "database",
-- "unset"
-- ],
-- "description": "env: GITHUB_TOKEN; database: saved in admin (Settings → GitHub)."
++ "cell": {
++ "type": "string",
++ "pattern": "^[a-z]{2,8}-[0-9]{1,3}$",
++ "description": "Cell the node belongs to, e.g. wnam-1."
}
}
},
@@@ -6781,8 +6944,8 @@
},
"principal_id": {
"type": "string",
-- "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
-- "description": "The machine's node principal on Network (nod_…): the credential it holds. Absent for a machine Bot knows but Network has no principal for."
++ "pattern": "^lnk_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's node principal on Network (lnk_…): the credential it holds. Absent for a machine Bot knows but Network has no principal for."
},
"name": {
"type": "string",
@@@ -7286,169 +7449,6 @@
}
]
},
-- "network.node-list-result.v1": {
-- "title": "NetworkNodeListResult",
-- "description": "network.node-list-result@1: GET /api/v1/nodes on OpenVibe.Network (public, cacheable): every node of the platform, filterable by ?role= and ?region=. The input of openvibe-sdk/geo's nearest().",
-- "type": "object",
-- "required": [
-- "nodes",
-- "generated_at"
-- ],
-- "additionalProperties": false,
-- "properties": {
-- "nodes": {
-- "type": "array",
-- "items": {
-- "$ref": "#/components/schemas/network.node.v1"
-- }
-- },
-- "generated_at": {
-- "type": "string",
-- "format": "date-time"
-- }
-- }
-- },
-- "network.node-report-request.v1": {
-- "title": "NetworkNodeReportRequest",
-- "description": "network.node-report-request@1: POST /internal/nodes/report on OpenVibe.Network (capability network.node.report, held by the Host principal). The complete set of nodes one Host inventory knows, with their health now: a node absent from a later report of the same source is marked down, never deleted silently.",
-- "type": "object",
-- "required": [
-- "source",
-- "nodes"
-- ],
-- "additionalProperties": false,
-- "properties": {
-- "source": {
-- "type": "string",
-- "pattern": "^[a-z][a-z0-9-]{1,39}$",
-- "description": "The reporting inventory (e.g. the host id)."
-- },
-- "nodes": {
-- "type": "array",
-- "maxItems": 500,
-- "items": {
-- "$ref": "#/components/schemas/network.node.v1"
-- }
-- }
-- }
-- },
-- "network.node.v1": {
-- "title": "NetworkNode",
-- "description": "network.node@1: one machine of the OpenVibe platform as the node registry lists it (ADR-034 section 12; roadmap WS-X1). Roles are what it serves; location is region-level; the beacon is an HTTPS URL a client times to measure its own round trip. No addresses, capacity or secrets are public: a node is found by role and location, and reached through the service that uses it.",
-- "type": "object",
-- "required": [
-- "id",
-- "roles",
-- "location",
-- "health",
-- "updated_at"
-- ],
-- "additionalProperties": false,
-- "properties": {
-- "id": {
-- "type": "string",
-- "pattern": "^[a-z][a-z0-9-]{1,39}$"
-- },
-- "name": {
-- "type": "string",
-- "maxLength": 80
-- },
-- "roles": {
-- "type": "array",
-- "minItems": 1,
-- "uniqueItems": true,
-- "items": {
-- "enum": [
-- "web",
-- "app",
-- "data",
-- "media-worker",
-- "ingest",
-- "edge-probe",
-- "edge-relay",
-- "edge-cache",
-- "gpu",
-- "staging",
-- "ci"
-- ]
-- }
-- },
-- "location": {
-- "type": "object",
-- "required": [
-- "region",
-- "country"
-- ],
-- "additionalProperties": false,
-- "properties": {
-- "region": {
-- "type": "string",
-- "pattern": "^[a-z]{2}-[a-z]+(-[0-9])?$",
-- "description": "e.g. us-west, eu-central"
-- },
-- "country": {
-- "type": "string",
-- "pattern": "^[A-Z]{2}$"
-- },
-- "city": {
-- "type": "string",
-- "maxLength": 80
-- },
-- "lat": {
-- "type": "number",
-- "minimum": -90,
-- "maximum": 90
-- },
-- "lon": {
-- "type": "number",
-- "minimum": -180,
-- "maximum": 180
-- }
-- }
-- },
-- "provider": {
-- "type": "string",
-- "maxLength": 40
-- },
-- "beacon": {
-- "type": "string",
-- "format": "uri",
-- "pattern": "^https://",
-- "description": "Timed by clients (GET, tiny body, no caching) to measure round-trip time."
-- },
-- "health": {
-- "type": "object",
-- "required": [
-- "status",
-- "checked_at"
-- ],
-- "additionalProperties": false,
-- "properties": {
-- "status": {
-- "enum": [
-- "up",
-- "degraded",
-- "down",
-- "unknown"
-- ]
-- },
-- "checked_at": {
-- "type": "string",
-- "format": "date-time"
-- }
-- }
-- },
-- "updated_at": {
-- "type": "string",
-- "format": "date-time"
-- },
-- "cell": {
-- "type": "string",
-- "pattern": "^[a-z]{2,8}-[0-9]{1,3}$",
-- "description": "Cell the node belongs to, e.g. wnam-1."
-- }
-- }
-- },
"network.notification-push-request.v1": {
"title": "NetworkNotificationPushRequest",
"description": "network.notification-push-request@1: bodies of network.notifications.push on OpenVibe.Network (a service token holding the capability). POST /internal/notifications/push: { user_id | subject, type?, title?, message?, … } creates one notification (400 without user_id or subject; 404 for an unknown subject). POST /internal/notifications/push-bulk: { user_ids | subjects, …same fields } creates it for each recipient (400 without a non-empty user_ids or subjects, or with more than 1000). Recipients are Network user ids or canonical usr_ subjects; a subject resolves to its account, so a service that knows the person by subject (Vision's consent request, Cam's event alert) need not hold the numeric id. Unknown fields are ignored; 503 when the notification service is not running. The other /internal/notifications/* routes (unread, mark-read, resolve-users) take the internal key only and are not part of this capability.",
ddiff --git a/generated/openapi/news.json b/generated/openapi/news.json
iindex 7a3948de..973ebde7 100644
---- a/generated/openapi/news.json
++++ b/generated/openapi/news.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.News API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/openre.json b/generated/openapi/openre.json
iindex 7d783574..d09af43c 100644
---- a/generated/openapi/openre.json
++++ b/generated/openapi/openre.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenRestream API",
-- "version": "0.371.0",
-- "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/play.json b/generated/openapi/play.json
iindex c7b3f05b..1b67381b 100644
---- a/generated/openapi/play.json
++++ b/generated/openapi/play.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Play API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/quest.json b/generated/openapi/quest.json
iindex aef67199..d2230709 100644
---- a/generated/openapi/quest.json
++++ b/generated/openapi/quest.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Quest API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/report.json b/generated/openapi/report.json
iindex 05b80218..2ce997da 100644
---- a/generated/openapi/report.json
++++ b/generated/openapi/report.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Report API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/reviews.json b/generated/openapi/reviews.json
iindex 1dd03c3e..a0f34147 100644
---- a/generated/openapi/reviews.json
++++ b/generated/openapi/reviews.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Reviews API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/run.json b/generated/openapi/run.json
iindex 13db0564..f0bd32eb 100644
---- a/generated/openapi/run.json
++++ b/generated/openapi/run.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Run API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -1973,7 +1973,7 @@
"null"
],
"maxLength": 128,
-- "description": "The offer's provider (the usage reading's `provider`); null for a first-party node."
++ "description": "The offer's provider (the usage reading's `provider`); null for a first-party machine."
},
"region": {
"type": [
ddiff --git a/generated/openapi/search.json b/generated/openapi/search.json
iindex 3450f139..09be7f24 100644
---- a/generated/openapi/search.json
++++ b/generated/openapi/search.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Search API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/services.json b/generated/openapi/services.json
iindex 0153d398..e429b194 100644
---- a/generated/openapi/services.json
++++ b/generated/openapi/services.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Services API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/sources.json b/generated/openapi/sources.json
iindex 0018efb4..16a3eb4d 100644
---- a/generated/openapi/sources.json
++++ b/generated/openapi/sources.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Sources API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/space.json b/generated/openapi/space.json
iindex 6597eb97..15debc41 100644
---- a/generated/openapi/space.json
++++ b/generated/openapi/space.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Space API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tips.json b/generated/openapi/tips.json
iindex f2e60876..e18ed721 100644
---- a/generated/openapi/tips.json
++++ b/generated/openapi/tips.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tips API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/today.json b/generated/openapi/today.json
iindex 4ec147dc..84f54397 100644
---- a/generated/openapi/today.json
++++ b/generated/openapi/today.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Today API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tools.json b/generated/openapi/tools.json
iindex d94ccead..98bd7e2a 100644
---- a/generated/openapi/tools.json
++++ b/generated/openapi/tools.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tools API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/trade.json b/generated/openapi/trade.json
iindex 508ac72a..41c62581 100644
---- a/generated/openapi/trade.json
++++ b/generated/openapi/trade.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Trade API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/vip.json b/generated/openapi/vip.json
iindex 9c375671..cb64d97c 100644
---- a/generated/openapi/vip.json
++++ b/generated/openapi/vip.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.VIP API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/vision.json b/generated/openapi/vision.json
iindex 2a88ea15..ce5bb907 100644
---- a/generated/openapi/vision.json
++++ b/generated/openapi/vision.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Vision API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/watch.json b/generated/openapi/watch.json
iindex 994a832e..3dfd98ca 100644
---- a/generated/openapi/watch.json
++++ b/generated/openapi/watch.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Watch API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -3006,7 +3006,7 @@
},
"watch.watch.v1": {
"title": "Watch",
-- "description": "watch.watch@1: a user-defined persistent observation (plan T18). It names a source (a push event, a webhook, a conditional HTTP URL, a feed or API, a local Node probe, or an expensive Run check), how a value is extracted, how it is compared with the last observation, the condition that fires, the actions it wakes, its budget and its retention. Carrier preference is binding: an event or webhook source is used when one exists, then ETag/Last-Modified, then a feed, then an API, and browser polling through Run only when none works. A price or stock is 'as of' the observation, null when it stated none.",
++ "description": "watch.watch@1: a user-defined persistent observation (plan T18). It names a source (a push event, a webhook, a conditional HTTP URL, a feed or API, a local Link probe, or an expensive Run check), how a value is extracted, how it is compared with the last observation, the condition that fires, the actions it wakes, its budget and its retention. Carrier preference is binding: an event or webhook source is used when one exists, then ETag/Last-Modified, then a feed, then an API, and browser polling through Run only when none works. A price or stock is 'as of' the observation, null when it stated none.",
"type": "object",
"additionalProperties": false,
"required": [
ddiff --git a/generated/openapi/wiki.json b/generated/openapi/wiki.json
iindex 9c8f7143..9ceb8395 100644
---- a/generated/openapi/wiki.json
++++ b/generated/openapi/wiki.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Wiki API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/work.json b/generated/openapi/work.json
iindex ef7b0d3c..9cbc2658 100644
---- a/generated/openapi/work.json
++++ b/generated/openapi/work.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Work API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/zone.json b/generated/openapi/zone.json
iindex 7e896219..0e2c5023 100644
---- a/generated/openapi/zone.json
++++ b/generated/openapi/zone.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Zone API",
-- "version": "0.371.0",
-- "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.371.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.372.0",
++ "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.372.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 7b3c066c..bf80a322 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -76,7 +76,7 @@ export type ServiceTokenClaims = {
*/
iss: string;
sub: string;
-- actor_type: "service" | "app" | "mod" | "node" | "agent";
++ actor_type: "service" | "app" | "mod" | "link" | "agent";
/**
* @minItems 1
*/
@@@ -159209,9 +159209,9 @@ export type AiCredentialPut = {
/**
* openai = any OpenAI-compatible endpoint. node = the person's own machine: a model on their own model server, through their OpenVibe Link and OpenVibe.Bot (no key, no base_url, no cost).
*/
-- provider: "openai" | "anthropic" | "node";
++ provider: "openai" | "anthropic" | "link";
/**
-- * provider node: the person's own machine (openvibe.bot/machines) whose OpenVibe Link runs the model (the ai plugin, contracts 0.219.0), or `any` (contracts 0.221.0): whichever of their machines is on, lets their AI use its models and offers the model, the fastest first, the next one when it is busy or away. Never anyone else's machine.
++ * provider link: the person's own machine (openvibe.link/machines) whose OpenVibe Link runs the model (the ai plugin, contracts 0.219.0), or `any` (contracts 0.221.0): whichever of their machines is on, lets their AI use its models and offers the model, the fastest first, the next one when it is busy or away. Never anyone else's machine.
*/
machine?: string;
base_url?: string;
@@@ -159245,7 +159245,7 @@ export type AiCredentialPutV2 = {
/**
* anthropic = Anthropic's Messages API; openai = any OpenAI-compatible endpoint; deepseek, minimax and openrouter = those providers; custom = the endpoint in base_url (a URL that speaks the OpenAI or the Anthropic format, by the route it is used on); node = the person's own machine (no key, no base_url, no cost).
*/
-- provider: "anthropic" | "openai" | "deepseek" | "minimax" | "openrouter" | "custom" | "node";
++ provider: "anthropic" | "openai" | "deepseek" | "minimax" | "openrouter" | "custom" | "link";
/**
* The provider key (ai.credential-put@2's name for v1's api_key). Required the first time, and whenever provider or base_url changes (a key is never sent to a new endpoint without being entered again); otherwise the stored key is kept, and it is never returned.
*/
@@@ -159259,7 +159259,7 @@ export type AiCredentialPutV2 = {
*/
base_url?: string;
/**
-- * provider node: the person's own machine (openvibe.bot/machines) whose OpenVibe Link runs the model, or `any` of their own.
++ * provider link: the person's own machine (openvibe.link/machines) whose OpenVibe Link runs the model, or `any` of their own.
*/
machine?: string;
/**
@@@ -159290,9 +159290,9 @@ export type AiCredential = {
* The service that stored it; only it may use or change it.
*/
owner: string;
-- provider: "openai" | "anthropic" | "node";
++ provider: "openai" | "anthropic" | "link";
/**
-- * provider node: the machine it runs on, or `any` of the person's own (contracts 0.221.0)
++ * provider link: the machine it runs on, or `any` of the person's own (contracts 0.221.0)
*/
machine?: string | null;
base_url?: string | null;
@@@ -159326,9 +159326,9 @@ export type AiCredentialV2 = {
* The service that stored it; only it may use or change it.
*/
owner: string;
-- provider: "anthropic" | "openai" | "deepseek" | "minimax" | "openrouter" | "custom" | "node";
++ provider: "anthropic" | "openai" | "deepseek" | "minimax" | "openrouter" | "custom" | "link";
/**
-- * provider node: the machine it runs on, or `any` of the person's own
++ * provider link: the machine it runs on, or `any` of the person's own
*/
machine?: string | null;
base_url?: string | null;
@@@ -160444,7 +160444,7 @@ export interface AiInferenceDeployment {
*/
trust_tier?: "first_party" | "owner_node" | "approved_wholesale" | "unverified";
/**
-- * Operator slug or `owner:<subject_id>` for a private node.
++ * Operator slug or `owner:<subject_id>` for a private machine.
*/
operator?: string;
/**
@@@ -197303,7 +197303,7 @@ export interface TradeWatchlist {
/** [email protected] (owner: watch) */
/**
-- * watch.watch@1: a user-defined persistent observation (plan T18). It names a source (a push event, a webhook, a conditional HTTP URL, a feed or API, a local Node probe, or an expensive Run check), how a value is extracted, how it is compared with the last observation, the condition that fires, the actions it wakes, its budget and its retention. Carrier preference is binding: an event or webhook source is used when one exists, then ETag/Last-Modified, then a feed, then an API, and browser polling through Run only when none works. A price or stock is 'as of' the observation, null when it stated none.
++ * watch.watch@1: a user-defined persistent observation (plan T18). It names a source (a push event, a webhook, a conditional HTTP URL, a feed or API, a local Link probe, or an expensive Run check), how a value is extracted, how it is compared with the last observation, the condition that fires, the actions it wakes, its budget and its retention. Carrier preference is binding: an event or webhook source is used when one exists, then ETag/Last-Modified, then a feed, then an API, and browser polling through Run only when none works. A price or stock is 'as of' the observation, null when it stated none.
*/
export interface Watch {
id: string;
@@@ -210287,11 +210287,11 @@ export interface HostSite {
updated_at: string;
}
--/** [email protected] (owner: network) */
++/** [email protected] (owner: network) */
/**
-- * network.node@1: one machine of the OpenVibe platform as the node registry lists it (ADR-034 section 12; roadmap WS-X1). Roles are what it serves; location is region-level; the beacon is an HTTPS URL a client times to measure its own round trip. No addresses, capacity or secrets are public: a node is found by role and location, and reached through the service that uses it.
++ * network.link@1: one machine of the OpenVibe platform as the node registry lists it (ADR-034 section 12; roadmap WS-X1). Roles are what it serves; location is region-level; the beacon is an HTTPS URL a client times to measure its own round trip. No addresses, capacity or secrets are public: a node is found by role and location, and reached through the service that uses it.
*/
--export interface NetworkNode {
++export interface NetworkLink {
id: string;
name?: string;
/**
@@@ -210351,11 +210351,11 @@ export interface NetworkNode {
cell?: string;
}
--/** [email protected] (owner: network) */
++/** [email protected] (owner: network) */
/**
-- * network.node-report-request@1: POST /internal/nodes/report on OpenVibe.Network (capability network.node.report, held by the Host principal). The complete set of nodes one Host inventory knows, with their health now: a node absent from a later report of the same source is marked down, never deleted silently.
++ * network.link-report-request@1: POST /internal/links/report on OpenVibe.Network (capability network.link.report, held by the Host principal). The complete set of nodes one Host inventory knows, with their health now: a node absent from a later report of the same source is marked down, never deleted silently.
*/
--export interface NetworkNodeReportRequest {
++export interface NetworkLinkReportRequest {
/**
* The reporting inventory (e.g. the host id).
*/
@@@ -210363,15 +210363,15 @@ export interface NetworkNodeReportRequest {
/**
* @maxItems 500
*/
-- nodes: NetworkNode[];
++ nodes: NetworkLink[];
}
--/** [email protected] (owner: network) */
++/** [email protected] (owner: network) */
/**
-- * network.node-list-result@1: GET /api/v1/nodes on OpenVibe.Network (public, cacheable): every node of the platform, filterable by ?role= and ?region=. The input of openvibe-sdk/geo's nearest().
++ * network.link-list-result@1: GET /api/v1/links on OpenVibe.Network (public, cacheable): every node of the platform, filterable by ?role= and ?region=. The input of openvibe-sdk/geo's nearest().
*/
--export interface NetworkNodeListResult {
-- nodes: NetworkNode[];
++export interface NetworkLinkListResult {
++ nodes: NetworkLink[];
generated_at: string;
}
@@@ -210385,7 +210385,7 @@ export interface NetworkMachine {
*/
id: string;
/**
-- * The machine's node principal on Network (nod_…): the credential it holds. Absent for a machine Bot knows but Network has no principal for.
++ * The machine's node principal on Network (lnk_…): the credential it holds. Absent for a machine Bot knows but Network has no principal for.
*/
principal_id?: string;
/**
@@@ -210539,7 +210539,7 @@ export interface NetworkMachineRolesPut {
/** [email protected] (owner: network) */
/**
-- * network.machine-pairing@1: a one-time code that pairs a machine with the person's account (POST /api/v1/machines/pairings, capability machines.manage). The code is shown once, expires, and works once: the machine redeems it and becomes the person's. The command is what the owner runs on the machine (openvibe.bot/install). The code is never stored in the clear and never returned again.
++ * network.machine-pairing@1: a one-time code that pairs a machine with the person's account (POST /api/v1/machines/pairings, capability machines.manage). The code is shown once, expires, and works once: the machine redeems it and becomes the person's. The command is what the owner runs on the machine (openvibe.link/install). The code is never stored in the clear and never returned again.
*/
export interface NetworkMachinePairing {
/**
@@@ -210555,7 +210555,7 @@ export interface NetworkMachinePairing {
*/
expires_at: string;
/**
-- * The command the owner runs on the machine (openvibe.bot/install, with the network, the pairing id and the code).
++ * The command the owner runs on the machine (openvibe.link/install, with the network, the pairing id and the code).
*/
installer: string;
}
@@@ -210596,7 +210596,7 @@ export interface NetworkMachineOfflinePayload {
/** [email protected] (owner: network) */
/**
-- * network.machine.paired v1 (OpenVibe.Network, plan L34: the machines hub). A machine was paired with a person's account: it redeemed the one-time code and became theirs. It holds a credential from then on; the code is spent. The machine's device id in Bot is absent until the machine checks in for the first time. Envelope: subject { type: machine, id: <nod_…> }, visibility internal, actor service:network.
++ * network.machine.paired v1 (OpenVibe.Network, plan L34: the machines hub). A machine was paired with a person's account: it redeemed the one-time code and became theirs. It holds a credential from then on; the code is spent. The machine's device id in Bot is absent until the machine checks in for the first time. Envelope: subject { type: machine, id: <lnk_…> }, visibility internal, actor service:network.
*/
export interface NetworkMachinePairedPayload {
/**
@@@ -210728,7 +210728,7 @@ export interface NetworkCoinsReadResult {
/** [email protected] (owner: network) */
/**
-- * What one node or external provider can run right now (roadmap WS-Z9, decision 43): capabilities, multidimensional capacity, measured latencies, health and pricing. First-party only: it carries capacity the public node registry (network.node@1) deliberately leaves out.
++ * What one node or external provider can run right now (roadmap WS-Z9, decision 43): capabilities, multidimensional capacity, measured latencies, health and pricing. First-party only: it carries capacity the public node registry (network.link@1) deliberately leaves out.
*/
export type ResourceOffer = {
[k: string]: unknown | undefined;
@@@ -210736,7 +210736,7 @@ export type ResourceOffer = {
offer_id: string;
kind: "node" | "provider" | "storage" | "delivery" | "runtime" | "agent" | "harness";
/**
-- * network.node@1 id, for kind node
++ * network.link@1 id, for kind node
*/
node_id?: string;
/**
@@@ -210758,7 +210758,7 @@ export type ResourceOffer = {
*/
owner?: string;
/**
-- * e.g. node:http, worker:browser, worker:ffmpeg, worker:ai-gpu, events:gateway, events:durable, object:r2. Kind harness: task:edit, task:review, task:browse, task:run, task:test and task:plan for the task kinds it takes (its detail's task_capabilities), and harness:mcp, harness:resume, harness:host-access and harness:long-autonomy for its detail's capabilities that are true. The `worker:<class>` names that map to platform.runtime-class@1 values are reserved for OpenVibe Link's job worker, one per class: worker:function, worker:code, worker:browser, worker:linux, worker:desktop and worker:gpu ($defs.reservedWorkerCapabilities). A node advertises the name of each class it runs (Node's status.capabilities.worker); every other `worker:` name (worker:ffmpeg, worker:ai-gpu) is an ordinary capability name. Beside them, a kind harness names `runtime:<class>` for each platform.runtime-class@1 class it can run (runtime:function, runtime:code, runtime:browser, runtime:linux, runtime:desktop, runtime:gpu); these are ordinary capability names, not reserved, and OpenVibe.Codes already advertises the classes it offers.
++ * e.g. node:http, worker:browser, worker:ffmpeg, worker:ai-gpu, events:gateway, events:durable, object:r2. Kind harness: task:edit, task:review, task:browse, task:run, task:test and task:plan for the task kinds it takes (its detail's task_capabilities), and harness:mcp, harness:resume, harness:host-access and harness:long-autonomy for its detail's capabilities that are true. The `worker:<class>` names that map to platform.runtime-class@1 values are reserved for OpenVibe Link's job worker, one per class: worker:function, worker:code, worker:browser, worker:linux, worker:desktop and worker:gpu ($defs.reservedWorkerCapabilities). A Link advertises the name of each class it runs (Link's status.capabilities.worker); every other `worker:` name (worker:ffmpeg, worker:ai-gpu) is an ordinary capability name. Beside them, a kind harness names `runtime:<class>` for each platform.runtime-class@1 class it can run (runtime:function, runtime:code, runtime:browser, runtime:linux, runtime:desktop, runtime:gpu); these are ordinary capability names, not reserved, and OpenVibe.Codes already advertises the classes it offers.
*/
capabilities: string[];
capacity?: {
@@@ -211209,11 +211209,48 @@ export interface TelemetrySample {
};
}
--/** [email protected] (owner: network) */
++/** [email protected] (owner: network) */
/**
-- * Node hardware, placement regions, tags, and operating costs (T1 Universal Fabric; ADR-034 proposed).
++ * Where a person's work can run. Only ready candidates may be picked; a missing rental quote is never a price.
*/
--export interface NodeCapabilities {
++export interface LinkCompute {
++ sources: {
++ id?: string;
++ principal_id?: string;
++ kind: "personal" | "cloud" | "exchange" | "community";
++ name: string;
++ status: "ready" | "unavailable" | "coming_soon";
++ reason: string | null;
++ price?: {
++ amount_usd: number;
++ unit: "hour";
++ } | null;
++ quoted_at?: string;
++ expires_at?: string;
++ }[];
++ candidates: ({
++ id?: string;
++ principal_id?: string;
++ kind: "personal" | "cloud" | "exchange" | "community";
++ name: string;
++ status: "ready" | "unavailable" | "coming_soon";
++ reason: string | null;
++ price?: {
++ amount_usd: number;
++ unit: "hour";
++ } | null;
++ quoted_at?: string;
++ expires_at?: string;
++ } & {
++ id: string;
++ })[];
++}
++
++/** [email protected] (owner: network) */
++/**
++ * Link machine hardware, placement regions, tags, and operating costs (T1 Universal Fabric; ADR-034 proposed).
++ */
++export interface LinkCapabilities {
node_id: string;
cpu: {
cores: number;
@@@ -262643,7 +262680,7 @@ export type BotJobDispatchResult =
*/
subject: string | null;
/**
-- * The offer's provider, as Run sent it; null for a first-party node.
++ * The offer's provider, as Run sent it; null for a first-party machine.
*/
provider: string | null;
/**
@@@ -262733,7 +262770,7 @@ export type BotJobDispatchResult =
*/
subject: string | null;
/**
-- * The offer's provider, as Run sent it; null for a first-party node.
++ * The offer's provider, as Run sent it; null for a first-party machine.
*/
provider: string | null;
/**
@@@ -263337,7 +263374,7 @@ export type RunJobReadResult = {
*/
node: string;
/**
-- * The offer's provider (the usage reading's `provider`); null for a first-party node.
++ * The offer's provider (the usage reading's `provider`); null for a first-party machine.
*/
provider: string | null;
region: string | null;
@@@ -263557,7 +263594,7 @@ export interface RunJobStartedPayload {
*/
node: string;
/**
-- * The offer's provider (the usage reading's `provider`); null for a first-party node.
++ * The offer's provider (the usage reading's `provider`); null for a first-party machine.
*/
provider: string | null;
region: string | null;
@@@ -263617,7 +263654,7 @@ export interface RunJobSucceededPayload {
*/
node: string;
/**
-- * The offer's provider (the usage reading's `provider`); null for a first-party node.
++ * The offer's provider (the usage reading's `provider`); null for a first-party machine.
*/
provider: string | null;
region: string | null;
@@@ -263689,7 +263726,7 @@ export interface RunJobFailedPayload {
*/
node: string;
/**
-- * The offer's provider (the usage reading's `provider`); null for a first-party node.
++ * The offer's provider (the usage reading's `provider`); null for a first-party machine.
*/
provider: string | null;
region: string | null;
@@@ -263764,7 +263801,7 @@ export interface RunJobCancelledPayload {
*/
node: string;
/**
-- * The offer's provider (the usage reading's `provider`); null for a first-party node.
++ * The offer's provider (the usage reading's `provider`); null for a first-party machine.
*/
provider: string | null;
region: string | null;
@@@ -263833,7 +263870,7 @@ export interface RunJobExpiredPayload {
*/
node: string;
/**
-- * The offer's provider (the usage reading's `provider`); null for a first-party node.
++ * The offer's provider (the usage reading's `provider`); null for a first-party machine.
*/
provider: string | null;
region: string | null;
@@@ -263988,11 +264025,11 @@ export interface ResourceControlResult {
at: string;
}
--/** [email protected] (owner: network) */
++/** [email protected] (owner: network) */
/**
-- * network.node.revoked v1 (OpenVibe.Network; plan T2, docs/t2-cells-and-node-principal.md section 10, migrations/0014_node_pairing.sql). A node principal (nod_…), a person's paired machine, was revoked: through POST /internal/node-principals/:id/revoke (network.node.manage, the service that paired it), through POST /api/v1/me/nodes/:id/revoke (the owner's session), or with the owner's account. Network revokes the row and clears the credential and its grace window; a machine holding a live node token is refused at its next use (≤ 300 s), and until this event only learned of the revoke at its next reauth (≤ 330 s). A consumer stops the machine at once; a second revoke changes nothing and emits nothing. The credential and its hash are never carried. Envelope: subject { type: node_principal, id: <nod_…> }, visibility internal, actor the service (svc:<service>) or the owner ({ type: user }) that revoked.
++ * network.link.revoked v1 (OpenVibe.Network; plan T2, docs/t2-cells-and-node-principal.md section 10, migrations/0014_node_pairing.sql). A node principal (lnk_…), a person's paired machine, was revoked: through POST /internal/link-principals/:id/revoke (network.link.manage, the service that paired it), through POST /api/v1/me/nodes/:id/revoke (the owner's session), or with the owner's account. Network revokes the row and clears the credential and its grace window; a machine holding a live node token is refused at its next use (≤ 300 s), and until this event only learned of the revoke at its next reauth (≤ 330 s). A consumer stops the machine at once; a second revoke changes nothing and emits nothing. The credential and its hash are never carried. Envelope: subject { type: node_principal, id: <lnk_…> }, visibility internal, actor the service (svc:<service>) or the owner ({ type: user }) that revoked.
*/
--export interface NetworkNodeRevokedPayload {
++export interface NetworkLinkRevokedPayload {
/**
* The machine's node id (platform_node_principals.node_id), minted with the principal and unique among principals.
*/
@@@ -294275,7 +294312,7 @@ export interface BenchRun {
};
executor: {
/**
-- * codes: an openvibe.codes task. run: a Run worker. node: the person's own machine. submitted: nobody ran it.
++ * codes: an openvibe.codes task. run: a Run worker. link: the person's own machine. submitted: nobody ran it.
*/
kind: "codes" | "run" | "node" | "submitted";
id: string;
@@@ -294346,7 +294383,7 @@ export interface BenchRunV2 {
};
executor: {
/**
-- * codes: an openvibe.codes task. run: a Run worker. node: the person's own machine. submitted: nobody ran it.
++ * codes: an openvibe.codes task. run: a Run worker. link: the person's own machine. submitted: nobody ran it.
*/
kind: "codes" | "run" | "node" | "submitted";
id: string;
@@@ -294807,7 +294844,7 @@ export interface TodayReminderResult {
/** [email protected] (owner: connect) */
/**
-- * website.share@1: one Share on OpenVibe.Website Connect, a local port published at a stable hostname through the edge relay. The record says who owns it, which project scopes it, which Node tunnel serves it, which local target it may reach, which policy version the edge enforces and where it is in its life (draft, review, active, paused, revoked). The tunnel token, the request bodies the inspector captures and the policy's password hash are never part of this document.
++ * website.share@1: one Share on OpenVibe.Website Connect, a local port published at a stable hostname through the edge relay. The record says who owns it, which project scopes it, which Link tunnel serves it, which local target it may reach, which policy version the edge enforces and where it is in its life (draft, review, active, paused, revoked). The tunnel token, the request bodies the inspector captures and the policy's password hash are never part of this document.
*/
export interface WebsiteShare {
id: string;
@@@ -294861,7 +294898,7 @@ export interface WebsiteShare {
*/
expires_at?: string | null;
/**
-- * The last successful Node liveness check; null until the first tunnel.
++ * The last successful Link liveness check; null until the first tunnel.
*/
last_seen_at?: string | null;
created_at: string;
@@@ -294896,7 +294933,7 @@ export interface WebsiteShareCreateRequest {
password_set?: never;
};
/**
-- * Optional: bind the share to this Node principal now. Omit to bind it when the first tunnel connects.
++ * Optional: bind the share to this Link principal now. Omit to bind it when the first tunnel connects.
*/
node_id?: string;
/**
@@@ -303479,7 +303516,7 @@ export interface ChatParty {
/** [email protected] (owner: bot) */
/**
-- * bot.machine-pool@1 (the pooled machines design, 2026-11-06): several of one owner's machines on the same local network that serve one model together through llama.cpp RPC, `rpc-server` on the members and `llama-server --rpc` on the head. OpenVibe.Network holds the record: the owner proposes it on /machines (POST /api/v1/machine-pools, two to eight of their own machines and an optional head; the default head has the most free VRAM), and each machine reads it with its own node token (GET /api/v1/node/self/pools/:id, network.node.self.manage) and approves it locally (POST /api/v1/node/self/pools/:id/approve with { certificate }). A machine's `certificate` is null until it approves on that machine: the Link generates an Ed25519 key there, keeps the private key, and uploads only its self-signed public certificate, which the other members pin for the pool's private link. A pool with any null certificate never serves. The record never carries a private key or a prompt. What a schema cannot say, Network and Node check at run time: the members are distinct and the head is one of them; every member has the pool's owner, today, through Bot; every member was seen from the same public address with a private address in the same subnet; the head's direct link probe to each member passes; each machine's certificate is its own; and any change of membership, owner, head or key revokes the pool, so all machines approve a new one. A machine that leaves (removed, signed out, a different owner) drops its link and its key at once.
++ * bot.machine-pool@1 (the pooled machines design, 2026-11-06): several of one owner's machines on the same local network that serve one model together through llama.cpp RPC, `rpc-server` on the members and `llama-server --rpc` on the head. OpenVibe.Network holds the record: the owner proposes it on /machines (POST /api/v1/machine-pools, two to eight of their own machines and an optional head; the default head has the most free VRAM), and each machine reads it with its own Link token (GET /api/v1/link/self/pools/:id, network.link.self.manage) and approves it locally (POST /api/v1/link/self/pools/:id/approve with { certificate }). A machine's `certificate` is null until it approves on that machine: the Link generates an Ed25519 key there, keeps the private key, and uploads only its self-signed public certificate, which the other members pin for the pool's private link. A pool with any null certificate never serves. The record never carries a private key or a prompt. What a schema cannot say, Network and Link check at run time: the members are distinct and the head is one of them; every member has the pool's owner, today, through Bot; every member was seen from the same public address with a private address in the same subnet; the head's direct link probe to each member passes; each machine's certificate is its own; and any change of membership, owner, head or key revokes the pool, so all machines approve a new one. A machine that leaves (removed, signed out, a different owner) drops its link and its key at once.
*/
export interface BotMachinePool {
/**
ddiff --git a/lib/ids.js b/lib/ids.js
iindex b4564c26..40ddefce 100644
---- a/lib/ids.js
++++ b/lib/ids.js
@@@ -6,7 +6,7 @@
const crypto = require('crypto');
const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
--const PREFIX = { user: 'usr', guest: 'gst', app: 'app', mod: 'mod', agent: 'agt', node: 'nod', event: 'evt', media: 'med', project: 'prj', confirmation: 'cnf', consent: 'cns', job: 'job', task: 'tsk', watch: 'wch', wait: 'wai', observation: 'wco', check: 'ckr', subscription: 'sub', deploy: 'dpl', domain: 'dom', manifest: 'mfs', release: 'rel', site: 'sit', robot: 'rob', stream: 'std', webhook_delivery: 'whd', share: 'shr', device: 'dev', hook: 'hok', policy: 'pol', audit: 'aud', agent_stream: 'ags', agent_stream_schedule: 'scd', play_request: 'req', sandbox_session: 'sbs', party: 'pty' };
++const PREFIX = { user: 'usr', guest: 'gst', app: 'app', mod: 'mod', agent: 'agt', link: 'lnk', event: 'evt', media: 'med', project: 'prj', confirmation: 'cnf', consent: 'cns', job: 'job', task: 'tsk', watch: 'wch', wait: 'wai', observation: 'wco', check: 'ckr', subscription: 'sub', deploy: 'dpl', domain: 'dom', manifest: 'mfs', release: 'rel', site: 'sit', robot: 'rob', stream: 'std', webhook_delivery: 'whd', share: 'shr', device: 'dev', hook: 'hok', policy: 'pol', audit: 'aud', agent_stream: 'ags', agent_stream_schedule: 'scd', play_request: 'req', sandbox_session: 'sbs', party: 'pty' };
const SLUG_TYPES = new Set(['service', 'system']);
const ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
const SLUG_RE = /^[a-z][a-z0-9-]{1,39}$/;
@@@ -50,11 +50,11 @@ function formatSubject(ref) {
return `${ref.type}:${ref.id}`;
}
--/** Service-token `sub` claim: svc:<slug> | app:app_<ULID> | mod:mod_<ULID> | agent:agt_<ULID> | node:nod_<ULID>. */
++/** Service-token `sub` claim: svc:<slug> | app:app_<ULID> | mod:mod_<ULID> | agent:agt_<ULID> | link:lnk_<ULID>. */
function principalSub(ref) {
if (ref.type === 'service' && SLUG_RE.test(ref.id)) return `svc:${ref.id}`;
-- if ((ref.type === 'app' || ref.type === 'mod' || ref.type === 'agent' || ref.type === 'node') && isSubjectId(ref.type, ref.id)) return `${ref.type}:${ref.id}`;
-- throw new TypeError('principals are service, app, mod, agent or node subjects');
++ if ((ref.type === 'app' || ref.type === 'mod' || ref.type === 'agent' || ref.type === 'link') && isSubjectId(ref.type, ref.id)) return `${ref.type}:${ref.id}`;
++ throw new TypeError('principals are service, app, mod, agent or link subjects');
}
/** Transitional Media reference for today's rows (Wave 4 introduces med_ ids). */
ddiff --git a/manifests/capabilities/bot.computer.control.json b/manifests/capabilities/bot.computer.control.json
iindex ca0ce9a1..2d888710 100644
---- a/manifests/capabilities/bot.computer.control.json
++++ b/manifests/capabilities/bot.computer.control.json
@@@ -5,7 +5,7 @@
"status": "active",
"visibility": "first-party",
"sensitive": true,
-- "description": "Use a person's own computer with a Network service token naming that person in X-OV-Subject: the computer kinds of bot.command@1 (exec, fs, screen, input, browser) through POST /api/v1/robots/:id/commands, for a machine (openvibe.bot/machines) whose owner is that person and who turned on 'Let my agent use this computer'. Bot takes them from no one else (never an operator or a queue member), and the machine's owner sets on the machine itself what may happen (OpenVibe Link's computer plugin: observe, ask, trusted or full; ask waits for the owner's approval there). The answer is bot.command-result@1's data; every command is audited, a refused one emits bot.command.refused. OpenVibe.Watch (a `node` source) may send only `probe` — one GET or one TCP connect to a private, loopback or link-local address from the machine — and only where the owner turned on \"Let my watches check things from this computer\". The live screen (screen_stream) is not part of it: only the machine's owner starts it, on openvibe.bot, and Bot fills in the machine's private stream.",
++ "description": "Use a person's own computer with a Network service token naming that person in X-OV-Subject: the computer kinds of bot.command@1 (exec, fs, screen, input, browser) through POST /api/v1/robots/:id/commands, for a machine (openvibe.link/machines) whose owner is that person and who turned on 'Let my agent use this computer'. Bot takes them from no one else (never an operator or a queue member), and the machine's owner sets on the machine itself what may happen (OpenVibe Link's computer plugin: observe, ask, trusted or full; ask waits for the owner's approval there). The answer is bot.command-result@1's data; every command is audited, a refused one emits bot.command.refused. OpenVibe.Watch (a `node` source) may send only `probe` — one GET or one TCP connect to a private, loopback or link-local address from the machine — and only where the owner turned on \"Let my watches check things from this computer\". The live screen (screen_stream) is not part of it: only the machine's owner starts it, on openvibe.bot, and Bot fills in the machine's private stream.",
"permissions": [
"bot:computer:control"
],
ddiff --git a/manifests/capabilities/bot.machine.manage.json b/manifests/capabilities/bot.machine.manage.json
iindex 1cd22f18..6403babd 100644
---- a/manifests/capabilities/bot.machine.manage.json
++++ b/manifests/capabilities/bot.machine.manage.json
@@@ -5,7 +5,7 @@
"status": "active",
"visibility": "first-party",
"sensitive": true,
-- "description": "A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.network/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
++ "description": "A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.link/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
"permissions": [
"bot:machine:read",
"bot:machine:rename",
ddiff --git a/manifests/capabilities/network.link.manage.json b/manifests/capabilities/network.link.manage.json
nnew file mode 100644
iindex 00000000..ef750433
---- /dev/null
++++ b/manifests/capabilities/network.link.manage.json
@@@ -0,0 +1,23 @@
++{
++ "id": "network.link.manage",
++ "version": "1.0",
++ "owner": "network",
++ "status": "active",
++ "visibility": "internal",
++ "description": "A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired. Never grantable to apps.",
++ "permissions": [
++ "nodes:pair",
++ "nodes:read",
++ "nodes:revoke"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "admin",
++ "events": ["network.link.revoked"],
++ "implementedBy": [
++ "POST /internal/link-pairings",
++ "GET /internal/link-principals/:id",
++ "POST /internal/link-principals/:id/revoke"
++ ]
++}
ddiff --git a/manifests/capabilities/network.link.report.json b/manifests/capabilities/network.link.report.json
nnew file mode 100644
iindex 00000000..d914e237
---- /dev/null
++++ b/manifests/capabilities/network.link.report.json
@@@ -0,0 +1,22 @@
++{
++ "id": "network.link.report",
++ "version": "1.0",
++ "owner": "network",
++ "status": "active",
++ "visibility": "internal",
++ "description": "Report the machines of the platform (roles, region-level location, beacon, health) to the node registry, which the geo API and every location-aware product read (ADR-034 section 12).",
++ "permissions": [
++ "nodes:write"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "admin",
++ "events": [],
++ "implementedBy": [
++ "POST /internal/links/report",
++ "POST /internal/registry/instances/report"
++ ],
++ "inputSchema": "network.link-report-request@1",
++ "outputSchema": "network.link-list-result@1"
++}
ddiff --git a/manifests/capabilities/network.link.self.manage.json b/manifests/capabilities/network.link.self.manage.json
nnew file mode 100644
iindex 00000000..5c38a7b4
---- /dev/null
++++ b/manifests/capabilities/network.link.self.manage.json
@@@ -0,0 +1,25 @@
++{
++ "id": "network.link.self.manage",
++ "version": "1.0",
++ "owner": "network",
++ "status": "active",
++ "visibility": "internal",
++ "description": "A paired machine presents its own capabilities and rotates its own credential, never another machine's. It also announces itself for its owner's machine pools (bot.machine-pool@1), reads, approves and renews the pools it is a member of, and leaves them; it never acts for another machine.",
++ "permissions": [
++ "nodes:self"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "admin",
++ "events": [],
++ "implementedBy": [
++ "PUT /api/v1/link/self/capabilities",
++ "POST /api/v1/link/self/credential",
++ "POST /api/v1/link/self/pools/announce",
++ "GET /api/v1/link/self/pools/:id",
++ "POST /api/v1/link/self/pools/:id/approve",
++ "POST /api/v1/link/self/pools/:id/status",
++ "POST /api/v1/link/self/pools/:id/leave"
++ ]
++}
ddiff --git a/manifests/capabilities/network.node.manage.json b/manifests/capabilities/network.node.manage.json
ddeleted file mode 100644
iindex b27d6c68..00000000
---- a/manifests/capabilities/network.node.manage.json
++++ /dev/null
@@@ -1,23 +0,0 @@
--{
-- "id": "network.node.manage",
-- "version": "1.0",
-- "owner": "network",
-- "status": "active",
-- "visibility": "internal",
-- "description": "A service mints one-time pairing codes for its users' machines and reads or revokes the node principals it paired. Never grantable to apps.",
-- "permissions": [
-- "nodes:pair",
-- "nodes:read",
-- "nodes:revoke"
-- ],
-- "resourceConstraints": [
-- "owner"
-- ],
-- "quotaClass": "admin",
-- "events": ["network.node.revoked"],
-- "implementedBy": [
-- "POST /internal/node-pairings",
-- "GET /internal/node-principals/:id",
-- "POST /internal/node-principals/:id/revoke"
-- ]
--}
ddiff --git a/manifests/capabilities/network.node.report.json b/manifests/capabilities/network.node.report.json
ddeleted file mode 100644
iindex e9b9c53e..00000000
---- a/manifests/capabilities/network.node.report.json
++++ /dev/null
@@@ -1,22 +0,0 @@
--{
-- "id": "network.node.report",
-- "version": "1.0",
-- "owner": "network",
-- "status": "active",
-- "visibility": "internal",
-- "description": "Report the machines of the platform (roles, region-level location, beacon, health) to the node registry, which the geo API and every location-aware product read (ADR-034 section 12).",
-- "permissions": [
-- "nodes:write"
-- ],
-- "resourceConstraints": [
-- "none"
-- ],
-- "quotaClass": "admin",
-- "events": [],
-- "implementedBy": [
-- "POST /internal/nodes/report",
-- "POST /internal/registry/instances/report"
-- ],
-- "inputSchema": "network.node-report-request@1",
-- "outputSchema": "network.node-list-result@1"
--}
ddiff --git a/manifests/capabilities/network.node.self.manage.json b/manifests/capabilities/network.node.self.manage.json
ddeleted file mode 100644
iindex 625e445b..00000000
---- a/manifests/capabilities/network.node.self.manage.json
++++ /dev/null
@@@ -1,25 +0,0 @@
--{
-- "id": "network.node.self.manage",
-- "version": "1.0",
-- "owner": "network",
-- "status": "active",
-- "visibility": "internal",
-- "description": "A paired machine presents its own capabilities and rotates its own credential, never another machine's. It also announces itself for its owner's machine pools (bot.machine-pool@1), reads, approves and renews the pools it is a member of, and leaves them; it never acts for another machine.",
-- "permissions": [
-- "nodes:self"
-- ],
-- "resourceConstraints": [
-- "owner"
-- ],
-- "quotaClass": "admin",
-- "events": [],
-- "implementedBy": [
-- "PUT /api/v1/node/self/capabilities",
-- "POST /api/v1/node/self/credential",
-- "POST /api/v1/node/self/pools/announce",
-- "GET /api/v1/node/self/pools/:id",
-- "POST /api/v1/node/self/pools/:id/approve",
-- "POST /api/v1/node/self/pools/:id/status",
-- "POST /api/v1/node/self/pools/:id/leave"
-- ]
--}
ddiff --git a/manifests/capabilities/website.share.connect.json b/manifests/capabilities/website.share.connect.json
iindex 6be2856d..7048136c 100644
---- a/manifests/capabilities/website.share.connect.json
++++ b/manifests/capabilities/website.share.connect.json
@@@ -5,7 +5,7 @@
"status": "planned",
"visibility": "first-party",
"sensitive": true,
-- "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, node principal): a Link opens one share's tunnel with POST /api/v1/shares/:id/tunnel and a website.share-connect-request@1. The edge checks the Network node token (client credentials, subject node:nod_…, audience openvibe.website) and the short-lived, single-use tunnel token minted by Connect; neither alone opens the route. The node token's subject must equal node:<node_id> from the request, and a mismatch is refused and audited. Not delegated to user or app tokens.",
++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, link principal): a Link opens one share's tunnel with POST /api/v1/shares/:id/tunnel and a website.share-connect-request@1. The edge checks the Network Link token (client credentials, subject link:lnk_…, audience openvibe.website) and the short-lived, single-use tunnel token minted by Connect; neither alone opens the route. The Link token's subject must equal link:<node_id> from the request, and a mismatch is refused and audited. Not delegated to user or app tokens.",
"permissions": [
"website:tunnels:connect"
],
ddiff --git a/manifests/capabilities/website.share.delete.json b/manifests/capabilities/website.share.delete.json
iindex 8b88b851..1cdff808 100644
---- a/manifests/capabilities/website.share.delete.json
++++ b/manifests/capabilities/website.share.delete.json
@@@ -5,7 +5,7 @@
"status": "planned",
"visibility": "public",
"sensitive": true,
-- "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): revoke and delete a share with DELETE /api/v1/shares/:id. Connect writes website.share.revoked, closes the edge route and tunnel within its 5-second revocation budget and keeps the audit trail; a public hostname returns 410, not a cached page. Deleting a node in Network closes every share for that node through network.node.revoked.",
++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): revoke and delete a share with DELETE /api/v1/shares/:id. Connect writes website.share.revoked, closes the edge route and tunnel within its 5-second revocation budget and keeps the audit trail; a public hostname returns 410, not a cached page. Deleting a node in Network closes every share for that node through network.link.revoked.",
"permissions": [
"website:shares:delete"
],
ddiff --git a/manifests/products/openvibe.actor.json b/manifests/products/openvibe.actor.json
iindex 9f1f20ad..a8d7adc7 100644
---- a/manifests/products/openvibe.actor.json
++++ b/manifests/products/openvibe.actor.json
@@@ -47,7 +47,7 @@
},
{
"question": "Can an agent take full control of my computer?",
-- "answer": "Only if you set that Node to Full control on the device itself. It is time-boxed and recorded, you can see when an agent is in control, and a button, a hotkey or one command ends it. Most people use Trusted: routine actions run and risky ones ask first."
++ "answer": "Only if you set that Link to Full control on the device itself. It is time-boxed and recorded, you can see when an agent is in control, and a button, a hotkey or one command ends it. Most people use Trusted: routine actions run and risky ones ask first."
},
{
"question": "Can I run Actor myself?",
@@@ -234,7 +234,7 @@
[
"fa-robot",
"Robots too",
-- "The same Node drives robots on OpenVibe.Bot, so one install covers your computers and your robots.",
++ "The same Link drives robots on OpenVibe.Bot, so one install covers your computers and your robots.",
"https://openvibe.bot"
]
]
@@@ -257,7 +257,7 @@
[
"fa-server",
"Devices",
-- "Every Node with its status, a terminal, its files and its screen."
++ "Every Link with its status, a terminal, its files and its screen."
],
[
"fa-robot",
ddiff --git a/manifests/repositories/OpenVibe.Node.json b/manifests/repositories/OpenVibe.Node.json
iindex 8e98285f..9406a103 100644
---- a/manifests/repositories/OpenVibe.Node.json
++++ b/manifests/repositories/OpenVibe.Node.json
@@@ -1,6 +1,6 @@
{
-- "name": "OpenVibe.Node",
-- "repository": "OpenVibers/OpenVibe.Node",
++ "name": "OpenVibe.Link",
++ "repository": "OpenVibers/OpenVibe.Link",
"service": null,
"product": null,
"stage": "alpha",
ddiff --git a/manifests/services/codes.json b/manifests/services/codes.json
iindex b490e138..047bf648 100644
---- a/manifests/services/codes.json
++++ b/manifests/services/codes.json
@@@ -32,7 +32,7 @@
},
{
"question": "Can it run on my own machine?",
-- "answer": "Yes. With OpenVibe.Run and the OpenVibe node on your computer, a task can run on your own hardware instead of a server."
++ "answer": "Yes. With OpenVibe.Run and the OpenVibe Link on your computer, a task can run on your own hardware instead of a server."
},
{
"question": "Where is the code?",
ddiff --git a/manifests/services/network.json b/manifests/services/network.json
iindex ac47303e..da77c56f 100644
---- a/manifests/services/network.json
++++ b/manifests/services/network.json
@@@ -69,9 +69,9 @@
"network.integration.github.read",
"network.modules.read",
"network.modules.write",
-- "network.node.manage",
-- "network.node.report",
-- "network.node.self.manage",
++ "network.link.manage",
++ "network.link.report",
++ "network.link.self.manage",
"network.notifications.push",
"network.operator.alert",
"network.project.manage",
@@@ -105,7 +105,7 @@
"network.grant.changed",
"network.mod.grants_changed",
"network.module.updated",
-- "network.node.revoked",
++ "network.link.revoked",
"network.notification.created",
"network.principal_grant.changed",
"network.subject.merged",
ddiff --git a/manifests/services/watch.json b/manifests/services/watch.json
iindex 68ec5bea..6fe43c16 100644
---- a/manifests/services/watch.json
++++ b/manifests/services/watch.json
@@@ -95,7 +95,7 @@
"none": "no claims: one process runs the scheduler; check.run() refuses a second check of a watch in that process"
}
},
-- "notes": "Port 4730, unit openvibe-watch.service. Persistent, user-defined observations and conditions over pages, feeds and APIs (plan T18): the registry, the check engine with the pull carriers (http, feed, api; conditional GET, per-host spacing, backoff, Retry-After), observations and watch.* events through a transactional outbox, the bearer API (/api/v1, loopback) and, since 0.2.0, the public site at openvibe.watch: sign in with OpenVibe (OAuth client watch, PKCE S256, sessions in Watch's database) and create, edit, pause, check and delete your watches in the browser. Event, webhook, Run and Node carriers, AI extraction and actions come in later T18 steps and are not offered. A fired condition becomes a WATCH_TRIGGERED notification through OpenVibe.Network.",
++ "notes": "Port 4730, unit openvibe-watch.service. Persistent, user-defined observations and conditions over pages, feeds and APIs (plan T18): the registry, the check engine with the pull carriers (http, feed, api; conditional GET, per-host spacing, backoff, Retry-After), observations and watch.* events through a transactional outbox, the bearer API (/api/v1, loopback) and, since 0.2.0, the public site at openvibe.watch: sign in with OpenVibe (OAuth client watch, PKCE S256, sessions in Watch's database) and create, edit, pause, check and delete your watches in the browser. Event, webhook, Run and Link carriers, AI extraction and actions come in later T18 steps and are not offered. A fired condition becomes a WATCH_TRIGGERED notification through OpenVibe.Network.",
"health": "/api/health",
"ready": "/api/ready",
"internalOrigin": "http://127.0.0.1:4730",
ddiff --git a/package-lock.json b/package-lock.json
iindex 1e841cf7..a1b8edc7 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-contracts",
-- "version": "0.371.0",
++ "version": "0.372.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-contracts",
-- "version": "0.371.0",
++ "version": "0.372.0",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 8f5b7eba..a1a49440 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.371.0",
++ "version": "0.372.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
ddiff --git a/scripts/docs-currency.js b/scripts/docs-currency.js
iindex 76e2b501..322aeb8f 100644
---- a/scripts/docs-currency.js
++++ b/scripts/docs-currency.js
@@@ -23,7 +23,7 @@
* touched package.json's pins (so a bump without a STATUS refresh is caught): checked by the date only.
*
* Every other checkout under the root named OpenVibe.* (a client or repository with no service manifest, such as
-- * OpenVibe.Node or OpenVibe.Extensions) needs a STATUS.json with repository, stage, deployed, features and updated, so
++ * OpenVibe.Link or OpenVibe.Extensions) needs a STATUS.json with repository, stage, deployed, features and updated, so
* the estate does not show it blank; its README is not held to the service headings. One whose STATUS `stage` is
* closed, retired or archived (OpenVibe.Realtime, closed by ADR-005) is skipped.
*
@@@ -438,7 +438,7 @@ function checkRepo(root, manifest, ctx) {
}
/**
-- * A checkout with no service manifest (OpenVibe.Node, OpenVibe.Extensions): it needs a STATUS.json the estate can
++ * A checkout with no service manifest (OpenVibe.Link, OpenVibe.Extensions): it needs a STATUS.json the estate can
* read, and its facts are checked; the service README headings are not asked of it.
*/
function checkCheckout(root, name, ctx) {
ddiff --git a/scripts/estate.js b/scripts/estate.js
iindex 173aa068..77075614 100644
---- a/scripts/estate.js
++++ b/scripts/estate.js
@@@ -15,7 +15,7 @@
* package.json lacks is read from a nested one (apps/server, examples/<name>: at most three levels
* down, never node_modules), then from STATUS.json's prose
* STATUS.json repository, stage, runtime, domain, database, deployed and wave; version when there is no
-- * package.json (a Go repository such as OpenVibe.Node)
++ * package.json (a Go repository such as OpenVibe.Link)
* migrations/ the count and the dialect the SQL declares
* this repository's manifests/services|products/*.json the service or product the checkout is
*
ddiff --git a/test/docs-currency.test.js b/test/docs-currency.test.js
iindex e4d4d3b6..9923d185 100644
---- a/test/docs-currency.test.js
++++ b/test/docs-currency.test.js
@@@ -188,20 +188,20 @@ try {
// A checkout with no service manifest: OpenVibe.* needs a STATUS.json; closed ones and other names are skipped.
const manifests = [service('lib', 'OpenVibe.Lib')];
-- checkout('OpenVibe.Node', { 'README.md': '# Node\n', 'go.mod': 'module x\n' });
++ checkout('OpenVibe.Link', { 'README.md': '# Link\n', 'go.mod': 'module x\n' });
checkout('OpenVibe.Realtime', { 'STATUS.json': { repository: 'OpenVibers/OpenVibe.Realtime', stage: 'closed' } });
checkout('AFResume', { 'README.md': '# CV\n' });
let all = collect(facts, manifests);
const byId = (id) => all.find((x) => x.id === id);
-- ok(byId('node') && /STATUS\.json missing/.test(gapsOf(byId('node'))), 'an OpenVibe.* checkout without a STATUS.json is a gap');
++ ok(byId('link') && /STATUS\.json missing/.test(gapsOf(byId('link'))), `an OpenVibe.* checkout without a STATUS.json is a gap: ${JSON.stringify(byId('link'))}`);
ok(byId('realtime') && byId('realtime').skipped === 'closed', 'a closed repository is skipped');
ok(!all.some((x) => /afresume/i.test(x.id)), 'a checkout not named OpenVibe.* is not the platform\'s');
ok(byId('wiki') && byId('wiki').gaps.some((g) => /STATUS\.json missing/.test(g)), 'every unmanifested OpenVibe.* checkout is held to a STATUS.json');
-- checkout('OpenVibe.Node', { 'README.md': '# Node\n', 'STATUS.json': { repository: 'OpenVibers/OpenVibe.Node', stage: 'alpha', version: '0.13.0', deployed: false, features: ['pairing'], updated: '2099-01-01' } });
-- all = collect(facts, manifests, new Set(['node']));
-- ok(all.length === 1 && all[0].id === 'node' && all[0].gaps.length === 0, `--only reaches a repository checkout, and a complete STATUS is current: ${JSON.stringify(all)}`);
-- checkout('OpenVibe.Node', { 'README.md': '# Node\n', 'STATUS.json': { repository: 'OpenVibers/OpenVibe.Node', stage: 'alpha' } });
-- all = collect(facts, manifests, new Set(['node']));
++ checkout('OpenVibe.Link', { 'README.md': '# Link\n', 'STATUS.json': { repository: 'OpenVibers/OpenVibe.Link', stage: 'alpha', version: '0.13.0', deployed: false, features: ['pairing'], updated: '2099-01-01' } });
++ all = collect(facts, manifests, new Set(['link']));
++ ok(all.length === 1 && all[0].id === 'link' && all[0].gaps.length === 0, `--only reaches a repository checkout, and a complete STATUS is current: ${JSON.stringify(all)}`);
++ checkout('OpenVibe.Link', { 'README.md': '# Link\n', 'STATUS.json': { repository: 'OpenVibers/OpenVibe.Link', stage: 'alpha' } });
++ all = collect(facts, manifests, new Set(['link']));
ok(all[0].gaps.some((g) => /no "features"/.test(g)) && all[0].gaps.some((g) => /no "deployed"/.test(g)), 'a repository STATUS needs deployed and features');
ok(summarize([{ gaps: [] }, { skipped: 'closed', gaps: [] }]) === '1/1 current, 1 closed skipped', 'the summary counts closed repositories apart');
ddiff --git a/test/link-brand.test.js b/test/link-brand.test.js
nnew file mode 100644
iindex 00000000..eaa904f2
---- /dev/null
++++ b/test/link-brand.test.js
@@@ -0,0 +1,10 @@
++'use strict';
++const assert = require('node:assert/strict');
++const { spawnSync } = require('node:child_process');
++const path = require('node:path');
++const root = path.resolve(__dirname, '..');
++const oldName = ['OpenVibe[ .-]No' + 'de', 'openvibe-' + 'node', 'no' + 'd_', 'node[.]openvibe[.]network', 'node:lnk_', 'openvibe[.](bot|my|network)/mach' + 'ines'].join('|');
++const result = spawnSync('git', ['grep', '-n', '-I', '-i', '-E', oldName, '--', '.', ':!migrations/**', ':!test/link-brand.test.js'], { cwd: root, encoding: 'utf8' });
++assert.ok(result.status === 0 || result.status === 1, result.stderr || 'git grep failed');
++assert.equal(result.status, 1, `Old Link brand or principal returned:\n${(result.stdout || '').split('\n').slice(0, 10).join('\n')}`);
++console.log(' ok old Link brand and principal IDs are absent');
ddiff --git a/test/link-compute.test.js b/test/link-compute.test.js
nnew file mode 100644
iindex 00000000..dcb5db6c
---- /dev/null
++++ b/test/link-compute.test.js
@@@ -0,0 +1,16 @@
++'use strict';
++const assert = require('node:assert/strict');
++const contracts = require('..');
++const example = require('../contracts/link/compute.v1.json').examples[0];
++
++assert.equal(contracts.validate('link.compute@1', example).valid, true);
++assert.equal(contracts.validate('link.compute@1', {
++ ...example, sources: [...example.sources, { kind: 'exchange', name: 'GPU', status: 'ready', reason: null, price: null }],
++}).valid, false, 'a ready rental needs a live price and quote lifetime');
++assert.equal(contracts.validate('link.compute@1', {
++ ...example, candidates: [{ ...example.candidates[0], price: { amount_usd: -1, unit: 'hour' } }],
++}).valid, false, 'negative prices cannot be displayed');
++assert.equal(contracts.validate('link.compute@1', {
++ ...example, candidates: [{ ...example.candidates[0], status: 'imaginary' }],
++}).valid, false, 'unknown placement state is rejected');
++console.log('link.compute contract: ok');
ddiff --git a/test/platform-fabric.test.js b/test/platform-fabric.test.js
iindex 3a4beaaf..4e90bba3 100644
---- a/test/platform-fabric.test.js
++++ b/test/platform-fabric.test.js
@@@ -3,7 +3,7 @@ const assert = require('assert');
const contracts = require('..');
const names = [
-- 'usage-sample', 'telemetry-sample', 'node-capabilities', 'service-instance',
++ 'usage-sample', 'telemetry-sample', 'link-capabilities', 'service-instance',
'runtime-offer', 'storage-offer', 'delivery-offer', 'agent-offer', 'harness-offer', 'job',
];
ddiff --git a/test/platform-run.test.js b/test/platform-run.test.js
iindex 4d7609e5..33e99cad 100644
---- a/test/platform-run.test.js
++++ b/test/platform-run.test.js
@@@ -38,7 +38,7 @@ function reading(id, startedMs, n, quantity) {
return {
id: key, idempotency_key: key, service: 'run', project: record.project, subject: record.subject, resource: id,
node: record.node, operation: 'function.invoke', quantity, unit: 's', at: new Date(startedMs + n * 1000).toISOString(),
-- source: 'openvibe-node.worker',
++ source: 'openvibe-link.worker',
};
}
const fromUsage = (f) => reading(f.id, f.started_ms, f.second, 1);
ddiff --git a/test/run.js b/test/run.js
iindex c113f5ed..d17b8da3 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -36,6 +36,9 @@ function sub(file, ...args) {
for (const line of skipsIn(`${r.stdout}${r.stderr}`)) if (!skips.includes(line)) skips.push(line);
}
++sub(path.join(ROOT, 'test/link-brand.test.js'));
++sub(path.join(ROOT, 'test/link-compute.test.js'));
++
// ── Catalog integrity ────────────────────────────────────────────────────
const files = [];
(function walk(d) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { const p = path.join(d, e.name); if (e.isDirectory()) walk(p); else if (/\.v\d+\.json$/.test(e.name)) files.push(path.relative(path.join(ROOT, 'contracts'), p)); } })(path.join(ROOT, 'contracts'));
@@@ -610,10 +613,10 @@ for (const kind of ['user', 'guest', 'app', 'mod']) {
ok(ids.newId('user', 1000) < ids.newId('user', 2000), 'ulids sort by time');
ok(ids.parseSubject('user:42') === null && ids.parseSubject('user') === null && ids.parseSubject('svc:live') === null, 'malformed subjects are rejected');
ok(ids.principalSub({ type: 'service', id: 'live' }) === 'svc:live', 'service principal sub');
--const nodeId = ids.newId('node');
--ok(nodeId.startsWith('nod_') && ids.principalSub({ type: 'node', id: nodeId }) === `node:${nodeId}`, 'node principal sub is node:nod_<ULID>');
--assert.throws(() => ids.principalSub({ type: 'node', id: 'nod_42' }), /principals/);
--ok(!ids.SUBJECT_TYPES.includes('node') && ids.parseSubject(`node:${nodeId}`) === null, 'a node is a principal, never a subject');
++const nodeId = ids.newId('link');
++ok(nodeId.startsWith('lnk_') && ids.principalSub({ type: 'link', id: nodeId }) === `link:${nodeId}`, 'node principal sub is link:lnk_<ULID>');
++assert.throws(() => ids.principalSub({ type: 'link', id: 'lnk_42' }), /principals/);
++ok(!ids.SUBJECT_TYPES.includes('node') && ids.parseSubject(`link:${nodeId}`) === null, 'a node is a principal, never a subject');
assert.throws(() => ids.principalSub({ type: 'user', id: ids.newId('user') }), /principals/);
for (const [kind, prefix] of [['watch', 'wch'], ['observation', 'wco'], ['check', 'ckr'], ['job', 'job'], ['task', 'tsk']]) ok(ids.newId(kind).startsWith(`${prefix}_`), `newId(${kind}) is ${prefix}_<ULID>`);
ok(contracts.validate('media.media-ref', { media_id: ids.legacyMediaId('live', 'vod', 42) }).valid, 'legacyMediaId is a valid MediaRef');
@@@ -678,9 +681,9 @@ ok(V(sandboxTok, { acceptSandbox: true }).ok, 'sandbox app token accepted when t
ok(V(serviceAuth.signServiceToken({ ...appClaims, env: 'production' }, kp.privateKey)).ok, 'production app token accepted');
ok(V(serviceAuth.signServiceToken((({ project_id, ...rest }) => rest)(appClaims), kp.privateKey)).code === 'token.invalid_claims', 'an app token needs project_id');
ok(contracts.ids.newId('project').startsWith('prj_'), 'project ids use the prj prefix');
--const nodeClaims = { ...claims, sub: 'node:nod_01J0000000000000000000000Z', actor_type: 'node' };
++const nodeClaims = { ...claims, sub: 'link:lnk_01J0000000000000000000000Z', actor_type: 'link' };
ok(V(serviceAuth.signServiceToken(nodeClaims, kp.privateKey)).ok, 'node token accepted');
--ok(V(serviceAuth.signServiceToken({ ...nodeClaims, sub: 'node:nod_42' }, kp.privateKey)).code === 'token.invalid_claims', 'a malformed node id is rejected');
++ok(V(serviceAuth.signServiceToken({ ...nodeClaims, sub: 'link:lnk_42' }, kp.privateKey)).code === 'token.invalid_claims', 'a malformed node id is rejected');
const agentId = contracts.ids.newId('agent');
const agentClaims = { ...claims, sub: contracts.ids.principalSub({ type: 'agent', id: agentId }), actor_type: 'agent', cap: ['chat.presence.read'], cap_confirm: ['chat.message.send'], project_id: 'prj_01J0000000000000000000000Z', env: 'production', on_behalf_of: 'usr_01J0000000000000000000000Z', act: { sub: 'svc:actor' } };
const agentTok = V(serviceAuth.signServiceToken(agentClaims, kp.privateKey));