openvibers/openvibe.contracts Commits

The machines hub: the machine, its roles and its events (plan L34) (#269)

OP OpenVibers committed 13 days ago · bb364ca1c2 · 121 files, +2976 −99

* The machines hub: the machine, its roles and its events (plan L34)

The network's hub (openvibe.network/machines) shows a person's machines:
Bot stays the registry underneath, Network fronts it.

- network.machine@1: one machine as the hub shows it (id, name, kind, os,
  online, last_seen_at, roles, running, node_version).
- network.machine-role@1, network.machine-roles@1, network.machine-roles-put@1:
  the catalogue of roles (codes.worker, desks.host, ai.host, bot.robot) and
  the whole set a machine has after a change.
- network.machine-pairing@1: a one-time code, its expiry and the command.
- network.machine-list-result@1, network.machine-manage-result@1,
  bot.machine-manage-request@1, bot.machine-manage-result@1.
- The capabilities network.machines.read, network.machines.manage (public: an
  app holds them with the person's consent) and bot.machine.manage.
- The events network.machine.online, .offline, .paired, .roles_changed, .revoked.
- registry.service-manifest@1 gains machineRoles: the roles a product declares.

The presence policy now allows the machine's connectivity, as it allows the
robot's: a machine coming online is the device changing state, not a person
being present.

* main into the machines hub, and the release is 0.258.0

main has released 0.257.0 (Your AI, a person's own keys), so the machines hub is
the next release: 0.258.0. The changelog keeps both sections, and the generated
artefacts are regenerated.

* 0.261.0: The machines hub: the machine, its roles and its events (plan L34)

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 1fe9ebea..5d21e89a 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,34 @@
# Changelog
++## 0.261.0 — 2026-10-31
++
++- **The machines hub** (plan L34, steps M2 and M4). A person's computers are the network's: Bot stays the device
++ registry underneath, and Network fronts it on openvibe.network/machines for the person and for apps the person
++ consented to.
++ - **`network.machine@1`:** one machine as the hub shows it — its id (Bot's `dev_…`), name, kind, os, whether it is
++ online, when it was last seen, its `roles` and what it does right now (`running`: its Codes jobs, its desks, its
++ local models, its robots).
++ - **`network.machine-roles@1` and `network.machine-role@1`:** the catalogue of roles. Each role has a name, what it
++ installs on the machine and what the hub shows. A product declares its roles in its own service manifest
++ (`machineRoles`), so the catalogue is the union of what the products declare: `codes.worker` (Codes), `desks.host`
++ (Run), `ai.host` (AI) and `bot.robot` (Bot) are the first four.
++ - **`network.machine-roles-put@1`:** the roles a machine has after a change — the whole set, not a diff.
++ - **`network.machine-pairing@1`:** a one-time pairing code, its expiry and the command to run on the machine.
++ - **`network.machine-list-result@1` and `network.machine-manage-result@1`:** what the hub answers with.
++ - **`bot.machine-manage-request@1` and `bot.machine-manage-result@1`:** what Bot's machines API takes and answers
++ with. `bot.device@1` gains `roles` and `os` (both optional; the old shape still holds).
++ - **The capabilities:** `network.machines.read` and `network.machines.manage` (owner `network`, public: an app may
++ hold them with the person's consent, and may then do only what the person could) and `bot.machine.manage` (owner
++ `bot`, first-party: Network holds it for the hub, so Bot stays the authority and checks the machine is the named
++ person's own).
++ - **The events:** `network.machine.online`, `network.machine.offline`, `network.machine.paired`,
++ `network.machine.roles_changed` and `network.machine.revoked` — Network publishes them through OpenVibe.Events.
++ - **Presence policy:** `network.machine.online` and `network.machine.offline` are a machine's connectivity (the
++ machines hub, plan L34), not a person's presence, and are allowed like `bot.robot.online` and `bot.robot.offline`
++ (ADR-043). Any other `.online`, `.offline` or presence event type is still refused.
++ - **`registry.service-manifest@1` gains `machineRoles`** (optional): the roles a product declares. The manifest
++ keeps its old shape; a manifest without the field means a product with no machine role.
++
## 0.260.0 — 2026-10-31
- **One AI meter across the network** (plan L40): every AI call on the network leaves one usage record, whoever made it.
ddiff --git a/contracts/bot/device.v1.json b/contracts/bot/device.v1.json
iindex c0998c10..0f60db48 100644
---- a/contracts/bot/device.v1.json
++++ b/contracts/bot/device.v1.json
@@@ -68,6 +68,21 @@
},
"online": {
"type": "boolean"
++ },
++ "roles": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$"
++ },
++ "description": "The machine roles the owner set (plan L34: the catalogue is network.machine-roles@1). Absent on a device that has none."
++ },
++ "os": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The operating system the device reported."
}
}
}
ddiff --git a/contracts/bot/machine-manage-request.v1.json b/contracts/bot/machine-manage-request.v1.json
nnew file mode 100644
iindex 00000000..0e1cf6e2
---- /dev/null
++++ b/contracts/bot/machine-manage-request.v1.json
@@@ -0,0 +1,27 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/machine-manage-request.v1.json",
++ "title": "BotMachineManageRequest",
++ "description": "bot.machine-manage-request@1: what a caller changes on a machine (plan L34, step M2). A rename gives a name; a roles change gives the whole set of roles (network.machine-roles-put@1's shape). The same body is what the network's hub sends to Bot, so both sides speak one shape.",
++ "type": "object",
++ "additionalProperties": false,
++ "anyOf": [
++ { "required": ["name"] },
++ { "required": ["roles"] }
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80,
++ "description": "The name the owner gives the machine. Absent when the call changes something else."
++ },
++ "roles": {
++ "type": "array",
++ "uniqueItems": true,
++ "maxItems": 8,
++ "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$" },
++ "description": "The machine's roles after the change. Absent when the call changes something else."
++ }
++ }
++}
ddiff --git a/contracts/bot/machine-manage-result.v1.json b/contracts/bot/machine-manage-result.v1.json
nnew file mode 100644
iindex 00000000..d07a65dd
---- /dev/null
++++ b/contracts/bot/machine-manage-result.v1.json
@@@ -0,0 +1,12 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/machine-manage-result.v1.json",
++ "title": "BotMachineManageResult",
++ "description": "bot.machine-manage-result@1: the machine as it is after the change (plan L34, step M2). Bot is the registry underneath: it answers with its own device record, never with a credential.",
++ "type": "object",
++ "required": ["machine"],
++ "additionalProperties": false,
++ "properties": {
++ "machine": { "$ref": "device.v1.json" }
++ }
++}
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 35732af4..a146443a 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -5887,6 +5887,132 @@
"status": "active",
"adr": "ADR-034"
},
++ {
++ "id": "network.machine",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/machine.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "network.machine-list-result",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/machine-list-result.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "network.machine-manage-result",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/machine-manage-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "bot.machine-manage-request",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/machine-manage-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "bot.machine-manage-result",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/machine-manage-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "network.machine-role",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/machine-role.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "network.machine-roles",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/machine-roles.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "network.machine-roles-put",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/machine-roles-put.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "network.machine-pairing",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/machine-pairing.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "network.machine.online",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "events/payloads/network.machine.online.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "network.machine.offline",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "events/payloads/network.machine.offline.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "network.machine.paired",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "events/payloads/network.machine.paired.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "network.machine.roles_changed",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "events/payloads/network.machine.roles_changed.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "network.machine.revoked",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "events/payloads/network.machine.revoked.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
{
"id": "network.avatar-write-request",
"version": "1.0.0",
ddiff --git a/contracts/events/payloads/network.machine.offline.v1.json b/contracts/events/payloads/network.machine.offline.v1.json
nnew file mode 100644
iindex 00000000..d91502d8
---- /dev/null
++++ b/contracts/events/payloads/network.machine.offline.v1.json
@@@ -0,0 +1,15 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.machine.offline.v1.json",
++ "title": "NetworkMachineOfflinePayload",
++ "description": "network.machine.offline v1 (OpenVibe.Network, plan L34: the machines hub). A machine of a person went offline: it is no longer connected to Bot. Network republishes Bot's change, so a consumer of the network's events hears about a machine once, in one place. Envelope: subject { type: machine, id: <dev_…> }, visibility internal, actor service:network.",
++ "type": "object",
++ "required": ["machine_id", "owner", "at"],
++ "additionalProperties": false,
++ "properties": {
++ "machine_id": { "type": "string", "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$" },
++ "principal_id": { "type": "string", "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The machine's node principal on Network, when it has one." },
++ "owner": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The person the machine belongs to." },
++ "at": { "type": "string", "format": "date-time" }
++ }
++}
ddiff --git a/contracts/events/payloads/network.machine.online.v1.json b/contracts/events/payloads/network.machine.online.v1.json
nnew file mode 100644
iindex 00000000..4c0e26f6
---- /dev/null
++++ b/contracts/events/payloads/network.machine.online.v1.json
@@@ -0,0 +1,15 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.machine.online.v1.json",
++ "title": "NetworkMachineOnlinePayload",
++ "description": "network.machine.online v1 (OpenVibe.Network, plan L34: the machines hub). A machine of a person (a Bot device of kind machine) came online: it checked in and is connected to Bot. Network republishes Bot's change (Bot reports it to Network, which writes this event), so a consumer of the network's events hears about a machine once, in one place. Envelope: subject { type: machine, id: <dev_…> }, visibility internal, actor service:network.",
++ "type": "object",
++ "required": ["machine_id", "owner", "at"],
++ "additionalProperties": false,
++ "properties": {
++ "machine_id": { "type": "string", "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$" },
++ "principal_id": { "type": "string", "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The machine's node principal on Network, when it has one." },
++ "owner": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The person the machine belongs to." },
++ "at": { "type": "string", "format": "date-time" }
++ }
++}
ddiff --git a/contracts/events/payloads/network.machine.paired.v1.json b/contracts/events/payloads/network.machine.paired.v1.json
nnew file mode 100644
iindex 00000000..5ee45de7
---- /dev/null
++++ b/contracts/events/payloads/network.machine.paired.v1.json
@@@ -0,0 +1,16 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.machine.paired.v1.json",
++ "title": "NetworkMachinePairedPayload",
++ "description": "network.machine.paired v1 (OpenVibe.Network, plan L34: the machines hub). A machine was paired with a person's account: it redeemed the one-time code and became theirs. It holds a credential from then on; the code is spent. The machine's device id in Bot is absent until the machine checks in for the first time. Envelope: subject { type: machine, id: <nod_…> }, visibility internal, actor service:network.",
++ "type": "object",
++ "required": ["principal_id", "owner", "at"],
++ "additionalProperties": false,
++ "properties": {
++ "machine_id": { "type": "string", "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The machine's device id in Bot's registry, when Bot already knows the device." },
++ "node_id": { "type": "string", "pattern": "^n-[a-z0-9]{26}$", "description": "The machine's node id (platform_node_principals.node_id)." },
++ "principal_id": { "type": "string", "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The node principal the pairing created. Also the envelope subject id." },
++ "owner": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The person the machine belongs to." },
++ "at": { "type": "string", "format": "date-time" }
++ }
++}
ddiff --git a/contracts/events/payloads/network.machine.revoked.v1.json b/contracts/events/payloads/network.machine.revoked.v1.json
nnew file mode 100644
iindex 00000000..f0577af2
---- /dev/null
++++ b/contracts/events/payloads/network.machine.revoked.v1.json
@@@ -0,0 +1,16 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.machine.revoked.v1.json",
++ "title": "NetworkMachineRevokedPayload",
++ "description": "network.machine.revoked v1 (OpenVibe.Network, plan L34: the machines hub). A machine of a person was revoked: it is no longer theirs and its credential is dead. The machine's credential and its hash are never carried. A second revoke changes nothing and emits nothing. Envelope: subject { type: machine, id: <dev_…> }, visibility internal, actor service:network.",
++ "type": "object",
++ "required": ["machine_id", "owner", "at"],
++ "additionalProperties": false,
++ "properties": {
++ "machine_id": { "type": "string", "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$" },
++ "principal_id": { "type": "string", "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The machine's node principal on Network, when it had one." },
++ "owner": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The person the machine belonged to." },
++ "reason": { "type": "string", "minLength": 1, "maxLength": 500, "description": "Why it was revoked, when the caller gave one. Absent otherwise." },
++ "at": { "type": "string", "format": "date-time" }
++ }
++}
ddiff --git a/contracts/events/payloads/network.machine.roles_changed.v1.json b/contracts/events/payloads/network.machine.roles_changed.v1.json
nnew file mode 100644
iindex 00000000..197bd991
---- /dev/null
++++ b/contracts/events/payloads/network.machine.roles_changed.v1.json
@@@ -0,0 +1,25 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.machine.roles_changed.v1.json",
++ "title": "NetworkMachineRolesChangedPayload",
++ "description": "network.machine.roles_changed v1 (OpenVibe.Network, plan L34, step M4). A machine's roles changed: the whole set after the change, from the catalogue (network.machine-roles@1). The machine applies them at its next check-in, so this event says what was asked for, not what the machine has already done. Envelope: subject { type: machine, id: <dev_…> }, visibility internal, actor service:network.",
++ "type": "object",
++ "required": ["machine_id", "owner", "roles", "at"],
++ "additionalProperties": false,
++ "properties": {
++ "machine_id": { "type": "string", "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$" },
++ "owner": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The person the machine belongs to." },
++ "roles": {
++ "type": "array",
++ "uniqueItems": true,
++ "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$" },
++ "description": "The machine's roles after the change."
++ },
++ "changed_by": {
++ "type": "string",
++ "pattern": "^(usr_[0-9A-HJKMNP-TV-Z]{26}|app:[a-z0-9-]+)$",
++ "description": "Who asked for the change: the person, or an app acting with their consent."
++ },
++ "at": { "type": "string", "format": "date-time" }
++ }
++}
ddiff --git a/contracts/network/machine-list-result.v1.json b/contracts/network/machine-list-result.v1.json
nnew file mode 100644
iindex 00000000..a2ba43f9
---- /dev/null
++++ b/contracts/network/machine-list-result.v1.json
@@@ -0,0 +1,15 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/machine-list-result.v1.json",
++ "title": "NetworkMachineListResult",
++ "description": "network.machine-list-result@1: a person's machines, as the network's hub lists them (plan L34, step M2: GET /api/v1/machines). Only the caller's own machines are ever in it.",
++ "type": "object",
++ "required": ["machines"],
++ "additionalProperties": false,
++ "properties": {
++ "machines": {
++ "type": "array",
++ "items": { "$ref": "machine.v1.json" }
++ }
++ }
++}
ddiff --git a/contracts/network/machine-manage-result.v1.json b/contracts/network/machine-manage-result.v1.json
nnew file mode 100644
iindex 00000000..10135f83
---- /dev/null
++++ b/contracts/network/machine-manage-result.v1.json
@@@ -0,0 +1,12 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/machine-manage-result.v1.json",
++ "title": "NetworkMachineManageResult",
++ "description": "network.machine-manage-result@1: the machine as it is after a change (plan L34, step M2: PATCH /api/v1/machines/:id, PUT /api/v1/machines/:id/roles, POST /api/v1/machines/:id/revoke). The pairing route answers network.machine-pairing@1 instead, because a pairing is a code and not a machine.",
++ "type": "object",
++ "required": ["machine"],
++ "additionalProperties": false,
++ "properties": {
++ "machine": { "$ref": "machine.v1.json" }
++ }
++}
ddiff --git a/contracts/network/machine-pairing.v1.json b/contracts/network/machine-pairing.v1.json
nnew file mode 100644
iindex 00000000..575cbf24
---- /dev/null
++++ b/contracts/network/machine-pairing.v1.json
@@@ -0,0 +1,27 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/machine-pairing.v1.json",
++ "title": "NetworkMachinePairing",
++ "description": "network.machine-pairing@1: a one-time code that pairs a machine with the person's account (POST /api/v1/machines/pairings, capability machines.manage). The code is shown once, expires, and works once: the machine redeems it and becomes the person's. The command is what the owner runs on the machine (openvibe.bot/install). The code is never stored in the clear and never returned again.",
++ "type": "object",
++ "required": ["code", "expires_at", "installer"],
++ "additionalProperties": false,
++ "properties": {
++ "code": {
++ "type": "string",
++ "pattern": "^[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$",
++ "description": "The one-time pairing code, eight Crockford base32 characters in two groups (XXXX-XXXX)."
++ },
++ "pairing_id": {
++ "type": "string",
++ "pattern": "^pair_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The pairing's id, as the install command carries it. Absent when the authority behind it does not name one."
++ },
++ "expires_at": { "type": "string", "format": "date-time", "description": "When the code stops working." },
++ "installer": {
++ "type": "string",
++ "maxLength": 500,
++ "description": "The command the owner runs on the machine (openvibe.bot/install, with the network, the pairing id and the code)."
++ }
++ }
++}
ddiff --git a/contracts/network/machine-role.v1.json b/contracts/network/machine-role.v1.json
nnew file mode 100644
iindex 00000000..5927f071
---- /dev/null
++++ b/contracts/network/machine-role.v1.json
@@@ -0,0 +1,19 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/machine-role.v1.json",
++ "title": "NetworkMachineRole",
++ "description": "network.machine-role@1: one role a machine can have (plan L34, step M4). A role is what the machine does for the network: a name for the owner, what it installs on the machine and what the hub shows. The catalogue is network.machine-roles@1; a role outside it is refused.",
++ "type": "object",
++ "required": ["id", "name", "installs", "shows"],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$",
++ "description": "The role's id: the product that owns it, then what it is (codes.worker, desks.host, ai.host, bot.robot)."
++ },
++ "name": { "type": "string", "maxLength": 60, "description": "The role's name, in the owner's words." },
++ "installs": { "type": "string", "maxLength": 200, "description": "What the role puts on the machine (the job runner, the desk sandbox, the local model server)." },
++ "shows": { "type": "string", "maxLength": 200, "description": "What the hub shows for a machine with this role." }
++ }
++}
ddiff --git a/contracts/network/machine-roles-put.v1.json b/contracts/network/machine-roles-put.v1.json
nnew file mode 100644
iindex 00000000..3ec09edb
---- /dev/null
++++ b/contracts/network/machine-roles-put.v1.json
@@@ -0,0 +1,18 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/machine-roles-put.v1.json",
++ "title": "NetworkMachineRolesPut",
++ "description": "network.machine-roles-put@1: the roles the owner gives a machine (PUT /api/v1/machines/:id/roles, capability machines.manage). The whole set, not a change: what is not named is turned off. Every id must be in the catalogue (network.machine-roles@1); an unknown role is refused and nothing is changed.",
++ "type": "object",
++ "required": ["roles"],
++ "additionalProperties": false,
++ "properties": {
++ "roles": {
++ "type": "array",
++ "uniqueItems": true,
++ "maxItems": 8,
++ "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$" },
++ "description": "The machine's roles after the change. An empty array turns every role off."
++ }
++ }
++}
ddiff --git a/contracts/network/machine-roles.v1.json b/contracts/network/machine-roles.v1.json
nnew file mode 100644
iindex 00000000..1976501d
---- /dev/null
++++ b/contracts/network/machine-roles.v1.json
@@@ -0,0 +1,17 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/machine-roles.v1.json",
++ "title": "NetworkMachineRoles",
++ "description": "network.machine-roles@1: the catalogue of machine roles (plan L34, step M4). A product declares a role — a name, what it installs on the machine and what the hub shows — and the Node turns it on at its next check-in. The first roles are Codes' worker, Desks' host, AI's local models and Bot's robot. A role outside this catalogue is refused (network.machine-roles-put@1).",
++ "type": "object",
++ "required": ["roles"],
++ "additionalProperties": false,
++ "properties": {
++ "roles": {
++ "type": "array",
++ "minItems": 1,
++ "uniqueItems": true,
++ "items": { "$ref": "machine-role.v1.json" }
++ }
++ }
++}
ddiff --git a/contracts/network/machine.v1.json b/contracts/network/machine.v1.json
nnew file mode 100644
iindex 00000000..4ecf5bd2
---- /dev/null
++++ b/contracts/network/machine.v1.json
@@@ -0,0 +1,50 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/machine.v1.json",
++ "title": "NetworkMachine",
++ "description": "network.machine@1: one machine of a person, as the network's machines hub shows it (plan L34, steps M2 and M4). A machine is a Bot device of kind machine: an OpenVibe Node install that paired with the person's account. Bot stays the registry underneath (the device id is its dev_…); Network fronts it for the person and for apps the person consented. The roles are what the machine does for the network (the catalogue: network.machine-roles@1), and running is what it does right now. Nothing here is a secret: no credential, no hash, no address.",
++ "type": "object",
++ "required": ["id", "kind", "online", "roles", "running"],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's device id in Bot's registry (dev_…)."
++ },
++ "principal_id": {
++ "type": "string",
++ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's node principal on Network (nod_…): the credential it holds. Absent for a machine Bot knows but Network has no principal for."
++ },
++ "name": { "type": "string", "maxLength": 80, "description": "The name the owner gave it. Absent when they gave none." },
++ "kind": {
++ "enum": ["onboard", "bridge", "server"],
++ "description": "What the machine is, as Bot's device record says: onboard (the robot's own machine), bridge (a machine that drives a robot over the robot's own link), server (a machine that runs its owner's jobs)."
++ },
++ "os": { "type": "string", "maxLength": 40, "description": "The operating system the Node reports (linux, darwin, windows). Absent until the machine has checked in." },
++ "online": { "type": "boolean", "description": "Whether the machine is connected to Bot right now." },
++ "last_seen_at": { "type": "string", "format": "date-time", "description": "When the machine was last seen. Absent before its first check-in." },
++ "node_version": { "type": "string", "maxLength": 40, "description": "The OpenVibe Node version the machine reports. Absent until it has checked in." },
++ "roles": {
++ "type": "array",
++ "uniqueItems": true,
++ "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$" },
++ "description": "What the machine does for the network: the ids from the catalogue (network.machine-roles@1). The owner sets these; the machine applies them at its next check-in."
++ },
++ "running": {
++ "type": "array",
++ "description": "What the machine is doing right now, as a short list: its Codes jobs, its desks, the local models it serves and the robots it drives.",
++ "items": {
++ "type": "object",
++ "required": ["kind", "count"],
++ "additionalProperties": false,
++ "properties": {
++ "kind": { "enum": ["codes.jobs", "desks", "models", "robots"] },
++ "count": { "type": "integer", "minimum": 0 },
++ "label": { "type": "string", "maxLength": 120, "description": "A short, plain description for the owner (the desk's name, the model's name)." }
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/registry/service-manifest.v1.json b/contracts/registry/service-manifest.v1.json
iindex 5e2fa114..e9b8fc18 100644
---- a/contracts/registry/service-manifest.v1.json
++++ b/contracts/registry/service-manifest.v1.json
@@@ -218,6 +218,37 @@
"type": "string"
}
},
++ "machineRoles": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "name",
++ "installs",
++ "shows"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$"
++ },
++ "name": {
++ "type": "string",
++ "maxLength": 60
++ },
++ "installs": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "shows": {
++ "type": "string",
++ "maxLength": 200
++ }
++ }
++ }
++ },
"contractRanges": {
"type": "object",
"additionalProperties": {
ddiff --git a/fixtures/bot.machine-manage-request/invalid/bad-role.json b/fixtures/bot.machine-manage-request/invalid/bad-role.json
nnew file mode 100644
iindex 00000000..1df8d0b0
---- /dev/null
++++ b/fixtures/bot.machine-manage-request/invalid/bad-role.json
@@@ -0,0 +1,5 @@
++{
++ "roles": [
++ "codesworker"
++ ]
++}
ddiff --git a/fixtures/bot.machine-manage-request/invalid/empty.json b/fixtures/bot.machine-manage-request/invalid/empty.json
nnew file mode 100644
iindex 00000000..0967ef42
---- /dev/null
++++ b/fixtures/bot.machine-manage-request/invalid/empty.json
@@@ -0,0 +1 @@
++{}
ddiff --git a/fixtures/bot.machine-manage-request/invalid/unknown-field.json b/fixtures/bot.machine-manage-request/invalid/unknown-field.json
nnew file mode 100644
iindex 00000000..a82cff0c
---- /dev/null
++++ b/fixtures/bot.machine-manage-request/invalid/unknown-field.json
@@@ -0,0 +1,3 @@
++{
++ "nickname": "x"
++}
ddiff --git a/fixtures/bot.machine-manage-request/valid/rename.json b/fixtures/bot.machine-manage-request/valid/rename.json
nnew file mode 100644
iindex 00000000..d015eb82
---- /dev/null
++++ b/fixtures/bot.machine-manage-request/valid/rename.json
@@@ -0,0 +1,3 @@
++{
++ "name": "Kitchen server"
++}
ddiff --git a/fixtures/bot.machine-manage-request/valid/roles.json b/fixtures/bot.machine-manage-request/valid/roles.json
nnew file mode 100644
iindex 00000000..1ee76c4e
---- /dev/null
++++ b/fixtures/bot.machine-manage-request/valid/roles.json
@@@ -0,0 +1,6 @@
++{
++ "roles": [
++ "codes.worker",
++ "ai.host"
++ ]
++}
ddiff --git a/fixtures/bot.machine-manage-result/invalid/missing-machine.json b/fixtures/bot.machine-manage-result/invalid/missing-machine.json
nnew file mode 100644
iindex 00000000..0967ef42
---- /dev/null
++++ b/fixtures/bot.machine-manage-result/invalid/missing-machine.json
@@@ -0,0 +1 @@
++{}
ddiff --git a/fixtures/bot.machine-manage-result/valid/machine.json b/fixtures/bot.machine-manage-result/valid/machine.json
nnew file mode 100644
iindex 00000000..eece7f9f
---- /dev/null
++++ b/fixtures/bot.machine-manage-result/valid/machine.json
@@@ -0,0 +1,20 @@
++{
++ "machine": {
++ "id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "robot_ids": [],
++ "kind": "server",
++ "name": "Kitchen server",
++ "agent_version": "0.15.0",
++ "drivers": [],
++ "capabilities": {},
++ "last_seen": "2026-10-31T09:00:00.000Z",
++ "revoked_at": null,
++ "created_at": "2026-10-31T09:00:00.000Z",
++ "updated_at": "2026-10-31T09:00:00.000Z",
++ "online": true,
++ "roles": [
++ "codes.worker"
++ ],
++ "os": "linux"
++ }
++}
ddiff --git a/fixtures/network.machine-list-result/invalid/missing.json b/fixtures/network.machine-list-result/invalid/missing.json
nnew file mode 100644
iindex 00000000..0967ef42
---- /dev/null
++++ b/fixtures/network.machine-list-result/invalid/missing.json
@@@ -0,0 +1 @@
++{}
ddiff --git a/fixtures/network.machine-list-result/invalid/not-a-list.json b/fixtures/network.machine-list-result/invalid/not-a-list.json
nnew file mode 100644
iindex 00000000..a7987973
---- /dev/null
++++ b/fixtures/network.machine-list-result/invalid/not-a-list.json
@@@ -0,0 +1,5 @@
++{
++ "machines": {
++ "id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS"
++ }
++}
ddiff --git a/fixtures/network.machine-list-result/valid/two-machines.json b/fixtures/network.machine-list-result/valid/two-machines.json
nnew file mode 100644
iindex 00000000..4108b661
---- /dev/null
++++ b/fixtures/network.machine-list-result/valid/two-machines.json
@@@ -0,0 +1,29 @@
++{
++ "machines": [
++ {
++ "id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "name": "Kitchen server",
++ "kind": "server",
++ "os": "linux",
++ "online": true,
++ "last_seen_at": "2026-10-31T09:00:00.000Z",
++ "node_version": "0.15.0",
++ "roles": [
++ "codes.worker"
++ ],
++ "running": [
++ {
++ "kind": "codes.jobs",
++ "count": 1
++ }
++ ]
++ },
++ {
++ "id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "kind": "onboard",
++ "online": false,
++ "roles": [],
++ "running": []
++ }
++ ]
++}
ddiff --git a/fixtures/network.machine-manage-result/invalid/bad-machine.json b/fixtures/network.machine-manage-result/invalid/bad-machine.json
nnew file mode 100644
iindex 00000000..b7a98919
---- /dev/null
++++ b/fixtures/network.machine-manage-result/invalid/bad-machine.json
@@@ -0,0 +1,9 @@
++{
++ "machine": {
++ "id": "x",
++ "kind": "server",
++ "online": false,
++ "roles": [],
++ "running": []
++ }
++}
ddiff --git a/fixtures/network.machine-manage-result/invalid/missing-machine.json b/fixtures/network.machine-manage-result/invalid/missing-machine.json
nnew file mode 100644
iindex 00000000..0967ef42
---- /dev/null
++++ b/fixtures/network.machine-manage-result/invalid/missing-machine.json
@@@ -0,0 +1 @@
++{}
ddiff --git a/fixtures/network.machine-manage-result/valid/machine.json b/fixtures/network.machine-manage-result/valid/machine.json
nnew file mode 100644
iindex 00000000..3e298897
---- /dev/null
++++ b/fixtures/network.machine-manage-result/valid/machine.json
@@@ -0,0 +1,20 @@
++{
++ "machine": {
++ "id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "name": "Kitchen server",
++ "kind": "server",
++ "os": "linux",
++ "online": true,
++ "last_seen_at": "2026-10-31T09:00:00.000Z",
++ "node_version": "0.15.0",
++ "roles": [
++ "codes.worker"
++ ],
++ "running": [
++ {
++ "kind": "codes.jobs",
++ "count": 1
++ }
++ ]
++ }
++}
ddiff --git a/fixtures/network.machine-pairing/invalid/bad-code.json b/fixtures/network.machine-pairing/invalid/bad-code.json
nnew file mode 100644
iindex 00000000..d528a0cb
---- /dev/null
++++ b/fixtures/network.machine-pairing/invalid/bad-code.json
@@@ -0,0 +1,5 @@
++{
++ "code": "abcd1234",
++ "expires_at": "2026-10-31T09:00:00.000Z",
++ "installer": "x"
++}
ddiff --git a/fixtures/network.machine-pairing/invalid/missing-installer.json b/fixtures/network.machine-pairing/invalid/missing-installer.json
nnew file mode 100644
iindex 00000000..38bcb249
---- /dev/null
++++ b/fixtures/network.machine-pairing/invalid/missing-installer.json
@@@ -0,0 +1,4 @@
++{
++ "code": "ABCD-1234",
++ "expires_at": "2026-10-31T09:00:00.000Z"
++}
ddiff --git a/fixtures/network.machine-pairing/valid/code.json b/fixtures/network.machine-pairing/valid/code.json
nnew file mode 100644
iindex 00000000..e72c019d
---- /dev/null
++++ b/fixtures/network.machine-pairing/valid/code.json
@@@ -0,0 +1,6 @@
++{
++ "code": "ABCD-1234",
++ "pairing_id": "pair_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "expires_at": "2026-10-31T09:00:00.000Z",
++ "installer": "curl -fsSL https://openvibe.bot/install | sh -s -- --network https://openvibe.network --pairing pair_01JAB2C3D4E5F6G7H8J9K0MNPS --code ABCD-1234"
++}
ddiff --git a/fixtures/network.machine-role/invalid/missing-shows.json b/fixtures/network.machine-role/invalid/missing-shows.json
nnew file mode 100644
iindex 00000000..31e81818
---- /dev/null
++++ b/fixtures/network.machine-role/invalid/missing-shows.json
@@@ -0,0 +1,5 @@
++{
++ "id": "codes.worker",
++ "name": "Codes worker",
++ "installs": "the Codes job runner"
++}
ddiff --git a/fixtures/network.machine-role/invalid/no-dot.json b/fixtures/network.machine-role/invalid/no-dot.json
nnew file mode 100644
iindex 00000000..c08c537f
---- /dev/null
++++ b/fixtures/network.machine-role/invalid/no-dot.json
@@@ -0,0 +1,6 @@
++{
++ "id": "worker",
++ "name": "Codes worker",
++ "installs": "x",
++ "shows": "y"
++}
ddiff --git a/fixtures/network.machine-role/valid/codes-worker.json b/fixtures/network.machine-role/valid/codes-worker.json
nnew file mode 100644
iindex 00000000..a827168e
---- /dev/null
++++ b/fixtures/network.machine-role/valid/codes-worker.json
@@@ -0,0 +1,6 @@
++{
++ "id": "codes.worker",
++ "name": "Codes worker",
++ "installs": "the Codes job runner",
++ "shows": "Runs your coding jobs"
++}
ddiff --git a/fixtures/network.machine-roles-put/invalid/duplicate.json b/fixtures/network.machine-roles-put/invalid/duplicate.json
nnew file mode 100644
iindex 00000000..73d24bf7
---- /dev/null
++++ b/fixtures/network.machine-roles-put/invalid/duplicate.json
@@@ -0,0 +1,6 @@
++{
++ "roles": [
++ "codes.worker",
++ "codes.worker"
++ ]
++}
ddiff --git a/fixtures/network.machine-roles-put/invalid/missing.json b/fixtures/network.machine-roles-put/invalid/missing.json
nnew file mode 100644
iindex 00000000..0967ef42
---- /dev/null
++++ b/fixtures/network.machine-roles-put/invalid/missing.json
@@@ -0,0 +1 @@
++{}
ddiff --git a/fixtures/network.machine-roles-put/invalid/not-a-list.json b/fixtures/network.machine-roles-put/invalid/not-a-list.json
nnew file mode 100644
iindex 00000000..124e3e1e
---- /dev/null
++++ b/fixtures/network.machine-roles-put/invalid/not-a-list.json
@@@ -0,0 +1,3 @@
++{
++ "roles": "codes.worker"
++}
ddiff --git a/fixtures/network.machine-roles-put/valid/none.json b/fixtures/network.machine-roles-put/valid/none.json
nnew file mode 100644
iindex 00000000..a00f09f4
---- /dev/null
++++ b/fixtures/network.machine-roles-put/valid/none.json
@@@ -0,0 +1,3 @@
++{
++ "roles": []
++}
ddiff --git a/fixtures/network.machine-roles-put/valid/two-roles.json b/fixtures/network.machine-roles-put/valid/two-roles.json
nnew file mode 100644
iindex 00000000..1ee76c4e
---- /dev/null
++++ b/fixtures/network.machine-roles-put/valid/two-roles.json
@@@ -0,0 +1,6 @@
++{
++ "roles": [
++ "codes.worker",
++ "ai.host"
++ ]
++}
ddiff --git a/fixtures/network.machine-roles/invalid/empty.json b/fixtures/network.machine-roles/invalid/empty.json
nnew file mode 100644
iindex 00000000..a00f09f4
---- /dev/null
++++ b/fixtures/network.machine-roles/invalid/empty.json
@@@ -0,0 +1,3 @@
++{
++ "roles": []
++}
ddiff --git a/fixtures/network.machine-roles/invalid/not-a-list.json b/fixtures/network.machine-roles/invalid/not-a-list.json
nnew file mode 100644
iindex 00000000..124e3e1e
---- /dev/null
++++ b/fixtures/network.machine-roles/invalid/not-a-list.json
@@@ -0,0 +1,3 @@
++{
++ "roles": "codes.worker"
++}
ddiff --git a/fixtures/network.machine-roles/valid/catalogue.json b/fixtures/network.machine-roles/valid/catalogue.json
nnew file mode 100644
iindex 00000000..1e21ea23
---- /dev/null
++++ b/fixtures/network.machine-roles/valid/catalogue.json
@@@ -0,0 +1,28 @@
++{
++ "roles": [
++ {
++ "id": "codes.worker",
++ "name": "Codes worker",
++ "installs": "Codes worker on the machine",
++ "shows": "Runs codes worker for you"
++ },
++ {
++ "id": "desks.host",
++ "name": "Desk host",
++ "installs": "Desk host on the machine",
++ "shows": "Runs desk host for you"
++ },
++ {
++ "id": "ai.host",
++ "name": "AI host",
++ "installs": "AI host on the machine",
++ "shows": "Runs ai host for you"
++ },
++ {
++ "id": "bot.robot",
++ "name": "Robot",
++ "installs": "Robot on the machine",
++ "shows": "Runs robot for you"
++ }
++ ]
++}
ddiff --git a/fixtures/network.machine.offline/invalid/bad-owner.json b/fixtures/network.machine.offline/invalid/bad-owner.json
nnew file mode 100644
iindex 00000000..5e49fc71
---- /dev/null
++++ b/fixtures/network.machine.offline/invalid/bad-owner.json
@@@ -0,0 +1,6 @@
++{
++ "machine_id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "owner": "usr_x",
++ "at": "2026-10-31T09:00:00.000Z"
++}
ddiff --git a/fixtures/network.machine.offline/valid/machine.json b/fixtures/network.machine.offline/valid/machine.json
nnew file mode 100644
iindex 00000000..a20faf5f
---- /dev/null
++++ b/fixtures/network.machine.offline/valid/machine.json
@@@ -0,0 +1,6 @@
++{
++ "machine_id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "at": "2026-10-31T09:00:00.000Z"
++}
ddiff --git a/fixtures/network.machine.online/invalid/bad-machine-id.json b/fixtures/network.machine.online/invalid/bad-machine-id.json
nnew file mode 100644
iindex 00000000..1c535e79
---- /dev/null
++++ b/fixtures/network.machine.online/invalid/bad-machine-id.json
@@@ -0,0 +1,6 @@
++{
++ "machine_id": "dev_x",
++ "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "at": "2026-10-31T09:00:00.000Z"
++}
ddiff --git a/fixtures/network.machine.online/invalid/missing-at.json b/fixtures/network.machine.online/invalid/missing-at.json
nnew file mode 100644
iindex 00000000..15e4a859
---- /dev/null
++++ b/fixtures/network.machine.online/invalid/missing-at.json
@@@ -0,0 +1,5 @@
++{
++ "machine_id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS"
++}
ddiff --git a/fixtures/network.machine.online/valid/machine.json b/fixtures/network.machine.online/valid/machine.json
nnew file mode 100644
iindex 00000000..a20faf5f
---- /dev/null
++++ b/fixtures/network.machine.online/valid/machine.json
@@@ -0,0 +1,6 @@
++{
++ "machine_id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "at": "2026-10-31T09:00:00.000Z"
++}
ddiff --git a/fixtures/network.machine.paired/invalid/missing-principal.json b/fixtures/network.machine.paired/invalid/missing-principal.json
nnew file mode 100644
iindex 00000000..f6f6a227
---- /dev/null
++++ b/fixtures/network.machine.paired/invalid/missing-principal.json
@@@ -0,0 +1,4 @@
++{
++ "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "at": "2026-10-31T09:00:00.000Z"
++}
ddiff --git a/fixtures/network.machine.paired/valid/paired.json b/fixtures/network.machine.paired/valid/paired.json
nnew file mode 100644
iindex 00000000..540ee237
---- /dev/null
++++ b/fixtures/network.machine.paired/valid/paired.json
@@@ -0,0 +1,6 @@
++{
++ "node_id": "n-01jab2c3d4e5f6g7h8j9k0mnps",
++ "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "at": "2026-10-31T09:00:00.000Z"
++}
ddiff --git a/fixtures/network.machine.revoked/invalid/missing-owner.json b/fixtures/network.machine.revoked/invalid/missing-owner.json
nnew file mode 100644
iindex 00000000..0fae6708
---- /dev/null
++++ b/fixtures/network.machine.revoked/invalid/missing-owner.json
@@@ -0,0 +1,4 @@
++{
++ "machine_id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "at": "2026-10-31T09:00:00.000Z"
++}
ddiff --git a/fixtures/network.machine.revoked/valid/revoked.json b/fixtures/network.machine.revoked/valid/revoked.json
nnew file mode 100644
iindex 00000000..7460a2eb
---- /dev/null
++++ b/fixtures/network.machine.revoked/valid/revoked.json
@@@ -0,0 +1,7 @@
++{
++ "machine_id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "reason": "the owner removed it",
++ "at": "2026-10-31T09:00:00.000Z"
++}
ddiff --git a/fixtures/network.machine.roles_changed/invalid/bad-role.json b/fixtures/network.machine.roles_changed/invalid/bad-role.json
nnew file mode 100644
iindex 00000000..c7a40f0f
---- /dev/null
++++ b/fixtures/network.machine.roles_changed/invalid/bad-role.json
@@@ -0,0 +1,8 @@
++{
++ "machine_id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "roles": [
++ "nope"
++ ],
++ "at": "2026-10-31T09:00:00.000Z"
++}
ddiff --git a/fixtures/network.machine.roles_changed/valid/changed.json b/fixtures/network.machine.roles_changed/valid/changed.json
nnew file mode 100644
iindex 00000000..d3e4ba54
---- /dev/null
++++ b/fixtures/network.machine.roles_changed/valid/changed.json
@@@ -0,0 +1,10 @@
++{
++ "machine_id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "roles": [
++ "codes.worker",
++ "ai.host"
++ ],
++ "changed_by": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "at": "2026-10-31T09:00:00.000Z"
++}
ddiff --git a/fixtures/network.machine/invalid/bad-id.json b/fixtures/network.machine/invalid/bad-id.json
nnew file mode 100644
iindex 00000000..310c6d44
---- /dev/null
++++ b/fixtures/network.machine/invalid/bad-id.json
@@@ -0,0 +1,7 @@
++{
++ "id": "machine-1",
++ "kind": "server",
++ "online": false,
++ "roles": [],
++ "running": []
++}
ddiff --git a/fixtures/network.machine/invalid/bad-role.json b/fixtures/network.machine/invalid/bad-role.json
nnew file mode 100644
iindex 00000000..6574fde3
---- /dev/null
++++ b/fixtures/network.machine/invalid/bad-role.json
@@@ -0,0 +1,9 @@
++{
++ "id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "kind": "server",
++ "online": false,
++ "roles": [
++ "codesworker"
++ ],
++ "running": []
++}
ddiff --git a/fixtures/network.machine/invalid/bad-time.json b/fixtures/network.machine/invalid/bad-time.json
nnew file mode 100644
iindex 00000000..fa08aa3e
---- /dev/null
++++ b/fixtures/network.machine/invalid/bad-time.json
@@@ -0,0 +1,8 @@
++{
++ "id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "kind": "server",
++ "online": false,
++ "roles": [],
++ "running": [],
++ "last_seen_at": "yesterday"
++}
ddiff --git a/fixtures/network.machine/invalid/missing-running.json b/fixtures/network.machine/invalid/missing-running.json
nnew file mode 100644
iindex 00000000..dd98b1e8
---- /dev/null
++++ b/fixtures/network.machine/invalid/missing-running.json
@@@ -0,0 +1,6 @@
++{
++ "id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "kind": "server",
++ "online": false,
++ "roles": []
++}
ddiff --git a/fixtures/network.machine/invalid/unknown-kind.json b/fixtures/network.machine/invalid/unknown-kind.json
nnew file mode 100644
iindex 00000000..99e82856
---- /dev/null
++++ b/fixtures/network.machine/invalid/unknown-kind.json
@@@ -0,0 +1,7 @@
++{
++ "id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "kind": "laptop",
++ "online": false,
++ "roles": [],
++ "running": []
++}
ddiff --git a/fixtures/network.machine/valid/robot.json b/fixtures/network.machine/valid/robot.json
nnew file mode 100644
iindex 00000000..3640a7f3
---- /dev/null
++++ b/fixtures/network.machine/valid/robot.json
@@@ -0,0 +1,9 @@
++{
++ "id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "kind": "onboard",
++ "online": false,
++ "roles": [
++ "bot.robot"
++ ],
++ "running": []
++}
ddiff --git a/fixtures/network.machine/valid/worker.json b/fixtures/network.machine/valid/worker.json
nnew file mode 100644
iindex 00000000..596297bc
---- /dev/null
++++ b/fixtures/network.machine/valid/worker.json
@@@ -0,0 +1,24 @@
++{
++ "id": "dev_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "principal_id": "nod_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "name": "Kitchen server",
++ "kind": "server",
++ "os": "linux",
++ "online": true,
++ "last_seen_at": "2026-10-31T09:00:00.000Z",
++ "node_version": "0.15.0",
++ "roles": [
++ "codes.worker"
++ ],
++ "running": [
++ {
++ "kind": "codes.jobs",
++ "count": 2
++ },
++ {
++ "kind": "models",
++ "count": 1,
++ "label": "llama3"
++ }
++ ]
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 0f62f1a7..85ef529d 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -939,6 +939,37 @@
"type": "string"
}
},
++ "machineRoles": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "name",
++ "installs",
++ "shows"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$"
++ },
++ "name": {
++ "type": "string",
++ "maxLength": 60
++ },
++ "installs": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "shows": {
++ "type": "string",
++ "maxLength": 200
++ }
++ }
++ }
++ },
"contractRanges": {
"type": "object",
"additionalProperties": {
@@@ -67245,6 +67276,566 @@
}
}
},
++ "network.machine": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/machine.v1.json",
++ "title": "NetworkMachine",
++ "description": "network.machine@1: one machine of a person, as the network's machines hub shows it (plan L34, steps M2 and M4). A machine is a Bot device of kind machine: an OpenVibe Node install that paired with the person's account. Bot stays the registry underneath (the device id is its dev_…); Network fronts it for the person and for apps the person consented. The roles are what the machine does for the network (the catalogue: network.machine-roles@1), and running is what it does right now. Nothing here is a secret: no credential, no hash, no address.",
++ "type": "object",
++ "required": [
++ "id",
++ "kind",
++ "online",
++ "roles",
++ "running"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's device id in Bot's registry (dev_…)."
++ },
++ "principal_id": {
++ "type": "string",
++ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's node principal on Network (nod_…): the credential it holds. Absent for a machine Bot knows but Network has no principal for."
++ },
++ "name": {
++ "type": "string",
++ "maxLength": 80,
++ "description": "The name the owner gave it. Absent when they gave none."
++ },
++ "kind": {
++ "enum": [
++ "onboard",
++ "bridge",
++ "server"
++ ],
++ "description": "What the machine is, as Bot's device record says: onboard (the robot's own machine), bridge (a machine that drives a robot over the robot's own link), server (a machine that runs its owner's jobs)."
++ },
++ "os": {
++ "type": "string",
++ "maxLength": 40,
++ "description": "The operating system the Node reports (linux, darwin, windows). Absent until the machine has checked in."
++ },
++ "online": {
++ "type": "boolean",
++ "description": "Whether the machine is connected to Bot right now."
++ },
++ "last_seen_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the machine was last seen. Absent before its first check-in."
++ },
++ "node_version": {
++ "type": "string",
++ "maxLength": 40,
++ "description": "The OpenVibe Node version the machine reports. Absent until it has checked in."
++ },
++ "roles": {
++ "type": "array",
++ "uniqueItems": true,
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$"
++ },
++ "description": "What the machine does for the network: the ids from the catalogue (network.machine-roles@1). The owner sets these; the machine applies them at its next check-in."
++ },
++ "running": {
++ "type": "array",
++ "description": "What the machine is doing right now, as a short list: its Codes jobs, its desks, the local models it serves and the robots it drives.",
++ "items": {
++ "type": "object",
++ "required": [
++ "kind",
++ "count"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "kind": {
++ "enum": [
++ "codes.jobs",
++ "desks",
++ "models",
++ "robots"
++ ]
++ },
++ "count": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "label": {
++ "type": "string",
++ "maxLength": 120,
++ "description": "A short, plain description for the owner (the desk's name, the model's name)."
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "network.machine-list-result": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/machine-list-result.v1.json",
++ "title": "NetworkMachineListResult",
++ "description": "network.machine-list-result@1: a person's machines, as the network's hub lists them (plan L34, step M2: GET /api/v1/machines). Only the caller's own machines are ever in it.",
++ "type": "object",
++ "required": [
++ "machines"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "machines": {
++ "type": "array",
++ "items": {
++ "$ref": "machine.v1.json"
++ }
++ }
++ }
++ }
++ },
++ "network.machine-manage-result": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/machine-manage-result.v1.json",
++ "title": "NetworkMachineManageResult",
++ "description": "network.machine-manage-result@1: the machine as it is after a change (plan L34, step M2: PATCH /api/v1/machines/:id, PUT /api/v1/machines/:id/roles, POST /api/v1/machines/:id/revoke). The pairing route answers network.machine-pairing@1 instead, because a pairing is a code and not a machine.",
++ "type": "object",
++ "required": [
++ "machine"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "machine": {
++ "$ref": "machine.v1.json"
++ }
++ }
++ }
++ },
++ "bot.machine-manage-request": {
++ "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-manage-request.v1.json",
++ "title": "BotMachineManageRequest",
++ "description": "bot.machine-manage-request@1: what a caller changes on a machine (plan L34, step M2). A rename gives a name; a roles change gives the whole set of roles (network.machine-roles-put@1's shape). The same body is what the network's hub sends to Bot, so both sides speak one shape.",
++ "type": "object",
++ "additionalProperties": false,
++ "anyOf": [
++ {
++ "required": [
++ "name"
++ ]
++ },
++ {
++ "required": [
++ "roles"
++ ]
++ }
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80,
++ "description": "The name the owner gives the machine. Absent when the call changes something else."
++ },
++ "roles": {
++ "type": "array",
++ "uniqueItems": true,
++ "maxItems": 8,
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$"
++ },
++ "description": "The machine's roles after the change. Absent when the call changes something else."
++ }
++ }
++ }
++ },
++ "bot.machine-manage-result": {
++ "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-manage-result.v1.json",
++ "title": "BotMachineManageResult",
++ "description": "bot.machine-manage-result@1: the machine as it is after the change (plan L34, step M2). Bot is the registry underneath: it answers with its own device record, never with a credential.",
++ "type": "object",
++ "required": [
++ "machine"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "machine": {
++ "$ref": "device.v1.json"
++ }
++ }
++ }
++ },
++ "network.machine-role": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/machine-role.v1.json",
++ "title": "NetworkMachineRole",
++ "description": "network.machine-role@1: one role a machine can have (plan L34, step M4). A role is what the machine does for the network: a name for the owner, what it installs on the machine and what the hub shows. The catalogue is network.machine-roles@1; a role outside it is refused.",
++ "type": "object",
++ "required": [
++ "id",
++ "name",
++ "installs",
++ "shows"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$",
++ "description": "The role's id: the product that owns it, then what it is (codes.worker, desks.host, ai.host, bot.robot)."
++ },
++ "name": {
++ "type": "string",
++ "maxLength": 60,
++ "description": "The role's name, in the owner's words."
++ },
++ "installs": {
++ "type": "string",
++ "maxLength": 200,
++ "description": "What the role puts on the machine (the job runner, the desk sandbox, the local model server)."
++ },
++ "shows": {
++ "type": "string",
++ "maxLength": 200,
++ "description": "What the hub shows for a machine with this role."
++ }
++ }
++ }
++ },
++ "network.machine-roles": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/machine-roles.v1.json",
++ "title": "NetworkMachineRoles",
++ "description": "network.machine-roles@1: the catalogue of machine roles (plan L34, step M4). A product declares a role — a name, what it installs on the machine and what the hub shows — and the Node turns it on at its next check-in. The first roles are Codes' worker, Desks' host, AI's local models and Bot's robot. A role outside this catalogue is refused (network.machine-roles-put@1).",
++ "type": "object",
++ "required": [
++ "roles"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "roles": {
++ "type": "array",
++ "minItems": 1,
++ "uniqueItems": true,
++ "items": {
++ "$ref": "machine-role.v1.json"
++ }
++ }
++ }
++ }
++ },
++ "network.machine-roles-put": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/machine-roles-put.v1.json",
++ "title": "NetworkMachineRolesPut",
++ "description": "network.machine-roles-put@1: the roles the owner gives a machine (PUT /api/v1/machines/:id/roles, capability machines.manage). The whole set, not a change: what is not named is turned off. Every id must be in the catalogue (network.machine-roles@1); an unknown role is refused and nothing is changed.",
++ "type": "object",
++ "required": [
++ "roles"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "roles": {
++ "type": "array",
++ "uniqueItems": true,
++ "maxItems": 8,
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$"
++ },
++ "description": "The machine's roles after the change. An empty array turns every role off."
++ }
++ }
++ }
++ },
++ "network.machine-pairing": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/machine-pairing.v1.json",
++ "title": "NetworkMachinePairing",
++ "description": "network.machine-pairing@1: a one-time code that pairs a machine with the person's account (POST /api/v1/machines/pairings, capability machines.manage). The code is shown once, expires, and works once: the machine redeems it and becomes the person's. The command is what the owner runs on the machine (openvibe.bot/install). The code is never stored in the clear and never returned again.",
++ "type": "object",
++ "required": [
++ "code",
++ "expires_at",
++ "installer"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "code": {
++ "type": "string",
++ "pattern": "^[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$",
++ "description": "The one-time pairing code, eight Crockford base32 characters in two groups (XXXX-XXXX)."
++ },
++ "pairing_id": {
++ "type": "string",
++ "pattern": "^pair_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The pairing's id, as the install command carries it. Absent when the authority behind it does not name one."
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the code stops working."
++ },
++ "installer": {
++ "type": "string",
++ "maxLength": 500,
++ "description": "The command the owner runs on the machine (openvibe.bot/install, with the network, the pairing id and the code)."
++ }
++ }
++ }
++ },
++ "network.machine.online": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.machine.online.v1.json",
++ "title": "NetworkMachineOnlinePayload",
++ "description": "network.machine.online v1 (OpenVibe.Network, plan L34: the machines hub). A machine of a person (a Bot device of kind machine) came online: it checked in and is connected to Bot. Network republishes Bot's change (Bot reports it to Network, which writes this event), so a consumer of the network's events hears about a machine once, in one place. Envelope: subject { type: machine, id: <dev_…> }, visibility internal, actor service:network.",
++ "type": "object",
++ "required": [
++ "machine_id",
++ "owner",
++ "at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "machine_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "principal_id": {
++ "type": "string",
++ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's node principal on Network, when it has one."
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person the machine belongs to."
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "network.machine.offline": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.machine.offline.v1.json",
++ "title": "NetworkMachineOfflinePayload",
++ "description": "network.machine.offline v1 (OpenVibe.Network, plan L34: the machines hub). A machine of a person went offline: it is no longer connected to Bot. Network republishes Bot's change, so a consumer of the network's events hears about a machine once, in one place. Envelope: subject { type: machine, id: <dev_…> }, visibility internal, actor service:network.",
++ "type": "object",
++ "required": [
++ "machine_id",
++ "owner",
++ "at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "machine_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "principal_id": {
++ "type": "string",
++ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's node principal on Network, when it has one."
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person the machine belongs to."
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "network.machine.paired": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.machine.paired.v1.json",
++ "title": "NetworkMachinePairedPayload",
++ "description": "network.machine.paired v1 (OpenVibe.Network, plan L34: the machines hub). A machine was paired with a person's account: it redeemed the one-time code and became theirs. It holds a credential from then on; the code is spent. The machine's device id in Bot is absent until the machine checks in for the first time. Envelope: subject { type: machine, id: <nod_…> }, visibility internal, actor service:network.",
++ "type": "object",
++ "required": [
++ "principal_id",
++ "owner",
++ "at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "machine_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's device id in Bot's registry, when Bot already knows the device."
++ },
++ "node_id": {
++ "type": "string",
++ "pattern": "^n-[a-z0-9]{26}$",
++ "description": "The machine's node id (platform_node_principals.node_id)."
++ },
++ "principal_id": {
++ "type": "string",
++ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The node principal the pairing created. Also the envelope subject id."
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person the machine belongs to."
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "network.machine.roles_changed": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.machine.roles_changed.v1.json",
++ "title": "NetworkMachineRolesChangedPayload",
++ "description": "network.machine.roles_changed v1 (OpenVibe.Network, plan L34, step M4). A machine's roles changed: the whole set after the change, from the catalogue (network.machine-roles@1). The machine applies them at its next check-in, so this event says what was asked for, not what the machine has already done. Envelope: subject { type: machine, id: <dev_…> }, visibility internal, actor service:network.",
++ "type": "object",
++ "required": [
++ "machine_id",
++ "owner",
++ "roles",
++ "at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "machine_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person the machine belongs to."
++ },
++ "roles": {
++ "type": "array",
++ "uniqueItems": true,
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$"
++ },
++ "description": "The machine's roles after the change."
++ },
++ "changed_by": {
++ "type": "string",
++ "pattern": "^(usr_[0-9A-HJKMNP-TV-Z]{26}|app:[a-z0-9-]+)$",
++ "description": "Who asked for the change: the person, or an app acting with their consent."
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "network.machine.revoked": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.machine.revoked.v1.json",
++ "title": "NetworkMachineRevokedPayload",
++ "description": "network.machine.revoked v1 (OpenVibe.Network, plan L34: the machines hub). A machine of a person was revoked: it is no longer theirs and its credential is dead. The machine's credential and its hash are never carried. A second revoke changes nothing and emits nothing. Envelope: subject { type: machine, id: <dev_…> }, visibility internal, actor service:network.",
++ "type": "object",
++ "required": [
++ "machine_id",
++ "owner",
++ "at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "machine_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "principal_id": {
++ "type": "string",
++ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's node principal on Network, when it had one."
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person the machine belonged to."
++ },
++ "reason": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 500,
++ "description": "Why it was revoked, when the caller gave one. Absent otherwise."
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
"network.avatar-write-request": {
"version": "1.0.0",
"owner": "network",
@@@ -72552,6 +73143,21 @@
},
"online": {
"type": "boolean"
++ },
++ "roles": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$"
++ },
++ "description": "The machine roles the owner set (plan L34: the catalogue is network.machine-roles@1). Absent on a device that has none."
++ },
++ "os": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The operating system the device reported."
}
}
}
ddiff --git a/generated/openapi/actor.json b/generated/openapi/actor.json
iindex d8af117a..8e66f930 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.260.0",
-- "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 147f9b48..62d0abe7 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.260.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 b4ccd5eb..be8fd374 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.260.0",
-- "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 13fda2a6..aa801400 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.260.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 26c90b17..090718af 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.260.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 bf19fd06..eaab4fc4 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.260.0",
-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.261.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"
@@@ -472,6 +472,178 @@
}
}
},
++ "/api/v1/machines": {
++ "get": {
++ "operationId": "get_api_v1_machines",
++ "summary": "A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject).",
++ "description": "**bot.machine.manage** (first-party): A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.network/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
++ "tags": [
++ "bot.machine"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.machine.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-machine",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.machine.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/bot.machine-manage-request.v1"
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.machine-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/machines/{id}": {
++ "get": {
++ "operationId": "get_api_v1_machines_id",
++ "summary": "A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject).",
++ "description": "**bot.machine.manage** (first-party): A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.network/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
++ "tags": [
++ "bot.machine"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.machine.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-machine",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.machine.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/bot.machine-manage-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.machine-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "patch": {
++ "operationId": "patch_api_v1_machines_id",
++ "summary": "A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject).",
++ "description": "**bot.machine.manage** (first-party): A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.network/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
++ "tags": [
++ "bot.machine"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.machine.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-machine",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.machine.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.machine-manage-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.machine-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/api/v1/machines/{id}/desk-leases": {
"post": {
"operationId": "post_api_v1_machines_id_desk_leases",
@@@ -816,6 +988,138 @@
}
}
},
++ "/api/v1/machines/{id}/revoke": {
++ "post": {
++ "operationId": "post_api_v1_machines_id_revoke",
++ "summary": "A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject).",
++ "description": "**bot.machine.manage** (first-party): A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.network/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
++ "tags": [
++ "bot.machine"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.machine.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-machine",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.machine.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.machine-manage-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.machine-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/machines/{id}/roles": {
++ "put": {
++ "operationId": "put_api_v1_machines_id_roles",
++ "summary": "A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject).",
++ "description": "**bot.machine.manage** (first-party): A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.network/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
++ "tags": [
++ "bot.machine"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.machine.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-machine",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.machine.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.machine-manage-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.machine-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/api/v1/resources": {
"get": {
"operationId": "get_api_v1_resources",
@@@ -2092,6 +2396,21 @@
},
"online": {
"type": "boolean"
++ },
++ "roles": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$"
++ },
++ "description": "The machine roles the owner set (plan L34: the catalogue is network.machine-roles@1). Absent on a device that has none."
++ },
++ "os": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The operating system the device reported."
}
}
},
@@@ -2356,6 +2675,56 @@
}
}
},
++ "bot.machine-manage-request.v1": {
++ "title": "BotMachineManageRequest",
++ "description": "bot.machine-manage-request@1: what a caller changes on a machine (plan L34, step M2). A rename gives a name; a roles change gives the whole set of roles (network.machine-roles-put@1's shape). The same body is what the network's hub sends to Bot, so both sides speak one shape.",
++ "type": "object",
++ "additionalProperties": false,
++ "anyOf": [
++ {
++ "required": [
++ "name"
++ ]
++ },
++ {
++ "required": [
++ "roles"
++ ]
++ }
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80,
++ "description": "The name the owner gives the machine. Absent when the call changes something else."
++ },
++ "roles": {
++ "type": "array",
++ "uniqueItems": true,
++ "maxItems": 8,
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$"
++ },
++ "description": "The machine's roles after the change. Absent when the call changes something else."
++ }
++ }
++ },
++ "bot.machine-manage-result.v1": {
++ "title": "BotMachineManageResult",
++ "description": "bot.machine-manage-result@1: the machine as it is after the change (plan L34, step M2). Bot is the registry underneath: it answers with its own device record, never with a credential.",
++ "type": "object",
++ "required": [
++ "machine"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "machine": {
++ "$ref": "#/components/schemas/bot.device.v1"
++ }
++ }
++ },
"bot.robot-control-result.v1": {
"title": "BotRobotControlResult",
"description": "bot.robot-control-result@1: answers of bot.robot.control on OpenVibe.Bot. POST /api/v1/robots/:id/estop → { robot } with its e-stop as it now stands.",
ddiff --git a/generated/openapi/chat.json b/generated/openapi/chat.json
iindex ac05aaef..37a60d13 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.260.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 d8da028c..c818b8c7 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.260.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 898d5550..ab8ee4a9 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.260.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 55dd4cde..cf54a2f9 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.260.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 45a49f1f..5f46e621 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.260.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 7f17d1c0..8ee1b193 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.260.0",
-- "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 4a912820..8bb4389a 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.260.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 3982bf4c..c3fd6cd2 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.260.0",
-- "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 2630b149..cd094c44 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.260.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 1e74b626..dc4b630e 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.260.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 4f484488..22c42bbc 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -40,8 +40,8 @@
"service": "bot",
"name": "OpenVibe.Bot",
"origin": "https://openvibe.bot",
-- "operations": 25,
-- "capabilities": 9
++ "operations": 30,
++ "capabilities": 10
},
{
"service": "chat",
@@@ -145,8 +145,8 @@
"service": "network",
"name": "OpenVibe.Network",
"origin": "https://openvibe.network",
-- "operations": 47,
-- "capabilities": 32
++ "operations": 54,
++ "capabilities": 34
},
{
"service": "news",
ddiff --git a/generated/openapi/inventory.json b/generated/openapi/inventory.json
iindex 68750847..e60c1bf9 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.260.0",
-- "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 cc5f8471..410bcf83 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.260.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 3aa10f7f..029d188e 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.260.0",
-- "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 03190463..4dc9cd38 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.260.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 b9d9280d..85033d48 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.260.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.261.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"
@@@ -136,6 +136,406 @@
}
}
},
++ "/api/v1/machines": {
++ "get": {
++ "operationId": "get_api_v1_machines",
++ "summary": "Read a person's own machines: the list, one machine, and the catalogue of roles.",
++ "description": "**network.machines.read** (public): Read a person's own machines: the list, one machine, and the catalogue of roles. A person reads their own with their own token; an app needs the person's consent for this scope (it is a public capability, so it can be granted to an app, and the app then acts only on behalf of the person who consented).",
++ "tags": [
++ "network.machines"
++ ],
++ "x-openvibe-capabilities": [
++ "network.machines.read"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "network-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "network.machines.read"
++ ]
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.machine-list-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/machines/{id}": {
++ "get": {
++ "operationId": "get_api_v1_machines_id",
++ "summary": "Read a person's own machines: the list, one machine, and the catalogue of roles.",
++ "description": "**network.machines.read** (public): Read a person's own machines: the list, one machine, and the catalogue of roles. A person reads their own with their own token; an app needs the person's consent for this scope (it is a public capability, so it can be granted to an app, and the app then acts only on behalf of the person who consented).",
++ "tags": [
++ "network.machines"
++ ],
++ "x-openvibe-capabilities": [
++ "network.machines.read"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "network-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "network.machines.read"
++ ]
++ }
++ ],
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.machine-list-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "patch": {
++ "operationId": "patch_api_v1_machines_id",
++ "summary": "Pair, rename, give roles to and revoke a person's own machines.",
++ "description": "**network.machines.manage** (public): Pair, rename, give roles to and revoke a person's own machines. Bot stays the registry underneath: Network asks Bot as the person, and Bot checks that the machine is theirs. An app needs the person's consent for this scope, and may then do only what the person could.",
++ "tags": [
++ "network.machines"
++ ],
++ "x-openvibe-capabilities": [
++ "network.machines.manage"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "network-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "network.machines.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.machine-manage-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.machine-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/machines/{id}/revoke": {
++ "post": {
++ "operationId": "post_api_v1_machines_id_revoke",
++ "summary": "Pair, rename, give roles to and revoke a person's own machines.",
++ "description": "**network.machines.manage** (public): Pair, rename, give roles to and revoke a person's own machines. Bot stays the registry underneath: Network asks Bot as the person, and Bot checks that the machine is theirs. An app needs the person's consent for this scope, and may then do only what the person could.",
++ "tags": [
++ "network.machines"
++ ],
++ "x-openvibe-capabilities": [
++ "network.machines.manage"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "network-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "network.machines.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.machine-manage-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.machine-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/machines/{id}/roles": {
++ "put": {
++ "operationId": "put_api_v1_machines_id_roles",
++ "summary": "Pair, rename, give roles to and revoke a person's own machines.",
++ "description": "**network.machines.manage** (public): Pair, rename, give roles to and revoke a person's own machines. Bot stays the registry underneath: Network asks Bot as the person, and Bot checks that the machine is theirs. An app needs the person's consent for this scope, and may then do only what the person could.",
++ "tags": [
++ "network.machines"
++ ],
++ "x-openvibe-capabilities": [
++ "network.machines.manage"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "network-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "network.machines.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.machine-manage-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.machine-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/machines/pairings": {
++ "post": {
++ "operationId": "post_api_v1_machines_pairings",
++ "summary": "Pair, rename, give roles to and revoke a person's own machines.",
++ "description": "**network.machines.manage** (public): Pair, rename, give roles to and revoke a person's own machines. Bot stays the registry underneath: Network asks Bot as the person, and Bot checks that the machine is theirs. An app needs the person's consent for this scope, and may then do only what the person could.",
++ "tags": [
++ "network.machines"
++ ],
++ "x-openvibe-capabilities": [
++ "network.machines.manage"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "network-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "network.machines.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.machine-manage-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.machine-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/machines/roles": {
++ "get": {
++ "operationId": "get_api_v1_machines_roles",
++ "summary": "Read a person's own machines: the list, one machine, and the catalogue of roles.",
++ "description": "**network.machines.read** (public): Read a person's own machines: the list, one machine, and the catalogue of roles. A person reads their own with their own token; an app needs the person's consent for this scope (it is a public capability, so it can be granted to an app, and the app then acts only on behalf of the person who consented).",
++ "tags": [
++ "network.machines"
++ ],
++ "x-openvibe-capabilities": [
++ "network.machines.read"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "network-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "network.machines.read"
++ ]
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.machine-list-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/api/v1/me/actions": {
"get": {
"operationId": "get_api_v1_me_actions",
@@@ -2898,6 +3298,42 @@
},
"components": {
"schemas": {
++ "bot.machine-manage-request.v1": {
++ "title": "BotMachineManageRequest",
++ "description": "bot.machine-manage-request@1: what a caller changes on a machine (plan L34, step M2). A rename gives a name; a roles change gives the whole set of roles (network.machine-roles-put@1's shape). The same body is what the network's hub sends to Bot, so both sides speak one shape.",
++ "type": "object",
++ "additionalProperties": false,
++ "anyOf": [
++ {
++ "required": [
++ "name"
++ ]
++ },
++ {
++ "required": [
++ "roles"
++ ]
++ }
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80,
++ "description": "The name the owner gives the machine. Absent when the call changes something else."
++ },
++ "roles": {
++ "type": "array",
++ "uniqueItems": true,
++ "maxItems": 8,
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$"
++ },
++ "description": "The machine's roles after the change. Absent when the call changes something else."
++ }
++ }
++ },
"common.action-item.v1": {
"title": "ActionItem",
"description": "One piece of a person's agent work in the Action Center (plan 2026-10-28 §3.10): an agent task, a coding task, an AI run, or an approval waiting on them. Each authority answers its own items in this shape (common.action-list-result@1); OpenVibe.Network merges them into buckets (network.me-actions-result@1). Text is plain (no markup); a cost is what the person was charged in USD, null when unknown or free; href is a page on the authority's own public site.",
@@@ -4608,6 +5044,134 @@
}
}
},
++ "network.machine-list-result.v1": {
++ "title": "NetworkMachineListResult",
++ "description": "network.machine-list-result@1: a person's machines, as the network's hub lists them (plan L34, step M2: GET /api/v1/machines). Only the caller's own machines are ever in it.",
++ "type": "object",
++ "required": [
++ "machines"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "machines": {
++ "type": "array",
++ "items": {
++ "$ref": "#/components/schemas/network.machine.v1"
++ }
++ }
++ }
++ },
++ "network.machine-manage-result.v1": {
++ "title": "NetworkMachineManageResult",
++ "description": "network.machine-manage-result@1: the machine as it is after a change (plan L34, step M2: PATCH /api/v1/machines/:id, PUT /api/v1/machines/:id/roles, POST /api/v1/machines/:id/revoke). The pairing route answers network.machine-pairing@1 instead, because a pairing is a code and not a machine.",
++ "type": "object",
++ "required": [
++ "machine"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "machine": {
++ "$ref": "#/components/schemas/network.machine.v1"
++ }
++ }
++ },
++ "network.machine.v1": {
++ "title": "NetworkMachine",
++ "description": "network.machine@1: one machine of a person, as the network's machines hub shows it (plan L34, steps M2 and M4). A machine is a Bot device of kind machine: an OpenVibe Node install that paired with the person's account. Bot stays the registry underneath (the device id is its dev_…); Network fronts it for the person and for apps the person consented. The roles are what the machine does for the network (the catalogue: network.machine-roles@1), and running is what it does right now. Nothing here is a secret: no credential, no hash, no address.",
++ "type": "object",
++ "required": [
++ "id",
++ "kind",
++ "online",
++ "roles",
++ "running"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's device id in Bot's registry (dev_…)."
++ },
++ "principal_id": {
++ "type": "string",
++ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine's node principal on Network (nod_…): the credential it holds. Absent for a machine Bot knows but Network has no principal for."
++ },
++ "name": {
++ "type": "string",
++ "maxLength": 80,
++ "description": "The name the owner gave it. Absent when they gave none."
++ },
++ "kind": {
++ "enum": [
++ "onboard",
++ "bridge",
++ "server"
++ ],
++ "description": "What the machine is, as Bot's device record says: onboard (the robot's own machine), bridge (a machine that drives a robot over the robot's own link), server (a machine that runs its owner's jobs)."
++ },
++ "os": {
++ "type": "string",
++ "maxLength": 40,
++ "description": "The operating system the Node reports (linux, darwin, windows). Absent until the machine has checked in."
++ },
++ "online": {
++ "type": "boolean",
++ "description": "Whether the machine is connected to Bot right now."
++ },
++ "last_seen_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the machine was last seen. Absent before its first check-in."
++ },
++ "node_version": {
++ "type": "string",
++ "maxLength": 40,
++ "description": "The OpenVibe Node version the machine reports. Absent until it has checked in."
++ },
++ "roles": {
++ "type": "array",
++ "uniqueItems": true,
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$"
++ },
++ "description": "What the machine does for the network: the ids from the catalogue (network.machine-roles@1). The owner sets these; the machine applies them at its next check-in."
++ },
++ "running": {
++ "type": "array",
++ "description": "What the machine is doing right now, as a short list: its Codes jobs, its desks, the local models it serves and the robots it drives.",
++ "items": {
++ "type": "object",
++ "required": [
++ "kind",
++ "count"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "kind": {
++ "enum": [
++ "codes.jobs",
++ "desks",
++ "models",
++ "robots"
++ ]
++ },
++ "count": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "label": {
++ "type": "string",
++ "maxLength": 120,
++ "description": "A short, plain description for the owner (the desk's name, the model's name)."
++ }
++ }
++ }
++ }
++ }
++ },
"network.me-actions-result.v1": {
"title": "MeActionsResult",
"description": "The Action Center for the signed-in person (capability network.actions.read, GET /api/v1/me/actions): every authority's items merged into five buckets, newest first in each. sources says how each authority answered; a slow or failing one is left out (partial true), never fails the answer.",
ddiff --git a/generated/openapi/news.json b/generated/openapi/news.json
iindex 0dcf325a..38218319 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.260.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 5b6e7ca4..25d456b7 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.260.0",
-- "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 f46e95d2..e4f8fa62 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.260.0",
-- "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 344b54f6..c667086f 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.260.0",
-- "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 97190ce3..533768b1 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.260.0",
-- "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 b69ee7f6..af4d929c 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.260.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 c2211bc4..637651ed 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.260.0",
-- "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 500abb47..dba69dc9 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.260.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 6c52a959..7e7c3841 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.260.0",
-- "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 149be20f..7692dc4e 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.260.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 d2c6fda6..c882a98d 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.260.0",
-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 48ad70ca..dd395258 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.260.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 888651c4..f7858605 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.260.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 189e4a19..613294d2 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.260.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 eb5aec31..0798d112 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.260.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 8f2a30b1..df38a3f1 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.260.0",
-- "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 d9fdd8da..a4470734 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.260.0",
-- "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 c4f2c35f..07246315 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.260.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 efc4f9ec..eafa84e3 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.260.0",
-- "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 b94efacc..f320bef7 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.260.0",
-- "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.260.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.261.0",
++ "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.261.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 d3209f11..5560e549 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -2042,6 +2042,12 @@ export interface ServiceManifest {
eventsProduced: string[];
eventsConsumed: string[];
namespacesOwned: string[];
++ machineRoles?: {
++ id: string;
++ name: string;
++ installs: string;
++ shows: string;
++ }[];
contractRanges?: {
[k: string]: string | undefined;
};
@@@ -201099,6 +201105,291 @@ export interface NetworkNodeListResult {
generated_at: string;
}
++/** [email protected] (owner: network) */
++/**
++ * network.machine@1: one machine of a person, as the network's machines hub shows it (plan L34, steps M2 and M4). A machine is a Bot device of kind machine: an OpenVibe Node install that paired with the person's account. Bot stays the registry underneath (the device id is its dev_…); Network fronts it for the person and for apps the person consented. The roles are what the machine does for the network (the catalogue: network.machine-roles@1), and running is what it does right now. Nothing here is a secret: no credential, no hash, no address.
++ */
++export interface NetworkMachine {
++ /**
++ * The machine's device id in Bot's registry (dev_…).
++ */
++ id: string;
++ /**
++ * The machine's node principal on Network (nod_…): the credential it holds. Absent for a machine Bot knows but Network has no principal for.
++ */
++ principal_id?: string;
++ /**
++ * The name the owner gave it. Absent when they gave none.
++ */
++ name?: string;
++ /**
++ * What the machine is, as Bot's device record says: onboard (the robot's own machine), bridge (a machine that drives a robot over the robot's own link), server (a machine that runs its owner's jobs).
++ */
++ kind: "onboard" | "bridge" | "server";
++ /**
++ * The operating system the Node reports (linux, darwin, windows). Absent until the machine has checked in.
++ */
++ os?: string;
++ /**
++ * Whether the machine is connected to Bot right now.
++ */
++ online: boolean;
++ /**
++ * When the machine was last seen. Absent before its first check-in.
++ */
++ last_seen_at?: string;
++ /**
++ * The OpenVibe Node version the machine reports. Absent until it has checked in.
++ */
++ node_version?: string;
++ /**
++ * What the machine does for the network: the ids from the catalogue (network.machine-roles@1). The owner sets these; the machine applies them at its next check-in.
++ */
++ roles: string[];
++ /**
++ * What the machine is doing right now, as a short list: its Codes jobs, its desks, the local models it serves and the robots it drives.
++ */
++ running: {
++ kind: "codes.jobs" | "desks" | "models" | "robots";
++ count: number;
++ /**
++ * A short, plain description for the owner (the desk's name, the model's name).
++ */
++ label?: string;
++ }[];
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.machine-list-result@1: a person's machines, as the network's hub lists them (plan L34, step M2: GET /api/v1/machines). Only the caller's own machines are ever in it.
++ */
++export interface NetworkMachineListResult {
++ machines: NetworkMachine[];
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.machine-manage-result@1: the machine as it is after a change (plan L34, step M2: PATCH /api/v1/machines/:id, PUT /api/v1/machines/:id/roles, POST /api/v1/machines/:id/revoke). The pairing route answers network.machine-pairing@1 instead, because a pairing is a code and not a machine.
++ */
++export interface NetworkMachineManageResult {
++ machine: NetworkMachine;
++}
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.machine-manage-request@1: what a caller changes on a machine (plan L34, step M2). A rename gives a name; a roles change gives the whole set of roles (network.machine-roles-put@1's shape). The same body is what the network's hub sends to Bot, so both sides speak one shape.
++ */
++export type BotMachineManageRequest = {
++ [k: string]: unknown | undefined;
++} & {
++ /**
++ * The name the owner gives the machine. Absent when the call changes something else.
++ */
++ name?: string;
++ /**
++ * The machine's roles after the change. Absent when the call changes something else.
++ *
++ * @maxItems 8
++ */
++ roles?:
++ | []
++ | [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, string];
++};
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.machine-manage-result@1: the machine as it is after the change (plan L34, step M2). Bot is the registry underneath: it answers with its own device record, never with a credential.
++ */
++export interface BotMachineManageResult {
++ machine: BotDevice;
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.machine-role@1: one role a machine can have (plan L34, step M4). A role is what the machine does for the network: a name for the owner, what it installs on the machine and what the hub shows. The catalogue is network.machine-roles@1; a role outside it is refused.
++ */
++export interface NetworkMachineRole {
++ /**
++ * The role's id: the product that owns it, then what it is (codes.worker, desks.host, ai.host, bot.robot).
++ */
++ id: string;
++ /**
++ * The role's name, in the owner's words.
++ */
++ name: string;
++ /**
++ * What the role puts on the machine (the job runner, the desk sandbox, the local model server).
++ */
++ installs: string;
++ /**
++ * What the hub shows for a machine with this role.
++ */
++ shows: string;
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.machine-roles@1: the catalogue of machine roles (plan L34, step M4). A product declares a role — a name, what it installs on the machine and what the hub shows — and the Node turns it on at its next check-in. The first roles are Codes' worker, Desks' host, AI's local models and Bot's robot. A role outside this catalogue is refused (network.machine-roles-put@1).
++ */
++export interface NetworkMachineRoles {
++ /**
++ * @minItems 1
++ */
++ roles: [NetworkMachineRole, ...NetworkMachineRole[]];
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.machine-roles-put@1: the roles the owner gives a machine (PUT /api/v1/machines/:id/roles, capability machines.manage). The whole set, not a change: what is not named is turned off. Every id must be in the catalogue (network.machine-roles@1); an unknown role is refused and nothing is changed.
++ */
++export interface NetworkMachineRolesPut {
++ /**
++ * The machine's roles after the change. An empty array turns every role off.
++ *
++ * @maxItems 8
++ */
++ roles:
++ | []
++ | [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, string];
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.machine-pairing@1: a one-time code that pairs a machine with the person's account (POST /api/v1/machines/pairings, capability machines.manage). The code is shown once, expires, and works once: the machine redeems it and becomes the person's. The command is what the owner runs on the machine (openvibe.bot/install). The code is never stored in the clear and never returned again.
++ */
++export interface NetworkMachinePairing {
++ /**
++ * The one-time pairing code, eight Crockford base32 characters in two groups (XXXX-XXXX).
++ */
++ code: string;
++ /**
++ * The pairing's id, as the install command carries it. Absent when the authority behind it does not name one.
++ */
++ pairing_id?: string;
++ /**
++ * When the code stops working.
++ */
++ expires_at: string;
++ /**
++ * The command the owner runs on the machine (openvibe.bot/install, with the network, the pairing id and the code).
++ */
++ installer: string;
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.machine.online v1 (OpenVibe.Network, plan L34: the machines hub). A machine of a person (a Bot device of kind machine) came online: it checked in and is connected to Bot. Network republishes Bot's change (Bot reports it to Network, which writes this event), so a consumer of the network's events hears about a machine once, in one place. Envelope: subject { type: machine, id: <dev_…> }, visibility internal, actor service:network.
++ */
++export interface NetworkMachineOnlinePayload {
++ machine_id: string;
++ /**
++ * The machine's node principal on Network, when it has one.
++ */
++ principal_id?: string;
++ /**
++ * The person the machine belongs to.
++ */
++ owner: string;
++ at: string;
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.machine.offline v1 (OpenVibe.Network, plan L34: the machines hub). A machine of a person went offline: it is no longer connected to Bot. Network republishes Bot's change, so a consumer of the network's events hears about a machine once, in one place. Envelope: subject { type: machine, id: <dev_…> }, visibility internal, actor service:network.
++ */
++export interface NetworkMachineOfflinePayload {
++ machine_id: string;
++ /**
++ * The machine's node principal on Network, when it has one.
++ */
++ principal_id?: string;
++ /**
++ * The person the machine belongs to.
++ */
++ owner: string;
++ at: string;
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.machine.paired v1 (OpenVibe.Network, plan L34: the machines hub). A machine was paired with a person's account: it redeemed the one-time code and became theirs. It holds a credential from then on; the code is spent. The machine's device id in Bot is absent until the machine checks in for the first time. Envelope: subject { type: machine, id: <nod_…> }, visibility internal, actor service:network.
++ */
++export interface NetworkMachinePairedPayload {
++ /**
++ * The machine's device id in Bot's registry, when Bot already knows the device.
++ */
++ machine_id?: string;
++ /**
++ * The machine's node id (platform_node_principals.node_id).
++ */
++ node_id?: string;
++ /**
++ * The node principal the pairing created. Also the envelope subject id.
++ */
++ principal_id: string;
++ /**
++ * The person the machine belongs to.
++ */
++ owner: string;
++ at: string;
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.machine.roles_changed v1 (OpenVibe.Network, plan L34, step M4). A machine's roles changed: the whole set after the change, from the catalogue (network.machine-roles@1). The machine applies them at its next check-in, so this event says what was asked for, not what the machine has already done. Envelope: subject { type: machine, id: <dev_…> }, visibility internal, actor service:network.
++ */
++export interface NetworkMachineRolesChangedPayload {
++ machine_id: string;
++ /**
++ * The person the machine belongs to.
++ */
++ owner: string;
++ /**
++ * The machine's roles after the change.
++ */
++ roles: string[];
++ /**
++ * Who asked for the change: the person, or an app acting with their consent.
++ */
++ changed_by?: string;
++ at: string;
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.machine.revoked v1 (OpenVibe.Network, plan L34: the machines hub). A machine of a person was revoked: it is no longer theirs and its credential is dead. The machine's credential and its hash are never carried. A second revoke changes nothing and emits nothing. Envelope: subject { type: machine, id: <dev_…> }, visibility internal, actor service:network.
++ */
++export interface NetworkMachineRevokedPayload {
++ machine_id: string;
++ /**
++ * The machine's node principal on Network, when it had one.
++ */
++ principal_id?: string;
++ /**
++ * The person the machine belonged to.
++ */
++ owner: string;
++ /**
++ * Why it was revoked, when the caller gave one. Absent otherwise.
++ */
++ reason?: string;
++ at: string;
++}
++
/** [email protected] (owner: network) */
/**
* network.avatar-write-request@1: the body of POST /internal/user-avatar on OpenVibe.Network (capability network.avatar.write, held by the site a person picked the avatar on; Live posts here through its notify helper). Adopts the picture as the account's avatar and fans it out to the other sites. Only an openvibe.media address is accepted, so an avatar can never be a tracking pixel or a third-party file. Refusals are { error }: 400 without a user_id, 404 for an unknown user, 422 for anything but an openvibe.media address, 503 while the avatar service is down.
@@@ -203351,6 +203642,14 @@ export interface BotDevice {
created_at: string;
updated_at: string;
online?: boolean;
++ /**
++ * The machine roles the owner set (plan L34: the catalogue is network.machine-roles@1). Absent on a device that has none.
++ */
++ roles?: string[];
++ /**
++ * The operating system the device reported.
++ */
++ os?: string | null;
}
/** [email protected] (owner: bot) */
ddiff --git a/manifests/capabilities/bot.machine.manage.json b/manifests/capabilities/bot.machine.manage.json
nnew file mode 100644
iindex 00000000..1cd22f18
---- /dev/null
++++ b/manifests/capabilities/bot.machine.manage.json
@@@ -0,0 +1,29 @@
++{
++ "id": "bot.machine.manage",
++ "version": "1.0",
++ "owner": "bot",
++ "status": "active",
++ "visibility": "first-party",
++ "sensitive": true,
++ "description": "A service acts for a person on their machines through Bot's machines API: it lists, renames, gives roles to and revokes the machines of the person it names (X-OV-Subject). Network holds it for the machines hub (openvibe.network/machines): Bot stays the registry underneath, so Bot checks that the machine is the named person's own. Never grantable to apps.",
++ "permissions": [
++ "bot:machine:read",
++ "bot:machine:rename",
++ "bot:machine:roles",
++ "bot:machine:revoke"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "bot-machine",
++ "events": [],
++ "implementedBy": [
++ "GET /api/v1/machines",
++ "GET /api/v1/machines/:id",
++ "PATCH /api/v1/machines/:id",
++ "PUT /api/v1/machines/:id/roles",
++ "POST /api/v1/machines/:id/revoke"
++ ],
++ "inputSchema": "bot.machine-manage-request@1",
++ "outputSchema": "bot.machine-manage-result@1"
++}
ddiff --git a/manifests/capabilities/network.machines.manage.json b/manifests/capabilities/network.machines.manage.json
nnew file mode 100644
iindex 00000000..58aefff0
---- /dev/null
++++ b/manifests/capabilities/network.machines.manage.json
@@@ -0,0 +1,32 @@
++{
++ "id": "network.machines.manage",
++ "version": "1.0",
++ "owner": "network",
++ "status": "active",
++ "visibility": "public",
++ "sensitive": true,
++ "description": "Pair, rename, give roles to and revoke a person's own machines. Bot stays the registry underneath: Network asks Bot as the person, and Bot checks that the machine is theirs. An app needs the person's consent for this scope, and may then do only what the person could.",
++ "permissions": [
++ "machines:pair",
++ "machines:rename",
++ "machines:roles",
++ "machines:revoke"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "network-write",
++ "events": [
++ "network.machine.paired",
++ "network.machine.roles_changed",
++ "network.machine.revoked"
++ ],
++ "implementedBy": [
++ "POST /api/v1/machines/pairings",
++ "PATCH /api/v1/machines/:id",
++ "PUT /api/v1/machines/:id/roles",
++ "POST /api/v1/machines/:id/revoke"
++ ],
++ "inputSchema": "bot.machine-manage-request@1",
++ "outputSchema": "network.machine-manage-result@1"
++}
ddiff --git a/manifests/capabilities/network.machines.read.json b/manifests/capabilities/network.machines.read.json
nnew file mode 100644
iindex 00000000..3878db3e
---- /dev/null
++++ b/manifests/capabilities/network.machines.read.json
@@@ -0,0 +1,23 @@
++{
++ "id": "network.machines.read",
++ "version": "1.0",
++ "owner": "network",
++ "status": "active",
++ "visibility": "public",
++ "description": "Read a person's own machines: the list, one machine, and the catalogue of roles. A person reads their own with their own token; an app needs the person's consent for this scope (it is a public capability, so it can be granted to an app, and the app then acts only on behalf of the person who consented).",
++ "permissions": [
++ "machines:read"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "network-read",
++ "events": [],
++ "implementedBy": [
++ "GET /api/v1/machines",
++ "GET /api/v1/machines/:id",
++ "GET /api/v1/machines/roles"
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "network.machine-list-result@1"
++}
ddiff --git a/manifests/services/ai.json b/manifests/services/ai.json
iindex 6c26b4d7..be613f76 100644
---- a/manifests/services/ai.json
++++ b/manifests/services/ai.json
@@@ -116,5 +116,13 @@
}
],
"neighbourhood": "build"
-- }
++ },
++ "machineRoles": [
++ {
++ "id": "ai.host",
++ "name": "AI host",
++ "installs": "the local model server (Ollama, LM Studio) as OpenVibe Node's ai plugin",
++ "shows": "Serves your own AI models, on your own machine."
++ }
++ ]
}
ddiff --git a/manifests/services/bot.json b/manifests/services/bot.json
iindex 1a3bf759..e8bcdb89 100644
---- a/manifests/services/bot.json
++++ b/manifests/services/bot.json
@@@ -37,7 +37,8 @@
"bot.job.dispatch",
"bot.resource.read",
"bot.resource.control",
-- "bot.desk.session"
++ "bot.desk.session",
++ "bot.machine.manage"
],
"eventsProduced": [
"bot.robot.online",
@@@ -104,5 +105,13 @@
"none": "no expiring work claims: PostgreSQL row locks serialize queue changes and an advisory lock serializes boot migrations; WebSocket sessions are in memory"
}
},
-- "notes": "Robot and device control service on openvibe-ovh since 2026-10-02; the public site uses HTTPS while health and readiness are loopback-only. Non-HTTP bindings: the device WebSocket /device carries bot.device-message@1 frames (and platform.job-frame@1 jobs); the operator WebSocket /control carries bot.command@1, answered by bot.command-result@1."
++ "notes": "Robot and device control service on openvibe-ovh since 2026-10-02; the public site uses HTTPS while health and readiness are loopback-only. Non-HTTP bindings: the device WebSocket /device carries bot.device-message@1 frames (and platform.job-frame@1 jobs); the operator WebSocket /control carries bot.command@1, answered by bot.command-result@1.",
++ "machineRoles": [
++ {
++ "id": "bot.robot",
++ "name": "Robot",
++ "installs": "the robot drivers the profile asks for",
++ "shows": "Drives a robot, and takes its commands."
++ }
++ ]
}
ddiff --git a/manifests/services/codes.json b/manifests/services/codes.json
iindex e15d64aa..7fd1ffb5 100644
---- a/manifests/services/codes.json
++++ b/manifests/services/codes.json
@@@ -109,5 +109,13 @@
"none": "no claims: one process owns the ov_codes PostgreSQL database; writes are transactions there"
}
},
-- "notes": "Port 4900, unit openvibe-codes.service. The open coding-agent harness (owner 2026-10-08): the harness catalog and router (GET /api/v1/harnesses, POST /api/v1/harnesses/route), the way to improve OpenVibe (contributing, code of conduct, the contributor ladder) and permanent redirects from the former developer-portal paths to openvibe.services, where the portal moved (manifests, releases, trust, playgrounds, docs, OAuth and webhook tools). Coding tasks on the person's own machine (plan T16) are Run jobs: their sessions are kept line by line in codes_session_lines as the stream arrives (back-filled from the machine's final log; GET /run/tasks/:id/session.json replays them), run.job.* events from OpenVibe.Run finalize a task without polling, and every state change is published as codes.task.queued|running|succeeded|failed|cancelled from a transactional outbox."
++ "notes": "Port 4900, unit openvibe-codes.service. The open coding-agent harness (owner 2026-10-08): the harness catalog and router (GET /api/v1/harnesses, POST /api/v1/harnesses/route), the way to improve OpenVibe (contributing, code of conduct, the contributor ladder) and permanent redirects from the former developer-portal paths to openvibe.services, where the portal moved (manifests, releases, trust, playgrounds, docs, OAuth and webhook tools). Coding tasks on the person's own machine (plan T16) are Run jobs: their sessions are kept line by line in codes_session_lines as the stream arrives (back-filled from the machine's final log; GET /run/tasks/:id/session.json replays them), run.job.* events from OpenVibe.Run finalize a task without polling, and every state change is published as codes.task.queued|running|succeeded|failed|cancelled from a transactional outbox.",
++ "machineRoles": [
++ {
++ "id": "codes.worker",
++ "name": "Codes worker",
++ "installs": "the Codes job runner (the coding agent and the job sandbox)",
++ "shows": "Runs your coding jobs, such as a task you start on openvibe.codes."
++ }
++ ]
}
ddiff --git a/manifests/services/network.json b/manifests/services/network.json
iindex ed8da547..2f6b39e8 100644
---- a/manifests/services/network.json
++++ b/manifests/services/network.json
@@@ -80,7 +80,9 @@
"network.status.incident",
"network.actions.read",
"network.consent.read",
-- "network.consent.manage"
++ "network.consent.manage",
++ "network.machines.read",
++ "network.machines.manage"
],
"eventsProduced": [
"network.account.deleted",
@@@ -102,7 +104,12 @@
"network.subject.merged",
"network.user.token_valid_after",
"network.user.updated",
-- "network.consent.changed"
++ "network.consent.changed",
++ "network.machine.online",
++ "network.machine.offline",
++ "network.machine.paired",
++ "network.machine.roles_changed",
++ "network.machine.revoked"
],
"eventsConsumed": [
"billing.staff.action",
ddiff --git a/manifests/services/run.json b/manifests/services/run.json
iindex 005be307..28dad38a 100644
---- a/manifests/services/run.json
++++ b/manifests/services/run.json
@@@ -139,5 +139,13 @@
],
"keywords": "openvibe run, job runner, run jobs on nodes, sandbox api, per-second metering",
"neighbourhood": "build"
-- }
++ },
++ "machineRoles": [
++ {
++ "id": "desks.host",
++ "name": "Desk host",
++ "installs": "the desk sandbox (a private browser desk for every session)",
++ "shows": "Hosts desks for agents, yours and other people's."
++ }
++ ]
}
ddiff --git a/package-lock.json b/package-lock.json
iindex 08f9b66c..e1c5752a 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-contracts",
-- "version": "0.260.0",
++ "version": "0.261.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-contracts",
-- "version": "0.260.0",
++ "version": "0.261.0",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex ebab33e4..76ac8530 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.260.0",
++ "version": "0.261.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/bot-manifest.test.js b/test/bot-manifest.test.js
iindex 28dd7bf2..79cb9fb6 100644
---- a/test/bot-manifest.test.js
++++ b/test/bot-manifest.test.js
@@@ -12,7 +12,9 @@ const c = require('..');
// bot.resource.control: an owner stops or resumes their robot from OpenVibe.Services (0.176.0, ADR-048 step 8 phase 2).
// bot.machine.ai: a model on the person's own machine, for that person (0.219.0, the AI-on-your-machines plan).
// bot.desk.session: Run's sessions lease one desk each (0.223.0, the Desks plan's D6).
--const BOT_CAPABILITIES = ['bot.robot.read', 'bot.robot.manage', 'bot.robot.control', 'bot.computer.control', 'bot.machine.ai', 'bot.desk.session', 'bot.device.connect', 'bot.job.dispatch', 'bot.resource.read', 'bot.resource.control'];
++// bot.machine.manage: the machines hub (0.258.0, plan L34): Network fronts Bot for a person and for the apps that
++// person consented to; Bot stays the authority and checks the machine is that person's own.
++const BOT_CAPABILITIES = ['bot.robot.read', 'bot.robot.manage', 'bot.robot.control', 'bot.computer.control', 'bot.machine.ai', 'bot.machine.manage', 'bot.desk.session', 'bot.device.connect', 'bot.job.dispatch', 'bot.resource.read', 'bot.resource.control'];
const BOT_EVENTS = ['bot.robot.online', 'bot.robot.offline', 'bot.estop.set', 'bot.estop.cleared', 'bot.command.refused'];
const bot = c.services.get('bot');
ddiff --git a/test/machines.test.js b/test/machines.test.js
nnew file mode 100644
iindex 00000000..e673f9b9
---- /dev/null
++++ b/test/machines.test.js
@@@ -0,0 +1,93 @@
++'use strict';
++// The machines hub (plan L34, steps M2 and M4): a person's machines are the network's, Bot stays the registry
++// underneath, and a machine's roles come from a catalogue a product declares in its own manifest. The schemas
++// carry the shapes; the rules they cannot say are checked here: one catalogue, one id shape everywhere, and a
++// role a product declared is always a role the hub accepts.
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const c = require('..');
++
++const ROOT = path.join(__dirname, '..');
++const schema = (f) => JSON.parse(fs.readFileSync(path.join(ROOT, 'contracts', f), 'utf8'));
++const manifest = (id) => JSON.parse(fs.readFileSync(path.join(ROOT, 'manifests', 'services', `${id}.json`), 'utf8'));
++const cap = (id) => JSON.parse(fs.readFileSync(path.join(ROOT, 'manifests', 'capabilities', `${id}.json`), 'utf8'));
++
++const ROLES = {
++ 'codes.worker': 'codes',
++ 'desks.host': 'run',
++ 'ai.host': 'ai',
++ 'bot.robot': 'bot',
++};
++
++// One catalogue: every product that declares a role declares it once, and nobody declares another's.
++const declared = new Map();
++for (const m of c.services.manifests) {
++ for (const role of m.machineRoles || []) {
++ assert.ok(!declared.has(role.id), `${role.id} is declared once (${m.id} and ${declared.get(role.id)})`);
++ declared.set(role.id, m.id);
++ assert.ok(role.name && role.installs && role.shows, `${role.id} says what it is, what it installs and what it shows`);
++ }
++}
++assert.deepStrictEqual(Object.keys(ROLES).sort(), [...declared.keys()].sort(), 'the catalogue holds the four first roles');
++for (const [id, owner] of Object.entries(ROLES)) assert.strictEqual(declared.get(id), owner, `${id} is ${owner}'s role`);
++
++// One id shape everywhere: a machine, the catalogue and a request all name a role the same way.
++const machine = schema('network/machine.v1.json');
++const role = schema('network/machine-role.v1.json');
++const roles = schema('network/machine-roles.v1.json');
++const put = schema('network/machine-roles-put.v1.json');
++const pattern = role.properties.id.pattern;
++assert.strictEqual(machine.properties.roles.items.pattern, pattern, 'a machine names its roles the same way');
++assert.strictEqual(put.properties.roles.items.pattern, pattern, 'a request names its roles the same way');
++for (const id of Object.keys(ROLES)) {
++ assert.ok(new RegExp(pattern).test(id), `${id} is a role id`);
++ assert.ok(c.validate('network.machine-role@1', { id, name: 'x', installs: 'y', shows: 'z' }).valid, `${id} is a role`);
++}
++
++// The catalogue answers with the roles, and only with them.
++const catalogue = { roles: [...declared.keys()].map((id) => ({ id, name: 'x', installs: 'y', shows: 'z' })) };
++assert.ok(c.validate('network.machine-roles@1', catalogue).valid, 'the catalogue answers with its roles');
++assert.ok(!c.validate('network.machine-roles@1', { roles: [] }).valid, 'an empty catalogue is no catalogue');
++assert.ok(!c.validate('network.machine-roles-put@1', { roles: ['not-a-role'] }).valid, 'a request names roles that exist as ids');
++
++// A machine is a machine: an id, a kind, whether it is online, its roles and what it does now.
++for (const k of ['id', 'kind', 'online', 'roles', 'running']) assert.ok(machine.required.includes(k), `network.machine@1 needs ${k}`);
++assert.deepStrictEqual(machine.properties.running.items.properties.kind.enum, ['codes.jobs', 'desks', 'models', 'robots'],
++ 'a machine says what it does now: its jobs, its desks, its models and its robots');
++assert.strictEqual(machine.additionalProperties, false, 'a machine carries nothing else');
++
++// A pairing is a code, an expiry and the command to run; the code is shown once.
++for (const k of ['code', 'expires_at', 'installer']) assert.ok(schema('network/machine-pairing.v1.json').required.includes(k), `the pairing needs ${k}`);
++
++// The capabilities: the hub's two are public (an app may hold them with the person's consent), Bot's is not.
++assert.strictEqual(cap('network.machines.read').visibility, 'public');
++assert.strictEqual(cap('network.machines.read').owner, 'network');
++assert.strictEqual(cap('network.machines.manage').visibility, 'public');
++assert.strictEqual(cap('network.machines.manage').owner, 'network');
++assert.strictEqual(cap('network.machines.manage').sensitive, true, 'revoking a machine is an act with an effect outside');
++assert.strictEqual(cap('bot.machine.manage').visibility, 'first-party');
++assert.strictEqual(cap('bot.machine.manage').owner, 'bot');
++
++// The manifests say who serves what.
++const network = manifest('network');
++assert.ok(network.capabilities.includes('network.machines.read') && network.capabilities.includes('network.machines.manage'));
++for (const e of ['network.machine.online', 'network.machine.offline', 'network.machine.paired', 'network.machine.roles_changed', 'network.machine.revoked']) {
++ assert.ok(network.eventsProduced.includes(e), `network produces ${e}`);
++}
++assert.ok(manifest('bot').capabilities.includes('bot.machine.manage'), 'bot serves bot.machine.manage');
++
++// The fixtures: each valid one validates, each invalid one does not.
++for (const id of ['network.machine', 'network.machine-role', 'network.machine-roles', 'network.machine-roles-put', 'network.machine-pairing',
++ 'network.machine-list-result', 'network.machine-manage-result', 'bot.machine-manage-request', 'bot.machine-manage-result',
++ 'network.machine.online', 'network.machine.offline', 'network.machine.paired', 'network.machine.roles_changed', 'network.machine.revoked']) {
++ for (const kind of ['valid', 'invalid']) {
++ for (const f of fs.readdirSync(path.join(ROOT, 'fixtures', id, kind))) {
++ const value = JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures', id, kind, f), 'utf8'));
++ const r = c.validate(`${id}@1`, value);
++ assert.strictEqual(r.valid, kind === 'valid', `${id} ${kind}/${f}: ${JSON.stringify(r.errors)}`);
++ }
++ }
++}
++
++console.log('network.machine@1: one catalogue, one role id shape, four roles, public hub capabilities, fixtures');
ddiff --git a/test/presence-policy.test.js b/test/presence-policy.test.js
iindex f923f35e..eb3842d2 100644
---- a/test/presence-policy.test.js
++++ b/test/presence-policy.test.js
@@@ -1,10 +1,16 @@
'use strict';
// ADR-005 amendment 1: people's presence is ephemeral, in Chat's delivery plane.
// ADR-043 records robot connectivity in Events; it is not a person's presence.
++// The machines hub (plan L34) records the same for a person's machines: a machine coming online is the
++// device changing state, never the person being present. Any other `.online`, `.offline` or presence event
++// type is still refused.
const assert = require('assert');
const c = require('..');
const produced = c.services.manifests.flatMap((m) => m.eventsProduced || []);
--const robotConnectivity = new Set(['bot.robot.online', 'bot.robot.offline']);
--const presenceEvents = produced.filter((t) => /presence|\.online|\.offline/i.test(t) && !robotConnectivity.has(t));
++const deviceConnectivity = new Set([
++ 'bot.robot.online', 'bot.robot.offline',
++ 'network.machine.online', 'network.machine.offline',
++]);
++const presenceEvents = produced.filter((t) => /presence|\.online|\.offline/i.test(t) && !deviceConnectivity.has(t));
assert.ok(presenceEvents.length === 0, `no person presence event type: ${presenceEvents}`);
console.log('presence policy: all checks passed');
ddiff --git a/test/run.js b/test/run.js
iindex 464e1f28..840087c2 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -929,6 +929,8 @@ sub(path.join(__dirname, 'resources.test.js'));
// The wait record (plan A2): a wait is a record, not a running loop.
sub(path.join(__dirname, 'actor-wait.test.js'));
sub(path.join(__dirname, 'network-consent.test.js'));
++// The machines hub (plan L34): one catalogue of roles, declared by the products that own them.
++sub(path.join(__dirname, 'machines.test.js'));
// Labels and choices (L1): one label list, one choice list, and the AI's suggestion stored as suggested.
sub(path.join(__dirname, 'content-labels.test.js'));
// Documentation currency (roadmap WS-U task 3): a placeholder service with no repository yet is skipped, not a gap.