openvibers/openvibe.contracts Commits
Add Bot profile, device protocol, command, pairing and event contracts
Changes
@@@ -4,6 +4,32 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) thfrom `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`.nnew file mode 100644@@@ -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"++ }++ }++}nnew file mode 100644@@@ -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++ }++ }++}nnew file mode 100644@@@ -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"++ }++ }++ }++ ]++}nnew file mode 100644@@@ -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++ }++ }++}nnew file mode 100644@@@ -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"++ }++ }++ }++ ]++}nnew file mode 100644@@@ -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++ }++ }++ }++ }++}nnew file mode 100644@@@ -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++ }++ }++}@@@ -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",nnew file mode 100644@@@ -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"++ ]++ }++ }++}nnew file mode 100644@@@ -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"++ ]++ }++ }++}nnew file mode 100644@@@ -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"++ ]++ }++ }++}nnew file mode 100644@@@ -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}$"++ }++ }++}nnew file mode 100644@@@ -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}$"++ }++ }++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "v": 1,++ "seq": 17,++ "ts": 1759600000017,++ "type": "command_result",++ "result": "ack"++}nnew file mode 100644@@@ -0,0 +1,8 @@++{++ "v": 1,++ "seq": 16,++ "ts": 1759600000016,++ "type": "command_result",++ "id": "op-7",++ "result": "ok"++}nnew file mode 100644@@@ -0,0 +1,10 @@++{++ "v": 1,++ "seq": 13,++ "ts": 1759600000013,++ "type": "command_result",++ "id": "op-7",++ "result": "ack",++ "reason": null,++ "latency_ms": 42++}nnew file mode 100644@@@ -0,0 +1,9 @@++{++ "v": 1,++ "seq": 15,++ "ts": 1759600000015,++ "type": "command_result",++ "id": "op-9",++ "result": "pending",++ "cached": true++}nnew file mode 100644@@@ -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"++}nnew file mode 100644@@@ -0,0 +1,9 @@++{++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "kind": "drive",++ "reason": "bot.cooldown",++ "role": "operator",++ "value": {++ "throttle": 1++ }++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "kind": "drive",++ "reason": "bot.cooldown",++ "role": "operator"++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "kind": "estop_state",++ "reason": "bot.forbidden",++ "role": null++}nnew file mode 100644@@@ -0,0 +1,9 @@++{++ "type": "command",++ "kind": "drive",++ "value": {},++ "operator": {++ "subject": "usr_01J8Z4P7T3W9Y1AC5N8R0S4V6X",++ "role": "owner"++ }++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "type": "command",++ "kind": "self_destruct"++}nnew file mode 100644@@@ -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++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "type": "command",++ "kind": "halt"++}nnew file mode 100644@@@ -0,0 +1,8 @@++{++ "v": 1,++ "seq": 7,++ "ts": 1759600000007,++ "type": "ack",++ "id": "cmd_1",++ "result": "ok"++}nnew file mode 100644@@@ -0,0 +1,8 @@++{++ "v": 1,++ "seq": 8,++ "ts": 1759600000008,++ "type": "job_started",++ "id": "job_01J8Z4Q9V5X1Z3BD6P9S2T5W7Y",++ "started_ms": 1++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "v": 1,++ "ts": 1759600000000,++ "type": "heartbeat",++ "t": 5++}nnew file mode 100644@@@ -0,0 +1,8 @@++{++ "v": 1,++ "seq": 3,++ "ts": 1759600000003,++ "type": "command",++ "id": "cmd_1",++ "kind": "fly"++}nnew file mode 100644@@@ -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"++}nnew file mode 100644@@@ -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++}nnew file mode 100644@@@ -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"++}nnew file mode 100644@@@ -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"++}nnew file mode 100644@@@ -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"++}nnew file mode 100644@@@ -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++ }++ }++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "v": 1,++ "seq": 9,++ "ts": 1759600000009,++ "type": "rotate"++}nnew file mode 100644@@@ -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++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "by": null++}nnew file mode 100644@@@ -0,0 +1,5 @@++{++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "by": null,++ "principal_kind": "user"++}nnew file mode 100644@@@ -0,0 +1,5 @@++{++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "by": "usr_01J8Z4P7T3W9Y1AC5N8R0S4V6X",++ "principal_kind": "robot"++}nnew file mode 100644@@@ -0,0 +1,5 @@++{++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "by": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W",++ "principal_kind": "device"++}nnew file mode 100644@@@ -0,0 +1,5 @@++{++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "by": "usr_01J8Z4P7T3W9Y1AC5N8R0S4V6X",++ "principal_kind": "user"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "code": "K7QM-3XRT",++ "device_kind": "cloud"++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "robot": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X"++}nnew file mode 100644@@@ -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"++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "code": "K7QM3XRT"++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W",++ "credential_hash": "x",++ "video": "not_configured",++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "profile": null++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W",++ "credential": "example-credential-shown-once",++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "profile": null++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W",++ "video": "not_configured",++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "profile": null++}nnew file mode 100644@@@ -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++ }++ }++}nnew file mode 100644@@@ -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++ }++}nnew file mode 100644@@@ -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++ }++}nnew file mode 100644@@@ -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++ }++}nnew file mode 100644@@@ -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++ }++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "limit": 500++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "owner": "usr_01J8Z4P7T3W9Y1AC5N8R0S4V6X",++ "include": "devices"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "limit": 50,++ "before": 1200++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "owner": "usr_01J8Z4P7T3W9Y1AC5N8R0S4V6X"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "robot_id": "robot-1",++ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W"++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W"++}@@@ -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",@@@ -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"@@@ -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"@@@ -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"@@@ -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)"++ }++ ]}@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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"@@@ -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.@@@ -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"@@@ -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"}@@@ -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."}@@@ -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",@@@ -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",nnew file mode 100644@@@ -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');@@@ -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).
@@ -4,6 +4,32 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
@@ -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,
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`).
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
## 0.92.0 — 2026-10-04
`platform.job@1` gains `inputs` (Media objects pinned by sha256) and the `net` values `none`, `public`, `openvibe-only`.
`platform.job@1` gains `inputs` (Media objects pinned by sha256) and the `net` values `none`, `public`, `openvibe-only`.
new file mode 100644
new file mode 100644
@@ -0,0 +1,63 @@
@@ -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"
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,50 @@
@@ -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
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,889 @@
@@ -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"
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,58 @@
@@ -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
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,91 @@
@@ -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"
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,424 @@
@@ -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
+ }
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -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
+ }
+ }
+}
@@ -5740,6 +5740,126 @@
@@ -5740,6 +5740,126 @@
"compatibility": "backward",
"compatibility": "backward",
"status": "active"
"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",
"id": "run.job-create-request",
"version": "1.0.0",
"version": "1.0.0",
new file mode 100644
new file mode 100644
@@ -0,0 +1,39 @@
@@ -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"
+ ]
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,32 @@
@@ -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"
+ ]
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,32 @@
@@ -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"
+ ]
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -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}$"
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -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}$"
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "v": 1,
+ "seq": 17,
+ "ts": 1759600000017,
+ "type": "command_result",
+ "result": "ack"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+{
+ "v": 1,
+ "seq": 16,
+ "ts": 1759600000016,
+ "type": "command_result",
+ "id": "op-7",
+ "result": "ok"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
+{
+ "v": 1,
+ "seq": 13,
+ "ts": 1759600000013,
+ "type": "command_result",
+ "id": "op-7",
+ "result": "ack",
+ "reason": null,
+ "latency_ms": 42
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+{
+ "v": 1,
+ "seq": 15,
+ "ts": 1759600000015,
+ "type": "command_result",
+ "id": "op-9",
+ "result": "pending",
+ "cached": true
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -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"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+{
+ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "kind": "drive",
+ "reason": "bot.cooldown",
+ "role": "operator",
+ "value": {
+ "throttle": 1
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "kind": "drive",
+ "reason": "bot.cooldown",
+ "role": "operator"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "kind": "estop_state",
+ "reason": "bot.forbidden",
+ "role": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+{
+ "type": "command",
+ "kind": "drive",
+ "value": {},
+ "operator": {
+ "subject": "usr_01J8Z4P7T3W9Y1AC5N8R0S4V6X",
+ "role": "owner"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "type": "command",
+ "kind": "self_destruct"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,13 @@
@@ -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
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "type": "command",
+ "kind": "halt"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+{
+ "v": 1,
+ "seq": 7,
+ "ts": 1759600000007,
+ "type": "ack",
+ "id": "cmd_1",
+ "result": "ok"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+{
+ "v": 1,
+ "seq": 8,
+ "ts": 1759600000008,
+ "type": "job_started",
+ "id": "job_01J8Z4Q9V5X1Z3BD6P9S2T5W7Y",
+ "started_ms": 1
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "v": 1,
+ "ts": 1759600000000,
+ "type": "heartbeat",
+ "t": 5
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+{
+ "v": 1,
+ "seq": 3,
+ "ts": 1759600000003,
+ "type": "command",
+ "id": "cmd_1",
+ "kind": "fly"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -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"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -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
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,12 @@
@@ -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"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -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"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,28 @@
@@ -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"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,70 @@
@@ -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
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "v": 1,
+ "seq": 9,
+ "ts": 1759600000009,
+ "type": "rotate"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -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
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "by": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+{
+ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "by": null,
+ "principal_kind": "user"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+{
+ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "by": "usr_01J8Z4P7T3W9Y1AC5N8R0S4V6X",
+ "principal_kind": "robot"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+{
+ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "by": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W",
+ "principal_kind": "device"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+{
+ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "by": "usr_01J8Z4P7T3W9Y1AC5N8R0S4V6X",
+ "principal_kind": "user"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "code": "K7QM-3XRT",
+ "device_kind": "cloud"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "robot": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -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"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "code": "K7QM3XRT"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W",
+ "credential_hash": "x",
+ "video": "not_configured",
+ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "profile": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W",
+ "credential": "example-credential-shown-once",
+ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "profile": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W",
+ "video": "not_configured",
+ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "profile": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,137 @@
@@ -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
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,90 @@
@@ -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
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,106 @@
@@ -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
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,130 @@
@@ -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
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,57 @@
@@ -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
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "limit": 500
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "owner": "usr_01J8Z4P7T3W9Y1AC5N8R0S4V6X",
+ "include": "devices"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "limit": 50,
+ "before": 1200
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "owner": "usr_01J8Z4P7T3W9Y1AC5N8R0S4V6X"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "robot_id": "robot-1",
+ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "robot_id": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "device_id": "dev_01J8Z4N5S2V8X0ZB4M7Q9R3T5W"
+}
@@ -65520,6 +65520,1812 @@
@@ -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": {
"run.job-create-request": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "run",
"owner": "run",
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.AI API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Billing API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Blog API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Bot API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -170,6 +170,39 @@
@@ -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": {
"responses": {
"2XX": {
"2XX": {
"description": "Success",
"description": "Success",
@@ -327,6 +360,9 @@
@@ -327,6 +360,9 @@
]
]
}
}
],
],
+ "x-openvibe-input": {
+ "$ref": "#/components/schemas/bot.robot-read-request.v1"
+ },
"parameters": [
"parameters": [
{
{
"name": "id",
"name": "id",
@@ -335,6 +371,34 @@
@@ -335,6 +371,34 @@
"schema": {
"schema": {
"type": "string"
"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": {
"responses": {
@@ -447,6 +511,9 @@
@@ -447,6 +511,9 @@
]
]
}
}
],
],
+ "x-openvibe-input": {
+ "$ref": "#/components/schemas/bot.robot-read-request.v1"
+ },
"parameters": [
"parameters": [
{
{
"name": "id",
"name": "id",
@@ -455,6 +522,34 @@
@@ -455,6 +522,34 @@
"schema": {
"schema": {
"type": "string"
"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": {
"responses": {
@@ -503,6 +598,9 @@
@@ -503,6 +598,9 @@
]
]
}
}
],
],
+ "x-openvibe-input": {
+ "$ref": "#/components/schemas/bot.robot-read-request.v1"
+ },
"parameters": [
"parameters": [
{
{
"name": "id",
"name": "id",
@@ -511,6 +609,34 @@
@@ -511,6 +609,34 @@
"schema": {
"schema": {
"type": "string"
"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": {
"responses": {
@@ -541,7 +667,7 @@
@@ -541,7 +667,7 @@
"post": {
"post": {
"operationId": "post_api_v1_robots_id_estop",
"operationId": "post_api_v1_robots_id_estop",
"summary": "Physically control a robot with a Network service token.",
"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": [
"tags": [
"bot.robot"
"bot.robot"
],
],
@@ -681,6 +807,9 @@
@@ -681,6 +807,9 @@
]
]
}
}
],
],
+ "x-openvibe-input": {
+ "$ref": "#/components/schemas/bot.robot-read-request.v1"
+ },
"parameters": [
"parameters": [
{
{
"name": "id",
"name": "id",
@@ -689,6 +818,34 @@
@@ -689,6 +818,34 @@
"schema": {
"schema": {
"type": "string"
"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": {
"responses": {
@@ -1237,6 +1394,28 @@
@@ -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": {
"bot.robot-read-result.v1": {
"title": "BotRobotReadResult",
"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).",
"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 @@
@@ -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."
"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)"
+ }
+ ]
}
}
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Chat API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Codes API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Community API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Coupons API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Deals API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Events API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Games API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Host API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Live API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Media API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Network API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.News API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenRe.Stream API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Reviews API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Search API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Sources API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Space API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Tips API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Tools API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Trade API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.VIP API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Wiki API",
"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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -52751,6 +52751,519 @@ export type BotDeviceConnectResult =
@@ -52751,6 +52751,519 @@ export type BotDeviceConnectResult =
device: BotDevice;
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) */
/** [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.
* 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.
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
"status": "active",
"status": "active",
"visibility": "first-party",
"visibility": "first-party",
"sensitive": true,
"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": [
"permissions": [
"bot:robot:control"
"bot:robot:control"
],
],
@@ -14,10 +14,15 @@
@@ -14,10 +14,15 @@
],
],
"quotaClass": "bot-control",
"quotaClass": "bot-control",
"events": [
"events": [
- "bot.estop.set"
+ "bot.estop.set",
+ "bot.estop.cleared",
+ "bot.command.refused"
],
],
"implementedBy": [
"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",
"inputSchema": "common.no-body@1",
"outputSchema": "bot.robot-control-result@1"
"outputSchema": "bot.robot-control-result@1"
@@ -20,6 +20,6 @@
@@ -20,6 +20,6 @@
"GET /api/v1/robots/:id/devices",
"GET /api/v1/robots/:id/devices",
"GET /api/v1/robots/:id/audit"
"GET /api/v1/robots/:id/audit"
],
],
- "inputSchema": "common.no-body@1",
+ "inputSchema": "bot.robot-read-request@1",
"outputSchema": "bot.robot-read-result@1"
"outputSchema": "bot.robot-read-result@1"
}
}
@@ -82,5 +82,5 @@
@@ -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"
"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."
}
}
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.92.0",
+ "version": "0.93.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.92.0",
+ "version": "0.93.0",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-contracts",
"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.",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
"types": "generated/typescript/index.d.ts",
new file mode 100644
new file mode 100644
@@ -0,0 +1,20 @@
@@ -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');
@@ -899,6 +899,8 @@ sub(path.join(__dirname, 'openapi.test.js'));
@@ -899,6 +899,8 @@ sub(path.join(__dirname, 'openapi.test.js'));
sub(path.join(__dirname, 'loyalty-policy.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).
// Presence is ephemeral, in Chat's delivery plane; no Events topic (ADR-005 amendment 1).
sub(path.join(__dirname, 'presence-policy.test.js'));
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).
// Every OpenVibe.Sites catalog domain has one home in Contracts (plan T11 lane D).
sub(path.join(__dirname, 'product-catalog.test.js'));
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).
// Named object zones: identity, retired names, list limits and per-zone usage (ADR-031 amendment 2026-10-02).