openvibers/openvibe.contracts Commits

Add Bot profile, device protocol, command, pairing and event contracts

OP OpenVibers committed 4 Oct 2026 · fd0aee64d2 · 98 files, +5418 −62

Asked: T15 step 2 (R7) — OpenVibe.Contracts: register OpenVibe.Bot (service manifest, its four capabilities with real input/output schemas, its contracts and its five event payloads). ONE pull request from current origin/main (git fetch first; main carries v0.92.0, tagged at 93e9bb2; the local checkout ma…
Harness job contracts-t15-step-2-r7-144821 (plan T15).

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex e02360e6..75318cd4 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`).
++## 0.93.0 — 2026-10-04
++
++**OpenVibe.Bot's profiles, device protocol, operator commands, pairing and events** (plan T15 R7; ADR-043). The Bot
++manifest and its capabilities came in 0.85.0; this adds the contracts Bot's code already speaks, taken from Bot
++`155accc` and OpenVibe.Node `1ebf1a7`. Additive: every contract id is new.
++- `bot.robot-profile@1` (public): a profile as `server/profiles/*.json` ships it and `GET /api/v1/profiles` serves it
++ (null vendor, kind, description, variants and camera; defaulted limits; `halt` always in `commands`). The per-kind
++ `commands` schema is Bot's (`drive`/`ptz` axes, `actuator` number, rgb, tone and bool, `say`, `display`); capability
++ and widget names stay open strings.
++- `bot.device-message@1`: one frame on the `/device` WebSocket, `v`, `seq`, `ts` on every frame. Server to device:
++ `hello`, `config`, `command`, `estop`, `heartbeat_ack`, `error`, `paired`, `rotate`; device to server: `pair`,
++ `status`, `telemetry`, `ack`, `nack`, `heartbeat`, `reauth`, `estop_state`. Job frames stay `platform.job-frame@1`.
++ The description names both implementations (Bot `server/realtime.js`, Node `internal/protocol/protocol.go`).
++- `bot.command@1` and `bot.command-result@1`: the `/control` command frame and its `command_result` answer (`ack`,
++ `nack`, `refused` with Bot's refusal code, `expired`, `pending`; `cached` for a repeated id).
++- `bot.robot-read-request@1`: the query of `bot.robot.read` (`owner`, and `limit`/`before` for the audit page); it is
++ now that capability's input, so its OpenAPI routes list the query parameters.
++- `bot.pair-request@1` and `bot.pair-result@1`: `POST /api/v1/pair` and `POST /api/v1/devices/bind`. The credential,
++ the publish key and the WHIP URL are shown once; without OpenRe the answer says `video: not_configured`.
++- Event payloads for the five events Bot produces: `bot.robot.online` and `bot.robot.offline` (`robot_id`,
++ `device_id`), `bot.estop.set` and `bot.estop.cleared` (`robot_id`, `by`, `principal_kind`), `bot.command.refused`
++ (`robot_id`, `kind`, `reason`, `role`; never the value).
++- `bot.robot.control` lists its `/control` WebSocket bindings (`command`, `estop`, `estop_clear`) as notes, and the
++ events they emit (`bot.estop.cleared`, `bot.command.refused`); the Bot manifest's notes name both WebSockets' contracts.
++- New `test/bot-manifest.test.js`: the manifest's capabilities and events equal Bot's `STATUS.json` lists.
++
## 0.92.0 — 2026-10-04
`platform.job@1` gains `inputs` (Media objects pinned by sha256) and the `net` values `none`, `public`, `openvibe-only`.
ddiff --git a/contracts/bot/command-result.v1.json b/contracts/bot/command-result.v1.json
nnew file mode 100644
iindex 00000000..93b79953
---- /dev/null
++++ b/contracts/bot/command-result.v1.json
@@@ -0,0 +1,63 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/command-result.v1.json",
++ "title": "BotCommandResult",
++ "description": "bot.command-result@1: the command_result frame OpenVibe.Bot answers a bot.command@1 with on the operator WebSocket (server/realtime.js). result: ack or nack (the device answered; reason is its fault code, latency_ms the round trip), refused (the gate refused it; code is Bot's refusal code such as bot.estop_latched, bot.cooldown or bot.device_offline, and reason says why), expired (no answer before the deadline) or pending (the same id is still in flight). cached is true when a repeated id is answered from the first result.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "id",
++ "result"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "command_result"
++ },
++ "id": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 64
++ },
++ "result": {
++ "enum": [
++ "ack",
++ "nack",
++ "refused",
++ "expired",
++ "pending"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "code": {
++ "type": "string",
++ "pattern": "^bot\\.[a-z_]+$"
++ },
++ "latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "cached": {
++ "type": "boolean"
++ }
++ }
++}
ddiff --git a/contracts/bot/command.v1.json b/contracts/bot/command.v1.json
nnew file mode 100644
iindex 00000000..469e75ec
---- /dev/null
++++ b/contracts/bot/command.v1.json
@@@ -0,0 +1,50 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/command.v1.json",
++ "title": "BotCommand",
++ "description": "bot.command@1: an operator's command frame on OpenVibe.Bot's operator WebSocket (wss://openvibe.bot/control, after join; server/realtime.js onOperatorCommand). id is the operator's idempotency key (at most 64 characters; Bot mints one when it is missing): a repeated id answers with the first result. Bot's gate stamps the robot, the operator and the role, builds the value from the robot profile's commands (clamped by the owner's limits) and sets the deadline; ms asks for a duration within max_command_ms. Answered by one command_result frame (bot.command-result@1).",
++ "type": "object",
++ "required": [
++ "type",
++ "kind"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "command"
++ },
++ "id": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 64
++ },
++ "kind": {
++ "enum": [
++ "drive",
++ "actuator",
++ "ptz",
++ "say",
++ "display",
++ "halt"
++ ]
++ },
++ "value": {
++ "type": "object"
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++}
ddiff --git a/contracts/bot/device-message.v1.json b/contracts/bot/device-message.v1.json
nnew file mode 100644
iindex 00000000..8956a9a7
---- /dev/null
++++ b/contracts/bot/device-message.v1.json
@@@ -0,0 +1,889 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/device-message.v1.json",
++ "title": "BotDeviceMessage",
++ "description": "bot.device-message@1 (ADR-043 decision 5): one frame on OpenVibe.Bot's device WebSocket (wss://openvibe.bot/device), JSON with v, seq and ts on every frame. Server to device: hello, config, command, estop, heartbeat_ack, error, paired (after a pair frame), rotate (a Network-paired machine is asked to rotate its credential with Network). Device to server: pair (the only frame an unauthenticated socket may send), status, telemetry, ack, nack, heartbeat, reauth (Network-paired machines), estop_state. The job frames on the same socket are platform.job-frame@1, not this contract. This duplicates two implementations that must not drift: OpenVibe.Bot server/realtime.js (the sender and reader in Bot) and OpenVibe.Node internal/protocol/protocol.go (the Go structs of the device agent).",
++ "oneOf": [
++ {
++ "description": "Server to device, once authenticated.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "session_id",
++ "device_id",
++ "robot_ids",
++ "server_time"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "hello"
++ },
++ "session_id": {
++ "type": "string"
++ },
++ "device_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}$"
++ }
++ },
++ "server_time": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ {
++ "description": "Server to device: the effective limits (the owner's clamped by the profile's), the kinds the profile takes and the latch; sent again when any of them changes.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "heartbeat_ms",
++ "limits",
++ "allowed_commands",
++ "estop_latched"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "config"
++ },
++ "heartbeat_ms": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "limits": {
++ "type": "object",
++ "required": [],
++ "additionalProperties": false,
++ "properties": {
++ "max_speed": {
++ "type": "number"
++ },
++ "max_turn": {
++ "type": "number"
++ },
++ "max_command_ms": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "heartbeat_ms": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ },
++ "allowed_commands": {
++ "type": "array",
++ "items": {
++ "enum": [
++ "drive",
++ "actuator",
++ "ptz",
++ "say",
++ "display",
++ "halt"
++ ]
++ }
++ },
++ "estop_latched": {
++ "type": "boolean"
++ }
++ }
++ },
++ {
++ "description": "Server to device: id is minted by Bot (the device's idempotency key), ref is the operator's own id. Never queued for an offline device, never replayed.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "id",
++ "kind"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "command"
++ },
++ "id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "ref": {
++ "type": "string"
++ },
++ "kind": {
++ "enum": [
++ "drive",
++ "actuator",
++ "ptz",
++ "say",
++ "display",
++ "halt"
++ ]
++ },
++ "value": {
++ "type": "object"
++ },
++ "deadline_ms": {
++ "type": "integer",
++ "description": "Absolute deadline, epoch milliseconds; the device stops the motion at it."
++ },
++ "operator": {
++ "type": "object",
++ "required": [
++ "role"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "subject": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "role": {
++ "type": "string"
++ }
++ }
++ },
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "target": {
++ "type": "string",
++ "description": "OpenVibe.Node only: the driver to send it to."
++ }
++ }
++ },
++ {
++ "description": "Server to device: the latch moved (true sets it, false is the owner clearing it).",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "latched"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "estop"
++ },
++ "latched": {
++ "type": "boolean"
++ },
++ "by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ {
++ "description": "Server to device: echo and t carry the device's t back.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "heartbeat_ack"
++ },
++ "t": {
++ "type": "integer"
++ },
++ "echo": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "server_time": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ {
++ "description": "Server to device: a refused frame (bot.not_paired, bot.bad_frame, …).",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "code"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "error"
++ },
++ "code": {
++ "type": "string"
++ },
++ "detail": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ },
++ {
++ "description": "Server to device: the answer to pair; the same fields as POST /api/v1/pair (bot.pair-result@1).",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "device_id",
++ "credential",
++ "robot_ids"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "paired"
++ },
++ "device_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "credential": {
++ "type": "string",
++ "minLength": 1,
++ "description": "Shown once."
++ },
++ "publish_key": {
++ "type": "string",
++ "minLength": 1,
++ "description": "Shown once."
++ },
++ "whip_url": {
++ "type": "string",
++ "format": "uri",
++ "description": "Carries the publish key: shown once."
++ },
++ "video": {
++ "const": "not_configured"
++ },
++ "robot_ids": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "profile_id": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "profile": {
++ "anyOf": [
++ {
++ "$ref": "robot-profile.v1.json"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ }
++ }
++ },
++ {
++ "description": "Server to device: a Network-paired machine rotates its credential with Network.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "rotate"
++ }
++ }
++ },
++ {
++ "description": "Device to server: redeem a one-time code (bot.pair-request@1).",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "code"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "pair"
++ },
++ "code": {
++ "type": "string",
++ "minLength": 1
++ },
++ "robot": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "agent_version": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "device_kind": {
++ "enum": [
++ "onboard",
++ "bridge",
++ "server"
++ ]
++ },
++ "drivers": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "capabilities": {
++ "type": "object"
++ },
++ "name": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ },
++ {
++ "description": "Device to server: what the agent runs; OpenVibe.Node sends drivers as objects, older agents as names.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "status"
++ },
++ "firmware": {
++ "type": "string"
++ },
++ "capabilities": {
++ "type": "object"
++ },
++ "agent_version": {
++ "type": "string",
++ "maxLength": 40
++ },
++ "device_kind": {
++ "enum": [
++ "onboard",
++ "bridge",
++ "server"
++ ]
++ },
++ "os": {
++ "type": "string"
++ },
++ "arch": {
++ "type": "string"
++ },
++ "drivers": {
++ "type": "array",
++ "items": {
++ "anyOf": [
++ {
++ "type": "string"
++ },
++ {
++ "type": "object",
++ "required": [
++ "name",
++ "state"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "name": {
++ "type": "string"
++ },
++ "driver": {
++ "type": "string"
++ },
++ "version": {
++ "type": "string"
++ },
++ "state": {
++ "type": "string"
++ },
++ "capabilities": {
++ "type": "object"
++ }
++ }
++ }
++ ]
++ }
++ },
++ "faults": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "code"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "code": {
++ "type": "string"
++ },
++ "driver": {
++ "type": "string"
++ },
++ "message": {
++ "type": "string"
++ }
++ }
++ }
++ },
++ "estop_latched": {
++ "type": "boolean"
++ },
++ "local_stop": {
++ "type": "boolean"
++ },
++ "video": {
++ "type": "string"
++ }
++ }
++ },
++ {
++ "description": "Device to server: samples at most 2 Hz; a frame with events is never dropped.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "telemetry"
++ },
++ "battery": {
++ "anyOf": [
++ {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ },
++ {
++ "type": "object",
++ "required": [],
++ "additionalProperties": false,
++ "properties": {
++ "volts": {
++ "type": "number"
++ },
++ "percent": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 100
++ }
++ }
++ }
++ ]
++ },
++ "voltage": {
++ "type": "number"
++ },
++ "rssi": {
++ "type": "integer"
++ },
++ "sensors": {
++ "type": "object"
++ },
++ "events": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "name",
++ "ts"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "name": {
++ "type": "string"
++ },
++ "driver": {
++ "type": "string"
++ },
++ "ts": {
++ "type": "integer"
++ },
++ "fields": {
++ "type": "object"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "description": "Device to server: the command ran.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "ack"
++ },
++ "id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ },
++ {
++ "description": "Device to server: the command was refused, with OpenVibe.Node's fault code (bad_value, estopped, local_stop, expired, …).",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "id",
++ "fault_code"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "nack"
++ },
++ "id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "fault_code": {
++ "type": "string",
++ "minLength": 1
++ },
++ "message": {
++ "type": "string"
++ }
++ }
++ },
++ {
++ "description": "Device to server, every heartbeat_ms.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "heartbeat"
++ },
++ "t": {
++ "type": "integer"
++ },
++ "rtt_ms": {
++ "type": "number",
++ "minimum": 0
++ }
++ }
++ },
++ {
++ "description": "Device to server: a Network-paired machine's fresh node token, before the last one expires.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "token"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "reauth"
++ },
++ "token": {
++ "type": "string",
++ "minLength": 1
++ }
++ }
++ },
++ {
++ "description": "Device to server: the device's own latch; a device may set the e-stop, never clear the owner's.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "latched"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "estop_state"
++ },
++ "latched": {
++ "type": "boolean"
++ },
++ "by": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "local_stop": {
++ "type": "boolean"
++ },
++ "reason": {
++ "type": "string"
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/bot/pair-request.v1.json b/contracts/bot/pair-request.v1.json
nnew file mode 100644
iindex 00000000..c8e77b3a
---- /dev/null
++++ b/contracts/bot/pair-request.v1.json
@@@ -0,0 +1,58 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/pair-request.v1.json",
++ "title": "BotPairRequest",
++ "description": "bot.pair-request@1: the body of POST /api/v1/pair on OpenVibe.Bot (and the fields of the pair frame on /device): a one-time pairing code (8 Crockford base32 characters, XXXX-XXXX) is its own credential and idempotency. robot narrows the code to one robot (wrong tries are then counted and lock the code); the rest is what the agent declares. With BOT_PAIRING_AUTHORITY=network the codes are Network's and this route answers 410 bot.pairing_moved.",
++ "type": "object",
++ "required": [
++ "code"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "code": {
++ "type": "string",
++ "pattern": "^[0-9A-Za-z]{4}-?[0-9A-Za-z]{4}$"
++ },
++ "robot": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "agent_version": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 40
++ },
++ "device_kind": {
++ "enum": [
++ "onboard",
++ "bridge",
++ "server"
++ ]
++ },
++ "drivers": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "capabilities": {
++ "type": "object"
++ },
++ "name": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80
++ }
++ }
++}
ddiff --git a/contracts/bot/pair-result.v1.json b/contracts/bot/pair-result.v1.json
nnew file mode 100644
iindex 00000000..6d087ed1
---- /dev/null
++++ b/contracts/bot/pair-result.v1.json
@@@ -0,0 +1,91 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/pair-result.v1.json",
++ "title": "BotPairResult",
++ "description": "bot.pair-result@1: the 201 answers of OpenVibe.Bot's pairing routes (server/api/v1.js). POST /api/v1/pair → { device_id, credential, publish_key, whip_url?, robot_id, profile }; POST /api/v1/devices/bind (a Network node token) → the same without credential. The credential, the publish key and the WHIP URL are shown once and never again. Without OpenRe configured there is no key: video is not_configured instead.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "device_id",
++ "publish_key",
++ "robot_id",
++ "profile"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "device_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "profile": {
++ "anyOf": [
++ {
++ "$ref": "robot-profile.v1.json"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "credential": {
++ "type": "string",
++ "minLength": 1,
++ "description": "The device credential: shown once (Bot keeps only its hash)."
++ },
++ "publish_key": {
++ "type": "string",
++ "minLength": 1,
++ "description": "The WHIP publish key, an ingest key OpenRe issued for the robot's stream: shown once."
++ },
++ "whip_url": {
++ "type": "string",
++ "format": "uri",
++ "description": "Where the device publishes its camera (BOT_WHIP_BASE/<publish key>); it carries the key, so it is shown once too. Absent when BOT_WHIP_BASE is unset."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "device_id",
++ "video",
++ "robot_id",
++ "profile"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "device_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "profile": {
++ "anyOf": [
++ {
++ "$ref": "robot-profile.v1.json"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "credential": {
++ "type": "string",
++ "minLength": 1,
++ "description": "The device credential: shown once (Bot keeps only its hash)."
++ },
++ "video": {
++ "const": "not_configured"
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/bot/robot-profile.v1.json b/contracts/bot/robot-profile.v1.json
nnew file mode 100644
iindex 00000000..6a1bfecb
---- /dev/null
++++ b/contracts/bot/robot-profile.v1.json
@@@ -0,0 +1,424 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/robot-profile.v1.json",
++ "title": "BotRobotProfile",
++ "description": "bot.robot-profile@1 (ADR-043 decision 7): a robot profile as OpenVibe.Bot ships it (server/profiles/*.json) and serves it (GET /api/v1/profiles, /profiles/:id; validateProfile in server/profiles/index.js). The panel is a function of the profile. Capability and widget names are open strings that Bot checks against registries in code, so a new one never needs a schema change. As served, vendor, kind, description, variants and camera are null when the file leaves them out, limits carry their defaults (max_speed 1, max_turn 1, max_command_ms 300, heartbeat_ms 1000) and commands always include halt. mapping is the driver's own hardware map, open beyond driver.",
++ "type": "object",
++ "required": [
++ "id",
++ "version",
++ "name",
++ "capabilities",
++ "mapping",
++ "widgets"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9._-]{1,63}$"
++ },
++ "version": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1
++ },
++ "vendor": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "kind": {
++ "enum": [
++ "onboard",
++ "bridge",
++ "server",
++ null
++ ]
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "capabilities": {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*$"
++ }
++ },
++ "variants": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "additionalProperties": {
++ "type": "object"
++ }
++ },
++ "mapping": {
++ "type": "object",
++ "required": [
++ "driver"
++ ],
++ "properties": {
++ "driver": {
++ "enum": [
++ "pca9685",
++ "ads7830",
++ "cozmo",
++ "onvif",
++ "sim"
++ ]
++ },
++ "plugin": {
++ "type": "string"
++ }
++ }
++ },
++ "commands": {
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "drive": {
++ "type": "object",
++ "required": [
++ "axes"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "axes": {
++ "type": "object",
++ "minProperties": 1,
++ "additionalProperties": false,
++ "properties": {
++ "throttle": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ },
++ "steer": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ },
++ "x": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ },
++ "y": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ },
++ "rotation": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ }
++ }
++ }
++ }
++ },
++ "ptz": {
++ "type": "object",
++ "required": [
++ "axes"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "axes": {
++ "type": "object",
++ "minProperties": 1,
++ "additionalProperties": false,
++ "properties": {
++ "pan": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ },
++ "tilt": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ },
++ "zoom": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ }
++ }
++ }
++ }
++ },
++ "actuator": {
++ "type": "object",
++ "required": [
++ "names"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "names": {
++ "type": "object",
++ "minProperties": 1,
++ "propertyNames": {
++ "pattern": "^[a-z][a-z0-9_]{0,23}$"
++ },
++ "additionalProperties": {
++ "oneOf": [
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "range"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "number"
++ },
++ "range": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "rgb"
++ },
++ "count": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "hz"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "tone"
++ },
++ "hz": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "bool"
++ }
++ }
++ }
++ ]
++ }
++ }
++ }
++ },
++ "say": {
++ "type": "object",
++ "required": [],
++ "additionalProperties": false,
++ "properties": {
++ "max_chars": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ },
++ "display": {
++ "type": "object",
++ "required": [
++ "modes"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "modes": {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "enum": [
++ "text",
++ "face",
++ "image_png_b64"
++ ]
++ }
++ },
++ "faces": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]{0,23}$"
++ }
++ },
++ "max_chars": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ },
++ "halt": {
++ "type": "object",
++ "required": [],
++ "additionalProperties": false,
++ "properties": {}
++ }
++ }
++ },
++ "widgets": {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "type": "object",
++ "required": [
++ "type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*$"
++ },
++ "capability": {
++ "type": "string"
++ },
++ "label": {
++ "type": "string",
++ "maxLength": 60
++ },
++ "order": {
++ "type": "integer"
++ },
++ "command": {
++ "type": "object",
++ "required": [
++ "kind"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "kind": {
++ "enum": [
++ "drive",
++ "actuator",
++ "ptz",
++ "say",
++ "display",
++ "halt"
++ ]
++ },
++ "names": {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]{0,23}$"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "camera": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "transport": {
++ "enum": [
++ "whip",
++ "onvif",
++ "rtsp"
++ ]
++ },
++ "resolution": {
++ "type": "string",
++ "pattern": "^[0-9]+x[0-9]+$"
++ }
++ }
++ },
++ "limits": {
++ "type": "object",
++ "required": [],
++ "additionalProperties": false,
++ "properties": {
++ "max_speed": {
++ "type": "number",
++ "exclusiveMinimum": 0
++ },
++ "max_turn": {
++ "type": "number",
++ "exclusiveMinimum": 0
++ },
++ "max_command_ms": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "heartbeat_ms": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/bot/robot-read-request.v1.json b/contracts/bot/robot-read-request.v1.json
nnew file mode 100644
iindex 00000000..b66eeab8
---- /dev/null
++++ b/contracts/bot/robot-read-request.v1.json
@@@ -0,0 +1,24 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/robot-read-request.v1.json",
++ "title": "BotRobotReadRequest",
++ "description": "bot.robot-read-request@1: the query of bot.robot.read on OpenVibe.Bot (server/api/v1.js); the routes take no body. owner names whose robots GET /api/v1/robots lists (a service must give it; a person may give only their own subject). limit (1 to 200, default 50) and before (the last audit id seen) page GET …/robots/:id/audit.",
++ "type": "object",
++ "required": [],
++ "additionalProperties": false,
++ "properties": {
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "limit": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 200
++ },
++ "before": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++}
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 847adca2..3804c261 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -5740,6 +5740,126 @@
"compatibility": "backward",
"status": "active"
},
++ {
++ "id": "bot.robot-profile",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/robot-profile.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-043"
++ },
++ {
++ "id": "bot.device-message",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/device-message.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-043"
++ },
++ {
++ "id": "bot.command",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/command.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-043"
++ },
++ {
++ "id": "bot.command-result",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/command-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-043"
++ },
++ {
++ "id": "bot.robot-read-request",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/robot-read-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-043"
++ },
++ {
++ "id": "bot.pair-request",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/pair-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-043"
++ },
++ {
++ "id": "bot.pair-result",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/pair-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-043"
++ },
++ {
++ "id": "bot.robot.online",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "events/payloads/bot.robot.online.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-043"
++ },
++ {
++ "id": "bot.robot.offline",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "events/payloads/bot.robot.offline.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-043"
++ },
++ {
++ "id": "bot.estop.set",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "events/payloads/bot.estop.set.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-043"
++ },
++ {
++ "id": "bot.estop.cleared",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "events/payloads/bot.estop.cleared.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-043"
++ },
++ {
++ "id": "bot.command.refused",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "events/payloads/bot.command.refused.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-043"
++ },
{
"id": "run.job-create-request",
"version": "1.0.0",
ddiff --git a/contracts/events/payloads/bot.command.refused.v1.json b/contracts/events/payloads/bot.command.refused.v1.json
nnew file mode 100644
iindex 00000000..a9e61299
---- /dev/null
++++ b/contracts/events/payloads/bot.command.refused.v1.json
@@@ -0,0 +1,39 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/bot.command.refused.v1.json",
++ "title": "BotCommandRefusedPayload",
++ "description": "bot.command.refused v1 (OpenVibe.Bot server/domain/index.js auditCommand). The gate refused a command (role, allowlist, limits, policy, e-stop, cooldown, turn, device offline) or a device reported an e-stop for a robot it is not attached to (kind estop_state). reason is Bot's refusal code; ids, kinds and codes only, never the value. Envelope: subject { type: robot, id: <robot id> }, visibility internal, priority important, actor service:bot.",
++ "type": "object",
++ "required": [
++ "robot_id",
++ "kind",
++ "reason",
++ "role"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "kind": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^bot\\.[a-z_]+$"
++ },
++ "role": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/bot.estop.cleared.v1.json b/contracts/events/payloads/bot.estop.cleared.v1.json
nnew file mode 100644
iindex 00000000..887d8360
---- /dev/null
++++ b/contracts/events/payloads/bot.estop.cleared.v1.json
@@@ -0,0 +1,32 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/bot.estop.cleared.v1.json",
++ "title": "BotEstopClearedPayload",
++ "description": "bot.estop.cleared v1 (OpenVibe.Bot server/domain/index.js writeEstop via clearEstop). The owner cleared the latched e-stop (only the owner can); principal_kind is user, and Bot sends by as null. Envelope: subject { type: robot, id: <robot id> }, visibility internal, priority important, actor service:bot.",
++ "type": "object",
++ "required": [
++ "robot_id",
++ "by",
++ "principal_kind"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "principal_kind": {
++ "enum": [
++ "user",
++ "service",
++ "device"
++ ]
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/bot.estop.set.v1.json b/contracts/events/payloads/bot.estop.set.v1.json
nnew file mode 100644
iindex 00000000..bf0cab4c
---- /dev/null
++++ b/contracts/events/payloads/bot.estop.set.v1.json
@@@ -0,0 +1,32 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/bot.estop.set.v1.json",
++ "title": "BotEstopSetPayload",
++ "description": "bot.estop.set v1 (OpenVibe.Bot server/domain/index.js writeEstop). The e-stop latched. by is the user subject, the service token's sub or the device id that set it; principal_kind says which. Envelope: subject { type: robot, id: <robot id> }, visibility internal, priority important, actor service:bot.",
++ "type": "object",
++ "required": [
++ "robot_id",
++ "by",
++ "principal_kind"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "principal_kind": {
++ "enum": [
++ "user",
++ "service",
++ "device"
++ ]
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/bot.robot.offline.v1.json b/contracts/events/payloads/bot.robot.offline.v1.json
nnew file mode 100644
iindex 00000000..f73805c5
---- /dev/null
++++ b/contracts/events/payloads/bot.robot.offline.v1.json
@@@ -0,0 +1,22 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/bot.robot.offline.v1.json",
++ "title": "BotRobotOfflinePayload",
++ "description": "bot.robot.offline v1 (OpenVibe.Bot server/domain/index.js setOnline). The device missed two heartbeats plus the grace window, or its socket closed; one event per robot it is attached to. Envelope: subject { type: robot, id: <robot id> }, visibility internal, priority important, actor service:bot.",
++ "type": "object",
++ "required": [
++ "robot_id",
++ "device_id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "device_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/bot.robot.online.v1.json b/contracts/events/payloads/bot.robot.online.v1.json
nnew file mode 100644
iindex 00000000..319b5896
---- /dev/null
++++ b/contracts/events/payloads/bot.robot.online.v1.json
@@@ -0,0 +1,22 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/bot.robot.online.v1.json",
++ "title": "BotRobotOnlinePayload",
++ "description": "bot.robot.online v1 (OpenVibe.Bot server/domain/index.js setOnline). A device authenticated on /device and serves the robot; one event per robot it is attached to. Envelope: subject { type: robot, id: <robot id> }, visibility internal, priority important, actor service:bot.",
++ "type": "object",
++ "required": [
++ "robot_id",
++ "device_id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "device_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++}
ddiff --git a/fixtures/bot.command-result/invalid/no-id.json b/fixtures/bot.command-result/invalid/no-id.json
nnew file mode 100644
iindex 00000000..a8430bd3
---- /dev/null
++++ b/fixtures/bot.command-result/invalid/no-id.json
@@@ -0,0 +1,7 @@
++{
++ "v": 1,
++ "seq": 17,
++ "ts": 1759600000017,
++ "type": "command_result",
++ "result": "ack"
++}
ddiff --git a/fixtures/bot.command-result/invalid/unknown-result.json b/fixtures/bot.command-result/invalid/unknown-result.json
nnew file mode 100644
iindex 00000000..51304011
---- /dev/null
++++ b/fixtures/bot.command-result/invalid/unknown-result.json
@@@ -0,0 +1,8 @@
++{
++ "v": 1,
++ "seq": 16,
++ "ts": 1759600000016,
++ "type": "command_result",
++ "id": "op-7",
++ "result": "ok"
++}
ddiff --git a/fixtures/bot.command-result/valid/ack.json b/fixtures/bot.command-result/valid/ack.json
nnew file mode 100644
iindex 00000000..ccaf2b9c
---- /dev/null
++++ b/fixtures/bot.command-result/valid/ack.json
@@@ -0,0 +1,10 @@
++{
++ "v": 1,
++ "seq": 13,
++ "ts": 1759600000013,
++ "type": "command_result",
++ "id": "op-7",
++ "result": "ack",
++ "reason": null,
++ "latency_ms": 42
++}
ddiff --git a/fixtures/bot.command-result/valid/cached-pending.json b/fixtures/bot.command-result/valid/cached-pending.json
nnew file mode 100644
iindex 00000000..3651a1b2
---- /dev/null
++++ b/fixtures/bot.command-result/valid/cached-pending.json
@@@ -0,0 +1,9 @@
++{
++ "v": 1,
++ "seq": 15,
++ "ts": 1759600000015,
++ "type": "command_result",
++ "id": "op-9",
++ "result": "pending",
++ "cached": true
++}
ddiff --git a/fixtures/bot.command-result/valid/refused.json b/fixtures/bot.command-result/valid/refused.json
nnew file mode 100644
iindex 00000000..f864a045
---- /dev/null
++++ b/fixtures/bot.command-result/valid/refused.json
@@@ -0,0 +1,10 @@
++{
++ "v": 1,
++ "seq": 14,
++ "ts": 1759600000014,
++ "type": "command_result",
++ "id": "op-8",
++ "result": "refused",
++ "code": "bot.estop_latched",
++ "reason": "the e-stop is latched; only the owner can clear it"
++}
ddiff --git a/fixtures/bot.command.refused/invalid/carries-value.json b/fixtures/bot.command.refused/invalid/carries-value.json
nnew file mode 100644
iindex 00000000..f1033425
---- /dev/null
++++ b/fixtures/bot.command.refused/invalid/carries-value.json
@@@ -0,0 +1,9 @@
++{
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "kind": "drive",
++ "reason": "bot.cooldown",
++ "role": "operator",
++ "value": {
++ "throttle": 1
++ }
++}
ddiff --git a/fixtures/bot.command.refused/valid/cooldown.json b/fixtures/bot.command.refused/valid/cooldown.json
nnew file mode 100644
iindex 00000000..1478b778
---- /dev/null
++++ b/fixtures/bot.command.refused/valid/cooldown.json
@@@ -0,0 +1,6 @@
++{
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "kind": "drive",
++ "reason": "bot.cooldown",
++ "role": "operator"
++}
ddiff --git a/fixtures/bot.command.refused/valid/device-estop-elsewhere.json b/fixtures/bot.command.refused/valid/device-estop-elsewhere.json
nnew file mode 100644
iindex 00000000..42a03517
---- /dev/null
++++ b/fixtures/bot.command.refused/valid/device-estop-elsewhere.json
@@@ -0,0 +1,6 @@
++{
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "kind": "estop_state",
++ "reason": "bot.forbidden",
++ "role": null
++}
ddiff --git a/fixtures/bot.command/invalid/stamped-operator.json b/fixtures/bot.command/invalid/stamped-operator.json
nnew file mode 100644
iindex 00000000..0c8f94a6
---- /dev/null
++++ b/fixtures/bot.command/invalid/stamped-operator.json
@@@ -0,0 +1,9 @@
++{
++ "type": "command",
++ "kind": "drive",
++ "value": {},
++ "operator": {
++ "subject": "usr_01J8Z4P7T3W9Y1AC5N8R0S4V6X",
++ "role": "owner"
++ }
++}
ddiff --git a/fixtures/bot.command/invalid/unknown-kind.json b/fixtures/bot.command/invalid/unknown-kind.json
nnew file mode 100644
iindex 00000000..1de8c279
---- /dev/null
++++ b/fixtures/bot.command/invalid/unknown-kind.json
@@@ -0,0 +1,4 @@
++{
++ "type": "command",
++ "kind": "self_destruct"
++}
ddiff --git a/fixtures/bot.command/valid/drive.json b/fixtures/bot.command/valid/drive.json
nnew file mode 100644
iindex 00000000..d891dd56
---- /dev/null
++++ b/fixtures/bot.command/valid/drive.json
@@@ -0,0 +1,13 @@
++{
++ "v": 1,
++ "seq": 12,
++ "ts": 1759600000000,
++ "type": "command",
++ "id": "op-7",
++ "kind": "drive",
++ "value": {
++ "throttle": 0.5,
++ "steer": -0.2
++ },
++ "ms": 300
++}
ddiff --git a/fixtures/bot.command/valid/halt.json b/fixtures/bot.command/valid/halt.json
nnew file mode 100644
iindex 00000000..26ecb72f
---- /dev/null
++++ b/fixtures/bot.command/valid/halt.json
@@@ -0,0 +1,4 @@
++{
++ "type": "command",
++ "kind": "halt"
++}
ddiff --git a/fixtures/bot.device-message/invalid/ack-extra-field.json b/fixtures/bot.device-message/invalid/ack-extra-field.json
nnew file mode 100644
iindex 00000000..de189225
---- /dev/null
++++ b/fixtures/bot.device-message/invalid/ack-extra-field.json
@@@ -0,0 +1,8 @@
++{
++ "v": 1,
++ "seq": 7,
++ "ts": 1759600000007,
++ "type": "ack",
++ "id": "cmd_1",
++ "result": "ok"
++}
ddiff --git a/fixtures/bot.device-message/invalid/job-frame.json b/fixtures/bot.device-message/invalid/job-frame.json
nnew file mode 100644
iindex 00000000..7637c544
---- /dev/null
++++ b/fixtures/bot.device-message/invalid/job-frame.json
@@@ -0,0 +1,8 @@
++{
++ "v": 1,
++ "seq": 8,
++ "ts": 1759600000008,
++ "type": "job_started",
++ "id": "job_01J8Z4Q9V5X1Z3BD6P9S2T5W7Y",
++ "started_ms": 1
++}
ddiff --git a/fixtures/bot.device-message/invalid/no-seq.json b/fixtures/bot.device-message/invalid/no-seq.json
nnew file mode 100644
iindex 00000000..3b62afff
---- /dev/null
++++ b/fixtures/bot.device-message/invalid/no-seq.json
@@@ -0,0 +1,6 @@
++{
++ "v": 1,
++ "ts": 1759600000000,
++ "type": "heartbeat",
++ "t": 5
++}
ddiff --git a/fixtures/bot.device-message/invalid/unknown-kind.json b/fixtures/bot.device-message/invalid/unknown-kind.json
nnew file mode 100644
iindex 00000000..7142e8eb
---- /dev/null
++++ b/fixtures/bot.device-message/invalid/unknown-kind.json
@@@ -0,0 +1,8 @@
++{
++ "v": 1,
++ "seq": 3,
++ "ts": 1759600000003,
++ "type": "command",
++ "id": "cmd_1",
++ "kind": "fly"
++}
ddiff --git a/fixtures/bot.device-message/valid/command.json b/fixtures/bot.device-message/valid/command.json
nnew file mode 100644
iindex 00000000..536e2641
---- /dev/null
++++ b/fixtures/bot.device-message/valid/command.json
@@@ -0,0 +1,19 @@
++{
++ "v": 1,
++ "seq": 3,
++ "ts": 1759600000003,
++ "type": "command",
++ "id": "cmd_01J8Z4Q9V5X1Z3BD6P9S2T5W7Y",
++ "ref": "op-7",
++ "kind": "drive",
++ "value": {
++ "throttle": 0.5,
++ "steer": 0
++ },
++ "deadline_ms": 1759600000300,
++ "operator": {
++ "subject": "usr_01J8Z4P7T3W9Y1AC5N8R0S4V6X",
++ "role": "owner"
++ },
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X"
++}
ddiff --git a/fixtures/bot.device-message/valid/config.json b/fixtures/bot.device-message/valid/config.json
nnew file mode 100644
iindex 00000000..cc79b212
---- /dev/null
++++ b/fixtures/bot.device-message/valid/config.json
@@@ -0,0 +1,19 @@
++{
++ "v": 1,
++ "seq": 2,
++ "ts": 1759600000002,
++ "type": "config",
++ "heartbeat_ms": 1000,
++ "limits": {
++ "max_speed": 0.6,
++ "max_turn": 1,
++ "max_command_ms": 300,
++ "heartbeat_ms": 1000
++ },
++ "allowed_commands": [
++ "drive",
++ "actuator",
++ "halt"
++ ],
++ "estop_latched": false
++}
ddiff --git a/fixtures/bot.device-message/valid/hello.json b/fixtures/bot.device-message/valid/hello.json
nnew file mode 100644
iindex 00000000..c5286796
---- /dev/null
++++ b/fixtures/bot.device-message/valid/hello.json
@@@ -0,0 +1,12 @@
++{
++ "v": 1,
++ "seq": 1,
++ "ts": 1759600000001,
++ "type": "hello",
++ "session_id": "ses_1",
++ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W",
++ "robot_ids": [
++ "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X"
++ ],
++ "server_time": "2026-10-04T12:00:00.000Z"
++}
ddiff --git a/fixtures/bot.device-message/valid/nack.json b/fixtures/bot.device-message/valid/nack.json
nnew file mode 100644
iindex 00000000..9371db78
---- /dev/null
++++ b/fixtures/bot.device-message/valid/nack.json
@@@ -0,0 +1,9 @@
++{
++ "v": 1,
++ "seq": 5,
++ "ts": 1759600000005,
++ "type": "nack",
++ "id": "cmd_01J8Z4Q9V5X1Z3BD6P9S2T5W7Y",
++ "fault_code": "estopped",
++ "message": "the remote e-stop is latched"
++}
ddiff --git a/fixtures/bot.device-message/valid/node-status.json b/fixtures/bot.device-message/valid/node-status.json
nnew file mode 100644
iindex 00000000..5f401f6e
---- /dev/null
++++ b/fixtures/bot.device-message/valid/node-status.json
@@@ -0,0 +1,28 @@
++{
++ "v": 1,
++ "seq": 4,
++ "ts": 1759600000004,
++ "type": "status",
++ "firmware": "0.1.2",
++ "capabilities": {
++ "adeept_adr036": {
++ "drive": true
++ }
++ },
++ "agent_version": "0.1.2",
++ "device_kind": "onboard",
++ "os": "linux",
++ "arch": "arm64",
++ "drivers": [
++ {
++ "name": "adeept_adr036",
++ "driver": "pca9685",
++ "version": "0.1.2",
++ "state": "running"
++ }
++ ],
++ "faults": [],
++ "estop_latched": false,
++ "local_stop": false,
++ "video": "whip"
++}
ddiff --git a/fixtures/bot.device-message/valid/paired-without-video.json b/fixtures/bot.device-message/valid/paired-without-video.json
nnew file mode 100644
iindex 00000000..13f8e1fb
---- /dev/null
++++ b/fixtures/bot.device-message/valid/paired-without-video.json
@@@ -0,0 +1,70 @@
++{
++ "v": 1,
++ "seq": 1,
++ "ts": 1759600000001,
++ "type": "paired",
++ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W",
++ "credential": "example-credential-shown-once",
++ "video": "not_configured",
++ "robot_ids": [
++ "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X"
++ ],
++ "profile_id": "sim.rover",
++ "profile": {
++ "id": "sim.rover",
++ "version": 1,
++ "name": "Simulated rover",
++ "vendor": null,
++ "kind": "server",
++ "description": null,
++ "capabilities": [
++ "drive.differential",
++ "camera"
++ ],
++ "variants": null,
++ "mapping": {
++ "driver": "sim"
++ },
++ "commands": {
++ "drive": {
++ "axes": {
++ "throttle": [
++ -1,
++ 1
++ ],
++ "steer": [
++ -1,
++ 1
++ ]
++ }
++ },
++ "say": {
++ "max_chars": 200
++ },
++ "halt": {}
++ },
++ "widgets": [
++ {
++ "type": "drive",
++ "capability": "drive.differential",
++ "label": "Drive",
++ "order": 1,
++ "command": {
++ "kind": "drive"
++ }
++ },
++ {
++ "type": "camera",
++ "capability": "camera",
++ "order": 2
++ }
++ ],
++ "camera": null,
++ "limits": {
++ "max_speed": 1,
++ "max_turn": 1,
++ "max_command_ms": 300,
++ "heartbeat_ms": 1000
++ }
++ }
++}
ddiff --git a/fixtures/bot.device-message/valid/rotate.json b/fixtures/bot.device-message/valid/rotate.json
nnew file mode 100644
iindex 00000000..0e30c500
---- /dev/null
++++ b/fixtures/bot.device-message/valid/rotate.json
@@@ -0,0 +1,6 @@
++{
++ "v": 1,
++ "seq": 9,
++ "ts": 1759600000009,
++ "type": "rotate"
++}
ddiff --git a/fixtures/bot.device-message/valid/telemetry.json b/fixtures/bot.device-message/valid/telemetry.json
nnew file mode 100644
iindex 00000000..0b172470
---- /dev/null
++++ b/fixtures/bot.device-message/valid/telemetry.json
@@@ -0,0 +1,19 @@
++{
++ "v": 1,
++ "seq": 6,
++ "ts": 1759600000006,
++ "type": "telemetry",
++ "battery": {
++ "volts": 7.6,
++ "percent": 81
++ },
++ "sensors": {
++ "distance_cm": 42
++ },
++ "events": [
++ {
++ "name": "low_battery",
++ "ts": 1759600000006
++ }
++ ]
++}
ddiff --git a/fixtures/bot.estop.cleared/invalid/no-principal.json b/fixtures/bot.estop.cleared/invalid/no-principal.json
nnew file mode 100644
iindex 00000000..5781033a
---- /dev/null
++++ b/fixtures/bot.estop.cleared/invalid/no-principal.json
@@@ -0,0 +1,4 @@
++{
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "by": null
++}
ddiff --git a/fixtures/bot.estop.cleared/valid/owner.json b/fixtures/bot.estop.cleared/valid/owner.json
nnew file mode 100644
iindex 00000000..26e4fe2f
---- /dev/null
++++ b/fixtures/bot.estop.cleared/valid/owner.json
@@@ -0,0 +1,5 @@
++{
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "by": null,
++ "principal_kind": "user"
++}
ddiff --git a/fixtures/bot.estop.set/invalid/unknown-principal.json b/fixtures/bot.estop.set/invalid/unknown-principal.json
nnew file mode 100644
iindex 00000000..b09fe5f7
---- /dev/null
++++ b/fixtures/bot.estop.set/invalid/unknown-principal.json
@@@ -0,0 +1,5 @@
++{
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "by": "usr_01J8Z4P7T3W9Y1AC5N8R0S4V6X",
++ "principal_kind": "robot"
++}
ddiff --git a/fixtures/bot.estop.set/valid/by-device.json b/fixtures/bot.estop.set/valid/by-device.json
nnew file mode 100644
iindex 00000000..1c8bfccf
---- /dev/null
++++ b/fixtures/bot.estop.set/valid/by-device.json
@@@ -0,0 +1,5 @@
++{
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "by": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W",
++ "principal_kind": "device"
++}
ddiff --git a/fixtures/bot.estop.set/valid/by-operator.json b/fixtures/bot.estop.set/valid/by-operator.json
nnew file mode 100644
iindex 00000000..10789d2b
---- /dev/null
++++ b/fixtures/bot.estop.set/valid/by-operator.json
@@@ -0,0 +1,5 @@
++{
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "by": "usr_01J8Z4P7T3W9Y1AC5N8R0S4V6X",
++ "principal_kind": "user"
++}
ddiff --git a/fixtures/bot.pair-request/invalid/bad-kind.json b/fixtures/bot.pair-request/invalid/bad-kind.json
nnew file mode 100644
iindex 00000000..6c53cd75
---- /dev/null
++++ b/fixtures/bot.pair-request/invalid/bad-kind.json
@@@ -0,0 +1,4 @@
++{
++ "code": "K7QM-3XRT",
++ "device_kind": "cloud"
++}
ddiff --git a/fixtures/bot.pair-request/invalid/no-code.json b/fixtures/bot.pair-request/invalid/no-code.json
nnew file mode 100644
iindex 00000000..c7175d1b
---- /dev/null
++++ b/fixtures/bot.pair-request/invalid/no-code.json
@@@ -0,0 +1,3 @@
++{
++ "robot": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X"
++}
ddiff --git a/fixtures/bot.pair-request/valid/agent.json b/fixtures/bot.pair-request/valid/agent.json
nnew file mode 100644
iindex 00000000..10071119
---- /dev/null
++++ b/fixtures/bot.pair-request/valid/agent.json
@@@ -0,0 +1,16 @@
++{
++ "code": "K7QM-3XRT",
++ "robot": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "agent_version": "0.1.2",
++ "device_kind": "onboard",
++ "drivers": [
++ "pca9685",
++ "ads7830"
++ ],
++ "capabilities": {
++ "pca9685": {
++ "channels": 16
++ }
++ },
++ "name": "garage pi"
++}
ddiff --git a/fixtures/bot.pair-request/valid/code-only.json b/fixtures/bot.pair-request/valid/code-only.json
nnew file mode 100644
iindex 00000000..e83dc981
---- /dev/null
++++ b/fixtures/bot.pair-request/valid/code-only.json
@@@ -0,0 +1,3 @@
++{
++ "code": "K7QM3XRT"
++}
ddiff --git a/fixtures/bot.pair-result/invalid/credential-hash.json b/fixtures/bot.pair-result/invalid/credential-hash.json
nnew file mode 100644
iindex 00000000..cb64d941
---- /dev/null
++++ b/fixtures/bot.pair-result/invalid/credential-hash.json
@@@ -0,0 +1,7 @@
++{
++ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W",
++ "credential_hash": "x",
++ "video": "not_configured",
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "profile": null
++}
ddiff --git a/fixtures/bot.pair-result/invalid/no-key-no-video.json b/fixtures/bot.pair-result/invalid/no-key-no-video.json
nnew file mode 100644
iindex 00000000..da8391f2
---- /dev/null
++++ b/fixtures/bot.pair-result/invalid/no-key-no-video.json
@@@ -0,0 +1,6 @@
++{
++ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W",
++ "credential": "example-credential-shown-once",
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "profile": null
++}
ddiff --git a/fixtures/bot.pair-result/valid/bound-without-video.json b/fixtures/bot.pair-result/valid/bound-without-video.json
nnew file mode 100644
iindex 00000000..4b515cfe
---- /dev/null
++++ b/fixtures/bot.pair-result/valid/bound-without-video.json
@@@ -0,0 +1,6 @@
++{
++ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W",
++ "video": "not_configured",
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "profile": null
++}
ddiff --git a/fixtures/bot.pair-result/valid/paired.json b/fixtures/bot.pair-result/valid/paired.json
nnew file mode 100644
iindex 00000000..20bb9239
---- /dev/null
++++ b/fixtures/bot.pair-result/valid/paired.json
@@@ -0,0 +1,137 @@
++{
++ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W",
++ "credential": "example-credential-shown-once",
++ "publish_key": "example-publish-key-shown-once",
++ "whip_url": "https://ingest.openre.stream/whip/example-publish-key-shown-once",
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "profile": {
++ "id": "adeept.adr036",
++ "version": 1,
++ "name": "Adeept ADR036 4WD Smart Car",
++ "vendor": "Adeept",
++ "kind": "onboard",
++ "capabilities": [
++ "drive.differential",
++ "servo.pan_tilt",
++ "lights.rgb",
++ "speaker.horn",
++ "sensor.ultrasonic",
++ "battery"
++ ],
++ "mapping": {
++ "driver": "pca9685",
++ "plugin": "adeept_adr036",
++ "wheels": "ordinary",
++ "i2c_bus": 1,
++ "address": "0x5f",
++ "motor_channels": [
++ 8,
++ 9,
++ 10,
++ 11,
++ 12,
++ 13,
++ 14,
++ 15
++ ],
++ "servo_channels": {
++ "pan": 0,
++ "tilt": 1
++ },
++ "battery": {
++ "driver": "ads7830",
++ "address": "0x48"
++ },
++ "lights": "ws2812",
++ "horn": "buzzer"
++ },
++ "commands": {
++ "drive": {
++ "axes": {
++ "throttle": [
++ -1,
++ 1
++ ],
++ "steer": [
++ -1,
++ 1
++ ]
++ }
++ },
++ "actuator": {
++ "names": {
++ "pan": {
++ "type": "number",
++ "range": [
++ -1,
++ 1
++ ]
++ },
++ "tilt": {
++ "type": "number",
++ "range": [
++ -1,
++ 1
++ ]
++ },
++ "buzzer": {
++ "type": "tone",
++ "hz": [
++ 220,
++ 880
++ ]
++ },
++ "lights": {
++ "type": "rgb",
++ "count": 8
++ }
++ }
++ }
++ },
++ "widgets": [
++ {
++ "type": "drive",
++ "capability": "drive.differential",
++ "label": "Drive",
++ "order": 1,
++ "command": {
++ "kind": "drive"
++ }
++ },
++ {
++ "type": "pan-tilt",
++ "capability": "servo.pan_tilt",
++ "label": "Pan / tilt",
++ "order": 2,
++ "command": {
++ "kind": "actuator",
++ "names": [
++ "pan",
++ "tilt"
++ ]
++ }
++ },
++ {
++ "type": "battery",
++ "capability": "battery",
++ "label": "Battery",
++ "order": 3
++ },
++ {
++ "type": "latency",
++ "label": "Latency",
++ "order": 4
++ }
++ ],
++ "camera": {
++ "transport": "whip",
++ "resolution": "640x480"
++ },
++ "limits": {
++ "max_speed": 1,
++ "max_turn": 1,
++ "max_command_ms": 300,
++ "heartbeat_ms": 1000
++ }
++ }
++}
ddiff --git a/fixtures/bot.robot-profile/invalid/unknown-command-kind.json b/fixtures/bot.robot-profile/invalid/unknown-command-kind.json
nnew file mode 100644
iindex 00000000..d1bc60b0
---- /dev/null
++++ b/fixtures/bot.robot-profile/invalid/unknown-command-kind.json
@@@ -0,0 +1,90 @@
++{
++ "id": "adeept.adr036",
++ "version": 1,
++ "name": "Adeept ADR036 4WD Smart Car",
++ "vendor": "Adeept",
++ "kind": "onboard",
++ "capabilities": [
++ "drive.differential",
++ "servo.pan_tilt",
++ "lights.rgb",
++ "speaker.horn",
++ "sensor.ultrasonic",
++ "battery"
++ ],
++ "mapping": {
++ "driver": "pca9685",
++ "plugin": "adeept_adr036",
++ "wheels": "ordinary",
++ "i2c_bus": 1,
++ "address": "0x5f",
++ "motor_channels": [
++ 8,
++ 9,
++ 10,
++ 11,
++ 12,
++ 13,
++ 14,
++ 15
++ ],
++ "servo_channels": {
++ "pan": 0,
++ "tilt": 1
++ },
++ "battery": {
++ "driver": "ads7830",
++ "address": "0x48"
++ },
++ "lights": "ws2812",
++ "horn": "buzzer"
++ },
++ "commands": {
++ "fly": {}
++ },
++ "widgets": [
++ {
++ "type": "drive",
++ "capability": "drive.differential",
++ "label": "Drive",
++ "order": 1,
++ "command": {
++ "kind": "drive"
++ }
++ },
++ {
++ "type": "pan-tilt",
++ "capability": "servo.pan_tilt",
++ "label": "Pan / tilt",
++ "order": 2,
++ "command": {
++ "kind": "actuator",
++ "names": [
++ "pan",
++ "tilt"
++ ]
++ }
++ },
++ {
++ "type": "battery",
++ "capability": "battery",
++ "label": "Battery",
++ "order": 3
++ },
++ {
++ "type": "latency",
++ "label": "Latency",
++ "order": 4
++ }
++ ],
++ "camera": {
++ "transport": "whip",
++ "resolution": "640x480"
++ },
++ "limits": {
++ "max_speed": 1,
++ "max_turn": 1,
++ "max_command_ms": 300,
++ "heartbeat_ms": 1000
++ }
++}
ddiff --git a/fixtures/bot.robot-profile/invalid/unknown-driver.json b/fixtures/bot.robot-profile/invalid/unknown-driver.json
nnew file mode 100644
iindex 00000000..1d657ea1
---- /dev/null
++++ b/fixtures/bot.robot-profile/invalid/unknown-driver.json
@@@ -0,0 +1,106 @@
++{
++ "id": "adeept.adr036",
++ "version": 1,
++ "name": "Adeept ADR036 4WD Smart Car",
++ "vendor": "Adeept",
++ "kind": "onboard",
++ "capabilities": [
++ "drive.differential",
++ "servo.pan_tilt",
++ "lights.rgb",
++ "speaker.horn",
++ "sensor.ultrasonic",
++ "battery"
++ ],
++ "mapping": {
++ "driver": "arduino"
++ },
++ "commands": {
++ "drive": {
++ "axes": {
++ "throttle": [
++ -1,
++ 1
++ ],
++ "steer": [
++ -1,
++ 1
++ ]
++ }
++ },
++ "actuator": {
++ "names": {
++ "pan": {
++ "type": "number",
++ "range": [
++ -1,
++ 1
++ ]
++ },
++ "tilt": {
++ "type": "number",
++ "range": [
++ -1,
++ 1
++ ]
++ },
++ "buzzer": {
++ "type": "tone",
++ "hz": [
++ 220,
++ 880
++ ]
++ },
++ "lights": {
++ "type": "rgb",
++ "count": 8
++ }
++ }
++ }
++ },
++ "widgets": [
++ {
++ "type": "drive",
++ "capability": "drive.differential",
++ "label": "Drive",
++ "order": 1,
++ "command": {
++ "kind": "drive"
++ }
++ },
++ {
++ "type": "pan-tilt",
++ "capability": "servo.pan_tilt",
++ "label": "Pan / tilt",
++ "order": 2,
++ "command": {
++ "kind": "actuator",
++ "names": [
++ "pan",
++ "tilt"
++ ]
++ }
++ },
++ {
++ "type": "battery",
++ "capability": "battery",
++ "label": "Battery",
++ "order": 3
++ },
++ {
++ "type": "latency",
++ "label": "Latency",
++ "order": 4
++ }
++ ],
++ "camera": {
++ "transport": "whip",
++ "resolution": "640x480"
++ },
++ "limits": {
++ "max_speed": 1,
++ "max_turn": 1,
++ "max_command_ms": 300,
++ "heartbeat_ms": 1000
++ }
++}
ddiff --git a/fixtures/bot.robot-profile/valid/adeept-file.json b/fixtures/bot.robot-profile/valid/adeept-file.json
nnew file mode 100644
iindex 00000000..724d3384
---- /dev/null
++++ b/fixtures/bot.robot-profile/valid/adeept-file.json
@@@ -0,0 +1,130 @@
++{
++ "id": "adeept.adr036",
++ "version": 1,
++ "name": "Adeept ADR036 4WD Smart Car",
++ "vendor": "Adeept",
++ "kind": "onboard",
++ "capabilities": [
++ "drive.differential",
++ "servo.pan_tilt",
++ "lights.rgb",
++ "speaker.horn",
++ "sensor.ultrasonic",
++ "battery"
++ ],
++ "mapping": {
++ "driver": "pca9685",
++ "plugin": "adeept_adr036",
++ "wheels": "ordinary",
++ "i2c_bus": 1,
++ "address": "0x5f",
++ "motor_channels": [
++ 8,
++ 9,
++ 10,
++ 11,
++ 12,
++ 13,
++ 14,
++ 15
++ ],
++ "servo_channels": {
++ "pan": 0,
++ "tilt": 1
++ },
++ "battery": {
++ "driver": "ads7830",
++ "address": "0x48"
++ },
++ "lights": "ws2812",
++ "horn": "buzzer"
++ },
++ "commands": {
++ "drive": {
++ "axes": {
++ "throttle": [
++ -1,
++ 1
++ ],
++ "steer": [
++ -1,
++ 1
++ ]
++ }
++ },
++ "actuator": {
++ "names": {
++ "pan": {
++ "type": "number",
++ "range": [
++ -1,
++ 1
++ ]
++ },
++ "tilt": {
++ "type": "number",
++ "range": [
++ -1,
++ 1
++ ]
++ },
++ "buzzer": {
++ "type": "tone",
++ "hz": [
++ 220,
++ 880
++ ]
++ },
++ "lights": {
++ "type": "rgb",
++ "count": 8
++ }
++ }
++ }
++ },
++ "widgets": [
++ {
++ "type": "drive",
++ "capability": "drive.differential",
++ "label": "Drive",
++ "order": 1,
++ "command": {
++ "kind": "drive"
++ }
++ },
++ {
++ "type": "pan-tilt",
++ "capability": "servo.pan_tilt",
++ "label": "Pan / tilt",
++ "order": 2,
++ "command": {
++ "kind": "actuator",
++ "names": [
++ "pan",
++ "tilt"
++ ]
++ }
++ },
++ {
++ "type": "battery",
++ "capability": "battery",
++ "label": "Battery",
++ "order": 3
++ },
++ {
++ "type": "latency",
++ "label": "Latency",
++ "order": 4
++ }
++ ],
++ "camera": {
++ "transport": "whip",
++ "resolution": "640x480"
++ },
++ "limits": {
++ "max_speed": 1,
++ "max_turn": 1,
++ "max_command_ms": 300,
++ "heartbeat_ms": 1000
++ }
++}
ddiff --git a/fixtures/bot.robot-profile/valid/sim-served.json b/fixtures/bot.robot-profile/valid/sim-served.json
nnew file mode 100644
iindex 00000000..9433feb0
---- /dev/null
++++ b/fixtures/bot.robot-profile/valid/sim-served.json
@@@ -0,0 +1,57 @@
++{
++ "id": "sim.rover",
++ "version": 1,
++ "name": "Simulated rover",
++ "vendor": null,
++ "kind": "server",
++ "description": null,
++ "capabilities": [
++ "drive.differential",
++ "camera"
++ ],
++ "variants": null,
++ "mapping": {
++ "driver": "sim"
++ },
++ "commands": {
++ "drive": {
++ "axes": {
++ "throttle": [
++ -1,
++ 1
++ ],
++ "steer": [
++ -1,
++ 1
++ ]
++ }
++ },
++ "say": {
++ "max_chars": 200
++ },
++ "halt": {}
++ },
++ "widgets": [
++ {
++ "type": "drive",
++ "capability": "drive.differential",
++ "label": "Drive",
++ "order": 1,
++ "command": {
++ "kind": "drive"
++ }
++ },
++ {
++ "type": "camera",
++ "capability": "camera",
++ "order": 2
++ }
++ ],
++ "camera": null,
++ "limits": {
++ "max_speed": 1,
++ "max_turn": 1,
++ "max_command_ms": 300,
++ "heartbeat_ms": 1000
++ }
++}
ddiff --git a/fixtures/bot.robot-read-request/invalid/limit-too-high.json b/fixtures/bot.robot-read-request/invalid/limit-too-high.json
nnew file mode 100644
iindex 00000000..0b3838f7
---- /dev/null
++++ b/fixtures/bot.robot-read-request/invalid/limit-too-high.json
@@@ -0,0 +1,3 @@
++{
++ "limit": 500
++}
ddiff --git a/fixtures/bot.robot-read-request/invalid/unknown-field.json b/fixtures/bot.robot-read-request/invalid/unknown-field.json
nnew file mode 100644
iindex 00000000..b339b2d2
---- /dev/null
++++ b/fixtures/bot.robot-read-request/invalid/unknown-field.json
@@@ -0,0 +1,4 @@
++{
++ "owner": "usr_01J8Z4P7T3W9Y1AC5N8R0S4V6X",
++ "include": "devices"
++}
ddiff --git a/fixtures/bot.robot-read-request/valid/audit-page.json b/fixtures/bot.robot-read-request/valid/audit-page.json
nnew file mode 100644
iindex 00000000..deacb087
---- /dev/null
++++ b/fixtures/bot.robot-read-request/valid/audit-page.json
@@@ -0,0 +1,4 @@
++{
++ "limit": 50,
++ "before": 1200
++}
ddiff --git a/fixtures/bot.robot-read-request/valid/list.json b/fixtures/bot.robot-read-request/valid/list.json
nnew file mode 100644
iindex 00000000..09934167
---- /dev/null
++++ b/fixtures/bot.robot-read-request/valid/list.json
@@@ -0,0 +1,3 @@
++{
++ "owner": "usr_01J8Z4P7T3W9Y1AC5N8R0S4V6X"
++}
ddiff --git a/fixtures/bot.robot.offline/invalid/bad-robot.json b/fixtures/bot.robot.offline/invalid/bad-robot.json
nnew file mode 100644
iindex 00000000..9de70df5
---- /dev/null
++++ b/fixtures/bot.robot.offline/invalid/bad-robot.json
@@@ -0,0 +1,4 @@
++{
++ "robot_id": "robot-1",
++ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W"
++}
ddiff --git a/fixtures/bot.robot.offline/valid/offline.json b/fixtures/bot.robot.offline/valid/offline.json
nnew file mode 100644
iindex 00000000..e3caa36d
---- /dev/null
++++ b/fixtures/bot.robot.offline/valid/offline.json
@@@ -0,0 +1,4 @@
++{
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W"
++}
ddiff --git a/fixtures/bot.robot.online/invalid/no-device.json b/fixtures/bot.robot.online/invalid/no-device.json
nnew file mode 100644
iindex 00000000..245714cd
---- /dev/null
++++ b/fixtures/bot.robot.online/invalid/no-device.json
@@@ -0,0 +1,3 @@
++{
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X"
++}
ddiff --git a/fixtures/bot.robot.online/valid/online.json b/fixtures/bot.robot.online/valid/online.json
nnew file mode 100644
iindex 00000000..e3caa36d
---- /dev/null
++++ b/fixtures/bot.robot.online/valid/online.json
@@@ -0,0 +1,4 @@
++{
++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W"
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 8f42cdb8..303d669b 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -65520,6 +65520,1812 @@
]
}
},
++ "bot.robot-profile": {
++ "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-profile.v1.json",
++ "title": "BotRobotProfile",
++ "description": "bot.robot-profile@1 (ADR-043 decision 7): a robot profile as OpenVibe.Bot ships it (server/profiles/*.json) and serves it (GET /api/v1/profiles, /profiles/:id; validateProfile in server/profiles/index.js). The panel is a function of the profile. Capability and widget names are open strings that Bot checks against registries in code, so a new one never needs a schema change. As served, vendor, kind, description, variants and camera are null when the file leaves them out, limits carry their defaults (max_speed 1, max_turn 1, max_command_ms 300, heartbeat_ms 1000) and commands always include halt. mapping is the driver's own hardware map, open beyond driver.",
++ "type": "object",
++ "required": [
++ "id",
++ "version",
++ "name",
++ "capabilities",
++ "mapping",
++ "widgets"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9._-]{1,63}$"
++ },
++ "version": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1
++ },
++ "vendor": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "kind": {
++ "enum": [
++ "onboard",
++ "bridge",
++ "server",
++ null
++ ]
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "capabilities": {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*$"
++ }
++ },
++ "variants": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "additionalProperties": {
++ "type": "object"
++ }
++ },
++ "mapping": {
++ "type": "object",
++ "required": [
++ "driver"
++ ],
++ "properties": {
++ "driver": {
++ "enum": [
++ "pca9685",
++ "ads7830",
++ "cozmo",
++ "onvif",
++ "sim"
++ ]
++ },
++ "plugin": {
++ "type": "string"
++ }
++ }
++ },
++ "commands": {
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "drive": {
++ "type": "object",
++ "required": [
++ "axes"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "axes": {
++ "type": "object",
++ "minProperties": 1,
++ "additionalProperties": false,
++ "properties": {
++ "throttle": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ },
++ "steer": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ },
++ "x": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ },
++ "y": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ },
++ "rotation": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ }
++ }
++ }
++ }
++ },
++ "ptz": {
++ "type": "object",
++ "required": [
++ "axes"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "axes": {
++ "type": "object",
++ "minProperties": 1,
++ "additionalProperties": false,
++ "properties": {
++ "pan": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ },
++ "tilt": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ },
++ "zoom": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ }
++ }
++ }
++ }
++ },
++ "actuator": {
++ "type": "object",
++ "required": [
++ "names"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "names": {
++ "type": "object",
++ "minProperties": 1,
++ "propertyNames": {
++ "pattern": "^[a-z][a-z0-9_]{0,23}$"
++ },
++ "additionalProperties": {
++ "oneOf": [
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "range"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "number"
++ },
++ "range": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "rgb"
++ },
++ "count": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "hz"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "tone"
++ },
++ "hz": {
++ "type": "array",
++ "minItems": 2,
++ "maxItems": 2,
++ "items": {
++ "type": "number"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "bool"
++ }
++ }
++ }
++ ]
++ }
++ }
++ }
++ },
++ "say": {
++ "type": "object",
++ "required": [],
++ "additionalProperties": false,
++ "properties": {
++ "max_chars": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ },
++ "display": {
++ "type": "object",
++ "required": [
++ "modes"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "modes": {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "enum": [
++ "text",
++ "face",
++ "image_png_b64"
++ ]
++ }
++ },
++ "faces": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]{0,23}$"
++ }
++ },
++ "max_chars": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ },
++ "halt": {
++ "type": "object",
++ "required": [],
++ "additionalProperties": false,
++ "properties": {}
++ }
++ }
++ },
++ "widgets": {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "type": "object",
++ "required": [
++ "type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]*$"
++ },
++ "capability": {
++ "type": "string"
++ },
++ "label": {
++ "type": "string",
++ "maxLength": 60
++ },
++ "order": {
++ "type": "integer"
++ },
++ "command": {
++ "type": "object",
++ "required": [
++ "kind"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "kind": {
++ "enum": [
++ "drive",
++ "actuator",
++ "ptz",
++ "say",
++ "display",
++ "halt"
++ ]
++ },
++ "names": {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]{0,23}$"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "camera": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "transport": {
++ "enum": [
++ "whip",
++ "onvif",
++ "rtsp"
++ ]
++ },
++ "resolution": {
++ "type": "string",
++ "pattern": "^[0-9]+x[0-9]+$"
++ }
++ }
++ },
++ "limits": {
++ "type": "object",
++ "required": [],
++ "additionalProperties": false,
++ "properties": {
++ "max_speed": {
++ "type": "number",
++ "exclusiveMinimum": 0
++ },
++ "max_turn": {
++ "type": "number",
++ "exclusiveMinimum": 0
++ },
++ "max_command_ms": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "heartbeat_ms": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ }
++ }
++ }
++ },
++ "bot.device-message": {
++ "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-message.v1.json",
++ "title": "BotDeviceMessage",
++ "description": "bot.device-message@1 (ADR-043 decision 5): one frame on OpenVibe.Bot's device WebSocket (wss://openvibe.bot/device), JSON with v, seq and ts on every frame. Server to device: hello, config, command, estop, heartbeat_ack, error, paired (after a pair frame), rotate (a Network-paired machine is asked to rotate its credential with Network). Device to server: pair (the only frame an unauthenticated socket may send), status, telemetry, ack, nack, heartbeat, reauth (Network-paired machines), estop_state. The job frames on the same socket are platform.job-frame@1, not this contract. This duplicates two implementations that must not drift: OpenVibe.Bot server/realtime.js (the sender and reader in Bot) and OpenVibe.Node internal/protocol/protocol.go (the Go structs of the device agent).",
++ "oneOf": [
++ {
++ "description": "Server to device, once authenticated.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "session_id",
++ "device_id",
++ "robot_ids",
++ "server_time"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "hello"
++ },
++ "session_id": {
++ "type": "string"
++ },
++ "device_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}$"
++ }
++ },
++ "server_time": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ {
++ "description": "Server to device: the effective limits (the owner's clamped by the profile's), the kinds the profile takes and the latch; sent again when any of them changes.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "heartbeat_ms",
++ "limits",
++ "allowed_commands",
++ "estop_latched"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "config"
++ },
++ "heartbeat_ms": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "limits": {
++ "type": "object",
++ "required": [],
++ "additionalProperties": false,
++ "properties": {
++ "max_speed": {
++ "type": "number"
++ },
++ "max_turn": {
++ "type": "number"
++ },
++ "max_command_ms": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "heartbeat_ms": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ },
++ "allowed_commands": {
++ "type": "array",
++ "items": {
++ "enum": [
++ "drive",
++ "actuator",
++ "ptz",
++ "say",
++ "display",
++ "halt"
++ ]
++ }
++ },
++ "estop_latched": {
++ "type": "boolean"
++ }
++ }
++ },
++ {
++ "description": "Server to device: id is minted by Bot (the device's idempotency key), ref is the operator's own id. Never queued for an offline device, never replayed.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "id",
++ "kind"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "command"
++ },
++ "id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "ref": {
++ "type": "string"
++ },
++ "kind": {
++ "enum": [
++ "drive",
++ "actuator",
++ "ptz",
++ "say",
++ "display",
++ "halt"
++ ]
++ },
++ "value": {
++ "type": "object"
++ },
++ "deadline_ms": {
++ "type": "integer",
++ "description": "Absolute deadline, epoch milliseconds; the device stops the motion at it."
++ },
++ "operator": {
++ "type": "object",
++ "required": [
++ "role"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "subject": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "role": {
++ "type": "string"
++ }
++ }
++ },
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "target": {
++ "type": "string",
++ "description": "OpenVibe.Node only: the driver to send it to."
++ }
++ }
++ },
++ {
++ "description": "Server to device: the latch moved (true sets it, false is the owner clearing it).",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "latched"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "estop"
++ },
++ "latched": {
++ "type": "boolean"
++ },
++ "by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ {
++ "description": "Server to device: echo and t carry the device's t back.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "heartbeat_ack"
++ },
++ "t": {
++ "type": "integer"
++ },
++ "echo": {
++ "type": [
++ "number",
++ "null"
++ ]
++ },
++ "server_time": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ {
++ "description": "Server to device: a refused frame (bot.not_paired, bot.bad_frame, …).",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "code"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "error"
++ },
++ "code": {
++ "type": "string"
++ },
++ "detail": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ },
++ {
++ "description": "Server to device: the answer to pair; the same fields as POST /api/v1/pair (bot.pair-result@1).",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "device_id",
++ "credential",
++ "robot_ids"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "paired"
++ },
++ "device_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "credential": {
++ "type": "string",
++ "minLength": 1,
++ "description": "Shown once."
++ },
++ "publish_key": {
++ "type": "string",
++ "minLength": 1,
++ "description": "Shown once."
++ },
++ "whip_url": {
++ "type": "string",
++ "format": "uri",
++ "description": "Carries the publish key: shown once."
++ },
++ "video": {
++ "const": "not_configured"
++ },
++ "robot_ids": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "profile_id": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "profile": {
++ "anyOf": [
++ {
++ "$ref": "robot-profile.v1.json"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ }
++ }
++ },
++ {
++ "description": "Server to device: a Network-paired machine rotates its credential with Network.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "rotate"
++ }
++ }
++ },
++ {
++ "description": "Device to server: redeem a one-time code (bot.pair-request@1).",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "code"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "pair"
++ },
++ "code": {
++ "type": "string",
++ "minLength": 1
++ },
++ "robot": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "agent_version": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "device_kind": {
++ "enum": [
++ "onboard",
++ "bridge",
++ "server"
++ ]
++ },
++ "drivers": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "capabilities": {
++ "type": "object"
++ },
++ "name": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ },
++ {
++ "description": "Device to server: what the agent runs; OpenVibe.Node sends drivers as objects, older agents as names.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "status"
++ },
++ "firmware": {
++ "type": "string"
++ },
++ "capabilities": {
++ "type": "object"
++ },
++ "agent_version": {
++ "type": "string",
++ "maxLength": 40
++ },
++ "device_kind": {
++ "enum": [
++ "onboard",
++ "bridge",
++ "server"
++ ]
++ },
++ "os": {
++ "type": "string"
++ },
++ "arch": {
++ "type": "string"
++ },
++ "drivers": {
++ "type": "array",
++ "items": {
++ "anyOf": [
++ {
++ "type": "string"
++ },
++ {
++ "type": "object",
++ "required": [
++ "name",
++ "state"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "name": {
++ "type": "string"
++ },
++ "driver": {
++ "type": "string"
++ },
++ "version": {
++ "type": "string"
++ },
++ "state": {
++ "type": "string"
++ },
++ "capabilities": {
++ "type": "object"
++ }
++ }
++ }
++ ]
++ }
++ },
++ "faults": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "code"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "code": {
++ "type": "string"
++ },
++ "driver": {
++ "type": "string"
++ },
++ "message": {
++ "type": "string"
++ }
++ }
++ }
++ },
++ "estop_latched": {
++ "type": "boolean"
++ },
++ "local_stop": {
++ "type": "boolean"
++ },
++ "video": {
++ "type": "string"
++ }
++ }
++ },
++ {
++ "description": "Device to server: samples at most 2 Hz; a frame with events is never dropped.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "telemetry"
++ },
++ "battery": {
++ "anyOf": [
++ {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 1
++ },
++ {
++ "type": "object",
++ "required": [],
++ "additionalProperties": false,
++ "properties": {
++ "volts": {
++ "type": "number"
++ },
++ "percent": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 100
++ }
++ }
++ }
++ ]
++ },
++ "voltage": {
++ "type": "number"
++ },
++ "rssi": {
++ "type": "integer"
++ },
++ "sensors": {
++ "type": "object"
++ },
++ "events": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "name",
++ "ts"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "name": {
++ "type": "string"
++ },
++ "driver": {
++ "type": "string"
++ },
++ "ts": {
++ "type": "integer"
++ },
++ "fields": {
++ "type": "object"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "description": "Device to server: the command ran.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "ack"
++ },
++ "id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ },
++ {
++ "description": "Device to server: the command was refused, with OpenVibe.Node's fault code (bad_value, estopped, local_stop, expired, …).",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "id",
++ "fault_code"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "nack"
++ },
++ "id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "fault_code": {
++ "type": "string",
++ "minLength": 1
++ },
++ "message": {
++ "type": "string"
++ }
++ }
++ },
++ {
++ "description": "Device to server, every heartbeat_ms.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "heartbeat"
++ },
++ "t": {
++ "type": "integer"
++ },
++ "rtt_ms": {
++ "type": "number",
++ "minimum": 0
++ }
++ }
++ },
++ {
++ "description": "Device to server: a Network-paired machine's fresh node token, before the last one expires.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "token"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "reauth"
++ },
++ "token": {
++ "type": "string",
++ "minLength": 1
++ }
++ }
++ },
++ {
++ "description": "Device to server: the device's own latch; a device may set the e-stop, never clear the owner's.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "latched"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "estop_state"
++ },
++ "latched": {
++ "type": "boolean"
++ },
++ "by": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "local_stop": {
++ "type": "boolean"
++ },
++ "reason": {
++ "type": "string"
++ }
++ }
++ }
++ ]
++ }
++ },
++ "bot.command": {
++ "version": "1.0.0",
++ "owner": "bot",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/command.v1.json",
++ "title": "BotCommand",
++ "description": "bot.command@1: an operator's command frame on OpenVibe.Bot's operator WebSocket (wss://openvibe.bot/control, after join; server/realtime.js onOperatorCommand). id is the operator's idempotency key (at most 64 characters; Bot mints one when it is missing): a repeated id answers with the first result. Bot's gate stamps the robot, the operator and the role, builds the value from the robot profile's commands (clamped by the owner's limits) and sets the deadline; ms asks for a duration within max_command_ms. Answered by one command_result frame (bot.command-result@1).",
++ "type": "object",
++ "required": [
++ "type",
++ "kind"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "command"
++ },
++ "id": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 64
++ },
++ "kind": {
++ "enum": [
++ "drive",
++ "actuator",
++ "ptz",
++ "say",
++ "display",
++ "halt"
++ ]
++ },
++ "value": {
++ "type": "object"
++ },
++ "ms": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ }
++ },
++ "bot.command-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/command-result.v1.json",
++ "title": "BotCommandResult",
++ "description": "bot.command-result@1: the command_result frame OpenVibe.Bot answers a bot.command@1 with on the operator WebSocket (server/realtime.js). result: ack or nack (the device answered; reason is its fault code, latency_ms the round trip), refused (the gate refused it; code is Bot's refusal code such as bot.estop_latched, bot.cooldown or bot.device_offline, and reason says why), expired (no answer before the deadline) or pending (the same id is still in flight). cached is true when a repeated id is answered from the first result.",
++ "type": "object",
++ "required": [
++ "v",
++ "seq",
++ "ts",
++ "type",
++ "id",
++ "result"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "v": {
++ "const": 1
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "ts": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "type": {
++ "const": "command_result"
++ },
++ "id": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 64
++ },
++ "result": {
++ "enum": [
++ "ack",
++ "nack",
++ "refused",
++ "expired",
++ "pending"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "code": {
++ "type": "string",
++ "pattern": "^bot\\.[a-z_]+$"
++ },
++ "latency_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "cached": {
++ "type": "boolean"
++ }
++ }
++ }
++ },
++ "bot.robot-read-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-read-request.v1.json",
++ "title": "BotRobotReadRequest",
++ "description": "bot.robot-read-request@1: the query of bot.robot.read on OpenVibe.Bot (server/api/v1.js); the routes take no body. owner names whose robots GET /api/v1/robots lists (a service must give it; a person may give only their own subject). limit (1 to 200, default 50) and before (the last audit id seen) page GET …/robots/:id/audit.",
++ "type": "object",
++ "required": [],
++ "additionalProperties": false,
++ "properties": {
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "limit": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 200
++ },
++ "before": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ }
++ },
++ "bot.pair-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/pair-request.v1.json",
++ "title": "BotPairRequest",
++ "description": "bot.pair-request@1: the body of POST /api/v1/pair on OpenVibe.Bot (and the fields of the pair frame on /device): a one-time pairing code (8 Crockford base32 characters, XXXX-XXXX) is its own credential and idempotency. robot narrows the code to one robot (wrong tries are then counted and lock the code); the rest is what the agent declares. With BOT_PAIRING_AUTHORITY=network the codes are Network's and this route answers 410 bot.pairing_moved.",
++ "type": "object",
++ "required": [
++ "code"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "code": {
++ "type": "string",
++ "pattern": "^[0-9A-Za-z]{4}-?[0-9A-Za-z]{4}$"
++ },
++ "robot": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "agent_version": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 40
++ },
++ "device_kind": {
++ "enum": [
++ "onboard",
++ "bridge",
++ "server"
++ ]
++ },
++ "drivers": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "capabilities": {
++ "type": "object"
++ },
++ "name": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80
++ }
++ }
++ }
++ },
++ "bot.pair-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/pair-result.v1.json",
++ "title": "BotPairResult",
++ "description": "bot.pair-result@1: the 201 answers of OpenVibe.Bot's pairing routes (server/api/v1.js). POST /api/v1/pair → { device_id, credential, publish_key, whip_url?, robot_id, profile }; POST /api/v1/devices/bind (a Network node token) → the same without credential. The credential, the publish key and the WHIP URL are shown once and never again. Without OpenRe configured there is no key: video is not_configured instead.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "device_id",
++ "publish_key",
++ "robot_id",
++ "profile"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "device_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "profile": {
++ "anyOf": [
++ {
++ "$ref": "robot-profile.v1.json"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "credential": {
++ "type": "string",
++ "minLength": 1,
++ "description": "The device credential: shown once (Bot keeps only its hash)."
++ },
++ "publish_key": {
++ "type": "string",
++ "minLength": 1,
++ "description": "The WHIP publish key, an ingest key OpenRe issued for the robot's stream: shown once."
++ },
++ "whip_url": {
++ "type": "string",
++ "format": "uri",
++ "description": "Where the device publishes its camera (BOT_WHIP_BASE/<publish key>); it carries the key, so it is shown once too. Absent when BOT_WHIP_BASE is unset."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "device_id",
++ "video",
++ "robot_id",
++ "profile"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "device_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "profile": {
++ "anyOf": [
++ {
++ "$ref": "robot-profile.v1.json"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "credential": {
++ "type": "string",
++ "minLength": 1,
++ "description": "The device credential: shown once (Bot keeps only its hash)."
++ },
++ "video": {
++ "const": "not_configured"
++ }
++ }
++ }
++ ]
++ }
++ },
++ "bot.robot.online": {
++ "version": "1.0.0",
++ "owner": "bot",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/bot.robot.online.v1.json",
++ "title": "BotRobotOnlinePayload",
++ "description": "bot.robot.online v1 (OpenVibe.Bot server/domain/index.js setOnline). A device authenticated on /device and serves the robot; one event per robot it is attached to. Envelope: subject { type: robot, id: <robot id> }, visibility internal, priority important, actor service:bot.",
++ "type": "object",
++ "required": [
++ "robot_id",
++ "device_id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "device_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ }
++ },
++ "bot.robot.offline": {
++ "version": "1.0.0",
++ "owner": "bot",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/bot.robot.offline.v1.json",
++ "title": "BotRobotOfflinePayload",
++ "description": "bot.robot.offline v1 (OpenVibe.Bot server/domain/index.js setOnline). The device missed two heartbeats plus the grace window, or its socket closed; one event per robot it is attached to. Envelope: subject { type: robot, id: <robot id> }, visibility internal, priority important, actor service:bot.",
++ "type": "object",
++ "required": [
++ "robot_id",
++ "device_id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "device_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ }
++ },
++ "bot.estop.set": {
++ "version": "1.0.0",
++ "owner": "bot",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/bot.estop.set.v1.json",
++ "title": "BotEstopSetPayload",
++ "description": "bot.estop.set v1 (OpenVibe.Bot server/domain/index.js writeEstop). The e-stop latched. by is the user subject, the service token's sub or the device id that set it; principal_kind says which. Envelope: subject { type: robot, id: <robot id> }, visibility internal, priority important, actor service:bot.",
++ "type": "object",
++ "required": [
++ "robot_id",
++ "by",
++ "principal_kind"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "principal_kind": {
++ "enum": [
++ "user",
++ "service",
++ "device"
++ ]
++ }
++ }
++ }
++ },
++ "bot.estop.cleared": {
++ "version": "1.0.0",
++ "owner": "bot",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/bot.estop.cleared.v1.json",
++ "title": "BotEstopClearedPayload",
++ "description": "bot.estop.cleared v1 (OpenVibe.Bot server/domain/index.js writeEstop via clearEstop). The owner cleared the latched e-stop (only the owner can); principal_kind is user, and Bot sends by as null. Envelope: subject { type: robot, id: <robot id> }, visibility internal, priority important, actor service:bot.",
++ "type": "object",
++ "required": [
++ "robot_id",
++ "by",
++ "principal_kind"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "principal_kind": {
++ "enum": [
++ "user",
++ "service",
++ "device"
++ ]
++ }
++ }
++ }
++ },
++ "bot.command.refused": {
++ "version": "1.0.0",
++ "owner": "bot",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/bot.command.refused.v1.json",
++ "title": "BotCommandRefusedPayload",
++ "description": "bot.command.refused v1 (OpenVibe.Bot server/domain/index.js auditCommand). The gate refused a command (role, allowlist, limits, policy, e-stop, cooldown, turn, device offline) or a device reported an e-stop for a robot it is not attached to (kind estop_state). reason is Bot's refusal code; ids, kinds and codes only, never the value. Envelope: subject { type: robot, id: <robot id> }, visibility internal, priority important, actor service:bot.",
++ "type": "object",
++ "required": [
++ "robot_id",
++ "kind",
++ "reason",
++ "role"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "robot_id": {
++ "type": "string",
++ "pattern": "^rob_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "kind": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^bot\\.[a-z_]+$"
++ },
++ "role": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ },
"run.job-create-request": {
"version": "1.0.0",
"owner": "run",
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex cde5f36b..f757d677 100644
---- a/generated/openapi/ai.json
++++ b/generated/openapi/ai.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.AI API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/billing.json b/generated/openapi/billing.json
iindex ddd74857..eeb54197 100644
---- a/generated/openapi/billing.json
++++ b/generated/openapi/billing.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Billing API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/blog.json b/generated/openapi/blog.json
iindex 00a7af90..dd7f7dbb 100644
---- a/generated/openapi/blog.json
++++ b/generated/openapi/blog.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Blog API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/bot.json b/generated/openapi/bot.json
iindex 60465e53..3a2cec4c 100644
---- a/generated/openapi/bot.json
++++ b/generated/openapi/bot.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Bot API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for 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"
@@@ -170,6 +170,39 @@
]
}
],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/bot.robot-read-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "owner",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ {
++ "name": "limit",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 200
++ }
++ },
++ {
++ "name": "before",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ ],
"responses": {
"2XX": {
"description": "Success",
@@@ -327,6 +360,9 @@
]
}
],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/bot.robot-read-request.v1"
++ },
"parameters": [
{
"name": "id",
@@@ -335,6 +371,34 @@
"schema": {
"type": "string"
}
++ },
++ {
++ "name": "owner",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ {
++ "name": "limit",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 200
++ }
++ },
++ {
++ "name": "before",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "integer",
++ "minimum": 1
++ }
}
],
"responses": {
@@@ -447,6 +511,9 @@
]
}
],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/bot.robot-read-request.v1"
++ },
"parameters": [
{
"name": "id",
@@@ -455,6 +522,34 @@
"schema": {
"type": "string"
}
++ },
++ {
++ "name": "owner",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ {
++ "name": "limit",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 200
++ }
++ },
++ {
++ "name": "before",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "integer",
++ "minimum": 1
++ }
}
],
"responses": {
@@@ -503,6 +598,9 @@
]
}
],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/bot.robot-read-request.v1"
++ },
"parameters": [
{
"name": "id",
@@@ -511,6 +609,34 @@
"schema": {
"type": "string"
}
++ },
++ {
++ "name": "owner",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ {
++ "name": "limit",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 200
++ }
++ },
++ {
++ "name": "before",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "integer",
++ "minimum": 1
++ }
}
],
"responses": {
@@@ -541,7 +667,7 @@
"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.",
++ "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. The operator WebSocket frames are bot.command@1, answered by bot.command-result@1; a refused command is audited and emits bot.command.refused.",
"tags": [
"bot.robot"
],
@@@ -681,6 +807,9 @@
]
}
],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/bot.robot-read-request.v1"
++ },
"parameters": [
{
"name": "id",
@@@ -689,6 +818,34 @@
"schema": {
"type": "string"
}
++ },
++ {
++ "name": "owner",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ {
++ "name": "limit",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 200
++ }
++ },
++ {
++ "name": "before",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "integer",
++ "minimum": 1
++ }
}
],
"responses": {
@@@ -1237,6 +1394,28 @@
}
]
},
++ "bot.robot-read-request.v1": {
++ "title": "BotRobotReadRequest",
++ "description": "bot.robot-read-request@1: the query of bot.robot.read on OpenVibe.Bot (server/api/v1.js); the routes take no body. owner names whose robots GET /api/v1/robots lists (a service must give it; a person may give only their own subject). limit (1 to 200, default 50) and before (the last audit id seen) page GET …/robots/:id/audit.",
++ "type": "object",
++ "required": [],
++ "additionalProperties": false,
++ "properties": {
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "limit": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 200
++ },
++ "before": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ },
"bot.robot-read-result.v1": {
"title": "BotRobotReadResult",
"description": "bot.robot-read-result@1: answers of bot.robot.read on OpenVibe.Bot (a service token needs no acting subject: only GET …/robots needs ?owner=). GET /api/v1/robots?owner= → { robots }; GET …/robots/:id → { robot, role }; GET …/robots/:id/operators → { operators }; GET …/robots/:id/devices → { devices } (no hashes, with online); GET …/robots/:id/audit?limit=&before= → { audit, next_before } (newest first).",
@@@ -1574,5 +1753,19 @@
"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."
}
}
-- }
++ },
++ "x-openvibe-other-bindings": [
++ {
++ "capability": "bot.robot.control",
++ "binding": "WS /control {type:\"command\"} (bot.command@1, answered by a command_result frame, bot.command-result@1)"
++ },
++ {
++ "capability": "bot.robot.control",
++ "binding": "WS /control {type:\"estop\"} (an owner or operator)"
++ },
++ {
++ "capability": "bot.robot.control",
++ "binding": "WS /control {type:\"estop_clear\"} (the owner only)"
++ }
++ ]
}
ddiff --git a/generated/openapi/chat.json b/generated/openapi/chat.json
iindex 52f5e65c..0dea2735 100644
---- a/generated/openapi/chat.json
++++ b/generated/openapi/chat.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Chat API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/codes.json b/generated/openapi/codes.json
iindex 0f147018..c93185e4 100644
---- a/generated/openapi/codes.json
++++ b/generated/openapi/codes.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Codes API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/community.json b/generated/openapi/community.json
iindex faa6916d..df303120 100644
---- a/generated/openapi/community.json
++++ b/generated/openapi/community.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Community API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/coupons.json b/generated/openapi/coupons.json
iindex f4f1a1e9..fd247878 100644
---- a/generated/openapi/coupons.json
++++ b/generated/openapi/coupons.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Coupons API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/deals.json b/generated/openapi/deals.json
iindex a9880258..c8b5f0bf 100644
---- a/generated/openapi/deals.json
++++ b/generated/openapi/deals.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Deals API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/events.json b/generated/openapi/events.json
iindex 98d65d94..4c265ae1 100644
---- a/generated/openapi/events.json
++++ b/generated/openapi/events.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Events API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/games.json b/generated/openapi/games.json
iindex a4ab76da..07abe2dc 100644
---- a/generated/openapi/games.json
++++ b/generated/openapi/games.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Games API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/host.json b/generated/openapi/host.json
iindex 49d09f7e..7ad19140 100644
---- a/generated/openapi/host.json
++++ b/generated/openapi/host.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Host API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/live.json b/generated/openapi/live.json
iindex 58223de1..068c1d3b 100644
---- a/generated/openapi/live.json
++++ b/generated/openapi/live.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Live API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/media.json b/generated/openapi/media.json
iindex 88cc2525..94885855 100644
---- a/generated/openapi/media.json
++++ b/generated/openapi/media.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Media API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/network.json b/generated/openapi/network.json
iindex 8197c1ce..3c093154 100644
---- a/generated/openapi/network.json
++++ b/generated/openapi/network.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Network API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/news.json b/generated/openapi/news.json
iindex 86315a07..9f6c84c9 100644
---- a/generated/openapi/news.json
++++ b/generated/openapi/news.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.News API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/openre.json b/generated/openapi/openre.json
iindex bb9ede62..efc0aeb0 100644
---- a/generated/openapi/openre.json
++++ b/generated/openapi/openre.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenRe.Stream API",
-- "version": "0.92.0",
-- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/reviews.json b/generated/openapi/reviews.json
iindex 545e3458..aab3b314 100644
---- a/generated/openapi/reviews.json
++++ b/generated/openapi/reviews.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Reviews API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/search.json b/generated/openapi/search.json
iindex b9975886..309cc4c3 100644
---- a/generated/openapi/search.json
++++ b/generated/openapi/search.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Search API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/sources.json b/generated/openapi/sources.json
iindex d8251db6..fc86220b 100644
---- a/generated/openapi/sources.json
++++ b/generated/openapi/sources.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Sources API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/space.json b/generated/openapi/space.json
iindex 6f160968..e9f52ca6 100644
---- a/generated/openapi/space.json
++++ b/generated/openapi/space.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Space API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tips.json b/generated/openapi/tips.json
iindex f7237c01..ce603726 100644
---- a/generated/openapi/tips.json
++++ b/generated/openapi/tips.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tips API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tools.json b/generated/openapi/tools.json
iindex 43c9982a..c115e6f9 100644
---- a/generated/openapi/tools.json
++++ b/generated/openapi/tools.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tools API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/trade.json b/generated/openapi/trade.json
iindex 6d6841fc..83ed38eb 100644
---- a/generated/openapi/trade.json
++++ b/generated/openapi/trade.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Trade API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/vip.json b/generated/openapi/vip.json
iindex 32259056..8b7ec493 100644
---- a/generated/openapi/vip.json
++++ b/generated/openapi/vip.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.VIP API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/wiki.json b/generated/openapi/wiki.json
iindex 8d07fa67..4b317e20 100644
---- a/generated/openapi/wiki.json
++++ b/generated/openapi/wiki.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Wiki API",
-- "version": "0.92.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.92.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.93.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.93.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 48225dbe..7c42594c 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -52751,6 +52751,519 @@ export type BotDeviceConnectResult =
device: BotDevice;
};
++/** [email protected] (owner: bot) */
++/**
++ * bot.robot-profile@1 (ADR-043 decision 7): a robot profile as OpenVibe.Bot ships it (server/profiles/*.json) and serves it (GET /api/v1/profiles, /profiles/:id; validateProfile in server/profiles/index.js). The panel is a function of the profile. Capability and widget names are open strings that Bot checks against registries in code, so a new one never needs a schema change. As served, vendor, kind, description, variants and camera are null when the file leaves them out, limits carry their defaults (max_speed 1, max_turn 1, max_command_ms 300, heartbeat_ms 1000) and commands always include halt. mapping is the driver's own hardware map, open beyond driver.
++ */
++export interface BotRobotProfile {
++ id: string;
++ version: number;
++ name: string;
++ vendor?: string | null;
++ kind?: "onboard" | "bridge" | "server" | null;
++ description?: string | null;
++ /**
++ * @minItems 1
++ */
++ capabilities: [string, ...string[]];
++ variants?: {
++ [k: string]: {} | undefined;
++ } | null;
++ mapping: {
++ driver: "pca9685" | "ads7830" | "cozmo" | "onvif" | "sim";
++ plugin?: string;
++ };
++ commands?: {
++ drive?: {
++ axes: {
++ /**
++ * @minItems 2
++ * @maxItems 2
++ */
++ throttle?: [number, number];
++ /**
++ * @minItems 2
++ * @maxItems 2
++ */
++ steer?: [number, number];
++ /**
++ * @minItems 2
++ * @maxItems 2
++ */
++ x?: [number, number];
++ /**
++ * @minItems 2
++ * @maxItems 2
++ */
++ y?: [number, number];
++ /**
++ * @minItems 2
++ * @maxItems 2
++ */
++ rotation?: [number, number];
++ };
++ };
++ ptz?: {
++ axes: {
++ /**
++ * @minItems 2
++ * @maxItems 2
++ */
++ pan?: [number, number];
++ /**
++ * @minItems 2
++ * @maxItems 2
++ */
++ tilt?: [number, number];
++ /**
++ * @minItems 2
++ * @maxItems 2
++ */
++ zoom?: [number, number];
++ };
++ };
++ actuator?: {
++ names: {
++ [k: string]:
++ | (
++ | {
++ type: "number";
++ /**
++ * @minItems 2
++ * @maxItems 2
++ */
++ range: [number, number];
++ }
++ | {
++ type: "rgb";
++ count?: number;
++ }
++ | {
++ type: "tone";
++ /**
++ * @minItems 2
++ * @maxItems 2
++ */
++ hz: [number, number];
++ }
++ | {
++ type: "bool";
++ }
++ )
++ | undefined;
++ };
++ };
++ say?: {
++ max_chars?: number;
++ };
++ display?: {
++ /**
++ * @minItems 1
++ */
++ modes: ["text" | "face" | "image_png_b64", ...("text" | "face" | "image_png_b64")[]];
++ faces?: string[];
++ max_chars?: number;
++ };
++ halt?: {};
++ };
++ /**
++ * @minItems 1
++ */
++ widgets: [
++ {
++ type: string;
++ capability?: string;
++ label?: string;
++ order?: number;
++ command?: {
++ kind: "drive" | "actuator" | "ptz" | "say" | "display" | "halt";
++ /**
++ * @minItems 1
++ */
++ names?: [string, ...string[]];
++ };
++ },
++ ...{
++ type: string;
++ capability?: string;
++ label?: string;
++ order?: number;
++ command?: {
++ kind: "drive" | "actuator" | "ptz" | "say" | "display" | "halt";
++ /**
++ * @minItems 1
++ */
++ names?: [string, ...string[]];
++ };
++ }[]
++ ];
++ camera?: {
++ transport?: "whip" | "onvif" | "rtsp";
++ resolution?: string;
++ } | null;
++ limits?: {
++ max_speed?: number;
++ max_turn?: number;
++ max_command_ms?: number;
++ heartbeat_ms?: number;
++ };
++}
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.device-message@1 (ADR-043 decision 5): one frame on OpenVibe.Bot's device WebSocket (wss://openvibe.bot/device), JSON with v, seq and ts on every frame. Server to device: hello, config, command, estop, heartbeat_ack, error, paired (after a pair frame), rotate (a Network-paired machine is asked to rotate its credential with Network). Device to server: pair (the only frame an unauthenticated socket may send), status, telemetry, ack, nack, heartbeat, reauth (Network-paired machines), estop_state. The job frames on the same socket are platform.job-frame@1, not this contract. This duplicates two implementations that must not drift: OpenVibe.Bot server/realtime.js (the sender and reader in Bot) and OpenVibe.Node internal/protocol/protocol.go (the Go structs of the device agent).
++ */
++export type BotDeviceMessage =
++ | {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "hello";
++ session_id: string;
++ device_id: string;
++ robot_ids: string[];
++ server_time: string;
++ }
++ | {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "config";
++ heartbeat_ms: number;
++ limits: {
++ max_speed?: number;
++ max_turn?: number;
++ max_command_ms?: number;
++ heartbeat_ms?: number;
++ };
++ allowed_commands: ("drive" | "actuator" | "ptz" | "say" | "display" | "halt")[];
++ estop_latched: boolean;
++ }
++ | {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "command";
++ id: string;
++ ref?: string;
++ kind: "drive" | "actuator" | "ptz" | "say" | "display" | "halt";
++ value?: {};
++ /**
++ * Absolute deadline, epoch milliseconds; the device stops the motion at it.
++ */
++ deadline_ms?: number;
++ operator?: {
++ subject?: string | null;
++ role: string;
++ };
++ robot_id?: string;
++ /**
++ * OpenVibe.Node only: the driver to send it to.
++ */
++ target?: string;
++ }
++ | {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "estop";
++ latched: boolean;
++ by?: string | null;
++ at?: string;
++ }
++ | {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "heartbeat_ack";
++ t?: number;
++ echo?: number | null;
++ server_time?: string;
++ }
++ | {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "error";
++ code: string;
++ detail?: string | null;
++ }
++ | {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "paired";
++ device_id: string;
++ /**
++ * Shown once.
++ */
++ credential: string;
++ /**
++ * Shown once.
++ */
++ publish_key?: string;
++ /**
++ * Carries the publish key: shown once.
++ */
++ whip_url?: string;
++ video?: "not_configured";
++ robot_ids: string[];
++ profile_id?: string | null;
++ profile?: BotRobotProfile | null;
++ }
++ | {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "rotate";
++ }
++ | {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "pair";
++ code: string;
++ robot?: string | null;
++ agent_version?: string | null;
++ device_kind?: "onboard" | "bridge" | "server";
++ drivers?: string[];
++ capabilities?: {};
++ name?: string | null;
++ }
++ | {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "status";
++ firmware?: string;
++ capabilities?: {};
++ agent_version?: string;
++ device_kind?: "onboard" | "bridge" | "server";
++ os?: string;
++ arch?: string;
++ drivers?: (
++ | string
++ | {
++ name: string;
++ driver?: string;
++ version?: string;
++ state: string;
++ capabilities?: {};
++ }
++ )[];
++ faults?: {
++ code: string;
++ driver?: string;
++ message?: string;
++ }[];
++ estop_latched?: boolean;
++ local_stop?: boolean;
++ video?: string;
++ }
++ | {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "telemetry";
++ battery?:
++ | number
++ | {
++ volts?: number;
++ percent?: number;
++ };
++ voltage?: number;
++ rssi?: number;
++ sensors?: {};
++ events?: {
++ name: string;
++ driver?: string;
++ ts: number;
++ fields?: {};
++ }[];
++ }
++ | {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "ack";
++ id: string;
++ latency_ms?: number;
++ }
++ | {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "nack";
++ id: string;
++ fault_code: string;
++ message?: string;
++ }
++ | {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "heartbeat";
++ t?: number;
++ rtt_ms?: number;
++ }
++ | {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "reauth";
++ token: string;
++ }
++ | {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "estop_state";
++ latched: boolean;
++ by?: string;
++ at?: string;
++ robot_id?: string;
++ local_stop?: boolean;
++ reason?: string;
++ };
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.command@1: an operator's command frame on OpenVibe.Bot's operator WebSocket (wss://openvibe.bot/control, after join; server/realtime.js onOperatorCommand). id is the operator's idempotency key (at most 64 characters; Bot mints one when it is missing): a repeated id answers with the first result. Bot's gate stamps the robot, the operator and the role, builds the value from the robot profile's commands (clamped by the owner's limits) and sets the deadline; ms asks for a duration within max_command_ms. Answered by one command_result frame (bot.command-result@1).
++ */
++export interface BotCommand {
++ v?: 1;
++ seq?: number;
++ ts?: number;
++ type: "command";
++ id?: string;
++ kind: "drive" | "actuator" | "ptz" | "say" | "display" | "halt";
++ value?: {};
++ ms?: number;
++}
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.command-result@1: the command_result frame OpenVibe.Bot answers a bot.command@1 with on the operator WebSocket (server/realtime.js). result: ack or nack (the device answered; reason is its fault code, latency_ms the round trip), refused (the gate refused it; code is Bot's refusal code such as bot.estop_latched, bot.cooldown or bot.device_offline, and reason says why), expired (no answer before the deadline) or pending (the same id is still in flight). cached is true when a repeated id is answered from the first result.
++ */
++export interface BotCommandResult {
++ v: 1;
++ seq: number;
++ ts: number;
++ type: "command_result";
++ id: string;
++ result: "ack" | "nack" | "refused" | "expired" | "pending";
++ reason?: string | null;
++ code?: string;
++ latency_ms?: number;
++ cached?: boolean;
++}
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.robot-read-request@1: the query of bot.robot.read on OpenVibe.Bot (server/api/v1.js); the routes take no body. owner names whose robots GET /api/v1/robots lists (a service must give it; a person may give only their own subject). limit (1 to 200, default 50) and before (the last audit id seen) page GET …/robots/:id/audit.
++ */
++export interface BotRobotReadRequest {
++ owner?: string;
++ limit?: number;
++ before?: number;
++}
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.pair-request@1: the body of POST /api/v1/pair on OpenVibe.Bot (and the fields of the pair frame on /device): a one-time pairing code (8 Crockford base32 characters, XXXX-XXXX) is its own credential and idempotency. robot narrows the code to one robot (wrong tries are then counted and lock the code); the rest is what the agent declares. With BOT_PAIRING_AUTHORITY=network the codes are Network's and this route answers 410 bot.pairing_moved.
++ */
++export interface BotPairRequest {
++ code: string;
++ robot?: string | null;
++ agent_version?: string | null;
++ device_kind?: "onboard" | "bridge" | "server";
++ drivers?: string[];
++ capabilities?: {};
++ name?: string | null;
++}
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.pair-result@1: the 201 answers of OpenVibe.Bot's pairing routes (server/api/v1.js). POST /api/v1/pair → { device_id, credential, publish_key, whip_url?, robot_id, profile }; POST /api/v1/devices/bind (a Network node token) → the same without credential. The credential, the publish key and the WHIP URL are shown once and never again. Without OpenRe configured there is no key: video is not_configured instead.
++ */
++export type BotPairResult =
++ | {
++ device_id: string;
++ robot_id: string;
++ profile: BotRobotProfile | null;
++ /**
++ * The device credential: shown once (Bot keeps only its hash).
++ */
++ credential?: string;
++ /**
++ * The WHIP publish key, an ingest key OpenRe issued for the robot's stream: shown once.
++ */
++ publish_key: string;
++ /**
++ * Where the device publishes its camera (BOT_WHIP_BASE/<publish key>); it carries the key, so it is shown once too. Absent when BOT_WHIP_BASE is unset.
++ */
++ whip_url?: string;
++ }
++ | {
++ device_id: string;
++ robot_id: string;
++ profile: BotRobotProfile | null;
++ /**
++ * The device credential: shown once (Bot keeps only its hash).
++ */
++ credential?: string;
++ video: "not_configured";
++ };
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.robot.online v1 (OpenVibe.Bot server/domain/index.js setOnline). A device authenticated on /device and serves the robot; one event per robot it is attached to. Envelope: subject { type: robot, id: <robot id> }, visibility internal, priority important, actor service:bot.
++ */
++export interface BotRobotOnlinePayload {
++ robot_id: string;
++ device_id: string;
++}
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.robot.offline v1 (OpenVibe.Bot server/domain/index.js setOnline). The device missed two heartbeats plus the grace window, or its socket closed; one event per robot it is attached to. Envelope: subject { type: robot, id: <robot id> }, visibility internal, priority important, actor service:bot.
++ */
++export interface BotRobotOfflinePayload {
++ robot_id: string;
++ device_id: string;
++}
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.estop.set v1 (OpenVibe.Bot server/domain/index.js writeEstop). The e-stop latched. by is the user subject, the service token's sub or the device id that set it; principal_kind says which. Envelope: subject { type: robot, id: <robot id> }, visibility internal, priority important, actor service:bot.
++ */
++export interface BotEstopSetPayload {
++ robot_id: string;
++ by: string | null;
++ principal_kind: "user" | "service" | "device";
++}
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.estop.cleared v1 (OpenVibe.Bot server/domain/index.js writeEstop via clearEstop). The owner cleared the latched e-stop (only the owner can); principal_kind is user, and Bot sends by as null. Envelope: subject { type: robot, id: <robot id> }, visibility internal, priority important, actor service:bot.
++ */
++export interface BotEstopClearedPayload {
++ robot_id: string;
++ by: string | null;
++ principal_kind: "user" | "service" | "device";
++}
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.command.refused v1 (OpenVibe.Bot server/domain/index.js auditCommand). The gate refused a command (role, allowlist, limits, policy, e-stop, cooldown, turn, device offline) or a device reported an e-stop for a robot it is not attached to (kind estop_state). reason is Bot's refusal code; ids, kinds and codes only, never the value. Envelope: subject { type: robot, id: <robot id> }, visibility internal, priority important, actor service:bot.
++ */
++export interface BotCommandRefusedPayload {
++ robot_id: string;
++ kind: string | null;
++ reason: string | null;
++ role: string | null;
++}
++
/** [email protected] (owner: run) */
/**
* run.job-create-request@1 (PLANNED; plan T14 R1, ADR-034): the body of POST /api/v1/jobs on OpenVibe.Run (capability run.job.submit). The job belongs to the project the caller's token names. CLASS is a platform.runtime-class@1 name; Run accepts any of them but places a job only on a node that advertises worker:<class> (platform.resource-offer@1), so a class no node runs today (browser, linux, desktop, gpu) ends expired with run.job.unplaceable. ARTIFACT is the pre-registered artifact to run (name and one exact version), required for function and code, as platform.job@1 carries it. ARGS is the artifact's declared input as JSON (at most 512 KiB serialized, else 413). INPUTS are files placed in the job directory before the process starts, each an OpenVibe.Media object pinned by its sha256: a size or digest mismatch fails the job (run.job.input_digest_mismatch) before the process ever runs; Run never fetches a caller-chosen URL. LIMITS are hard caps merged stricter with the node's own (platform.job@1 limits); a request over the project's tier cap is refused 422 run.limits.exceeded, never silently lowered. EGRESS is the network the job may reach, the values of OpenVibe.Node's worker.egress, carried per job as the same-named platform.job@1 net: none (default; no network), public or openvibe-only. A job asking for public or openvibe-only is placed only on a node whose worker.egress allows it; a worker whose host policy is stricter runs the stricter one. REQUIREMENTS (platform.workload-requirements@1) narrow placement; absent means { kind: run.<class>, mobility: job, latency_class: background, objective: balanced }. IDEMPOTENCY_KEY: a repeat from the same project within 24 h with an identical body answers the first job (200, created false); with a different body 409 run.idempotency.conflict.
ddiff --git a/manifests/capabilities/bot.robot.control.json b/manifests/capabilities/bot.robot.control.json
iindex 27eb228e..2ca96409 100644
---- a/manifests/capabilities/bot.robot.control.json
++++ b/manifests/capabilities/bot.robot.control.json
@@@ -5,7 +5,7 @@
"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.",
++ "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. The operator WebSocket frames are bot.command@1, answered by bot.command-result@1; a refused command is audited and emits bot.command.refused.",
"permissions": [
"bot:robot:control"
],
@@@ -14,10 +14,15 @@
],
"quotaClass": "bot-control",
"events": [
-- "bot.estop.set"
++ "bot.estop.set",
++ "bot.estop.cleared",
++ "bot.command.refused"
],
"implementedBy": [
-- "POST /api/v1/robots/:id/estop"
++ "POST /api/v1/robots/:id/estop",
++ "WS /control {type:\"command\"} (bot.command@1, answered by a command_result frame, bot.command-result@1)",
++ "WS /control {type:\"estop\"} (an owner or operator)",
++ "WS /control {type:\"estop_clear\"} (the owner only)"
],
"inputSchema": "common.no-body@1",
"outputSchema": "bot.robot-control-result@1"
ddiff --git a/manifests/capabilities/bot.robot.read.json b/manifests/capabilities/bot.robot.read.json
iindex 504fa8eb..90ac2f37 100644
---- a/manifests/capabilities/bot.robot.read.json
++++ b/manifests/capabilities/bot.robot.read.json
@@@ -20,6 +20,6 @@
"GET /api/v1/robots/:id/devices",
"GET /api/v1/robots/:id/audit"
],
-- "inputSchema": "common.no-body@1",
++ "inputSchema": "bot.robot-read-request@1",
"outputSchema": "bot.robot-read-result@1"
}
ddiff --git a/manifests/services/bot.json b/manifests/services/bot.json
iindex c44a9ff7..44f8f3d9 100644
---- a/manifests/services/bot.json
++++ b/manifests/services/bot.json
@@@ -82,5 +82,5 @@
"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."
++ "notes": "Robot and device control service on openvibe-ovh since 2026-10-02; the public site uses HTTPS while health and readiness are loopback-only. Non-HTTP bindings: the device WebSocket /device carries bot.device-message@1 frames (and platform.job-frame@1 jobs); the operator WebSocket /control carries bot.command@1, answered by bot.command-result@1."
}
ddiff --git a/package-lock.json b/package-lock.json
iindex 35ffb883..9149e510 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-contracts",
-- "version": "0.92.0",
++ "version": "0.93.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-contracts",
-- "version": "0.92.0",
++ "version": "0.93.0",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 1445f5ca..1719ff8e 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.92.0",
++ "version": "0.93.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
ddiff --git a/test/bot-manifest.test.js b/test/bot-manifest.test.js
nnew file mode 100644
iindex 00000000..fa001300
---- /dev/null
++++ b/test/bot-manifest.test.js
@@@ -0,0 +1,20 @@
++'use strict';
++// OpenVibe.Bot (ADR-043) declares its capabilities and events in its own STATUS.json; the manifest here must
++// list the same ones, and every bot.* event it produces has a payload contract. A drift between the repositories
++// fails here first.
++const assert = require('assert');
++const c = require('..');
++
++// OpenVibe.Bot STATUS.json "capabilities" and "events" at origin/main 155accc (2026-10-04).
++const BOT_CAPABILITIES = ['bot.robot.read', 'bot.robot.manage', 'bot.robot.control', 'bot.device.connect'];
++const BOT_EVENTS = ['bot.robot.online', 'bot.robot.offline', 'bot.estop.set', 'bot.estop.cleared', 'bot.command.refused'];
++
++const bot = c.services.get('bot');
++assert.deepStrictEqual([...bot.capabilities].sort(), [...BOT_CAPABILITIES].sort(), 'bot manifest capabilities are Bot STATUS.json capabilities');
++assert.deepStrictEqual([...bot.eventsProduced].sort(), [...BOT_EVENTS].sort(), 'bot manifest eventsProduced are Bot STATUS.json events');
++for (const t of BOT_EVENTS) assert.ok(c.resolve(`${t}@1`), `${t} has a payload contract`);
++
++// The device and operator WebSocket frames are contracts too, and a command frame is refused for an unknown kind.
++for (const id of ['bot.device-message@1', 'bot.command@1', 'bot.command-result@1', 'bot.robot-profile@1']) assert.ok(c.resolve(id), `${id} resolves`);
++assert.ok(!c.validate('bot.command@1', { type: 'command', kind: 'fly' }).valid, 'an unknown command kind is refused');
++console.log('bot manifest: all checks passed');
ddiff --git a/test/run.js b/test/run.js
iindex 7211e2ab..1b6bce43 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -899,6 +899,8 @@ sub(path.join(__dirname, 'openapi.test.js'));
sub(path.join(__dirname, 'loyalty-policy.test.js'));
// Presence is ephemeral, in Chat's delivery plane; no Events topic (ADR-005 amendment 1).
sub(path.join(__dirname, 'presence-policy.test.js'));
++// OpenVibe.Bot's manifest matches the capabilities and events Bot declares (ADR-043).
++sub(path.join(__dirname, 'bot-manifest.test.js'));
// Every OpenVibe.Sites catalog domain has one home in Contracts (plan T11 lane D).
sub(path.join(__dirname, 'product-catalog.test.js'));
// Named object zones: identity, retired names, list limits and per-zone usage (ADR-031 amendment 2026-10-02).