openvibers/openvibe.contracts Commits

Add the Bot service manifest with capabilities bound to Bot's guards (#16)

OP OpenVibers committed 3 Oct 2026 · 2d224b52da · 44 files, +3627 −5

local checks ✓ 3/3 · CI ✓ 3/3 · review SHIP by DeepSeek harness · 1 fix round

https://github.com/OpenVibers/OpenVibe.Contracts/pull/16

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 47ecb1bd..1caea47b 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,32 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
the notes were in the tag and commit messages (`git tag -n1`).
++## Unreleased
++
++**The Bot service manifest** (plan T15 step 1; ADR-043; supersedes #8). OpenVibe.Bot has run on openvibe-ovh since
++2026-10-02 (`openvibe-bot.service`, `/opt/openvibe.bot`), and `ovhost validate` found no `bot` manifest to read its
++lifecycle from.
++- `manifests/services/bot.json`: status `live` on `openvibe.bot`, internal origin `127.0.0.1:4630`, the five `bot.*`
++ events it produces, and all six lifecycle parts. Shutdown is what Bot's `openvibe-sdk/service` `gracefulStop` does:
++ stop the job timers and Network key refresh, close the device and operator WebSockets and the outbox relay, drain HTTP
++ for up to 4 s, close PostgreSQL and Valkey, exit 0, with a 5 s hard deadline inside the unit's 10 s `TimeoutStopSec`.
++ The contracts range `>=0.85.0 <1.0.0` covers the v0.85.0 Bot installs.
++- Three active capabilities, each bound to the routes whose guard checks it in the deployed Bot (dae7c59), for a
++ Network service token: `bot.robot.read` (list by `?owner=`, a robot, its operators and devices), `bot.robot.manage`
++ (create, update, delete, pairing codes, operators, the command audit, clearing the e-stop, rotating and revoking a
++ device) and `bot.robot.control` (latching the e-stop, which also needs `bot.robot.read`, and the operator WebSocket,
++ which acts for `X-OV-Subject` with that person's role). Their resource constraint is `none`: Bot does not check the
++ acting person against the robot for a service token on the REST routes, and the descriptions say so. All are
++ first-party, none is a staff capability, and each names an input and an output schema: new `bot.robot@1`,
++ `bot.device@1`, `bot.robot-read-result@1`, `bot.robot-manage-request@1`, `bot.robot-manage-result@1` (which includes
++ the audit page and `bot.device-connect-result@1`), `bot.robot-control-result@1` and `bot.device-connect-result@1`,
++ with valid and invalid fixtures.
++- `bot.device.connect` is `planned` (the Bot manifest lists it, as owners list their planned capabilities): Bot
++ declares the name but no route checks it, so it names no routes or schemas until one does.
++- `openvibe.bot` product: names its service `bot` and repository `OpenVibe.Bot` instead of `noRepo`.
++- Presence policy: `bot.robot.online` and `bot.robot.offline` are robot connectivity (ADR-043), not a person's presence,
++ and are allowed. Any other `.online`, `.offline` or presence event type is still refused.
++
## 0.87.0 — 2026-10-03
**User-owned trust class and the estate table** (plan T1 step 3; ADR-034 §5 control plane/data plane; the Fabric ADR,
ddiff --git a/contracts/bot/device-connect-result.v1.json b/contracts/bot/device-connect-result.v1.json
nnew file mode 100644
iindex 00000000..387c5a29
---- /dev/null
++++ b/contracts/bot/device-connect-result.v1.json
@@@ -0,0 +1,42 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/device-connect-result.v1.json",
++ "title": "BotDeviceConnectResult",
++ "description": "bot.device-connect-result@1: answers of OpenVibe.Bot's device credential routes, which Bot gates with bot.robot.manage (bot.device.connect is reserved, not enforced). POST /api/v1/devices/:id/rotate → { device, credential, publish_key } (both secrets shown once); POST …/devices/:id/revoke → { device } (its socket closes at once).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "device",
++ "credential",
++ "publish_key"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "device": {
++ "$ref": "device.v1.json"
++ },
++ "credential": {
++ "type": "string",
++ "minLength": 1
++ },
++ "publish_key": {
++ "type": "string",
++ "minLength": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "device"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "device": {
++ "$ref": "device.v1.json"
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/bot/device.v1.json b/contracts/bot/device.v1.json
nnew file mode 100644
iindex 00000000..c0998c10
---- /dev/null
++++ b/contracts/bot/device.v1.json
@@@ -0,0 +1,73 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/device.v1.json",
++ "title": "BotDevice",
++ "description": "bot.device@1: a paired device as the API presents it (domain.present.device): never a credential or publish-key hash; online is added when a robot's devices are listed.",
++ "type": "object",
++ "required": [
++ "id",
++ "robot_ids",
++ "kind",
++ "created_at",
++ "updated_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "robot_ids": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "name": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "kind": {
++ "type": "string"
++ },
++ "agent_version": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "drivers": {
++ "type": "array"
++ },
++ "capabilities": {
++ "type": "object"
++ },
++ "last_seen": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "revoked_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "online": {
++ "type": "boolean"
++ }
++ }
++}
ddiff --git a/contracts/bot/robot-control-result.v1.json b/contracts/bot/robot-control-result.v1.json
nnew file mode 100644
iindex 00000000..59504c57
---- /dev/null
++++ b/contracts/bot/robot-control-result.v1.json
@@@ -0,0 +1,15 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/robot-control-result.v1.json",
++ "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.",
++ "type": "object",
++ "required": [
++ "robot"
++ ],
++ "properties": {
++ "robot": {
++ "$ref": "robot.v1.json"
++ }
++ }
++}
ddiff --git a/contracts/bot/robot-manage-request.v1.json b/contracts/bot/robot-manage-request.v1.json
nnew file mode 100644
iindex 00000000..94b5d9ab
---- /dev/null
++++ b/contracts/bot/robot-manage-request.v1.json
@@@ -0,0 +1,79 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/robot-manage-request.v1.json",
++ "title": "BotRobotManageRequest",
++ "description": "bot.robot-manage-request@1: bodies of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → { owner (the person a service acts for), name, profile_id, access_policy?, limits? }; PATCH …/robots/:id → { name?, access_policy?, limits? }; POST …/robots/:id/operators → { subject, role? (operator by default) }. POST …/robots/:id/pairing-code and both DELETE routes take no body.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "name",
++ "profile_id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1
++ },
++ "profile_id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "access_policy": {
++ "enum": [
++ "private",
++ "invite",
++ "queue"
++ ]
++ },
++ "limits": {
++ "type": "object"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "name": {
++ "type": "string",
++ "minLength": 1
++ },
++ "access_policy": {
++ "enum": [
++ "private",
++ "invite",
++ "queue"
++ ]
++ },
++ "limits": {
++ "type": "object"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "subject"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "role": {
++ "enum": [
++ "operator",
++ "viewer"
++ ]
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/bot/robot-manage-result.v1.json b/contracts/bot/robot-manage-result.v1.json
nnew file mode 100644
iindex 00000000..4ab1379d
---- /dev/null
++++ b/contracts/bot/robot-manage-result.v1.json
@@@ -0,0 +1,216 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/robot-manage-result.v1.json",
++ "title": "BotRobotManageResult",
++ "description": "bot.robot-manage-result@1: answers of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → 201 { robot, pairing }; PATCH …/robots/:id and POST …/robots/:id/estop/clear → { robot }; DELETE …/robots/:id → 204 (no body); POST …/robots/:id/pairing-code → 201 { code, expires_at, installer }; POST …/robots/:id/operators → 201 { operators }; DELETE …/robots/:id/operators/:subject → { operators }; GET …/robots/:id/audit?limit=&before= → { audit, next_before } (newest first); POST …/devices/:id/rotate and /revoke → bot.device-connect-result@1.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "robot",
++ "pairing"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "robot": {
++ "$ref": "robot.v1.json"
++ },
++ "pairing": {
++ "type": "object",
++ "required": [
++ "code",
++ "expires_at",
++ "installer"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "code": {
++ "type": "string",
++ "minLength": 1
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "installer": {
++ "type": "string"
++ }
++ },
++ "description": "A one-time pairing code (shown once), when it expires and the one-line installer command that uses it."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "robot"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "robot": {
++ "$ref": "robot.v1.json"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "code",
++ "expires_at",
++ "installer"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "code": {
++ "type": "string",
++ "minLength": 1
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "installer": {
++ "type": "string"
++ }
++ },
++ "description": "A one-time pairing code (shown once), when it expires and the one-line installer command that uses it."
++ },
++ {
++ "type": "object",
++ "required": [
++ "operators"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "operators": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "subject",
++ "role"
++ ],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "role": {
++ "enum": [
++ "operator",
++ "viewer"
++ ]
++ },
++ "added_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "$ref": "device-connect-result.v1.json"
++ },
++ {
++ "type": "object",
++ "required": [
++ "audit",
++ "next_before"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "next_before": {
++ "type": [
++ "integer",
++ "null"
++ ]
++ },
++ "audit": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "robot_id",
++ "kind",
++ "result",
++ "at"
++ ],
++ "properties": {
++ "id": {
++ "type": "integer"
++ },
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "device_id": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "operator_subject": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "operator_kind": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "role": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "kind": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "value": {},
++ "result": {
++ "enum": [
++ "ack",
++ "nack",
++ "refused",
++ "expired"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "latency_ms": {
++ "type": [
++ "integer",
++ "null"
++ ]
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/bot/robot-read-result.v1.json b/contracts/bot/robot-read-result.v1.json
nnew file mode 100644
iindex 00000000..2ce42e82
---- /dev/null
++++ b/contracts/bot/robot-read-result.v1.json
@@@ -0,0 +1,97 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/robot-read-result.v1.json",
++ "title": "BotRobotReadResult",
++ "description": "bot.robot-read-result@1: answers of bot.robot.read on OpenVibe.Bot. GET /api/v1/robots?owner= → { robots }; GET …/robots/:id → { robot, role } (role \"service\" for a service token); GET …/robots/:id/operators → { operators }; GET …/robots/:id/devices → { devices } (no hashes, with online).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "robots"
++ ],
++ "properties": {
++ "robots": {
++ "type": "array",
++ "items": {
++ "$ref": "robot.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "robot",
++ "role"
++ ],
++ "properties": {
++ "robot": {
++ "$ref": "robot.v1.json"
++ },
++ "role": {
++ "enum": [
++ "owner",
++ "operator",
++ "viewer",
++ "queue",
++ "service"
++ ]
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "operators"
++ ],
++ "properties": {
++ "operators": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "subject",
++ "role"
++ ],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "role": {
++ "enum": [
++ "operator",
++ "viewer"
++ ]
++ },
++ "added_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "devices"
++ ],
++ "properties": {
++ "devices": {
++ "type": "array",
++ "items": {
++ "$ref": "device.v1.json"
++ }
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/bot/robot.v1.json b/contracts/bot/robot.v1.json
nnew file mode 100644
iindex 00000000..2a279ba5
---- /dev/null
++++ b/contracts/bot/robot.v1.json
@@@ -0,0 +1,76 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/robot.v1.json",
++ "title": "BotRobot",
++ "description": "bot.robot@1: a robot on OpenVibe.Bot as its API presents it (domain.present.robot): its owner-chosen name, the device profile it runs, who may drive it (access_policy) and the latched e-stop.",
++ "type": "object",
++ "required": [
++ "id",
++ "name",
++ "profile_id",
++ "access_policy",
++ "limits",
++ "estop",
++ "created_at",
++ "updated_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1
++ },
++ "profile_id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "profile_version": {},
++ "access_policy": {
++ "enum": [
++ "private",
++ "invite",
++ "queue"
++ ]
++ },
++ "limits": {
++ "type": "object"
++ },
++ "estop": {
++ "type": "object",
++ "required": [
++ "latched",
++ "by",
++ "at"
++ ],
++ "properties": {
++ "latched": {
++ "type": "boolean"
++ },
++ "by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++}
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 9b07d205..4eb3b39c 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -5557,6 +5557,69 @@
"compatibility": "backward",
"status": "planned",
"adr": "ADR-031"
++ },
++ {
++ "id": "bot.robot",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/robot.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "bot.device",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/device.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "bot.robot-read-result",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/robot-read-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "bot.robot-manage-request",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/robot-manage-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "bot.robot-manage-result",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/robot-manage-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "bot.robot-control-result",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/robot-control-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "bot.device-connect-result",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/device-connect-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
}
]
}
ddiff --git a/fixtures/bot.device-connect-result/invalid/rotated-without-key.json b/fixtures/bot.device-connect-result/invalid/rotated-without-key.json
nnew file mode 100644
iindex 00000000..68d8370b
---- /dev/null
++++ b/fixtures/bot.device-connect-result/invalid/rotated-without-key.json
@@@ -0,0 +1,20 @@
++{
++ "device": {
++ "id": "dev_01J8Z4N0A1B2C3D4E5F6G7H8J9",
++ "robot_ids": [
++ "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X"
++ ],
++ "name": "rover-pi",
++ "kind": "agent",
++ "agent_version": "0.1.0",
++ "drivers": [
++ "adeept"
++ ],
++ "capabilities": {},
++ "last_seen": "2026-09-29T19:20:00.000Z",
++ "revoked_at": null,
++ "created_at": "2026-09-29T19:20:00.000Z",
++ "updated_at": "2026-09-29T19:20:00.000Z"
++ },
++ "credential": "devc_secret"
++}
ddiff --git a/fixtures/bot.device-connect-result/valid/revoked.json b/fixtures/bot.device-connect-result/valid/revoked.json
nnew file mode 100644
iindex 00000000..e01f379f
---- /dev/null
++++ b/fixtures/bot.device-connect-result/valid/revoked.json
@@@ -0,0 +1,19 @@
++{
++ "device": {
++ "id": "dev_01J8Z4N0A1B2C3D4E5F6G7H8J9",
++ "robot_ids": [
++ "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X"
++ ],
++ "name": "rover-pi",
++ "kind": "agent",
++ "agent_version": "0.1.0",
++ "drivers": [
++ "adeept"
++ ],
++ "capabilities": {},
++ "last_seen": "2026-09-29T19:20:00.000Z",
++ "revoked_at": "2026-09-29T19:20:00.000Z",
++ "created_at": "2026-09-29T19:20:00.000Z",
++ "updated_at": "2026-09-29T19:20:00.000Z"
++ }
++}
ddiff --git a/fixtures/bot.device-connect-result/valid/rotated.json b/fixtures/bot.device-connect-result/valid/rotated.json
nnew file mode 100644
iindex 00000000..20fa94c1
---- /dev/null
++++ b/fixtures/bot.device-connect-result/valid/rotated.json
@@@ -0,0 +1,21 @@
++{
++ "device": {
++ "id": "dev_01J8Z4N0A1B2C3D4E5F6G7H8J9",
++ "robot_ids": [
++ "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X"
++ ],
++ "name": "rover-pi",
++ "kind": "agent",
++ "agent_version": "0.1.0",
++ "drivers": [
++ "adeept"
++ ],
++ "capabilities": {},
++ "last_seen": "2026-09-29T19:20:00.000Z",
++ "revoked_at": null,
++ "created_at": "2026-09-29T19:20:00.000Z",
++ "updated_at": "2026-09-29T19:20:00.000Z"
++ },
++ "credential": "devc_secret",
++ "publish_key": "pk_secret"
++}
ddiff --git a/fixtures/bot.device/invalid/bad-robot-id.json b/fixtures/bot.device/invalid/bad-robot-id.json
nnew file mode 100644
iindex 00000000..1dc70cd9
---- /dev/null
++++ b/fixtures/bot.device/invalid/bad-robot-id.json
@@@ -0,0 +1,17 @@
++{
++ "id": "dev_01J8Z4N0A1B2C3D4E5F6G7H8J9",
++ "robot_ids": [
++ "robot-1"
++ ],
++ "name": "rover-pi",
++ "kind": "agent",
++ "agent_version": "0.1.0",
++ "drivers": [
++ "adeept"
++ ],
++ "capabilities": {},
++ "last_seen": "2026-09-29T19:20:00.000Z",
++ "revoked_at": null,
++ "created_at": "2026-09-29T19:20:00.000Z",
++ "updated_at": "2026-09-29T19:20:00.000Z"
++}
ddiff --git a/fixtures/bot.device/valid/agent.json b/fixtures/bot.device/valid/agent.json
nnew file mode 100644
iindex 00000000..1fd7d167
---- /dev/null
++++ b/fixtures/bot.device/valid/agent.json
@@@ -0,0 +1,17 @@
++{
++ "id": "dev_01J8Z4N0A1B2C3D4E5F6G7H8J9",
++ "robot_ids": [
++ "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X"
++ ],
++ "name": "rover-pi",
++ "kind": "agent",
++ "agent_version": "0.1.0",
++ "drivers": [
++ "adeept"
++ ],
++ "capabilities": {},
++ "last_seen": "2026-09-29T19:20:00.000Z",
++ "revoked_at": null,
++ "created_at": "2026-09-29T19:20:00.000Z",
++ "updated_at": "2026-09-29T19:20:00.000Z"
++}
ddiff --git a/fixtures/bot.robot-control-result/invalid/estop-without-latched.json b/fixtures/bot.robot-control-result/invalid/estop-without-latched.json
nnew file mode 100644
iindex 00000000..4b5c9290
---- /dev/null
++++ b/fixtures/bot.robot-control-result/invalid/estop-without-latched.json
@@@ -0,0 +1,18 @@
++{
++ "robot": {
++ "id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "name": "Rover",
++ "profile_id": "adeept.adr036",
++ "profile_version": 1,
++ "access_policy": "private",
++ "limits": {
++ "max_command_ms": 300
++ },
++ "estop": {
++ "by": null,
++ "at": null
++ },
++ "created_at": "2026-09-29T19:20:00.000Z",
++ "updated_at": "2026-09-29T19:20:00.000Z"
++ }
++}
ddiff --git a/fixtures/bot.robot-control-result/valid/latched.json b/fixtures/bot.robot-control-result/valid/latched.json
nnew file mode 100644
iindex 00000000..f23a76e4
---- /dev/null
++++ b/fixtures/bot.robot-control-result/valid/latched.json
@@@ -0,0 +1,19 @@
++{
++ "robot": {
++ "id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "name": "Rover",
++ "profile_id": "adeept.adr036",
++ "profile_version": 1,
++ "access_policy": "private",
++ "limits": {
++ "max_command_ms": 300
++ },
++ "estop": {
++ "latched": true,
++ "by": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
++ "at": "2026-09-29T19:20:00.000Z"
++ },
++ "created_at": "2026-09-29T19:20:00.000Z",
++ "updated_at": "2026-09-29T19:20:00.000Z"
++ }
++}
ddiff --git a/fixtures/bot.robot-manage-request/invalid/owner-role.json b/fixtures/bot.robot-manage-request/invalid/owner-role.json
nnew file mode 100644
iindex 00000000..fc713902
---- /dev/null
++++ b/fixtures/bot.robot-manage-request/invalid/owner-role.json
@@@ -0,0 +1,4 @@
++{
++ "subject": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
++ "role": "owner"
++}
ddiff --git a/fixtures/bot.robot-manage-request/valid/create.json b/fixtures/bot.robot-manage-request/valid/create.json
nnew file mode 100644
iindex 00000000..507f3aee
---- /dev/null
++++ b/fixtures/bot.robot-manage-request/valid/create.json
@@@ -0,0 +1,6 @@
++{
++ "owner": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
++ "name": "Rover",
++ "profile_id": "adeept.adr036",
++ "access_policy": "queue"
++}
ddiff --git a/fixtures/bot.robot-manage-request/valid/operator.json b/fixtures/bot.robot-manage-request/valid/operator.json
nnew file mode 100644
iindex 00000000..6a56d9f0
---- /dev/null
++++ b/fixtures/bot.robot-manage-request/valid/operator.json
@@@ -0,0 +1,4 @@
++{
++ "subject": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
++ "role": "viewer"
++}
ddiff --git a/fixtures/bot.robot-manage-result/invalid/bad-audit-result.json b/fixtures/bot.robot-manage-result/invalid/bad-audit-result.json
nnew file mode 100644
iindex 00000000..33594738
---- /dev/null
++++ b/fixtures/bot.robot-manage-result/invalid/bad-audit-result.json
@@@ -0,0 +1,12 @@
++{
++ "audit": [
++ {
++ "id": 1,
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "kind": "drive",
++ "result": "maybe",
++ "at": "2026-09-29T19:20:00.000Z"
++ }
++ ],
++ "next_before": null
++}
ddiff --git a/fixtures/bot.robot-manage-result/invalid/operator-without-subject.json b/fixtures/bot.robot-manage-result/invalid/operator-without-subject.json
nnew file mode 100644
iindex 00000000..fef2201e
---- /dev/null
++++ b/fixtures/bot.robot-manage-result/invalid/operator-without-subject.json
@@@ -0,0 +1,7 @@
++{
++ "operators": [
++ {
++ "role": "operator"
++ }
++ ]
++}
ddiff --git a/fixtures/bot.robot-manage-result/valid/audit.json b/fixtures/bot.robot-manage-result/valid/audit.json
nnew file mode 100644
iindex 00000000..d3d5b168
---- /dev/null
++++ b/fixtures/bot.robot-manage-result/valid/audit.json
@@@ -0,0 +1,21 @@
++{
++ "audit": [
++ {
++ "id": 42,
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "device_id": "dev_01J8Z4N0A1B2C3D4E5F6G7H8J9",
++ "operator_subject": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
++ "operator_kind": "user",
++ "role": "operator",
++ "kind": "drive",
++ "value": {
++ "throttle": 0.6
++ },
++ "result": "ack",
++ "reason": null,
++ "latency_ms": 42,
++ "at": "2026-09-29T19:20:00.000Z"
++ }
++ ],
++ "next_before": 42
++}
ddiff --git a/fixtures/bot.robot-manage-result/valid/created.json b/fixtures/bot.robot-manage-result/valid/created.json
nnew file mode 100644
iindex 00000000..1bc4722a
---- /dev/null
++++ b/fixtures/bot.robot-manage-result/valid/created.json
@@@ -0,0 +1,24 @@
++{
++ "robot": {
++ "id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "name": "Rover",
++ "profile_id": "adeept.adr036",
++ "profile_version": 1,
++ "access_policy": "private",
++ "limits": {
++ "max_command_ms": 300
++ },
++ "estop": {
++ "latched": false,
++ "by": null,
++ "at": null
++ },
++ "created_at": "2026-09-29T19:20:00.000Z",
++ "updated_at": "2026-09-29T19:20:00.000Z"
++ },
++ "pairing": {
++ "code": "ABCD-EFGH",
++ "expires_at": "2026-09-29T19:20:00.000Z",
++ "installer": "curl -fsSL https://openvibe.bot/install.sh | sh -s ABCD-EFGH"
++ }
++}
ddiff --git a/fixtures/bot.robot-manage-result/valid/device-rotated.json b/fixtures/bot.robot-manage-result/valid/device-rotated.json
nnew file mode 100644
iindex 00000000..20fa94c1
---- /dev/null
++++ b/fixtures/bot.robot-manage-result/valid/device-rotated.json
@@@ -0,0 +1,21 @@
++{
++ "device": {
++ "id": "dev_01J8Z4N0A1B2C3D4E5F6G7H8J9",
++ "robot_ids": [
++ "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X"
++ ],
++ "name": "rover-pi",
++ "kind": "agent",
++ "agent_version": "0.1.0",
++ "drivers": [
++ "adeept"
++ ],
++ "capabilities": {},
++ "last_seen": "2026-09-29T19:20:00.000Z",
++ "revoked_at": null,
++ "created_at": "2026-09-29T19:20:00.000Z",
++ "updated_at": "2026-09-29T19:20:00.000Z"
++ },
++ "credential": "devc_secret",
++ "publish_key": "pk_secret"
++}
ddiff --git a/fixtures/bot.robot-manage-result/valid/estop-cleared.json b/fixtures/bot.robot-manage-result/valid/estop-cleared.json
nnew file mode 100644
iindex 00000000..0fecf6d8
---- /dev/null
++++ b/fixtures/bot.robot-manage-result/valid/estop-cleared.json
@@@ -0,0 +1,19 @@
++{
++ "robot": {
++ "id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "name": "Rover",
++ "profile_id": "adeept.adr036",
++ "profile_version": 1,
++ "access_policy": "private",
++ "limits": {
++ "max_command_ms": 300
++ },
++ "estop": {
++ "latched": false,
++ "by": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
++ "at": "2026-09-29T19:20:00.000Z"
++ },
++ "created_at": "2026-09-29T19:20:00.000Z",
++ "updated_at": "2026-09-29T19:20:00.000Z"
++ }
++}
ddiff --git a/fixtures/bot.robot-manage-result/valid/operators.json b/fixtures/bot.robot-manage-result/valid/operators.json
nnew file mode 100644
iindex 00000000..2f98682d
---- /dev/null
++++ b/fixtures/bot.robot-manage-result/valid/operators.json
@@@ -0,0 +1,10 @@
++{
++ "operators": [
++ {
++ "subject": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
++ "role": "operator",
++ "added_by": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
++ "created_at": "2026-09-29T19:20:00.000Z"
++ }
++ ]
++}
ddiff --git a/fixtures/bot.robot-read-result/invalid/robots-not-a-list.json b/fixtures/bot.robot-read-result/invalid/robots-not-a-list.json
nnew file mode 100644
iindex 00000000..833f46b3
---- /dev/null
++++ b/fixtures/bot.robot-read-result/invalid/robots-not-a-list.json
@@@ -0,0 +1,3 @@
++{
++ "robots": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X"
++}
ddiff --git a/fixtures/bot.robot-read-result/valid/list.json b/fixtures/bot.robot-read-result/valid/list.json
nnew file mode 100644
iindex 00000000..eab8febe
---- /dev/null
++++ b/fixtures/bot.robot-read-result/valid/list.json
@@@ -0,0 +1,21 @@
++{
++ "robots": [
++ {
++ "id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "name": "Rover",
++ "profile_id": "adeept.adr036",
++ "profile_version": 1,
++ "access_policy": "private",
++ "limits": {
++ "max_command_ms": 300
++ },
++ "estop": {
++ "latched": false,
++ "by": null,
++ "at": null
++ },
++ "created_at": "2026-09-29T19:20:00.000Z",
++ "updated_at": "2026-09-29T19:20:00.000Z"
++ }
++ ]
++}
ddiff --git a/fixtures/bot.robot-read-result/valid/robot.json b/fixtures/bot.robot-read-result/valid/robot.json
nnew file mode 100644
iindex 00000000..ef7ebd2b
---- /dev/null
++++ b/fixtures/bot.robot-read-result/valid/robot.json
@@@ -0,0 +1,20 @@
++{
++ "robot": {
++ "id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "name": "Rover",
++ "profile_id": "adeept.adr036",
++ "profile_version": 1,
++ "access_policy": "private",
++ "limits": {
++ "max_command_ms": 300
++ },
++ "estop": {
++ "latched": false,
++ "by": null,
++ "at": null
++ },
++ "created_at": "2026-09-29T19:20:00.000Z",
++ "updated_at": "2026-09-29T19:20:00.000Z"
++ },
++ "role": "owner"
++}
ddiff --git a/fixtures/bot.robot/invalid/bad-policy.json b/fixtures/bot.robot/invalid/bad-policy.json
nnew file mode 100644
iindex 00000000..5ede4139
---- /dev/null
++++ b/fixtures/bot.robot/invalid/bad-policy.json
@@@ -0,0 +1,17 @@
++{
++ "id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "name": "Rover",
++ "profile_id": "adeept.adr036",
++ "profile_version": 1,
++ "access_policy": "public",
++ "limits": {
++ "max_command_ms": 300
++ },
++ "estop": {
++ "latched": false,
++ "by": null,
++ "at": null
++ },
++ "created_at": "2026-09-29T19:20:00.000Z",
++ "updated_at": "2026-09-29T19:20:00.000Z"
++}
ddiff --git a/fixtures/bot.robot/valid/rover.json b/fixtures/bot.robot/valid/rover.json
nnew file mode 100644
iindex 00000000..f027c747
---- /dev/null
++++ b/fixtures/bot.robot/valid/rover.json
@@@ -0,0 +1,17 @@
++{
++ "id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "name": "Rover",
++ "profile_id": "adeept.adr036",
++ "profile_version": 1,
++ "access_policy": "private",
++ "limits": {
++ "max_command_ms": 300
++ },
++ "estop": {
++ "latched": false,
++ "by": null,
++ "at": null
++ },
++ "created_at": "2026-09-29T19:20:00.000Z",
++ "updated_at": "2026-09-29T19:20:00.000Z"
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex a475063e..35822d17 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -61722,6 +61722,639 @@
}
}
}
++ },
++ "bot.robot": {
++ "version": "1.0.0",
++ "owner": "bot",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/robot.v1.json",
++ "title": "BotRobot",
++ "description": "bot.robot@1: a robot on OpenVibe.Bot as its API presents it (domain.present.robot): its owner-chosen name, the device profile it runs, who may drive it (access_policy) and the latched e-stop.",
++ "type": "object",
++ "required": [
++ "id",
++ "name",
++ "profile_id",
++ "access_policy",
++ "limits",
++ "estop",
++ "created_at",
++ "updated_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1
++ },
++ "profile_id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "profile_version": {},
++ "access_policy": {
++ "enum": [
++ "private",
++ "invite",
++ "queue"
++ ]
++ },
++ "limits": {
++ "type": "object"
++ },
++ "estop": {
++ "type": "object",
++ "required": [
++ "latched",
++ "by",
++ "at"
++ ],
++ "properties": {
++ "latched": {
++ "type": "boolean"
++ },
++ "by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "bot.device": {
++ "version": "1.0.0",
++ "owner": "bot",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/device.v1.json",
++ "title": "BotDevice",
++ "description": "bot.device@1: a paired device as the API presents it (domain.present.device): never a credential or publish-key hash; online is added when a robot's devices are listed.",
++ "type": "object",
++ "required": [
++ "id",
++ "robot_ids",
++ "kind",
++ "created_at",
++ "updated_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "robot_ids": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "name": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "kind": {
++ "type": "string"
++ },
++ "agent_version": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "drivers": {
++ "type": "array"
++ },
++ "capabilities": {
++ "type": "object"
++ },
++ "last_seen": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "revoked_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "online": {
++ "type": "boolean"
++ }
++ }
++ }
++ },
++ "bot.robot-read-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/robot-read-result.v1.json",
++ "title": "BotRobotReadResult",
++ "description": "bot.robot-read-result@1: answers of bot.robot.read on OpenVibe.Bot. GET /api/v1/robots?owner= → { robots }; GET …/robots/:id → { robot, role } (role \"service\" for a service token); GET …/robots/:id/operators → { operators }; GET …/robots/:id/devices → { devices } (no hashes, with online).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "robots"
++ ],
++ "properties": {
++ "robots": {
++ "type": "array",
++ "items": {
++ "$ref": "robot.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "robot",
++ "role"
++ ],
++ "properties": {
++ "robot": {
++ "$ref": "robot.v1.json"
++ },
++ "role": {
++ "enum": [
++ "owner",
++ "operator",
++ "viewer",
++ "queue",
++ "service"
++ ]
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "operators"
++ ],
++ "properties": {
++ "operators": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "subject",
++ "role"
++ ],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "role": {
++ "enum": [
++ "operator",
++ "viewer"
++ ]
++ },
++ "added_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "devices"
++ ],
++ "properties": {
++ "devices": {
++ "type": "array",
++ "items": {
++ "$ref": "device.v1.json"
++ }
++ }
++ }
++ }
++ ]
++ }
++ },
++ "bot.robot-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/robot-manage-request.v1.json",
++ "title": "BotRobotManageRequest",
++ "description": "bot.robot-manage-request@1: bodies of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → { owner (the person a service acts for), name, profile_id, access_policy?, limits? }; PATCH …/robots/:id → { name?, access_policy?, limits? }; POST …/robots/:id/operators → { subject, role? (operator by default) }. POST …/robots/:id/pairing-code and both DELETE routes take no body.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "name",
++ "profile_id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1
++ },
++ "profile_id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "access_policy": {
++ "enum": [
++ "private",
++ "invite",
++ "queue"
++ ]
++ },
++ "limits": {
++ "type": "object"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "name": {
++ "type": "string",
++ "minLength": 1
++ },
++ "access_policy": {
++ "enum": [
++ "private",
++ "invite",
++ "queue"
++ ]
++ },
++ "limits": {
++ "type": "object"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "subject"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "role": {
++ "enum": [
++ "operator",
++ "viewer"
++ ]
++ }
++ }
++ }
++ ]
++ }
++ },
++ "bot.robot-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/robot-manage-result.v1.json",
++ "title": "BotRobotManageResult",
++ "description": "bot.robot-manage-result@1: answers of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → 201 { robot, pairing }; PATCH …/robots/:id and POST …/robots/:id/estop/clear → { robot }; DELETE …/robots/:id → 204 (no body); POST …/robots/:id/pairing-code → 201 { code, expires_at, installer }; POST …/robots/:id/operators → 201 { operators }; DELETE …/robots/:id/operators/:subject → { operators }; GET …/robots/:id/audit?limit=&before= → { audit, next_before } (newest first); POST …/devices/:id/rotate and /revoke → bot.device-connect-result@1.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "robot",
++ "pairing"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "robot": {
++ "$ref": "robot.v1.json"
++ },
++ "pairing": {
++ "type": "object",
++ "required": [
++ "code",
++ "expires_at",
++ "installer"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "code": {
++ "type": "string",
++ "minLength": 1
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "installer": {
++ "type": "string"
++ }
++ },
++ "description": "A one-time pairing code (shown once), when it expires and the one-line installer command that uses it."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "robot"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "robot": {
++ "$ref": "robot.v1.json"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "code",
++ "expires_at",
++ "installer"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "code": {
++ "type": "string",
++ "minLength": 1
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "installer": {
++ "type": "string"
++ }
++ },
++ "description": "A one-time pairing code (shown once), when it expires and the one-line installer command that uses it."
++ },
++ {
++ "type": "object",
++ "required": [
++ "operators"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "operators": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "subject",
++ "role"
++ ],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "role": {
++ "enum": [
++ "operator",
++ "viewer"
++ ]
++ },
++ "added_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "$ref": "device-connect-result.v1.json"
++ },
++ {
++ "type": "object",
++ "required": [
++ "audit",
++ "next_before"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "next_before": {
++ "type": [
++ "integer",
++ "null"
++ ]
++ },
++ "audit": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "robot_id",
++ "kind",
++ "result",
++ "at"
++ ],
++ "properties": {
++ "id": {
++ "type": "integer"
++ },
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "device_id": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "operator_subject": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "operator_kind": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "role": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "kind": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "value": {},
++ "result": {
++ "enum": [
++ "ack",
++ "nack",
++ "refused",
++ "expired"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "latency_ms": {
++ "type": [
++ "integer",
++ "null"
++ ]
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ }
++ ]
++ }
++ },
++ "bot.robot-control-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/robot-control-result.v1.json",
++ "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.",
++ "type": "object",
++ "required": [
++ "robot"
++ ],
++ "properties": {
++ "robot": {
++ "$ref": "robot.v1.json"
++ }
++ }
++ }
++ },
++ "bot.device-connect-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/device-connect-result.v1.json",
++ "title": "BotDeviceConnectResult",
++ "description": "bot.device-connect-result@1: answers of OpenVibe.Bot's device credential routes, which Bot gates with bot.robot.manage (bot.device.connect is reserved, not enforced). POST /api/v1/devices/:id/rotate → { device, credential, publish_key } (both secrets shown once); POST …/devices/:id/revoke → { device } (its socket closes at once).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "device",
++ "credential",
++ "publish_key"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "device": {
++ "$ref": "device.v1.json"
++ },
++ "credential": {
++ "type": "string",
++ "minLength": 1
++ },
++ "publish_key": {
++ "type": "string",
++ "minLength": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "device"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "device": {
++ "$ref": "device.v1.json"
++ }
++ }
++ }
++ ]
++ }
}
}
}
ddiff --git a/generated/openapi/bot.json b/generated/openapi/bot.json
nnew file mode 100644
iindex 00000000..569725f5
---- /dev/null
++++ b/generated/openapi/bot.json
@@@ -0,0 +1,1581 @@
++{
++ "openapi": "3.1.0",
++ "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
++ "info": {
++ "title": "OpenVibe.Bot API",
++ "version": "0.87.0",
++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.87.0): each operation lists the capabilities it performs; a token for 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"
++ }
++ },
++ "servers": [
++ {
++ "url": "https://openvibe.bot"
++ }
++ ],
++ "paths": {
++ "/api/v1/devices/{id}/revoke": {
++ "post": {
++ "operationId": "post_api_v1_devices_id_revoke",
++ "summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, read its command audit (newest first), clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "tags": [
++ "bot.robot"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.robot.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.robot.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.robot-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.robot-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/devices/{id}/rotate": {
++ "post": {
++ "operationId": "post_api_v1_devices_id_rotate",
++ "summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, read its command audit (newest first), clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "tags": [
++ "bot.robot"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.robot.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.robot.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.robot-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.robot-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/robots": {
++ "get": {
++ "operationId": "get_api_v1_robots",
++ "summary": "Read robots with a Network service token.",
++ "description": "**bot.robot.read** (first-party): Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit is not here: Bot gates it with bot.robot.manage. Bot also requires this capability, beside bot.robot.control, to latch the e-stop.",
++ "tags": [
++ "bot.robot"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.robot.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.robot.read"
++ ]
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.robot-read-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "post": {
++ "operationId": "post_api_v1_robots",
++ "summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, read its command audit (newest first), clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "tags": [
++ "bot.robot"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.robot.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.robot.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.robot-manage-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.robot-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/robots/{id}": {
++ "delete": {
++ "operationId": "delete_api_v1_robots_id",
++ "summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, read its command audit (newest first), clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "tags": [
++ "bot.robot"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.robot.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.robot.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/bot.robot-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.robot-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "get": {
++ "operationId": "get_api_v1_robots_id",
++ "summary": "Read robots with a Network service token.",
++ "description": "**bot.robot.read** (first-party): Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit is not here: Bot gates it with bot.robot.manage. Bot also requires this capability, beside bot.robot.control, to latch the e-stop.",
++ "tags": [
++ "bot.robot"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.robot.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.robot.read"
++ ]
++ }
++ ],
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.robot-read-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_robots_id",
++ "summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, read its command audit (newest first), clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "tags": [
++ "bot.robot"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.robot.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.robot.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.robot-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.robot-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/robots/{id}/audit": {
++ "get": {
++ "operationId": "get_api_v1_robots_id_audit",
++ "summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, read its command audit (newest first), clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "tags": [
++ "bot.robot"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.robot.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.robot.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/bot.robot-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.robot-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/robots/{id}/devices": {
++ "get": {
++ "operationId": "get_api_v1_robots_id_devices",
++ "summary": "Read robots with a Network service token.",
++ "description": "**bot.robot.read** (first-party): Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit is not here: Bot gates it with bot.robot.manage. Bot also requires this capability, beside bot.robot.control, to latch the e-stop.",
++ "tags": [
++ "bot.robot"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.robot.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.robot.read"
++ ]
++ }
++ ],
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.robot-read-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/robots/{id}/estop": {
++ "post": {
++ "operationId": "post_api_v1_robots_id_estop",
++ "summary": "Physically control a robot with a Network service token.",
++ "description": "**bot.robot.control** (first-party): Physically control a robot with a Network service token. POST /robots/:id/estop latches the e-stop for any robot id and needs bot.robot.read as well; Bot records the service as the actor and does not check X-OV-Subject. On the operator WebSocket (/control) a token with this capability must name a person in X-OV-Subject, and Bot applies that person's role (owner, operator or queue), allowlist and limits. Clearing the e-stop is bot.robot.manage.",
++ "tags": [
++ "bot.robot"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.robot.control"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-control",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.robot.control"
++ ]
++ }
++ ],
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.robot-control-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/robots/{id}/estop/clear": {
++ "post": {
++ "operationId": "post_api_v1_robots_id_estop_clear",
++ "summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, read its command audit (newest first), clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "tags": [
++ "bot.robot"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.robot.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.robot.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.robot-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.robot-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/robots/{id}/operators": {
++ "get": {
++ "operationId": "get_api_v1_robots_id_operators",
++ "summary": "Read robots with a Network service token.",
++ "description": "**bot.robot.read** (first-party): Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit is not here: Bot gates it with bot.robot.manage. Bot also requires this capability, beside bot.robot.control, to latch the e-stop.",
++ "tags": [
++ "bot.robot"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.robot.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.robot.read"
++ ]
++ }
++ ],
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.robot-read-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "post": {
++ "operationId": "post_api_v1_robots_id_operators",
++ "summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, read its command audit (newest first), clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "tags": [
++ "bot.robot"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.robot.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.robot.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.robot-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.robot-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/robots/{id}/operators/{subject}": {
++ "delete": {
++ "operationId": "delete_api_v1_robots_id_operators_subject",
++ "summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, read its command audit (newest first), clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "tags": [
++ "bot.robot"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.robot.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.robot.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/bot.robot-manage-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "subject",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.robot-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/robots/{id}/pairing-code": {
++ "post": {
++ "operationId": "post_api_v1_robots_id_pairing_code",
++ "summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, read its command audit (newest first), clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "tags": [
++ "bot.robot"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.robot.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.robot.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.robot-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.robot-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "components": {
++ "schemas": {
++ "bot.device-connect-result.v1": {
++ "title": "BotDeviceConnectResult",
++ "description": "bot.device-connect-result@1: answers of OpenVibe.Bot's device credential routes, which Bot gates with bot.robot.manage (bot.device.connect is reserved, not enforced). POST /api/v1/devices/:id/rotate → { device, credential, publish_key } (both secrets shown once); POST …/devices/:id/revoke → { device } (its socket closes at once).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "device",
++ "credential",
++ "publish_key"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "device": {
++ "$ref": "#/components/schemas/bot.device.v1"
++ },
++ "credential": {
++ "type": "string",
++ "minLength": 1
++ },
++ "publish_key": {
++ "type": "string",
++ "minLength": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "device"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "device": {
++ "$ref": "#/components/schemas/bot.device.v1"
++ }
++ }
++ }
++ ]
++ },
++ "bot.device.v1": {
++ "title": "BotDevice",
++ "description": "bot.device@1: a paired device as the API presents it (domain.present.device): never a credential or publish-key hash; online is added when a robot's devices are listed.",
++ "type": "object",
++ "required": [
++ "id",
++ "robot_ids",
++ "kind",
++ "created_at",
++ "updated_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "robot_ids": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "name": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "kind": {
++ "type": "string"
++ },
++ "agent_version": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "drivers": {
++ "type": "array"
++ },
++ "capabilities": {
++ "type": "object"
++ },
++ "last_seen": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "revoked_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "online": {
++ "type": "boolean"
++ }
++ }
++ },
++ "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.",
++ "type": "object",
++ "required": [
++ "robot"
++ ],
++ "properties": {
++ "robot": {
++ "$ref": "#/components/schemas/bot.robot.v1"
++ }
++ }
++ },
++ "bot.robot-manage-request.v1": {
++ "title": "BotRobotManageRequest",
++ "description": "bot.robot-manage-request@1: bodies of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → { owner (the person a service acts for), name, profile_id, access_policy?, limits? }; PATCH …/robots/:id → { name?, access_policy?, limits? }; POST …/robots/:id/operators → { subject, role? (operator by default) }. POST …/robots/:id/pairing-code and both DELETE routes take no body.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "name",
++ "profile_id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1
++ },
++ "profile_id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "access_policy": {
++ "enum": [
++ "private",
++ "invite",
++ "queue"
++ ]
++ },
++ "limits": {
++ "type": "object"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "name": {
++ "type": "string",
++ "minLength": 1
++ },
++ "access_policy": {
++ "enum": [
++ "private",
++ "invite",
++ "queue"
++ ]
++ },
++ "limits": {
++ "type": "object"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "subject"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "role": {
++ "enum": [
++ "operator",
++ "viewer"
++ ]
++ }
++ }
++ }
++ ]
++ },
++ "bot.robot-manage-result.v1": {
++ "title": "BotRobotManageResult",
++ "description": "bot.robot-manage-result@1: answers of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → 201 { robot, pairing }; PATCH …/robots/:id and POST …/robots/:id/estop/clear → { robot }; DELETE …/robots/:id → 204 (no body); POST …/robots/:id/pairing-code → 201 { code, expires_at, installer }; POST …/robots/:id/operators → 201 { operators }; DELETE …/robots/:id/operators/:subject → { operators }; GET …/robots/:id/audit?limit=&before= → { audit, next_before } (newest first); POST …/devices/:id/rotate and /revoke → bot.device-connect-result@1.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "robot",
++ "pairing"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "robot": {
++ "$ref": "#/components/schemas/bot.robot.v1"
++ },
++ "pairing": {
++ "type": "object",
++ "required": [
++ "code",
++ "expires_at",
++ "installer"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "code": {
++ "type": "string",
++ "minLength": 1
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "installer": {
++ "type": "string"
++ }
++ },
++ "description": "A one-time pairing code (shown once), when it expires and the one-line installer command that uses it."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "robot"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "robot": {
++ "$ref": "#/components/schemas/bot.robot.v1"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "code",
++ "expires_at",
++ "installer"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "code": {
++ "type": "string",
++ "minLength": 1
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "installer": {
++ "type": "string"
++ }
++ },
++ "description": "A one-time pairing code (shown once), when it expires and the one-line installer command that uses it."
++ },
++ {
++ "type": "object",
++ "required": [
++ "operators"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "operators": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "subject",
++ "role"
++ ],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "role": {
++ "enum": [
++ "operator",
++ "viewer"
++ ]
++ },
++ "added_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "$ref": "#/components/schemas/bot.device-connect-result.v1"
++ },
++ {
++ "type": "object",
++ "required": [
++ "audit",
++ "next_before"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "next_before": {
++ "type": [
++ "integer",
++ "null"
++ ]
++ },
++ "audit": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "robot_id",
++ "kind",
++ "result",
++ "at"
++ ],
++ "properties": {
++ "id": {
++ "type": "integer"
++ },
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "device_id": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "operator_subject": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "operator_kind": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "role": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "kind": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "value": {},
++ "result": {
++ "enum": [
++ "ack",
++ "nack",
++ "refused",
++ "expired"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "latency_ms": {
++ "type": [
++ "integer",
++ "null"
++ ]
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ }
++ ]
++ },
++ "bot.robot-read-result.v1": {
++ "title": "BotRobotReadResult",
++ "description": "bot.robot-read-result@1: answers of bot.robot.read on OpenVibe.Bot. GET /api/v1/robots?owner= → { robots }; GET …/robots/:id → { robot, role } (role \"service\" for a service token); GET …/robots/:id/operators → { operators }; GET …/robots/:id/devices → { devices } (no hashes, with online).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "robots"
++ ],
++ "properties": {
++ "robots": {
++ "type": "array",
++ "items": {
++ "$ref": "#/components/schemas/bot.robot.v1"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "robot",
++ "role"
++ ],
++ "properties": {
++ "robot": {
++ "$ref": "#/components/schemas/bot.robot.v1"
++ },
++ "role": {
++ "enum": [
++ "owner",
++ "operator",
++ "viewer",
++ "queue",
++ "service"
++ ]
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "operators"
++ ],
++ "properties": {
++ "operators": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "subject",
++ "role"
++ ],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "role": {
++ "enum": [
++ "operator",
++ "viewer"
++ ]
++ },
++ "added_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "devices"
++ ],
++ "properties": {
++ "devices": {
++ "type": "array",
++ "items": {
++ "$ref": "#/components/schemas/bot.device.v1"
++ }
++ }
++ }
++ }
++ ]
++ },
++ "bot.robot.v1": {
++ "title": "BotRobot",
++ "description": "bot.robot@1: a robot on OpenVibe.Bot as its API presents it (domain.present.robot): its owner-chosen name, the device profile it runs, who may drive it (access_policy) and the latched e-stop.",
++ "type": "object",
++ "required": [
++ "id",
++ "name",
++ "profile_id",
++ "access_policy",
++ "limits",
++ "estop",
++ "created_at",
++ "updated_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1
++ },
++ "profile_id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "profile_version": {},
++ "access_policy": {
++ "enum": [
++ "private",
++ "invite",
++ "queue"
++ ]
++ },
++ "limits": {
++ "type": "object"
++ },
++ "estop": {
++ "type": "object",
++ "required": [
++ "latched",
++ "by",
++ "at"
++ ],
++ "properties": {
++ "latched": {
++ "type": "boolean"
++ },
++ "by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "errors.problem.v1": {
++ "title": "Problem",
++ "description": "RFC 9457 problem details (application/problem+json) with a stable OpenVibe error code and trace identifiers. The legacy 'error' string is allowed during migration so existing clients keep working.",
++ "type": "object",
++ "required": [
++ "type",
++ "title",
++ "status",
++ "code"
++ ],
++ "additionalProperties": true,
++ "properties": {
++ "type": {
++ "type": "string",
++ "format": "uri-reference"
++ },
++ "title": {
++ "type": "string",
++ "minLength": 1
++ },
++ "status": {
++ "type": "integer",
++ "minimum": 400,
++ "maximum": 599
++ },
++ "detail": {
++ "type": "string"
++ },
++ "instance": {
++ "type": "string",
++ "format": "uri-reference"
++ },
++ "code": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*$"
++ },
++ "request_id": {
++ "type": "string"
++ },
++ "trace_id": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{32}$"
++ },
++ "errors": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "message"
++ ],
++ "properties": {
++ "path": {
++ "type": "string"
++ },
++ "message": {
++ "type": "string"
++ }
++ }
++ }
++ },
++ "error": {
++ "type": "string",
++ "description": "Deprecated compatibility field: same text as title/detail for clients that read {error}."
++ }
++ }
++ }
++ },
++ "securitySchemes": {
++ "openvibeToken": {
++ "type": "http",
++ "scheme": "bearer",
++ "bearerFormat": "JWT",
++ "description": "An OpenVibe Network token for this service: a person's token, an app token (client credentials, audience of the service) or a first-party service token. Its `cap` claim must include the capability the operation names."
++ }
++ }
++ }
++}
ddiff --git a/generated/openapi/index.json b/generated/openapi/index.json
iindex d51dba8e..8ba4cf19 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -22,6 +22,13 @@
"operations": 22,
"capabilities": 12
},
++ {
++ "service": "bot",
++ "name": "OpenVibe.Bot",
++ "origin": "https://openvibe.bot",
++ "operations": 15,
++ "capabilities": 3
++ },
{
"service": "chat",
"name": "OpenVibe.Chat",
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 19dedcd6..4c326115 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -51931,3 +51931,160 @@ export interface ObjectZoneUsage {
quantity: number;
}[];
}
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.robot@1: a robot on OpenVibe.Bot as its API presents it (domain.present.robot): its owner-chosen name, the device profile it runs, who may drive it (access_policy) and the latched e-stop.
++ */
++export interface BotRobot {
++ id: string;
++ name: string;
++ profile_id: string;
++ profile_version?: unknown;
++ access_policy: "private" | "invite" | "queue";
++ limits: {};
++ estop: {
++ latched: boolean;
++ by: string | null;
++ at: string | null;
++ };
++ created_at: string;
++ updated_at: string;
++}
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.device@1: a paired device as the API presents it (domain.present.device): never a credential or publish-key hash; online is added when a robot's devices are listed.
++ */
++export interface BotDevice {
++ id: string;
++ robot_ids: string[];
++ name?: string | null;
++ kind: string;
++ agent_version?: string | null;
++ drivers?: unknown[];
++ capabilities?: {};
++ last_seen?: string | null;
++ revoked_at?: string | null;
++ created_at: string;
++ updated_at: string;
++ online?: boolean;
++}
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.robot-read-result@1: answers of bot.robot.read on OpenVibe.Bot. GET /api/v1/robots?owner= → { robots }; GET …/robots/:id → { robot, role } (role "service" for a service token); GET …/robots/:id/operators → { operators }; GET …/robots/:id/devices → { devices } (no hashes, with online).
++ */
++export type BotRobotReadResult =
++ | {
++ robots: BotRobot[];
++ }
++ | {
++ robot: BotRobot;
++ role: "owner" | "operator" | "viewer" | "queue" | "service";
++ }
++ | {
++ operators: {
++ subject: string;
++ role: "operator" | "viewer";
++ added_by?: string | null;
++ created_at?: string;
++ }[];
++ }
++ | {
++ devices: BotDevice[];
++ };
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.robot-manage-request@1: bodies of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → { owner (the person a service acts for), name, profile_id, access_policy?, limits? }; PATCH …/robots/:id → { name?, access_policy?, limits? }; POST …/robots/:id/operators → { subject, role? (operator by default) }. POST …/robots/:id/pairing-code and both DELETE routes take no body.
++ */
++export type BotRobotManageRequest =
++ | {
++ owner?: string;
++ name: string;
++ profile_id: string;
++ access_policy?: "private" | "invite" | "queue";
++ limits?: {};
++ }
++ | {
++ name?: string;
++ access_policy?: "private" | "invite" | "queue";
++ limits?: {};
++ }
++ | {
++ subject: string;
++ role?: "operator" | "viewer";
++ };
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.robot-manage-result@1: answers of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → 201 { robot, pairing }; PATCH …/robots/:id and POST …/robots/:id/estop/clear → { robot }; DELETE …/robots/:id → 204 (no body); POST …/robots/:id/pairing-code → 201 { code, expires_at, installer }; POST …/robots/:id/operators → 201 { operators }; DELETE …/robots/:id/operators/:subject → { operators }; GET …/robots/:id/audit?limit=&before= → { audit, next_before } (newest first); POST …/devices/:id/rotate and /revoke → bot.device-connect-result@1.
++ */
++export type BotRobotManageResult =
++ | {
++ robot: BotRobot;
++ /**
++ * A one-time pairing code (shown once), when it expires and the one-line installer command that uses it.
++ */
++ pairing: {
++ code: string;
++ expires_at: string;
++ installer: string;
++ };
++ }
++ | {
++ robot: BotRobot;
++ }
++ | {
++ code: string;
++ expires_at: string;
++ installer: string;
++ }
++ | {
++ operators: {
++ subject: string;
++ role: "operator" | "viewer";
++ added_by?: string | null;
++ created_at?: string;
++ }[];
++ }
++ | BotDeviceConnectResult
++ | {
++ next_before: number | null;
++ audit: {
++ id: number;
++ robot_id: string;
++ device_id?: string | null;
++ operator_subject?: string | null;
++ operator_kind?: string | null;
++ role?: string | null;
++ kind: string | null;
++ value?: unknown;
++ result: "ack" | "nack" | "refused" | "expired";
++ reason?: string | null;
++ latency_ms?: number | null;
++ at: string;
++ }[];
++ };
++/** [email protected] (owner: bot) */
++/**
++ * 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.
++ */
++export interface BotRobotControlResult {
++ robot: BotRobot;
++}
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.device-connect-result@1: answers of OpenVibe.Bot's device credential routes, which Bot gates with bot.robot.manage (bot.device.connect is reserved, not enforced). POST /api/v1/devices/:id/rotate → { device, credential, publish_key } (both secrets shown once); POST …/devices/:id/revoke → { device } (its socket closes at once).
++ */
++export type BotDeviceConnectResult =
++ | {
++ device: BotDevice;
++ credential: string;
++ publish_key: string;
++ }
++ | {
++ device: BotDevice;
++ };
ddiff --git a/manifests/capabilities/bot.device.connect.json b/manifests/capabilities/bot.device.connect.json
nnew file mode 100644
iindex 00000000..bebc1ef8
---- /dev/null
++++ b/manifests/capabilities/bot.device.connect.json
@@@ -0,0 +1,17 @@
++{
++ "id": "bot.device.connect",
++ "version": "1.0",
++ "owner": "bot",
++ "status": "planned",
++ "visibility": "first-party",
++ "sensitive": true,
++ "description": "Reserved for managing a paired device's credentials (rotate, revoke) apart from robot management. Bot declares this name but checks it nowhere: today POST /api/v1/devices/:id/rotate and /revoke are gated by bot.robot.manage. It becomes active when Bot's routes enforce it.",
++ "permissions": [
++ "bot:device:connect"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "bot-device",
++ "events": []
++}
ddiff --git a/manifests/capabilities/bot.robot.control.json b/manifests/capabilities/bot.robot.control.json
nnew file mode 100644
iindex 00000000..27eb228e
---- /dev/null
++++ b/manifests/capabilities/bot.robot.control.json
@@@ -0,0 +1,24 @@
++{
++ "id": "bot.robot.control",
++ "version": "1.0",
++ "owner": "bot",
++ "status": "active",
++ "visibility": "first-party",
++ "sensitive": true,
++ "description": "Physically control a robot with a Network service token. POST /robots/:id/estop latches the e-stop for any robot id and needs bot.robot.read as well; Bot records the service as the actor and does not check X-OV-Subject. On the operator WebSocket (/control) a token with this capability must name a person in X-OV-Subject, and Bot applies that person's role (owner, operator or queue), allowlist and limits. Clearing the e-stop is bot.robot.manage.",
++ "permissions": [
++ "bot:robot:control"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "bot-control",
++ "events": [
++ "bot.estop.set"
++ ],
++ "implementedBy": [
++ "POST /api/v1/robots/:id/estop"
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "bot.robot-control-result@1"
++}
ddiff --git a/manifests/capabilities/bot.robot.manage.json b/manifests/capabilities/bot.robot.manage.json
nnew file mode 100644
iindex 00000000..af017c5a
---- /dev/null
++++ b/manifests/capabilities/bot.robot.manage.json
@@@ -0,0 +1,33 @@
++{
++ "id": "bot.robot.manage",
++ "version": "1.0",
++ "owner": "bot",
++ "status": "active",
++ "visibility": "first-party",
++ "sensitive": true,
++ "description": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, read its command audit (newest first), clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "permissions": [
++ "bot:robot:manage"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "bot-manage",
++ "events": [
++ "bot.estop.cleared"
++ ],
++ "implementedBy": [
++ "POST /api/v1/robots",
++ "PATCH /api/v1/robots/:id",
++ "DELETE /api/v1/robots/:id",
++ "POST /api/v1/robots/:id/pairing-code",
++ "POST /api/v1/robots/:id/operators",
++ "DELETE /api/v1/robots/:id/operators/:subject",
++ "GET /api/v1/robots/:id/audit",
++ "POST /api/v1/robots/:id/estop/clear",
++ "POST /api/v1/devices/:id/rotate",
++ "POST /api/v1/devices/:id/revoke"
++ ],
++ "inputSchema": "bot.robot-manage-request@1",
++ "outputSchema": "bot.robot-manage-result@1"
++}
ddiff --git a/manifests/capabilities/bot.robot.read.json b/manifests/capabilities/bot.robot.read.json
nnew file mode 100644
iindex 00000000..1560006e
---- /dev/null
++++ b/manifests/capabilities/bot.robot.read.json
@@@ -0,0 +1,24 @@
++{
++ "id": "bot.robot.read",
++ "version": "1.0",
++ "owner": "bot",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit is not here: Bot gates it with bot.robot.manage. Bot also requires this capability, beside bot.robot.control, to latch the e-stop.",
++ "permissions": [
++ "bot:robot:read"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "bot-read",
++ "events": [],
++ "implementedBy": [
++ "GET /api/v1/robots",
++ "GET /api/v1/robots/:id",
++ "GET /api/v1/robots/:id/operators",
++ "GET /api/v1/robots/:id/devices"
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "bot.robot-read-result@1"
++}
ddiff --git a/manifests/products/openvibe.bot.json b/manifests/products/openvibe.bot.json
iindex 4217e45e..7330a1b1 100644
---- a/manifests/products/openvibe.bot.json
++++ b/manifests/products/openvibe.bot.json
@@@ -203,7 +203,8 @@
},
"launch": "the Bot service, its device agent and the first robots paired",
"relationships": {
-- "noRepo": true,
++ "service": "bot",
++ "plannedRepo": "OpenVibe.Bot",
"meanwhile": [
{
"label": "Robots on OpenVibe.Live",
ddiff --git a/manifests/services/bot.json b/manifests/services/bot.json
nnew file mode 100644
iindex 00000000..c44a9ff7
---- /dev/null
++++ b/manifests/services/bot.json
@@@ -0,0 +1,86 @@
++{
++ "version": "0.1.0",
++ "contractRanges": {
++ "openvibe-contracts": ">=0.85.0 <1.0.0"
++ },
++ "id": "bot",
++ "name": "OpenVibe.Bot",
++ "status": "alpha",
++ "repository": "OpenVibers/OpenVibe.Bot",
++ "domains": [
++ "openvibe.bot"
++ ],
++ "publicOrigin": "https://openvibe.bot",
++ "health": "/api/health",
++ "ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4630",
++ "exposure": {
++ "state": "live",
++ "publicSite": "service"
++ },
++ "capabilities": [
++ "bot.robot.read",
++ "bot.robot.manage",
++ "bot.robot.control",
++ "bot.device.connect"
++ ],
++ "eventsProduced": [
++ "bot.robot.online",
++ "bot.robot.offline",
++ "bot.estop.set",
++ "bot.estop.cleared",
++ "bot.command.refused"
++ ],
++ "eventsConsumed": [],
++ "namespacesOwned": [],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process answers HTTP and reports online devices and outbox status; database, Valkey and Network readiness are checked by /api/ready"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 5,
++ "drains": [
++ "job timers cleared and the Network key refresh stopped",
++ "device and operator WebSockets closed (1001) and the outbox relay stopped",
++ "HTTP server closed: requests in flight get Connection: close and up to 4 s to finish, idle connections close at once, whatever is open after 4 s is cut",
++ "PostgreSQL pool and Valkey client closed, then exit 0"
++ ],
++ "note": "openvibe-sdk/service gracefulStop (drainMs 4000, deadlineMs 5000, deadlineExitCode 0) runs these steps on SIGTERM or SIGINT and exits 0 after 5 s whatever is still open; the unit sends SIGTERM and allows 10 s (TimeoutStopSec)"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "schedule",
++ "what": "the one-second turn-queue sweep expires overdue turns and promotes waiting operators; pairing-code and audit-prune timers restart"
++ },
++ {
++ "kind": "sessions",
++ "what": "device and operator WebSockets reconnect; commands are never queued or replayed after a disconnect"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent bot.* events in bot_event_outbox are published when the relay restarts (at least once)"
++ }
++ ]
++ },
++ "rollback": {
++ "conditions": [
++ "automatic: ovhost deploy restores the previous sha when /api/ready does not return 2xx within the configured readiness timeout",
++ "manual: ovhost rollback bot --to <sha>"
++ ],
++ "window": "automatic while ovhost waits for readiness after restart; manual afterwards while the earlier release remains compatible with the database",
++ "blockers": {
++ "none": "the current migration creates Bot's initial PostgreSQL schema and does not drop or rename existing data"
++ }
++ },
++ "contracts": {
++ "range": ">=0.85.0 <1.0.0"
++ },
++ "leases": {
++ "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."
++}
ddiff --git a/scripts/generate.js b/scripts/generate.js
iindex 84ea9e20..b139d3b0 100644
---- a/scripts/generate.js
++++ b/scripts/generate.js
@@@ -43,20 +43,49 @@ function inlineDefs(schema, file) {
return walk(schema, file, new Set());
}
++// json-schema-to-typescript declares every schema it resolves, including one another catalog entry
++// $refs; that entry then emits the same alias again. Keep each top-level declaration once: drop a
++// referenced declaration whose name another entry owns (that entry declares it), and any name a
++// previous compile block already emitted. A compile block is a run of top-level declarations, each
++// introduced by its JSDoc comment.
++function onlyOnce(ts, title, owned, emitted) {
++ const kept = [];
++ let declaration = null;
++ let pending = [];
++ const flush = () => {
++ if (!declaration) return;
++ const drop = (declaration.name !== title && owned.has(declaration.name)) || emitted.has(declaration.name);
++ if (!drop) { kept.push(...declaration.lines); emitted.add(declaration.name); }
++ declaration = null;
++ };
++ for (const line of ts.split('\n')) {
++ const head = line.match(/^export (?:interface|type|enum|const|declare) (\w+)/);
++ if (head) { flush(); declaration = { name: head[1], lines: [...pending, line] }; pending = []; }
++ else if (declaration && /^\/\*\*/.test(line)) { flush(); pending = [line]; }
++ else if (declaration) declaration.lines.push(line);
++ else pending.push(line);
++ }
++ flush();
++ return [...kept, ...pending].join('\n');
++}
++
(async () => {
const stale = [];
const bundle = { _about: 'Generated by scripts/generate.js. Do not edit.', contracts: {} };
let dts = '/* Generated by scripts/generate.js from contracts/. Do not edit. */\n/* eslint-disable */\n';
++ const schemas = new Map(catalog.map((entry) => [entry.id, JSON.parse(fs.readFileSync(path.join(ROOT, 'contracts', entry.schema), 'utf8'))]));
++ const owned = new Set([...schemas.values()].map((s) => s.title));
++ const emitted = new Set();
for (const entry of catalog) {
const file = path.join(ROOT, 'contracts', entry.schema);
-- const schema = JSON.parse(fs.readFileSync(file, 'utf8'));
++ const schema = schemas.get(entry.id);
bundle.contracts[entry.id] = { version: entry.version, owner: entry.owner, status: entry.status, schema };
const ts = await compile(inlineDefs(schema, file), schema.title, {
bannerComment: '', cwd: path.dirname(file), declareExternallyReferenced: false,
additionalProperties: false, strictIndexSignatures: true, format: true,
$refOptions: { resolve: { http: false } },
});
-- dts += `\n/** ${entry.id}@${entry.version} (owner: ${entry.owner}) */\n${ts}`;
++ dts += `\n/** ${entry.id}@${entry.version} (owner: ${entry.owner}) */\n${onlyOnce(ts, schema.title, owned, emitted)}`;
}
const out = {
'generated/json-schema/bundle.json': JSON.stringify(bundle, null, 2) + '\n',
ddiff --git a/test/presence-policy.test.js b/test/presence-policy.test.js
iindex cd6b3d91..f923f35e 100644
---- a/test/presence-policy.test.js
++++ b/test/presence-policy.test.js
@@@ -1,7 +1,10 @@
'use strict';
--// ADR-005 amendment 1: presence is ephemeral, in Chat's delivery plane; no Events topic carries it.
++// 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.
const assert = require('assert');
const c = require('..');
const produced = c.services.manifests.flatMap((m) => m.eventsProduced || []);
--assert.ok(!produced.some((t) => /presence|\.online|\.offline/i.test(t)), `no presence event type: ${produced.filter((t) => /presence|online/i.test(t))}`);
++const robotConnectivity = new Set(['bot.robot.online', 'bot.robot.offline']);
++const presenceEvents = produced.filter((t) => /presence|\.online|\.offline/i.test(t) && !robotConnectivity.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 4571747a..001e4eeb 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -885,6 +885,9 @@ sub(path.join(ROOT, 'scripts/compat.js'));
// json-schema-to-typescript emit a type name it never declares; every name used as a type must be declared.
const dts = fs.readFileSync(path.join(ROOT, 'generated/typescript/index.d.ts'), 'utf8');
const declared = new Set([...dts.matchAll(/^export (?:interface|type) (\w+)/gm)].map(m => m[1]));
++ const names = [...dts.matchAll(/^export (?:interface|type) (\w+)/gm)].map(m => m[1]);
++ const twice = [...new Set(names.filter((n, i) => names.indexOf(n) !== i))];
++ ok(twice.length === 0, `generated types declare a name more than once: ${twice.join(', ')}`);
const code = dts.replace(/\/\*[\s\S]*?\*\//g, '').replace(/"(?:[^"\\]|\\.)*"/g, '""');
const used = [...code.matchAll(/(?:[:|&=<(]|\bextends)\s*([A-Z]\w*)\b(?!\s*\??:)/g)].map(m => m[1]);
const dangling = [...new Set(used)].filter(n => !declared.has(n));