openvibers/openvibe.contracts Commits

Pooled machines: bot.machine-pool@1 and the AI offer's pool part (0.309.0) (#316)

OP OpenVibers committed 6 days ago · 915768cffd · 82 files, +3355 −94

* Pooled machines: bot.machine-pool@1 and the AI offer's pool part (0.306.0)

bot.machine-pool@1 is the record Network keeps for several of one owner's
machines on one local network serving one model through llama.cpp RPC: the
members, the head, each member's private address, link port and free memory,
and its self-signed Ed25519 public certificate once it approves on itself.
A private key, a public, loopback or wildcard address and any unnamed field
are refused.

ai.offer@1 is the community AI offer Bot reports and AI reads from Network,
and ai.offer-pool@1 its pool part, { machines, head }, which Network adds to
the head's offer from its own record. network.node.self.manage names the
five pool routes a machine calls with its own token.

* Merge origin/main (0.308.0) into feature/pooled-machines

The pooled-machines entry keeps its reserved 0.306.0, placed below 0.308.0
in the changelog; generated files regenerated.

* Pooled machines release as 0.309.0: main took 0.308.0 (Memory M5) first, and a lower tag after it would read as a downgrade

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 2f8eef95..e25280af 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,14 @@
# Changelog
++## 0.309.0 — 2026-11-07
++
++- Add `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, `llama-server --rpc` on the head). OpenVibe.Network holds the record: the owner proposes it on /machines with two to eight of their own machines (the default head has the most free VRAM), and each machine reads it with its own node token and approves it on itself. A member's `certificate` is null until it approves; then it is that machine's self-signed Ed25519 public certificate, which the other members pin for the pool's private link. The private key stays on the machine: a certificate slot holding anything with `PRIVATE KEY` in it is refused, and so is any field the schema does not name. A member's `address` is a private RFC 1918 address and prefix (never 0.0.0.0, loopback or a public address) and its `public_ip` is the address Network saw it from. The rules a schema cannot say are written in the description and checked by Network and Node: the head is a member, every member is the owner's today, all share one public address and one private subnet, the head's direct link probe to each passes, every key is its own, and any change of membership, owner, head or key revokes the pool so every machine approves a new one.
++- Add `ai.offer@1`: one community AI offer as Bot reports it to Network and AI reads it back (the fields Network already stores: machine, owner, models, hours, concurrency, credit price, audience, readiness and health), with the new optional `pool`.
++- Add `ai.offer-pool@1`, the `pool` part of the AI offer: `{ machines, head }`, two to eight machine ids and the head, which is the offer's own machine. Network adds it from its approved pool record, never from what a machine reports, and leaves a member's own offer out while it is in a pool. It names machines only: no address, key or certificate reaches AI. One price, audience, hours, idle-only and verification apply to the whole pool; AI verifies a pool's answers as it would one machine's, keys that verification on the exact membership, keeps a new or failed pool to the free tier only, and labels it "Community pool (N machines)".
++- `network.node.self.manage` names the five pool routes a machine calls with its own token (announce, read, approve, renew readiness, leave) in `implementedBy`.
++- Valid and invalid fixtures cover a pool waiting for approvals, an approved pool of three, a single-machine offer, a pool's offer and one still waiting, a group offer, a missing head or owner, a one-machine pool, a repeated member, a private key in the certificate slot, a public, loopback or wildcard address, a port under 1024, and an address, certificate or key smuggled into a field the schema does not name.
++
++
## 0.308.0 — 2026-11-07
- Add `ai.memory-export@1` (Memory M5): the one portable memory file. It carries `format: openvibe.memory-export@1`, `version: 1`, `exported_at` and every item's text, kind, scope, source, confidence and source reference. It deliberately has no ids, owners, merge links or conflict pair, so a file can be imported into another account without carrying anyone's identity, and a profile is derived from the items and rebuilt, never imported. OpenVibe.AI's `GET /api/v1/memory/export` writes it and `POST /api/v1/memory/import` accepts the same document; both redact again.
ddiff --git a/STATUS.json b/STATUS.json
iindex ea0a4c7d..5f7a5dc9 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -11,6 +11,7 @@
"deployment": "none of its own: a library and artifact repository published as release tags; services pin a tag and deploy it with their own releases",
"contracts": "openvibe-contracts (this package: package.json's version, released as the matching tag)",
"features": [
++ "bot.machine-pool@1, ai.offer@1 and ai.offer-pool@1 (the pooled machines design): several of one owner's machines on one local network serving one model through llama.cpp RPC, each approving on itself with its own public certificate, and the community AI offer with the pool part Network adds to the head's offer",
"chat.play-request@1 and codes.sandbox-session@1 (the Agent Streams plan, S6): the queue a chat plays room keeps and the sandbox desk that works it, with the owner's time box, cap and Go, and the scratch site that lives 24 hours",
"the capabilities chat.play_queue.read and chat.play_queue.write, for Codes acting for the stream's owner",
"chat.agent-digest@1, chat.agent-poll@1 and live.agent-stream-control@1 (the Agent Streams plan, S5): the digest a Co-pilot room gives an agent, the poll an agent asks that room to run, and the four controls the owner and the moderators have over a live agent stream",
ddiff --git a/contracts/ai/offer-pool.v1.json b/contracts/ai/offer-pool.v1.json
nnew file mode 100644
iindex 00000000..44b21072
---- /dev/null
++++ b/contracts/ai/offer-pool.v1.json
@@@ -0,0 +1,30 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/ai/offer-pool.v1.json",
++ "title": "AiOfferPool",
++ "description": "ai.offer-pool@1 (the pooled machines design, 2026-11-06): the `pool` part of a community AI offer (ai.offer@1) served by several of one owner's machines together (bot.machine-pool@1). Network adds it from its own approved pool record, never from what a machine reports: the offer is the head's, `head` equals the offer's `machine_id`, and while a machine is in a pool it advertises no offer of its own. It names machines only; no address, key or certificate reaches AI. AI routes the request to the head, runs the same verification as for one machine (canaries, the model fingerprint, spot re-runs) on the pool's answers, keys that verification on this exact membership (another member or head is a new, unverified pool), keeps a pool that is new or failed to the free tier only, and labels its answers \"Community pool (N machines)\".",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "machines",
++ "head"
++ ],
++ "properties": {
++ "machines": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 8,
++ "uniqueItems": true,
++ "description": "The pool's machines, head included (Bot device ids), two to eight.",
++ "items": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "head": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine that answers: one of `machines`, and the offer's own `machine_id`."
++ }
++ }
++}
ddiff --git a/contracts/ai/offer.v1.json b/contracts/ai/offer.v1.json
nnew file mode 100644
iindex 00000000..fe0937f6
---- /dev/null
++++ b/contracts/ai/offer.v1.json
@@@ -0,0 +1,146 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/ai/offer.v1.json",
++ "title": "AiOffer",
++ "description": "ai.offer@1: one community AI offer, a person's machine (OpenVibe.Node's ai plugin, \"Offer to the network\") serving its local models to requests that opt in to community capacity. OpenVibe.Bot reports these fields (POST /internal/ai/offers/report on Network, network.resource.report; a report with any other field is refused whole), and OpenVibe.AI reads them from Network (GET /internal/ai/offers, network.registry.read, { offers, generated_at, count }). Network adds `pool` (ai.offer-pool@1) to the head's offer when the machine is the head of an approved pool (bot.machine-pool@1), and leaves a pool member's own offer out of the list while it is in a pool. For a pool the hours, audience, price, readiness, idle-only and verification apply to the whole pool: it is ready only while every member has approved, renewed its readiness in the last few seconds, and is still the owner's. Nothing here is public: AI shows Exchange a hashed offer id and no owner or machine id.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "offer_id",
++ "machine_id",
++ "owner",
++ "models",
++ "hours",
++ "concurrency",
++ "price",
++ "audience",
++ "group",
++ "ready",
++ "health"
++ ],
++ "properties": {
++ "offer_id": {
++ "type": "string",
++ "pattern": "^ai:dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "`ai:<machine_id>`: one offer per machine."
++ },
++ "machine_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine that answers (for a pool, its head)."
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's owner, who earns the offer's credits (a self-served run earns none)."
++ },
++ "models": {
++ "type": "array",
++ "minItems": 1,
++ "maxItems": 40,
++ "description": "The models offered, as the machine's model server names them.",
++ "items": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 200
++ }
++ },
++ "hours": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "start",
++ "end",
++ "tz"
++ ],
++ "description": "The local-time window the offer is open; start equal to end is all day, and an overnight window wraps.",
++ "properties": {
++ "start": {
++ "type": "string",
++ "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$"
++ },
++ "end": {
++ "type": "string",
++ "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$"
++ },
++ "tz": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 64,
++ "description": "An IANA time zone, such as Europe/Paris, or UTC."
++ }
++ }
++ },
++ "concurrency": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 8,
++ "description": "How many requests may run at once; another is refused as busy, never queued."
++ },
++ "price": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "in_per_million",
++ "out_per_million"
++ ],
++ "description": "Credits per million input and output tokens; 0 and 0 is a volunteer, who earns kudos. One price for a pool.",
++ "properties": {
++ "in_per_million": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1000000
++ },
++ "out_per_million": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1000000
++ }
++ }
++ },
++ "audience": {
++ "enum": [
++ "everyone",
++ "followers",
++ "group"
++ ],
++ "description": "Who may use it: everyone, the owner's followers, or one group (`group`)."
++ },
++ "group": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80,
++ "description": "The group's id when the audience is `group`, otherwise null."
++ },
++ "ready": {
++ "type": "boolean",
++ "description": "Whether the machine (for a pool, every member) can answer now: online, inside its hours, not paused by idle-only or a kill switch."
++ },
++ "health": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "status"
++ ],
++ "properties": {
++ "status": {
++ "enum": [
++ "up",
++ "degraded",
++ "down"
++ ]
++ },
++ "checked_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "pool": {
++ "$ref": "offer-pool.v1.json",
++ "description": "Present when the offer is served by a pool of the owner's machines (ai.offer-pool@1)."
++ }
++ }
++}
ddiff --git a/contracts/bot/machine-pool.v1.json b/contracts/bot/machine-pool.v1.json
nnew file mode 100644
iindex 00000000..21d8ab29
---- /dev/null
++++ b/contracts/bot/machine-pool.v1.json
@@@ -0,0 +1,119 @@
++{
++ "$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 Node 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.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "owner",
++ "head",
++ "machines"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^pool_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The pool's id (ULID, Crockford base32). A pool is never edited: a change is a new pool with a new id."
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person every machine in the pool belongs to. Pools of different owners are not allowed."
++ },
++ "head": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine that runs `llama-server --rpc`, sees the prompt and answers; it must be one of `machines`. The others receive tensors only."
++ },
++ "machines": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 8,
++ "description": "The members, head included, two to eight.",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "owner",
++ "address",
++ "public_ip",
++ "port",
++ "free_vram",
++ "free_ram",
++ "hardware",
++ "certificate"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body."
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's owner when it announced itself; it must equal the pool's owner."
++ },
++ "address": {
++ "type": "string",
++ "pattern": "^(10\\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])|172\\.(1[6-9]|2[0-9]|3[01])|192\\.168)\\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])/([89]|[12][0-9]|30)$",
++ "maxLength": 32,
++ "description": "The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address."
++ },
++ "public_ip": {
++ "type": "string",
++ "maxLength": 45,
++ "anyOf": [
++ {
++ "format": "ipv4"
++ },
++ {
++ "format": "ipv6"
++ }
++ ],
++ "description": "The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head."
++ },
++ "port": {
++ "type": "integer",
++ "minimum": 1024,
++ "maximum": 65535,
++ "description": "The TCP port of the machine's private link on `address`."
++ },
++ "free_vram": {
++ "type": "integer",
++ "minimum": 0,
++ "maximum": 16777216,
++ "description": "Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it."
++ },
++ "free_ram": {
++ "type": "integer",
++ "minimum": 0,
++ "maximum": 16777216,
++ "description": "Free system memory when it announced itself, in MB."
++ },
++ "hardware": {
++ "type": "string",
++ "maxLength": 120,
++ "description": "A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model."
++ },
++ "certificate": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 8192,
++ "pattern": "^-----BEGIN CERTIFICATE-----",
++ "not": {
++ "type": "string",
++ "pattern": "PRIVATE KEY"
++ },
++ "description": "The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key."
++ }
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 9f3bf3c9..e9e6cded 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -9636,6 +9636,33 @@
"visibility": "first-party",
"compatibility": "backward",
"status": "active"
++ },
++ {
++ "id": "bot.machine-pool",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/machine-pool.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "ai.offer-pool",
++ "version": "1.0.0",
++ "owner": "ai",
++ "schema": "ai/offer-pool.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "ai.offer",
++ "version": "1.0.0",
++ "owner": "ai",
++ "schema": "ai/offer.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
}
]
}
ddiff --git a/fixtures/ai.offer-pool/invalid/address-field.json b/fixtures/ai.offer-pool/invalid/address-field.json
nnew file mode 100644
iindex 00000000..c45c78c8
---- /dev/null
++++ b/fixtures/ai.offer-pool/invalid/address-field.json
@@@ -0,0 +1,8 @@
++{
++ "machines": [
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2"
++ ],
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "address": "192.168.1.20/24"
++}
ddiff --git a/fixtures/ai.offer-pool/invalid/certificate-field.json b/fixtures/ai.offer-pool/invalid/certificate-field.json
nnew file mode 100644
iindex 00000000..07cc1f56
---- /dev/null
++++ b/fixtures/ai.offer-pool/invalid/certificate-field.json
@@@ -0,0 +1,10 @@
++{
++ "machines": [
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2"
++ ],
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "certificates": [
++ "-----BEGIN CERTIFICATE-----\nMIIBRDCB96ADAgECAhQ1F6qS8nQmfA4Z1EQWbuTK6c+/CDAFBgMrZXAwGDEWMBQG\nA1UEAwwNT3BlblZpYmUtcG9vbDAeFw0yNjExMDcwMjE1NThaFw0zNjExMDQwMjE1\nNThaMBgxFjAUBgNVBAMMDU9wZW5WaWJlLXBvb2wwKjAFBgMrZXADIQBzzRaEd6ZR\nqYbsbYK3W8CCJPIAbsNTlIXZYgPEC9yf76NTMFEwHQYDVR0OBBYEFPiGTnP9vRST\nUlo0W1Eh0xcOxA49MB8GA1UdIwQYMBaAFPiGTnP9vRSTUlo0W1Eh0xcOxA49MA8G\nA1UdEwEB/wQFMAMBAf8wBQYDK2VwA0EAOlCHoX3ONMZYTHx4GSAlxP4EQase+99L\nGykZvgPSVDs2alKLXW5fBxvn7OcEJ3g/EMmk9iZ+AL+CmLxdcYBLCw==\n-----END CERTIFICATE-----\n"
++ ]
++}
ddiff --git a/fixtures/ai.offer-pool/invalid/duplicate-member.json b/fixtures/ai.offer-pool/invalid/duplicate-member.json
nnew file mode 100644
iindex 00000000..6e27061a
---- /dev/null
++++ b/fixtures/ai.offer-pool/invalid/duplicate-member.json
@@@ -0,0 +1,7 @@
++{
++ "machines": [
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1"
++ ],
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1"
++}
ddiff --git a/fixtures/ai.offer-pool/invalid/missing-head.json b/fixtures/ai.offer-pool/invalid/missing-head.json
nnew file mode 100644
iindex 00000000..4cafd5af
---- /dev/null
++++ b/fixtures/ai.offer-pool/invalid/missing-head.json
@@@ -0,0 +1,6 @@
++{
++ "machines": [
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2"
++ ]
++}
ddiff --git a/fixtures/ai.offer-pool/invalid/one-machine.json b/fixtures/ai.offer-pool/invalid/one-machine.json
nnew file mode 100644
iindex 00000000..6ecfded6
---- /dev/null
++++ b/fixtures/ai.offer-pool/invalid/one-machine.json
@@@ -0,0 +1,6 @@
++{
++ "machines": [
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1"
++ ],
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1"
++}
ddiff --git a/fixtures/ai.offer-pool/valid/three-machines.json b/fixtures/ai.offer-pool/valid/three-machines.json
nnew file mode 100644
iindex 00000000..a9d4128f
---- /dev/null
++++ b/fixtures/ai.offer-pool/valid/three-machines.json
@@@ -0,0 +1,8 @@
++{
++ "machines": [
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2",
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D3"
++ ],
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2"
++}
ddiff --git a/fixtures/ai.offer-pool/valid/two-machines.json b/fixtures/ai.offer-pool/valid/two-machines.json
nnew file mode 100644
iindex 00000000..d8c414a5
---- /dev/null
++++ b/fixtures/ai.offer-pool/valid/two-machines.json
@@@ -0,0 +1,7 @@
++{
++ "machines": [
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2"
++ ],
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1"
++}
ddiff --git a/fixtures/ai.offer/invalid/extra-field.json b/fixtures/ai.offer/invalid/extra-field.json
nnew file mode 100644
iindex 00000000..923e2fb3
---- /dev/null
++++ b/fixtures/ai.offer/invalid/extra-field.json
@@@ -0,0 +1,33 @@
++{
++ "offer_id": "ai:dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machine_id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "models": [
++ "llama-3.3-70b-instruct-q4_k_m"
++ ],
++ "hours": {
++ "start": "22:00",
++ "end": "08:00",
++ "tz": "Europe/Paris"
++ },
++ "concurrency": 1,
++ "price": {
++ "in_per_million": 0.2,
++ "out_per_million": 0.6
++ },
++ "audience": "everyone",
++ "group": null,
++ "ready": true,
++ "health": {
++ "status": "up",
++ "checked_at": "2026-11-07T10:00:00.000Z"
++ },
++ "pool": {
++ "machines": [
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2"
++ ],
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1"
++ },
++ "private_key": "not-permitted"
++}
ddiff --git a/fixtures/ai.offer/invalid/missing-owner.json b/fixtures/ai.offer/invalid/missing-owner.json
nnew file mode 100644
iindex 00000000..54416386
---- /dev/null
++++ b/fixtures/ai.offer/invalid/missing-owner.json
@@@ -0,0 +1,31 @@
++{
++ "offer_id": "ai:dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machine_id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "models": [
++ "llama-3.3-70b-instruct-q4_k_m"
++ ],
++ "hours": {
++ "start": "22:00",
++ "end": "08:00",
++ "tz": "Europe/Paris"
++ },
++ "concurrency": 1,
++ "price": {
++ "in_per_million": 0.2,
++ "out_per_million": 0.6
++ },
++ "audience": "everyone",
++ "group": null,
++ "ready": true,
++ "health": {
++ "status": "up",
++ "checked_at": "2026-11-07T10:00:00.000Z"
++ },
++ "pool": {
++ "machines": [
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2"
++ ],
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1"
++ }
++}
ddiff --git a/fixtures/ai.offer/invalid/offer-id-not-a-machine.json b/fixtures/ai.offer/invalid/offer-id-not-a-machine.json
nnew file mode 100644
iindex 00000000..5a222c7e
---- /dev/null
++++ b/fixtures/ai.offer/invalid/offer-id-not-a-machine.json
@@@ -0,0 +1,25 @@
++{
++ "offer_id": "ai:everything",
++ "machine_id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "models": [
++ "qwen3:8b"
++ ],
++ "hours": {
++ "start": "22:00",
++ "end": "08:00",
++ "tz": "Europe/Paris"
++ },
++ "concurrency": 1,
++ "price": {
++ "in_per_million": 0.2,
++ "out_per_million": 0.6
++ },
++ "audience": "everyone",
++ "group": null,
++ "ready": true,
++ "health": {
++ "status": "up",
++ "checked_at": "2026-11-07T10:00:00.000Z"
++ }
++}
ddiff --git a/fixtures/ai.offer/invalid/one-machine-pool.json b/fixtures/ai.offer/invalid/one-machine-pool.json
nnew file mode 100644
iindex 00000000..e9007dd3
---- /dev/null
++++ b/fixtures/ai.offer/invalid/one-machine-pool.json
@@@ -0,0 +1,31 @@
++{
++ "offer_id": "ai:dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machine_id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "models": [
++ "llama-3.3-70b-instruct-q4_k_m"
++ ],
++ "hours": {
++ "start": "22:00",
++ "end": "08:00",
++ "tz": "Europe/Paris"
++ },
++ "concurrency": 1,
++ "price": {
++ "in_per_million": 0.2,
++ "out_per_million": 0.6
++ },
++ "audience": "everyone",
++ "group": null,
++ "ready": true,
++ "health": {
++ "status": "up",
++ "checked_at": "2026-11-07T10:00:00.000Z"
++ },
++ "pool": {
++ "machines": [
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1"
++ ],
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1"
++ }
++}
ddiff --git a/fixtures/ai.offer/invalid/pool-with-address.json b/fixtures/ai.offer/invalid/pool-with-address.json
nnew file mode 100644
iindex 00000000..2a9bcd24
---- /dev/null
++++ b/fixtures/ai.offer/invalid/pool-with-address.json
@@@ -0,0 +1,33 @@
++{
++ "offer_id": "ai:dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machine_id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "models": [
++ "llama-3.3-70b-instruct-q4_k_m"
++ ],
++ "hours": {
++ "start": "22:00",
++ "end": "08:00",
++ "tz": "Europe/Paris"
++ },
++ "concurrency": 1,
++ "price": {
++ "in_per_million": 0.2,
++ "out_per_million": 0.6
++ },
++ "audience": "everyone",
++ "group": null,
++ "ready": true,
++ "health": {
++ "status": "up",
++ "checked_at": "2026-11-07T10:00:00.000Z"
++ },
++ "pool": {
++ "machines": [
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2"
++ ],
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "address": "192.168.1.20/24"
++ }
++}
ddiff --git a/fixtures/ai.offer/valid/group-audience.json b/fixtures/ai.offer/valid/group-audience.json
nnew file mode 100644
iindex 00000000..cbf798dd
---- /dev/null
++++ b/fixtures/ai.offer/valid/group-audience.json
@@@ -0,0 +1,25 @@
++{
++ "offer_id": "ai:dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machine_id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "models": [
++ "qwen3:8b"
++ ],
++ "hours": {
++ "start": "22:00",
++ "end": "08:00",
++ "tz": "Europe/Paris"
++ },
++ "concurrency": 1,
++ "price": {
++ "in_per_million": 0.2,
++ "out_per_million": 0.6
++ },
++ "audience": "group",
++ "group": "grp_friends",
++ "ready": true,
++ "health": {
++ "status": "up",
++ "checked_at": "2026-11-07T10:00:00.000Z"
++ }
++}
ddiff --git a/fixtures/ai.offer/valid/pool-waiting-for-approvals.json b/fixtures/ai.offer/valid/pool-waiting-for-approvals.json
nnew file mode 100644
iindex 00000000..8875671e
---- /dev/null
++++ b/fixtures/ai.offer/valid/pool-waiting-for-approvals.json
@@@ -0,0 +1,31 @@
++{
++ "offer_id": "ai:dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machine_id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "models": [
++ "llama-3.3-70b-instruct-q4_k_m"
++ ],
++ "hours": {
++ "start": "22:00",
++ "end": "08:00",
++ "tz": "Europe/Paris"
++ },
++ "concurrency": 1,
++ "price": {
++ "in_per_million": 0.2,
++ "out_per_million": 0.6
++ },
++ "audience": "everyone",
++ "group": null,
++ "ready": false,
++ "health": {
++ "status": "down"
++ },
++ "pool": {
++ "machines": [
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2"
++ ],
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1"
++ }
++}
ddiff --git a/fixtures/ai.offer/valid/pool.json b/fixtures/ai.offer/valid/pool.json
nnew file mode 100644
iindex 00000000..a3df8b64
---- /dev/null
++++ b/fixtures/ai.offer/valid/pool.json
@@@ -0,0 +1,32 @@
++{
++ "offer_id": "ai:dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machine_id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "models": [
++ "llama-3.3-70b-instruct-q4_k_m"
++ ],
++ "hours": {
++ "start": "22:00",
++ "end": "08:00",
++ "tz": "Europe/Paris"
++ },
++ "concurrency": 1,
++ "price": {
++ "in_per_million": 0.2,
++ "out_per_million": 0.6
++ },
++ "audience": "everyone",
++ "group": null,
++ "ready": true,
++ "health": {
++ "status": "up",
++ "checked_at": "2026-11-07T10:00:00.000Z"
++ },
++ "pool": {
++ "machines": [
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2"
++ ],
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1"
++ }
++}
ddiff --git a/fixtures/ai.offer/valid/single-machine.json b/fixtures/ai.offer/valid/single-machine.json
nnew file mode 100644
iindex 00000000..0e8df776
---- /dev/null
++++ b/fixtures/ai.offer/valid/single-machine.json
@@@ -0,0 +1,25 @@
++{
++ "offer_id": "ai:dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machine_id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "models": [
++ "qwen3:8b"
++ ],
++ "hours": {
++ "start": "22:00",
++ "end": "08:00",
++ "tz": "Europe/Paris"
++ },
++ "concurrency": 1,
++ "price": {
++ "in_per_million": 0.2,
++ "out_per_million": 0.6
++ },
++ "audience": "everyone",
++ "group": null,
++ "ready": true,
++ "health": {
++ "status": "up",
++ "checked_at": "2026-11-07T10:00:00.000Z"
++ }
++}
ddiff --git a/fixtures/bot.machine-pool/invalid/extra-field.json b/fixtures/bot.machine-pool/invalid/extra-field.json
nnew file mode 100644
iindex 00000000..3ec08c0e
---- /dev/null
++++ b/fixtures/bot.machine-pool/invalid/extra-field.json
@@@ -0,0 +1,30 @@
++{
++ "id": "pool_01J8Z3Q4R5S6T7V8W9X0Y1Z2P1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machines": [
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.20/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 24000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ },
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.21/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 12000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ }
++ ],
++ "private_key": "not-permitted"
++}
ddiff --git a/fixtures/bot.machine-pool/invalid/loopback-address.json b/fixtures/bot.machine-pool/invalid/loopback-address.json
nnew file mode 100644
iindex 00000000..3c0f99b7
---- /dev/null
++++ b/fixtures/bot.machine-pool/invalid/loopback-address.json
@@@ -0,0 +1,29 @@
++{
++ "id": "pool_01J8Z3Q4R5S6T7V8W9X0Y1Z2P1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machines": [
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.20/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 24000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ },
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "127.0.0.1/8",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 12000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ }
++ ]
++}
ddiff --git a/fixtures/bot.machine-pool/invalid/member-extra-field.json b/fixtures/bot.machine-pool/invalid/member-extra-field.json
nnew file mode 100644
iindex 00000000..6a014095
---- /dev/null
++++ b/fixtures/bot.machine-pool/invalid/member-extra-field.json
@@@ -0,0 +1,30 @@
++{
++ "id": "pool_01J8Z3Q4R5S6T7V8W9X0Y1Z2P1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machines": [
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.20/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 24000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ },
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.21/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 12000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null,
++ "node_credential": "not-permitted"
++ }
++ ]
++}
ddiff --git a/fixtures/bot.machine-pool/invalid/missing-head.json b/fixtures/bot.machine-pool/invalid/missing-head.json
nnew file mode 100644
iindex 00000000..ad4f748d
---- /dev/null
++++ b/fixtures/bot.machine-pool/invalid/missing-head.json
@@@ -0,0 +1,28 @@
++{
++ "id": "pool_01J8Z3Q4R5S6T7V8W9X0Y1Z2P1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "machines": [
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.20/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 24000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ },
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.21/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 12000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ }
++ ]
++}
ddiff --git a/fixtures/bot.machine-pool/invalid/one-machine.json b/fixtures/bot.machine-pool/invalid/one-machine.json
nnew file mode 100644
iindex 00000000..16ff0b74
---- /dev/null
++++ b/fixtures/bot.machine-pool/invalid/one-machine.json
@@@ -0,0 +1,18 @@
++{
++ "id": "pool_01J8Z3Q4R5S6T7V8W9X0Y1Z2P1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machines": [
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.20/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 24000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ }
++ ]
++}
ddiff --git a/fixtures/bot.machine-pool/invalid/private-key-as-certificate.json b/fixtures/bot.machine-pool/invalid/private-key-as-certificate.json
nnew file mode 100644
iindex 00000000..d73f2158
---- /dev/null
++++ b/fixtures/bot.machine-pool/invalid/private-key-as-certificate.json
@@@ -0,0 +1,29 @@
++{
++ "id": "pool_01J8Z3Q4R5S6T7V8W9X0Y1Z2P1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machines": [
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.20/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 24000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ },
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.21/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 12000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": "-----BEGIN CERTIFICATE-----\nthe PRIVATE KEY pasted here by mistake\n-----END CERTIFICATE-----\n"
++ }
++ ]
++}
ddiff --git a/fixtures/bot.machine-pool/invalid/privileged-port.json b/fixtures/bot.machine-pool/invalid/privileged-port.json
nnew file mode 100644
iindex 00000000..ee91559d
---- /dev/null
++++ b/fixtures/bot.machine-pool/invalid/privileged-port.json
@@@ -0,0 +1,29 @@
++{
++ "id": "pool_01J8Z3Q4R5S6T7V8W9X0Y1Z2P1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machines": [
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.20/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 24000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ },
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.21/24",
++ "public_ip": "203.0.113.7",
++ "port": 80,
++ "free_vram": 12000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ }
++ ]
++}
ddiff --git a/fixtures/bot.machine-pool/invalid/public-address.json b/fixtures/bot.machine-pool/invalid/public-address.json
nnew file mode 100644
iindex 00000000..eb2205e1
---- /dev/null
++++ b/fixtures/bot.machine-pool/invalid/public-address.json
@@@ -0,0 +1,29 @@
++{
++ "id": "pool_01J8Z3Q4R5S6T7V8W9X0Y1Z2P1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machines": [
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.20/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 24000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ },
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "8.8.8.8/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 12000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ }
++ ]
++}
ddiff --git a/fixtures/bot.machine-pool/invalid/wildcard-address.json b/fixtures/bot.machine-pool/invalid/wildcard-address.json
nnew file mode 100644
iindex 00000000..97ed7bf9
---- /dev/null
++++ b/fixtures/bot.machine-pool/invalid/wildcard-address.json
@@@ -0,0 +1,29 @@
++{
++ "id": "pool_01J8Z3Q4R5S6T7V8W9X0Y1Z2P1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machines": [
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.20/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 24000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ },
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "0.0.0.0/0",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 12000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ }
++ ]
++}
ddiff --git a/fixtures/bot.machine-pool/valid/approved-three-machines.json b/fixtures/bot.machine-pool/valid/approved-three-machines.json
nnew file mode 100644
iindex 00000000..0d0befb9
---- /dev/null
++++ b/fixtures/bot.machine-pool/valid/approved-three-machines.json
@@@ -0,0 +1,40 @@
++{
++ "id": "pool_01J8Z3Q4R5S6T7V8W9X0Y1Z2P1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machines": [
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.20/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 24000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": "-----BEGIN CERTIFICATE-----\nMIIBRDCB96ADAgECAhQ1F6qS8nQmfA4Z1EQWbuTK6c+/CDAFBgMrZXAwGDEWMBQG\nA1UEAwwNT3BlblZpYmUtcG9vbDAeFw0yNjExMDcwMjE1NThaFw0zNjExMDQwMjE1\nNThaMBgxFjAUBgNVBAMMDU9wZW5WaWJlLXBvb2wwKjAFBgMrZXADIQBzzRaEd6ZR\nqYbsbYK3W8CCJPIAbsNTlIXZYgPEC9yf76NTMFEwHQYDVR0OBBYEFPiGTnP9vRST\nUlo0W1Eh0xcOxA49MB8GA1UdIwQYMBaAFPiGTnP9vRSTUlo0W1Eh0xcOxA49MA8G\nA1UdEwEB/wQFMAMBAf8wBQYDK2VwA0EAOlCHoX3ONMZYTHx4GSAlxP4EQase+99L\nGykZvgPSVDs2alKLXW5fBxvn7OcEJ3g/EMmk9iZ+AL+CmLxdcYBLCw==\n-----END CERTIFICATE-----\n"
++ },
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.21/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 12000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": "-----BEGIN CERTIFICATE-----\nMIIBRDCB96ADAgECAhRrJ/6HQgidfd9Sah9rLZOC0022ejAFBgMrZXAwGDEWMBQG\nA1UEAwwNT3BlblZpYmUtcG9vbDAeFw0yNjExMDcwMjE2MjRaFw0zNjExMDQwMjE2\nMjRaMBgxFjAUBgNVBAMMDU9wZW5WaWJlLXBvb2wwKjAFBgMrZXADIQBIJ8mF2e9W\nbILuPZQVwA2/LgC2VFtQAnpPDXExV2jXYaNTMFEwHQYDVR0OBBYEFKi5Za/SzebG\nkYOs4rzWz72zsM55MB8GA1UdIwQYMBaAFKi5Za/SzebGkYOs4rzWz72zsM55MA8G\nA1UdEwEB/wQFMAMBAf8wBQYDK2VwA0EAtZp/I6vCQ72SrCrQeH4QCz+sXqwnn24U\np9v1GeWXRE26rNmM3/ZQ4J9Vqmb/inSL5fMSKw9MJG+yZMShheRiDA==\n-----END CERTIFICATE-----\n"
++ },
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D3",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.22/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 8000,
++ "free_ram": 32000,
++ "hardware": "x86_64",
++ "certificate": "-----BEGIN CERTIFICATE-----\nMIIBRDCB96ADAgECAhQqwkAcayM2me9QLHZC1hfsr6SzhTAFBgMrZXAwGDEWMBQG\nA1UEAwwNT3BlblZpYmUtcG9vbDAeFw0yNjExMDcwMjE2MjRaFw0zNjExMDQwMjE2\nMjRaMBgxFjAUBgNVBAMMDU9wZW5WaWJlLXBvb2wwKjAFBgMrZXADIQDpnjnjucPk\nMQo1LhbrbSQyafLHqnODj0osY1fgTX0OnKNTMFEwHQYDVR0OBBYEFNhCSBHW/m2E\naFc9D6a7JI0pgTs8MB8GA1UdIwQYMBaAFNhCSBHW/m2EaFc9D6a7JI0pgTs8MA8G\nA1UdEwEB/wQFMAMBAf8wBQYDK2VwA0EA9/RQDBo4SEF1PnY2eSnS1TcK5pL/qQ2y\nnOXOtppCWn8tcnR4or3aIqO+Ie/8nhFQlkwCfSZ9tcc2JuZi2PGDDg==\n-----END CERTIFICATE-----\n"
++ }
++ ]
++}
ddiff --git a/fixtures/bot.machine-pool/valid/pending-approval.json b/fixtures/bot.machine-pool/valid/pending-approval.json
nnew file mode 100644
iindex 00000000..a88841d2
---- /dev/null
++++ b/fixtures/bot.machine-pool/valid/pending-approval.json
@@@ -0,0 +1,29 @@
++{
++ "id": "pool_01J8Z3Q4R5S6T7V8W9X0Y1Z2P1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "head": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "machines": [
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.20/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 24000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ },
++ {
++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2",
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1",
++ "address": "192.168.1.21/24",
++ "public_ip": "203.0.113.7",
++ "port": 50053,
++ "free_vram": 12000,
++ "free_ram": 32000,
++ "hardware": "NVIDIA GeForce RTX 4090",
++ "certificate": null
++ }
++ ]
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex fe9bb76b..da998509 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -106348,6 +106348,316 @@
}
}
}
++ },
++ "bot.machine-pool": {
++ "version": "1.0.0",
++ "owner": "bot",
++ "status": "active",
++ "schema": {
++ "$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 Node 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.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "owner",
++ "head",
++ "machines"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^pool_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The pool's id (ULID, Crockford base32). A pool is never edited: a change is a new pool with a new id."
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person every machine in the pool belongs to. Pools of different owners are not allowed."
++ },
++ "head": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine that runs `llama-server --rpc`, sees the prompt and answers; it must be one of `machines`. The others receive tensors only."
++ },
++ "machines": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 8,
++ "description": "The members, head included, two to eight.",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "owner",
++ "address",
++ "public_ip",
++ "port",
++ "free_vram",
++ "free_ram",
++ "hardware",
++ "certificate"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body."
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's owner when it announced itself; it must equal the pool's owner."
++ },
++ "address": {
++ "type": "string",
++ "pattern": "^(10\\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])|172\\.(1[6-9]|2[0-9]|3[01])|192\\.168)\\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])/([89]|[12][0-9]|30)$",
++ "maxLength": 32,
++ "description": "The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address."
++ },
++ "public_ip": {
++ "type": "string",
++ "maxLength": 45,
++ "anyOf": [
++ {
++ "format": "ipv4"
++ },
++ {
++ "format": "ipv6"
++ }
++ ],
++ "description": "The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head."
++ },
++ "port": {
++ "type": "integer",
++ "minimum": 1024,
++ "maximum": 65535,
++ "description": "The TCP port of the machine's private link on `address`."
++ },
++ "free_vram": {
++ "type": "integer",
++ "minimum": 0,
++ "maximum": 16777216,
++ "description": "Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it."
++ },
++ "free_ram": {
++ "type": "integer",
++ "minimum": 0,
++ "maximum": 16777216,
++ "description": "Free system memory when it announced itself, in MB."
++ },
++ "hardware": {
++ "type": "string",
++ "maxLength": 120,
++ "description": "A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model."
++ },
++ "certificate": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 8192,
++ "pattern": "^-----BEGIN CERTIFICATE-----",
++ "not": {
++ "type": "string",
++ "pattern": "PRIVATE KEY"
++ },
++ "description": "The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key."
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "ai.offer-pool": {
++ "version": "1.0.0",
++ "owner": "ai",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/ai/offer-pool.v1.json",
++ "title": "AiOfferPool",
++ "description": "ai.offer-pool@1 (the pooled machines design, 2026-11-06): the `pool` part of a community AI offer (ai.offer@1) served by several of one owner's machines together (bot.machine-pool@1). Network adds it from its own approved pool record, never from what a machine reports: the offer is the head's, `head` equals the offer's `machine_id`, and while a machine is in a pool it advertises no offer of its own. It names machines only; no address, key or certificate reaches AI. AI routes the request to the head, runs the same verification as for one machine (canaries, the model fingerprint, spot re-runs) on the pool's answers, keys that verification on this exact membership (another member or head is a new, unverified pool), keeps a pool that is new or failed to the free tier only, and labels its answers \"Community pool (N machines)\".",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "machines",
++ "head"
++ ],
++ "properties": {
++ "machines": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 8,
++ "uniqueItems": true,
++ "description": "The pool's machines, head included (Bot device ids), two to eight.",
++ "items": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "head": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine that answers: one of `machines`, and the offer's own `machine_id`."
++ }
++ }
++ }
++ },
++ "ai.offer": {
++ "version": "1.0.0",
++ "owner": "ai",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/ai/offer.v1.json",
++ "title": "AiOffer",
++ "description": "ai.offer@1: one community AI offer, a person's machine (OpenVibe.Node's ai plugin, \"Offer to the network\") serving its local models to requests that opt in to community capacity. OpenVibe.Bot reports these fields (POST /internal/ai/offers/report on Network, network.resource.report; a report with any other field is refused whole), and OpenVibe.AI reads them from Network (GET /internal/ai/offers, network.registry.read, { offers, generated_at, count }). Network adds `pool` (ai.offer-pool@1) to the head's offer when the machine is the head of an approved pool (bot.machine-pool@1), and leaves a pool member's own offer out of the list while it is in a pool. For a pool the hours, audience, price, readiness, idle-only and verification apply to the whole pool: it is ready only while every member has approved, renewed its readiness in the last few seconds, and is still the owner's. Nothing here is public: AI shows Exchange a hashed offer id and no owner or machine id.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "offer_id",
++ "machine_id",
++ "owner",
++ "models",
++ "hours",
++ "concurrency",
++ "price",
++ "audience",
++ "group",
++ "ready",
++ "health"
++ ],
++ "properties": {
++ "offer_id": {
++ "type": "string",
++ "pattern": "^ai:dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "`ai:<machine_id>`: one offer per machine."
++ },
++ "machine_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine that answers (for a pool, its head)."
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's owner, who earns the offer's credits (a self-served run earns none)."
++ },
++ "models": {
++ "type": "array",
++ "minItems": 1,
++ "maxItems": 40,
++ "description": "The models offered, as the machine's model server names them.",
++ "items": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 200
++ }
++ },
++ "hours": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "start",
++ "end",
++ "tz"
++ ],
++ "description": "The local-time window the offer is open; start equal to end is all day, and an overnight window wraps.",
++ "properties": {
++ "start": {
++ "type": "string",
++ "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$"
++ },
++ "end": {
++ "type": "string",
++ "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$"
++ },
++ "tz": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 64,
++ "description": "An IANA time zone, such as Europe/Paris, or UTC."
++ }
++ }
++ },
++ "concurrency": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 8,
++ "description": "How many requests may run at once; another is refused as busy, never queued."
++ },
++ "price": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "in_per_million",
++ "out_per_million"
++ ],
++ "description": "Credits per million input and output tokens; 0 and 0 is a volunteer, who earns kudos. One price for a pool.",
++ "properties": {
++ "in_per_million": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1000000
++ },
++ "out_per_million": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1000000
++ }
++ }
++ },
++ "audience": {
++ "enum": [
++ "everyone",
++ "followers",
++ "group"
++ ],
++ "description": "Who may use it: everyone, the owner's followers, or one group (`group`)."
++ },
++ "group": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80,
++ "description": "The group's id when the audience is `group`, otherwise null."
++ },
++ "ready": {
++ "type": "boolean",
++ "description": "Whether the machine (for a pool, every member) can answer now: online, inside its hours, not paused by idle-only or a kill switch."
++ },
++ "health": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "status"
++ ],
++ "properties": {
++ "status": {
++ "enum": [
++ "up",
++ "degraded",
++ "down"
++ ]
++ },
++ "checked_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "pool": {
++ "$ref": "offer-pool.v1.json",
++ "description": "Present when the offer is served by a pool of the owner's machines (ai.offer-pool@1)."
++ }
++ }
++ }
}
}
}
ddiff --git a/generated/openapi/actor.json b/generated/openapi/actor.json
iindex d85bbed2..e1c5ec35 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.308.0",
-- "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 a19602c1..81e38e5c 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.308.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.309.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/bench.json b/generated/openapi/bench.json
iindex 61b6a1e0..646c19a7 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.308.0",
-- "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.309.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/billing.json b/generated/openapi/billing.json
iindex 269f29da..9f622fee 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.308.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.309.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/blog.json b/generated/openapi/blog.json
iindex 87834934..2014f3d2 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.308.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 6f458c60..f3570aca 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.308.0",
-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.309.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/chat.json b/generated/openapi/chat.json
iindex e2f6772a..12c74e87 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.308.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 47c469f0..b069241d 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.308.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 8090b63a..fd26a853 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.308.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 e851bf3c..c14a6a38 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.308.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 b7d66431..ef159b84 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.308.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 36bd2eb6..b04e788e 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.308.0",
-- "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 798c3062..cab5bce0 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.308.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 531b5a8e..d59f42c4 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.308.0",
-- "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 e01ca088..fd29edba 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.308.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 e9fe4718..a667cc39 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.308.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.309.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/index.json b/generated/openapi/index.json
iindex 180afbb3..50763bd8 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -145,7 +145,7 @@
"service": "network",
"name": "OpenVibe.Network",
"origin": "https://openvibe.network",
-- "operations": 55,
++ "operations": 60,
"capabilities": 35
},
{
ddiff --git a/generated/openapi/inventory.json b/generated/openapi/inventory.json
iindex bcfef083..29db81b8 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.308.0",
-- "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 78bf7320..99641bc7 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.308.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 8759ee19..a1893412 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.308.0",
-- "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 2e161036..cfc9fee0 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.308.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 1f4acc92..afc6c53e 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.308.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.309.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"
@@@ -1004,7 +1004,7 @@
"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.",
++ "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"
],
@@@ -1043,7 +1043,242 @@
"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.",
++ "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)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/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.",
++ "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": "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"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/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.",
++ "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": "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"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/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": "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"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/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.",
++ "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": "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"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/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.",
"tags": [
"network.node"
],
ddiff --git a/generated/openapi/news.json b/generated/openapi/news.json
iindex 3f8ac00e..b014ef0c 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.308.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 5b7f740f..4bc3b3a7 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.308.0",
-- "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 277c0ae1..385a907d 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.308.0",
-- "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 4a216da3..61ca3ca3 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.308.0",
-- "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 322a1b95..18b09a49 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.308.0",
-- "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 82588a57..2b26c947 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.308.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 8514c441..ed2fd1ee 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.308.0",
-- "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.309.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/search.json b/generated/openapi/search.json
iindex f12b974c..b99f1022 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.308.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 2097ebbf..3139c836 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.308.0",
-- "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 7ac6c586..dcae7708 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.308.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 56583255..a54364e7 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.308.0",
-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 f139ee23..8ab0a87d 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.308.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 a1cbb311..353bfbfe 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.308.0",
-- "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 43c7d96b..d4d3ce12 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.308.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 257f8407..c32a1946 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.308.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 fae52822..510d92b7 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.308.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 e8a12146..6fe0bc95 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.308.0",
-- "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 7e2441fd..4c81c299 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.308.0",
-- "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.309.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/wiki.json b/generated/openapi/wiki.json
iindex 630522a5..15a65e5f 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.308.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 fd88e5d4..307abd39 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.308.0",
-- "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 96d7482d..a27daabc 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.308.0",
-- "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.308.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.309.0",
++ "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.309.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 162b11d2..900efc4e 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -295149,3 +295149,1743 @@ export interface ChatParty {
created_at: string;
ended_at?: string | null;
}
++
++/** [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 Node 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.
++ */
++export interface BotMachinePool {
++ /**
++ * The pool's id (ULID, Crockford base32). A pool is never edited: a change is a new pool with a new id.
++ */
++ id: string;
++ /**
++ * The person every machine in the pool belongs to. Pools of different owners are not allowed.
++ */
++ owner: string;
++ /**
++ * The machine that runs `llama-server --rpc`, sees the prompt and answers; it must be one of `machines`. The others receive tensors only.
++ */
++ head: string;
++ /**
++ * The members, head included, two to eight.
++ *
++ * @minItems 2
++ * @maxItems 8
++ */
++ machines:
++ | [
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ }
++ ]
++ | [
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ }
++ ]
++ | [
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ }
++ ]
++ | [
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ }
++ ]
++ | [
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ }
++ ]
++ | [
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ }
++ ]
++ | [
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ },
++ {
++ /**
++ * The machine (Bot's device id), resolved by Network from the machine's own node token, never taken from the body.
++ */
++ id: string;
++ /**
++ * The machine's owner when it announced itself; it must equal the pool's owner.
++ */
++ owner: string;
++ /**
++ * The machine's private IPv4 address and prefix on the local network (RFC 1918 only, a /8 to a /30), such as 192.168.1.2/24, configured on one of its own interfaces. The private link listens only here; never 0.0.0.0, loopback or a public address.
++ */
++ address: string;
++ /**
++ * The public address Network saw the announcement come from (never the body's claim). Every member must share it with the head.
++ */
++ public_ip: (
++ | {
++ [k: string]: unknown | undefined;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ }
++ ) &
++ string;
++ /**
++ * The TCP port of the machine's private link on `address`.
++ */
++ port: number;
++ /**
++ * Free GPU memory when it announced itself, in MB. The head's default and each member's share of the pool's credits follow it.
++ */
++ free_vram: number;
++ /**
++ * Free system memory when it announced itself, in MB.
++ */
++ free_ram: number;
++ /**
++ * A short hardware name the fit table matches measured speeds on, such as x86_64 or the GPU model.
++ */
++ hardware: string;
++ /**
++ * The machine's self-signed Ed25519 public certificate (PEM) for this pool's private link, or null until the machine approves the pool on itself. Never a private key.
++ */
++ certificate: string | null;
++ }
++ ];
++}
++
++/** [email protected] (owner: ai) */
++/**
++ * ai.offer-pool@1 (the pooled machines design, 2026-11-06): the `pool` part of a community AI offer (ai.offer@1) served by several of one owner's machines together (bot.machine-pool@1). Network adds it from its own approved pool record, never from what a machine reports: the offer is the head's, `head` equals the offer's `machine_id`, and while a machine is in a pool it advertises no offer of its own. It names machines only; no address, key or certificate reaches AI. AI routes the request to the head, runs the same verification as for one machine (canaries, the model fingerprint, spot re-runs) on the pool's answers, keys that verification on this exact membership (another member or head is a new, unverified pool), keeps a pool that is new or failed to the free tier only, and labels its answers "Community pool (N machines)".
++ */
++export interface AiOfferPool {
++ /**
++ * The pool's machines, head included (Bot device ids), two to eight.
++ *
++ * @minItems 2
++ * @maxItems 8
++ */
++ machines:
++ | [string, string]
++ | [string, string, string]
++ | [string, string, string, string]
++ | [string, string, string, string, string]
++ | [string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string];
++ /**
++ * The machine that answers: one of `machines`, and the offer's own `machine_id`.
++ */
++ head: string;
++}
++
++/** [email protected] (owner: ai) */
++/**
++ * ai.offer@1: one community AI offer, a person's machine (OpenVibe.Node's ai plugin, "Offer to the network") serving its local models to requests that opt in to community capacity. OpenVibe.Bot reports these fields (POST /internal/ai/offers/report on Network, network.resource.report; a report with any other field is refused whole), and OpenVibe.AI reads them from Network (GET /internal/ai/offers, network.registry.read, { offers, generated_at, count }). Network adds `pool` (ai.offer-pool@1) to the head's offer when the machine is the head of an approved pool (bot.machine-pool@1), and leaves a pool member's own offer out of the list while it is in a pool. For a pool the hours, audience, price, readiness, idle-only and verification apply to the whole pool: it is ready only while every member has approved, renewed its readiness in the last few seconds, and is still the owner's. Nothing here is public: AI shows Exchange a hashed offer id and no owner or machine id.
++ */
++export interface AiOffer {
++ /**
++ * `ai:<machine_id>`: one offer per machine.
++ */
++ offer_id: string;
++ /**
++ * The machine that answers (for a pool, its head).
++ */
++ machine_id: string;
++ /**
++ * The machine's owner, who earns the offer's credits (a self-served run earns none).
++ */
++ owner: string;
++ /**
++ * The models offered, as the machine's model server names them.
++ *
++ * @minItems 1
++ * @maxItems 40
++ */
++ models: [string, ...string[]];
++ /**
++ * The local-time window the offer is open; start equal to end is all day, and an overnight window wraps.
++ */
++ hours: {
++ start: string;
++ end: string;
++ /**
++ * An IANA time zone, such as Europe/Paris, or UTC.
++ */
++ tz: string;
++ };
++ /**
++ * How many requests may run at once; another is refused as busy, never queued.
++ */
++ concurrency: number;
++ /**
++ * Credits per million input and output tokens; 0 and 0 is a volunteer, who earns kudos. One price for a pool.
++ */
++ price: {
++ in_per_million: number;
++ out_per_million: number;
++ };
++ /**
++ * Who may use it: everyone, the owner's followers, or one group (`group`).
++ */
++ audience: "everyone" | "followers" | "group";
++ /**
++ * The group's id when the audience is `group`, otherwise null.
++ */
++ group: string | null;
++ /**
++ * Whether the machine (for a pool, every member) can answer now: online, inside its hours, not paused by idle-only or a kill switch.
++ */
++ ready: boolean;
++ health: {
++ status: "up" | "degraded" | "down";
++ checked_at?: string;
++ };
++ pool?: AiOfferPool;
++}
ddiff --git a/manifests/capabilities/network.node.self.manage.json b/manifests/capabilities/network.node.self.manage.json
iindex 1ab348cb..625e445b 100644
---- a/manifests/capabilities/network.node.self.manage.json
++++ b/manifests/capabilities/network.node.self.manage.json
@@@ -4,7 +4,7 @@
"owner": "network",
"status": "active",
"visibility": "internal",
-- "description": "A paired machine presents its own capabilities and rotates its own credential, never another machine's.",
++ "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"
],
@@@ -15,6 +15,11 @@
"events": [],
"implementedBy": [
"PUT /api/v1/node/self/capabilities",
-- "POST /api/v1/node/self/credential"
++ "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/package-lock.json b/package-lock.json
iindex 7becfa73..f7f3f8e5 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-contracts",
-- "version": "0.308.0",
++ "version": "0.309.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-contracts",
-- "version": "0.308.0",
++ "version": "0.309.0",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 5be50b9a..ee2d9cbf 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.308.0",
++ "version": "0.309.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/test/changelog-history.test.js b/test/changelog-history.test.js
iindex 3d7b59dc..4c913b3a 100644
---- a/test/changelog-history.test.js
++++ b/test/changelog-history.test.js
@@@ -12,8 +12,9 @@ const pkg = require(path.join(ROOT, 'package.json'));
const versions = [...text.matchAll(/^## (\d+)\.(\d+)\.(\d+)\b/gm)].map((m) => [+m[1], +m[2], +m[3]]);
const [major, minor] = pkg.version.split('.').map(Number);
--// Minor versions that were never released (no tag), so they have no entry. 306 is the pooled-machines branch (it
--// carries its own 0.306.0 entry and has not merged); 307 is reserved by work in flight, so Memory M5 takes 308.
++// Minor versions that were never released (no tag), so they have no entry. 306 and 307 were reserved for the pooled
++// machines and consent branches, which merged after Memory M5 took 308: they became 0.309.0 and 0.310.0, and the two
++// numbers stay unused (a tag below the newest one would read as a downgrade to the pin bot).
const NEVER_RELEASED = new Set([293, 306, 307]);
const FIRST_MINOR = 30;