openvibers/openvibe.contracts Commits

Contracts: Stop is a plain control (#468)

OP OpenVibers committed 1 days ago · a2295ab5f0 · 36 files, +443 −284

Contracts describe Stop as a plain optional control, not an E-STOP.

- `bot.robot@1` and `bot.device-message@1` mark the old latch fields as legacy (kept, never set by Bot).
- `bot.command-result@1`, `bot.robot-control-result@1`, `bot.robot-manage-result@1` and `bot.command.refused@1` drop the old stop endpoint, refusal code and latch prose.
- `bot.estop.set@1` and `bot.estop.cleared@1` are retired; schemas and fixtures stay for the contract window.
- `ui.surface@1` makes Stop an ordinary part; `ui.action@1` and `ui.trigger@1` say Stop (the trigger enum also gets `stop`).
- Manifests and ADRs say Stop; resource control's stop sends halt and resume is a calm no-op.

No schema is removed and no release is tagged. Checked: `npm run generate`, `npm test` (31,357 checks), compatibility vs 0.414.0.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 449cca00..6d9f698e 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,18 @@
# Changelog
++## Unreleased — a plain Stop, not an E-STOP
++
++Small hobby robots: the panel's Stop is one ordinary, optional control that sends the machine's own halt.
++
++- `bot.robot@1`: the description and the old latch field say it is legacy (always false); schemas are unchanged.
++- `bot.device-message@1`: the old latch frame and the `estop_*` fields are described as legacy names devices may still
++ send, not as a remote latch; Bot's config always sends the latch false.
++- `bot.command-result@1`, `bot.robot-control-result@1`, `bot.robot-manage-result@1`, `bot.command.refused@1`: the old
++ stop endpoint and refusal code are gone from the descriptions.
++- `bot.estop.set@1` and `bot.estop.cleared@1` are retired (fixtures and schemas stay for the contract window).
++- `ui.surface@1`: Stop is not a required part; `ui.action@1` no longer names an E-STOP; `ui.trigger@1` adds `stop`.
++- Manifests and ADRs use Stop; `bot.resource.control` stop sends halt and resume is a calm no-op.
++
## 0.416.0 — 2026-11-12
One view API for every content kind (the owner, 11-12): a preview in the feed and a watch on a page count the same
ddiff --git a/contracts/bot/command-result.v1.json b/contracts/bot/command-result.v1.json
iindex 42fbdd6d..04c5a161 100644
---- a/contracts/bot/command-result.v1.json
++++ b/contracts/bot/command-result.v1.json
@@@ -2,7 +2,7 @@
"$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. An ack of a computer command (exec, fs, screen) carries its answer in data. delivery says whether the command reached the machine (delivered, not_delivered, uncertain); retry says whether repeating it is safe. Only not_delivered is safe to repeat: a delivered command already ran, and an uncertain one may have.",
++ "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.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. An ack of a computer command (exec, fs, screen) carries its answer in data. delivery says whether the command reached the machine (delivered, not_delivered, uncertain); retry says whether repeating it is safe. Only not_delivered is safe to repeat: a delivered command already ran, and an uncertain one may have.",
"type": "object",
"required": [
"v",
ddiff --git a/contracts/bot/device-message.v1.json b/contracts/bot/device-message.v1.json
iindex ecc9afe6..0239479a 100644
---- a/contracts/bot/device-message.v1.json
++++ b/contracts/bot/device-message.v1.json
@@@ -54,7 +54,7 @@
}
},
{
-- "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.",
++ "description": "Server to device: the effective limits (the owner's clamped by the profile's), the kinds the profile takes and the legacy latch field; sent again when any of them changes.",
"type": "object",
"required": [
"v",
@@@ -121,6 +121,7 @@
}
},
"estop_latched": {
++ "description": "The old latch field: Bot always sends false and no longer latches anything.",
"type": "boolean"
},
"voice": {
@@@ -230,7 +231,7 @@
}
},
{
-- "description": "Server to device: the latch moved (true sets it, false is the owner clearing it).",
++ "description": "Legacy server frame: the old stop latch moved. Bot no longer sends it; a device that still accepts it keeps working.",
"type": "object",
"required": [
"v",
@@@ -761,7 +762,7 @@
}
},
{
-- "description": "Device to server: the command was refused, with OpenVibe Link's fault code (bad_value, estopped, local_stop, expired, …).",
++ "description": "Device to server: the command was refused, with OpenVibe Link's fault code (bad_value, local_stop, expired, …).",
"type": "object",
"required": [
"v",
@@@ -915,7 +916,7 @@
}
},
{
-- "description": "Device to server: the device's own latch; a device may set the e-stop, never clear the owner's.",
++ "description": "Device to server: the old name of a machine's own Stop report; accepted and ignored. The tray's own Stop stays local.",
"type": "object",
"required": [
"v",
ddiff --git a/contracts/bot/robot-control-result.v1.json b/contracts/bot/robot-control-result.v1.json
iindex 59504c57..8244dfef 100644
---- a/contracts/bot/robot-control-result.v1.json
++++ b/contracts/bot/robot-control-result.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/bot/robot-control-result.v1.json",
"title": "BotRobotControlResult",
-- "description": "bot.robot-control-result@1: answers of bot.robot.control on OpenVibe.Bot. POST /api/v1/robots/:id/estop → { robot } with its e-stop as it now stands.",
++ "description": "bot.robot-control-result@1: answers of bot.robot.control on OpenVibe.Bot. It now describes the shape of a robot answer; the old stop endpoint is gone.",
"type": "object",
"required": [
"robot"
ddiff --git a/contracts/bot/robot-manage-result.v1.json b/contracts/bot/robot-manage-result.v1.json
iindex e59e313d..ad571f18 100644
---- a/contracts/bot/robot-manage-result.v1.json
++++ b/contracts/bot/robot-manage-result.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/bot/robot-manage-result.v1.json",
"title": "BotRobotManageResult",
-- "description": "bot.robot-manage-result@1: answers of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → 201 { robot, pairing }; PATCH …/robots/:id and POST …/robots/:id/estop/clear → { robot }; DELETE …/robots/:id → 204 (no body); POST …/robots/:id/pairing-code → 201 { code, expires_at, installer }; POST …/robots/:id/operators → 201 { operators }; DELETE …/robots/:id/operators/:subject → { operators }; POST …/devices/:id/rotate and /revoke → bot.device-connect-result@1.",
++ "description": "bot.robot-manage-result@1: answers of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → 201 { robot, pairing }; PATCH …/robots/:id and DELETE …/robots/:id → 204 (no body); POST …/robots/:id/pairing-code → 201 { code, expires_at, installer }; POST …/robots/:id/operators → 201 { operators }; DELETE …/robots/:id/operators/:subject → { operators }; POST …/devices/:id/rotate and /revoke → bot.device-connect-result@1.",
"anyOf": [
{
"type": "object",
ddiff --git a/contracts/bot/robot.v1.json b/contracts/bot/robot.v1.json
iindex 2a279ba5..f49bc07c 100644
---- a/contracts/bot/robot.v1.json
++++ b/contracts/bot/robot.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/bot/robot.v1.json",
"title": "BotRobot",
-- "description": "bot.robot@1: a robot on OpenVibe.Bot as its API presents it (domain.present.robot): its owner-chosen name, the device profile it runs, who may drive it (access_policy) and the latched e-stop.",
++ "description": "bot.robot@1: a robot on OpenVibe.Bot as its API presents it (domain.present.robot): its owner-chosen name, the device profile it runs and who may drive it (access_policy). The old stop-latch field stays for the contract window and is always false.",
"type": "object",
"required": [
"id",
@@@ -39,6 +39,7 @@
"type": "object"
},
"estop": {
++ "description": "The old stop-latch field, kept for the contract window: Bot sends latched false and never reads or writes it. A later contract migration removes it.",
"type": "object",
"required": [
"latched",
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex f5d3be79..8e452f55 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -7344,7 +7344,7 @@
"schema": "events/payloads/bot.estop.set.v1.json",
"visibility": "first-party",
"compatibility": "backward",
-- "status": "active",
++ "status": "retired",
"adr": "ADR-043"
},
{
@@@ -7354,7 +7354,7 @@
"schema": "events/payloads/bot.estop.cleared.v1.json",
"visibility": "first-party",
"compatibility": "backward",
-- "status": "active",
++ "status": "retired",
"adr": "ADR-043"
},
{
ddiff --git a/contracts/events/payloads/bot.command.refused.v1.json b/contracts/events/payloads/bot.command.refused.v1.json
iindex a9e61299..51a62c65 100644
---- a/contracts/events/payloads/bot.command.refused.v1.json
++++ b/contracts/events/payloads/bot.command.refused.v1.json
@@@ -2,7 +2,7 @@
"$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.",
++ "description": "bot.command.refused v1 (OpenVibe.Bot server/domain/index.js auditCommand). The gate refused a command (role, allowlist, limits, policy, cooldown, turn, device offline). 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",
ddiff --git a/contracts/events/payloads/bot.estop.cleared.v1.json b/contracts/events/payloads/bot.estop.cleared.v1.json
iindex 887d8360..1b775d06 100644
---- a/contracts/events/payloads/bot.estop.cleared.v1.json
++++ b/contracts/events/payloads/bot.estop.cleared.v1.json
@@@ -2,7 +2,7 @@
"$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.",
++ "description": "bot.estop.cleared v1 (OpenVibe.Bot server/domain/index.js writeEstop via clearEstop). Legacy event: the old stop latch was cleared. Bot no longer emits it; kept for the contract window. 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",
ddiff --git a/contracts/events/payloads/bot.estop.set.v1.json b/contracts/events/payloads/bot.estop.set.v1.json
iindex bf0cab4c..690a2341 100644
---- a/contracts/events/payloads/bot.estop.set.v1.json
++++ b/contracts/events/payloads/bot.estop.set.v1.json
@@@ -2,7 +2,7 @@
"$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.",
++ "description": "bot.estop.set v1 (OpenVibe.Bot server/domain/index.js writeEstop). Legacy event: the old stop latch. Bot no longer emits it; kept for the contract window. 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",
ddiff --git a/contracts/platform/job-frame.v1.json b/contracts/platform/job-frame.v1.json
iindex 551c1a61..dfff51cb 100644
---- a/contracts/platform/job-frame.v1.json
++++ b/contracts/platform/job-frame.v1.json
@@@ -177,7 +177,7 @@
"stopped",
"failed"
],
-- "description": "exited: the process ended by itself; cancelled: `job_cancel`; ttl: `ttl_ms` ran out; limit: a `limits` cap was hit; stopped: the local kill switch or e-stop; failed: the worker could not run it (spawn error, missing artifact)"
++ "description": "exited: the process ended by itself; cancelled: `job_cancel`; ttl: `ttl_ms` ran out; limit: a `limits` cap was hit; stopped: the local kill switch or Stop; failed: the worker could not run it (spawn error, missing artifact)"
},
"code": {
"type": [
ddiff --git a/contracts/ui/action.v1.json b/contracts/ui/action.v1.json
iindex 898c9ef0..9423ca4e 100644
---- a/contracts/ui/action.v1.json
++++ b/contracts/ui/action.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/ui/action.v1.json",
"title": "UiAction",
-- "description": "ui.action@1: one action an Element's recipe may request, from the fixed catalog, with the product that authorises it. An element never has a power of its own: the host asks the product named here, and that product checks the presser's role, the element's declared capability, its cooldown and cost, and its own owner limits before it does anything. kind is also the capability id an element declares in ui.element@1, and product is fixed by kind: bot for robot.drive_pulse, robot.head_preset, robot.headlight, robot.say and robot.macro; chat for chat.message, chat.pin and chat.poll; live for the screen, stream and webhook actions; ai for ai.ask; actor for actor.task; billing for money.tip_goal. A robot action is a request through Bot's existing command path: turns, Stop, the e-stop and owner takeover are never bypassed. Money never moves from an action without Billing's own confirmation.",
++ "description": "ui.action@1: one action an Element's recipe may request, from the fixed catalog, with the product that authorises it. An element never has a power of its own: the host asks the product named here, and that product checks the presser's role, the element's declared capability, its cooldown and cost, and its own owner limits before it does anything. kind is also the capability id an element declares in ui.element@1, and product is fixed by kind: bot for robot.drive_pulse, robot.head_preset, robot.headlight, robot.say and robot.macro; chat for chat.message, chat.pin and chat.poll; live for the screen, stream and webhook actions; ai for ai.ask; actor for actor.task; billing for money.tip_goal. A robot action is a request through Bot's existing command path: turns, Stop and owner takeover are never bypassed. Money never moves from an action without Billing's own confirmation.",
"oneOf": [
{
"type": "object",
ddiff --git a/contracts/ui/surface.v1.json b/contracts/ui/surface.v1.json
iindex 47c10507..41302440 100644
---- a/contracts/ui/surface.v1.json
++++ b/contracts/ui/surface.v1.json
@@@ -99,7 +99,7 @@
"kind": { "enum": ["stick", "button", "slider", "readout", "video", "list"] },
"required": {
"type": "boolean",
-- "description": "A required part is always visible and at its minimum size (Stop and the e-stop). A layout that hides or shrinks it is refused."
++ "description": "A required part is always visible and at its minimum size. A layout that hides or shrinks it is refused. Stop is not required: it is an ordinary part a person may add."
},
"pressed": {
"type": "boolean",
ddiff --git a/contracts/ui/trigger.v1.json b/contracts/ui/trigger.v1.json
iindex 84020577..6580455b 100644
---- a/contracts/ui/trigger.v1.json
++++ b/contracts/ui/trigger.v1.json
@@@ -159,7 +159,7 @@
"required": ["kind", "event"],
"properties": {
"kind": { "const": "robot_event" },
-- "event": { "enum": ["estop", "stopped", "disconnected", "command_refused", "button"] }
++ "event": { "enum": ["stop", "estop", "stopped", "disconnected", "command_refused", "button"] }
}
},
{
ddiff --git a/docs/adr/ADR-043-bot-devices-and-control.md b/docs/adr/ADR-043-bot-devices-and-control.md
iindex 1fe8d6ea..cceb5924 100644
---- a/docs/adr/ADR-043-bot-devices-and-control.md
++++ b/docs/adr/ADR-043-bot-devices-and-control.md
@@@ -48,13 +48,13 @@ a Cozmo-based robot).
second control transport when measured latency asks for it; the message set does not change.
5. **One message set** (JSON, fields `v`, `seq`, `ts` on every frame): server→device `hello`, `config` (limits, heartbeat,
the operator's allowed commands), `command` (`id` as an idempotency key, `kind` drive/actuator/ptz/say/display/halt,
-- `value`, `deadline_ms`, operator and role), `estop`, `heartbeat_ack`; device→server `status`, `telemetry` (≤ 2 Hz),
-- `ack`/`nack` (with a fault code), `heartbeat`, `estop_state`. Commands are never queued for an offline device and
++ `value`, `deadline_ms`, operator and role), `heartbeat_ack`; device→server `status`, `telemetry` (≤ 2 Hz),
++ `ack`/`nack` (with a fault code), `heartbeat`, and the legacy Stop report frame. A Stop is the ordinary `halt` command. Commands are never queued for an offline device and
never replayed after a reconnect.
6. **Safety is not optional.** Every motion command carries an absolute deadline (default 300 ms, at most the robot's
`max_command_ms`); a held control re-sends every 150 ms; the **device** stops the motors at the deadline, on a lost
heartbeat (1 s interval, stop after 2 missed) and on every disconnect or crash path, without asking the network. The
-- e-stop is latched on the device and in Bot and only the owner clears it. Speed, turn and range limits and time windows
++ device stops itself, and Bot sends a plain `halt`; a Stop on the tray stays local. Speed, turn and range limits and time windows
are set by the owner; an operator never exceeds them. Cozmo additionally stops on a cliff or pick-up event. The agent
never runs or exposes a kit's stock control server (the Adeept `admin:123456` port stays off).
7. **Robot profiles, not rows.** `bot.robot-profile@1` (Contracts): capabilities (drive differential/mecanum, pan-tilt,
@@@ -68,7 +68,7 @@ a Cozmo-based robot).
kill switch. Chat control (Live, openvibe.chat) is a caller of the same gate: Chat or Live sends a command on behalf of
the viewer with a service token holding `bot.robot.control`, and Bot applies the role, the allowlist and the limits.
9. **Everything is recorded.** Each command (robot, device, operator principal, kind, result, latency) goes to an audit
-- table kept 30 days; `bot.robot.online|offline`, `bot.command.refused`, `bot.estop.set|cleared` and telemetry summaries
++ table kept 30 days; `bot.robot.online|offline`, `bot.command.refused` and telemetry summaries
are Events; Actor agents drive robots only through a delegated `bot.robot.control` grant and the owner's approval.
10. **The service.** OpenVibe.Bot is its own repository and service on PostgreSQL (the platform chassis: openvibe-sdk db,
auth, limits, outbox; openvibe-shared Frame and showcase), public origin openvibe.bot. Capabilities
@@@ -107,7 +107,7 @@ a Cozmo-based robot).
- Pairing: a code works once, expires after 10 minutes, dies after 5 wrong tries; the credential is never logged or
returned twice; rotation keeps the old credential for 60 s; revocation disconnects the device at once.
- Safety: a drive command without a newer one stops the motors at its deadline; killing the network, the WebSocket or
-- the agent process stops the motors; the e-stop latches on the device and only the owner clears it; limits cap every
++ the agent process stops the motors; a Stop is the ordinary halt command and the tray's own Stop stays local; limits cap every
operator; Cozmo stops on a cliff event.
- Access: a viewer cannot drive a private robot; a queued turn ends on its timer; chat commands pass the same gate.
- Profiles: the Adeept and Cozmo profiles validate and render a panel with no robot-specific frontend code.
ddiff --git a/docs/adr/ADR-045-node-universal-runtime.md b/docs/adr/ADR-045-node-universal-runtime.md
iindex 6c4875ed..dc55ec43 100644
---- a/docs/adr/ADR-045-node-universal-runtime.md
++++ b/docs/adr/ADR-045-node-universal-runtime.md
@@@ -25,7 +25,7 @@ link.
leases and physical safety — not universal device identity."*
- **What the code already does** (OpenVibe Link, `origin/main`): one Go static binary per platform — pairing
(device credential or a Network node principal), one outbound WebSocket to `wss://openvibe.bot/device`, heartbeats
-- and a deadman, owner limits clamped before any driver sees a value, a persisted e-stop and a local kill switch, a
++ and a deadman, owner limits clamped before any driver sees a value, a persisted stop and a local kill switch, a
plugin supervisor, WHIP video publishing (pion), and the system service (systemd, launchd, Windows). Plugins are
separate processes over JSON lines: `dryrun`, `adeept_adr036`, `cozmo`. The job worker runs `platform.job@1`
`function`/`code` jobs from locally declared artifacts only, with namespaces, a seccomp allowlist, CPU/memory/wall
ddiff --git a/docs/adr/ADR-048-services-control-plane.md b/docs/adr/ADR-048-services-control-plane.md
iindex 10fd61f6..e3db8b92 100644
---- a/docs/adr/ADR-048-services-control-plane.md
++++ b/docs/adr/ADR-048-services-control-plane.md
@@@ -148,7 +148,7 @@ are in no project and have no OVRN, so the project-scoped request cannot name th
- **Answers:** `common.resource-control-result@1` without `resource`.
- **Actions per authority**, as each `<service>.resource.control` capability lists them; any other is refused
`unsupported`:
-- - `bot.robot`: stop (the remote e-stop) and resume (clear it);
++ - `bot.robot`: stop (send the machine's own halt) and resume (a calm no-op: nothing is latched);
- `chat.room`: archive;
- `openre.stream`: suspend and resume (the definition disabled or active);
- `community.paste`: delete.
ddiff --git a/docs/adr/README.md b/docs/adr/README.md
iindex a42e9aa5..9e0b614c 100644
---- a/docs/adr/README.md
++++ b/docs/adr/README.md
@@@ -40,7 +40,7 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each
| [ADR-035](ADR-035-postgresql-and-valkey.md) | PostgreSQL 18 as every service's system of record (database + roles per service, PgBouncer transaction pooling, pgBackRest PITR to B2) and Valkey as the shared non-authoritative store; the SDK data layer; services migrate one at a time, smallest first; supersedes ADR-007's trigger rule; amendment: PostgreSQL dialect only (PGlite in tests), a verified SQLite importer, every service async and migrated | Accepted 2026-09-28 (owner decision), amended the same day |
| [ADR-036](ADR-036-run-execution-authority.md) | Run is an execution authority, not a Host detail: Host deploys persistent applications, Run executes isolated jobs/environments (`function`, `code`, `browser`, `linux`, `desktop`, `gpu`), placed by the Fabric on the cheapest capable offer; provider adapters are offers, not code paths; per-second metering, TTL, idle suspend, cancel, snapshot/resume, stdout streaming, artifacts via Media, limits, network policy and strong isolation | Proposed 2026-10-05 (plan T14; plan §8 lists it to write before its code, and Run's code is owner-blocked), for the owner's review |
| [ADR-042](ADR-042-events-fabric.md) | The events fabric: the delivery-policy class is the one vocabulary and TOPIC/QUEUE/STREAM a derived carrier class; PostgreSQL stays the record, carriers are transports that fail open to `pg-v1`; a delivery lease before any second worker; per-key ordering; the SDK planner places deliveries and explains them; JetStream for STREAM only; opaque cursors replace the global seq after one overlap release; hot/replay/archive tiers; amends ADR-004's NATS rejection | Accepted 2026-09-29 (plan T7) |
--| [ADR-043](ADR-043-bot-devices-and-control.md) | Bot — devices, pairing, control and safety: robots, devices and credentials are separate (one hashed, rotatable, revocable device credential); pairing is a one-time code; one outbound WebSocket per device with absolute deadlines, device-side stops and a latched e-stop; robot profiles rather than rows; owner/operators/viewers with a timed public queue; everything audited; OpenVibe.Bot is its own service | Accepted 2026-09-29 (plan track T15, "Bot complete"; owner direction the same evening: openvibe.bot is an open control panel for robots, streaming optional; the first robots are the owner's Adeept 4WD Smart Car for Raspberry Pi and a Cozmo-based robot) |
++| [ADR-043](ADR-043-bot-devices-and-control.md) | Bot — devices, pairing, control and safety: robots, devices and credentials are separate (one hashed, rotatable, revocable device credential); pairing is a one-time code; one outbound WebSocket per device with absolute deadlines, device-side stops and a plain optional Stop; robot profiles rather than rows; owner/operators/viewers with a timed public queue; everything audited; OpenVibe.Bot is its own service | Accepted 2026-09-29 (plan track T15, "Bot complete"; owner direction the same evening: openvibe.bot is an open control panel for robots, streaming optional; the first robots are the owner's Adeept 4WD Smart Car for Raspberry Pi and a Cozmo-based robot) |
| [ADR-044](ADR-044-actor-runtime.md) | The Actor runtime: one `platform.task@1` task (`POST /v1/tasks`) with a mode, a per-task and per-day budget, result, cost and explanation (a chain of `platform.placement-result@1`); adapters declare capabilities, limits, a reviewed rate card, a trust class and BYO keys; modes are `openvibe-sdk/placement` objectives; live signals auto-balance with hysteresis and immediate failover; a `verifying` state holds cheap-first cross-family checks and the task's own tests; the five ADR-046 trust classes, `private` narrowing to OpenVibe-only, Link control levels underneath; progress over SSE, cancel and webhooks; owner `network` (no Actor service manifest exists yet) | Proposed 2026-10-05 (plan T17), for the owner's review |
| [ADR-045](ADR-045-node-universal-runtime.md) | The Link is OpenVibe's universal runtime, not a Bot/Actor helper: one checksummed release binary and one control link serving the execution worker (`worker:<class>`, per ADR-046 §5), computer control under device-set control levels with a local kill switch and recording, and cache/storage/delivery; local policy always overrides cloud commands; ADR-043 stays the robot/device half | Proposed 2026-10-05 (plan T14 and §8 revision; consistent with ADR-046 and ADR-048, amends ADR-043's credential paragraph), for the owner's review |
| [ADR-046](ADR-046-universal-adaptive-fabric.md) | The universal adaptive fabric: offers, requirements, placement results and signed plans through one placer (`openvibe-sdk/placement`); health, cost, telemetry and usage samples; five trust classes, with `user-owned` eligible only when a workload names it; `worker:<class>` Link capabilities; the data plane keeps running on the last valid signed plan; T6 AI the reference consumer | Proposed 2026-10-03 (plan T1 step 3), for the owner's review |
ddiff --git a/fixtures/ai.deep-dive/valid/quick-robot-panel.json b/fixtures/ai.deep-dive/valid/quick-robot-panel.json
iindex 5938cc20..fe9f906e 100644
---- a/fixtures/ai.deep-dive/valid/quick-robot-panel.json
++++ b/fixtures/ai.deep-dive/valid/quick-robot-panel.json
@@@ -68,7 +68,7 @@
"motion": "The frame pulses when connected.",
"words": "Verbs on buttons, state in the status line.",
"strengths": ["Every control sits in the bottom third", "State is written, not only coloured"],
-- "weaknesses": ["The emergency stop is the same size as the rest"],
++ "weaknesses": ["The Stop button is the same size as the rest"],
"accessibility": "The stop button has no accessible name.",
"speed": "The first frame arrived once the socket opened; no skeleton.",
"evidence": [
@@@ -97,7 +97,7 @@
"evidence": [{ "claim": "The frame carries the status line", "source_id": "src_1", "capture_id": "cap_1", "crop_id": "crop_1", "url": "https://example.com/robot", "read_at": "2026-11-11T10:00:45.000Z" }]
},
"mistakes": [
-- { "what": "The emergency stop is not separated from the other controls.", "evidence": [{ "claim": "All buttons share one row", "source_id": "src_1", "capture_id": "cap_1", "crop_id": "crop_2", "url": "https://example.com/robot", "read_at": "2026-11-11T10:00:45.000Z" }] }
++ { "what": "The Stop button is not separated from the other controls.", "evidence": [{ "claim": "All buttons share one row", "source_id": "src_1", "capture_id": "cap_1", "crop_id": "crop_2", "url": "https://example.com/robot", "read_at": "2026-11-11T10:00:45.000Z" }] }
],
"gap": "No control panel shows what the robot is about to do before the tap is sent.",
"reference_tokens": {
@@@ -113,7 +113,7 @@
"recommendations": [
{
"id": "rec_1",
-- "what": "Put the emergency stop alone, full width, at the bottom.",
++ "what": "Put the Stop button alone, at the bottom.",
"why": "The best panel suggests it by layout; the failure here is that it does not.",
"rule": { "id": "control.destructive-separation", "status": "follows", "text": "Destructive actions are separated from ordinary ones." },
"evidence": [{ "claim": "The stop shares the row", "source_id": "src_1", "capture_id": "cap_1", "crop_id": "crop_2", "url": "https://example.com/robot", "read_at": "2026-11-11T10:00:45.000Z" }]
ddiff --git a/fixtures/bot.command-result/valid/refused.json b/fixtures/bot.command-result/valid/refused.json
iindex f864a045..5b9f5434 100644
---- a/fixtures/bot.command-result/valid/refused.json
++++ b/fixtures/bot.command-result/valid/refused.json
@@@ -5,6 +5,6 @@
"type": "command_result",
"id": "op-8",
"result": "refused",
-- "code": "bot.estop_latched",
-- "reason": "the e-stop is latched; only the owner can clear it"
++ "code": "bot.device_offline",
++ "reason": "the robot is offline"
}
ddiff --git a/fixtures/bot.device-message/valid/nack.json b/fixtures/bot.device-message/valid/nack.json
iindex 9371db78..ac1fa0bd 100644
---- a/fixtures/bot.device-message/valid/nack.json
++++ b/fixtures/bot.device-message/valid/nack.json
@@@ -5,5 +5,5 @@
"type": "nack",
"id": "cmd_01J8Z4Q9V5X1Z3BD6P9S2T5W7Y",
"fault_code": "estopped",
-- "message": "the remote e-stop is latched"
++ "message": "a stop is latched"
}
ddiff --git a/fixtures/ui.layout/valid/phone-controls.json b/fixtures/ui.layout/valid/phone-controls.json
iindex b0cb9abb..01d4d3a0 100644
---- a/fixtures/ui.layout/valid/phone-controls.json
++++ b/fixtures/ui.layout/valid/phone-controls.json
@@@ -3,14 +3,106 @@
"surface_version": 1,
"device": "phone",
"name": "My phone controls",
-- "author": { "name": "Ana", "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" } },
++ "author": {
++ "name": "Ana",
++ "subject": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ }
++ },
"sharing": "private",
"parts": [
-- { "id": "stop", "visible": true, "place": { "x": 0, "y": 0, "w": 2, "h": 2 }, "style": { "tint": "danger", "shape": "round", "opacity": 1, "label": true }, "bindings": { "keys": ["Space"], "gamepad": ["south"] } },
-- { "id": "estop", "visible": true, "place": { "x": 2, "y": 0, "w": 2, "h": 2 }, "style": { "tint": "danger", "shape": "square", "opacity": 1, "label": true }, "bindings": { "keys": ["Escape"], "gamepad": ["start"] } },
-- { "id": "stick", "visible": true, "place": { "x": 0, "y": 2, "w": 4, "h": 3 }, "style": { "tint": "surface", "shape": "round", "opacity": 1, "label": false }, "bindings": { "keys": ["W", "A", "S", "D"], "gamepad": ["left-stick"] } },
-- { "id": "battery", "visible": true, "place": { "x": 0, "y": 5, "w": 2, "h": 1 }, "style": { "tint": "success", "shape": "pill", "opacity": 1, "label": true }, "bindings": { "keys": [], "gamepad": [] } },
-- { "id": "latency", "visible": false, "place": { "x": 2, "y": 5, "w": 2, "h": 1 }, "style": { "tint": "muted", "shape": "pill", "opacity": 1, "label": true }, "bindings": { "keys": [], "gamepad": [] } }
++ {
++ "id": "stop",
++ "visible": true,
++ "place": {
++ "x": 0,
++ "y": 0,
++ "w": 1,
++ "h": 1
++ },
++ "style": {
++ "tint": "surface",
++ "shape": "pill",
++ "opacity": 1,
++ "label": true
++ },
++ "bindings": {
++ "keys": [
++ "Space"
++ ],
++ "gamepad": [
++ "south"
++ ]
++ }
++ },
++ {
++ "id": "stick",
++ "visible": true,
++ "place": {
++ "x": 0,
++ "y": 1,
++ "w": 4,
++ "h": 3
++ },
++ "style": {
++ "tint": "surface",
++ "shape": "round",
++ "opacity": 1,
++ "label": false
++ },
++ "bindings": {
++ "keys": [
++ "W",
++ "A",
++ "S",
++ "D"
++ ],
++ "gamepad": [
++ "left-stick"
++ ]
++ }
++ },
++ {
++ "id": "battery",
++ "visible": true,
++ "place": {
++ "x": 0,
++ "y": 4,
++ "w": 2,
++ "h": 1
++ },
++ "style": {
++ "tint": "success",
++ "shape": "pill",
++ "opacity": 1,
++ "label": true
++ },
++ "bindings": {
++ "keys": [],
++ "gamepad": []
++ }
++ },
++ {
++ "id": "latency",
++ "visible": false,
++ "place": {
++ "x": 2,
++ "y": 4,
++ "w": 2,
++ "h": 1
++ },
++ "style": {
++ "tint": "muted",
++ "shape": "pill",
++ "opacity": 1,
++ "label": true
++ },
++ "bindings": {
++ "keys": [],
++ "gamepad": []
++ }
++ }
],
"updated_at": "2026-11-11T12:00:00.000Z"
}
ddiff --git a/fixtures/ui.surface/valid/bot-controls.json b/fixtures/ui.surface/valid/bot-controls.json
iindex 07c4b554..b1ff26da 100644
---- a/fixtures/ui.surface/valid/bot-controls.json
++++ b/fixtures/ui.surface/valid/bot-controls.json
@@@ -2,63 +2,233 @@
"id": "bot.controls",
"version": 1,
"name": "Robot controls",
-- "description": "The robot's own panel: Stop and the e-stop always visible, then the stick, the head slider and the readouts.",
-- "devices": ["phone", "tablet", "computer"],
++ "description": "The robot's own panel: an optional plain Stop, the stick, the head slider and the readouts.",
++ "devices": [
++ "phone",
++ "tablet",
++ "computer"
++ ],
"grid": {
-- "columns": { "phone": 4, "tablet": 8, "computer": 12 },
-- "cell_px": { "phone": 44, "tablet": 44, "computer": 44 }
++ "columns": {
++ "phone": 4,
++ "tablet": 8,
++ "computer": 12
++ },
++ "cell_px": {
++ "phone": 44,
++ "tablet": 44,
++ "computer": 44
++ }
},
-- "theme_tokens": ["accent", "surface", "surface-raised", "text", "muted", "danger", "warning", "success"],
++ "theme_tokens": [
++ "accent",
++ "surface",
++ "surface-raised",
++ "text",
++ "muted",
++ "danger",
++ "warning",
++ "success"
++ ],
"parts": [
{
-- "id": "stop", "name": "Stop", "kind": "button",
-- "required": true, "pressed": true,
-- "min": { "w": 2, "h": 2, "px": 64 },
-- "sizes": [{ "w": 2, "h": 2 }, { "w": 3, "h": 2 }],
-- "styles": { "tints": ["danger", "warning"], "shapes": ["round", "pill", "square"], "opacity": { "min": 0.7, "max": 1 }, "label": true },
-- "bindings": { "keys": ["Space", "Escape"], "gamepad": ["south", "start"] },
-- "never_covers": []
-- },
-- {
-- "id": "estop", "name": "E-stop", "kind": "button",
-- "required": true, "pressed": true,
-- "min": { "w": 2, "h": 2, "px": 64 },
-- "styles": { "tints": ["danger"], "shapes": ["round", "square"], "opacity": { "min": 0.8, "max": 1 }, "label": true },
-- "bindings": { "keys": ["Escape"], "gamepad": ["start"] },
++ "id": "stop",
++ "name": "Stop",
++ "kind": "button",
++ "required": false,
++ "pressed": true,
++ "min": {
++ "w": 1,
++ "h": 1,
++ "px": 44
++ },
++ "sizes": [
++ {
++ "w": 1,
++ "h": 1
++ },
++ {
++ "w": 2,
++ "h": 2
++ }
++ ],
++ "styles": {
++ "tints": [
++ "surface"
++ ],
++ "shapes": [
++ "pill",
++ "square"
++ ],
++ "opacity": {
++ "min": 0.4,
++ "max": 1
++ },
++ "label": true
++ },
++ "bindings": {
++ "keys": [
++ "Space"
++ ],
++ "gamepad": [
++ "south"
++ ]
++ },
"never_covers": []
},
{
-- "id": "stick", "name": "Drive stick", "kind": "stick",
-- "required": false, "pressed": true,
-- "min": { "w": 3, "h": 3 },
-- "sizes": [{ "w": 3, "h": 3 }, { "w": 4, "h": 4 }],
-- "styles": { "tints": ["surface", "surface-raised"], "shapes": ["round"], "opacity": { "min": 0.6, "max": 1 }, "label": false },
-- "bindings": { "keys": ["W", "A", "S", "D"], "gamepad": ["left-stick"] },
-- "never_covers": ["stop", "estop"]
++ "id": "stick",
++ "name": "Drive stick",
++ "kind": "stick",
++ "required": false,
++ "pressed": true,
++ "min": {
++ "w": 3,
++ "h": 3
++ },
++ "sizes": [
++ {
++ "w": 3,
++ "h": 3
++ },
++ {
++ "w": 4,
++ "h": 4
++ }
++ ],
++ "styles": {
++ "tints": [
++ "surface",
++ "surface-raised"
++ ],
++ "shapes": [
++ "round"
++ ],
++ "opacity": {
++ "min": 0.6,
++ "max": 1
++ },
++ "label": false
++ },
++ "bindings": {
++ "keys": [
++ "W",
++ "A",
++ "S",
++ "D"
++ ],
++ "gamepad": [
++ "left-stick"
++ ]
++ },
++ "never_covers": [
++ "stop"
++ ]
},
{
-- "id": "head", "name": "Head", "kind": "slider",
-- "required": false, "pressed": true,
-- "min": { "w": 2, "h": 2 },
-- "styles": { "tints": ["accent", "surface-raised"], "shapes": ["pill", "square"], "opacity": { "min": 0.4, "max": 1 }, "label": true },
-- "bindings": { "keys": ["ArrowUp", "ArrowDown"], "gamepad": ["right-stick-y"] },
-- "never_covers": ["stop", "estop"]
++ "id": "head",
++ "name": "Head",
++ "kind": "slider",
++ "required": false,
++ "pressed": true,
++ "min": {
++ "w": 2,
++ "h": 2
++ },
++ "styles": {
++ "tints": [
++ "accent",
++ "surface-raised"
++ ],
++ "shapes": [
++ "pill",
++ "square"
++ ],
++ "opacity": {
++ "min": 0.4,
++ "max": 1
++ },
++ "label": true
++ },
++ "bindings": {
++ "keys": [
++ "ArrowUp",
++ "ArrowDown"
++ ],
++ "gamepad": [
++ "right-stick-y"
++ ]
++ },
++ "never_covers": [
++ "stop"
++ ]
},
{
-- "id": "battery", "name": "Battery", "kind": "readout",
-- "required": false, "pressed": false,
-- "min": { "w": 1, "h": 1 },
-- "styles": { "tints": ["text", "muted", "success"], "shapes": ["pill", "square"], "opacity": { "min": 0.4, "max": 1 }, "label": true },
-- "bindings": { "keys": [], "gamepad": [] },
-- "never_covers": ["stop", "estop"]
++ "id": "battery",
++ "name": "Battery",
++ "kind": "readout",
++ "required": false,
++ "pressed": false,
++ "min": {
++ "w": 1,
++ "h": 1
++ },
++ "styles": {
++ "tints": [
++ "text",
++ "muted",
++ "success"
++ ],
++ "shapes": [
++ "pill",
++ "square"
++ ],
++ "opacity": {
++ "min": 0.4,
++ "max": 1
++ },
++ "label": true
++ },
++ "bindings": {
++ "keys": [],
++ "gamepad": []
++ },
++ "never_covers": [
++ "stop"
++ ]
},
{
-- "id": "latency", "name": "Latency", "kind": "readout",
-- "required": false, "pressed": false,
-- "min": { "w": 1, "h": 1 },
-- "styles": { "tints": ["text", "muted"], "shapes": ["pill", "square"], "opacity": { "min": 0.4, "max": 1 }, "label": true },
-- "bindings": { "keys": [], "gamepad": [] },
-- "never_covers": ["stop", "estop"]
++ "id": "latency",
++ "name": "Latency",
++ "kind": "readout",
++ "required": false,
++ "pressed": false,
++ "min": {
++ "w": 1,
++ "h": 1
++ },
++ "styles": {
++ "tints": [
++ "text",
++ "muted"
++ ],
++ "shapes": [
++ "pill",
++ "square"
++ ],
++ "opacity": {
++ "min": 0.4,
++ "max": 1
++ },
++ "label": true
++ },
++ "bindings": {
++ "keys": [],
++ "gamepad": []
++ },
++ "never_covers": [
++ "stop"
++ ]
}
]
}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 8110cee0..a0440999 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -79342,7 +79342,7 @@
"stopped",
"failed"
],
-- "description": "exited: the process ended by itself; cancelled: `job_cancel`; ttl: `ttl_ms` ran out; limit: a `limits` cap was hit; stopped: the local kill switch or e-stop; failed: the worker could not run it (spawn error, missing artifact)"
++ "description": "exited: the process ended by itself; cancelled: `job_cancel`; ttl: `ttl_ms` ran out; limit: a `limits` cap was hit; stopped: the local kill switch or Stop; failed: the worker could not run it (spawn error, missing artifact)"
},
"code": {
"type": [
@@@ -81309,7 +81309,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/bot/robot.v1.json",
"title": "BotRobot",
-- "description": "bot.robot@1: a robot on OpenVibe.Bot as its API presents it (domain.present.robot): its owner-chosen name, the device profile it runs, who may drive it (access_policy) and the latched e-stop.",
++ "description": "bot.robot@1: a robot on OpenVibe.Bot as its API presents it (domain.present.robot): its owner-chosen name, the device profile it runs and who may drive it (access_policy). The old stop-latch field stays for the contract window and is always false.",
"type": "object",
"required": [
"id",
@@@ -81346,6 +81346,7 @@
"type": "object"
},
"estop": {
++ "description": "The old stop-latch field, kept for the contract window: Bot sends latched false and never reads or writes it. A later contract migration removes it.",
"type": "object",
"required": [
"latched",
@@@ -81762,7 +81763,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/bot/robot-manage-result.v1.json",
"title": "BotRobotManageResult",
-- "description": "bot.robot-manage-result@1: answers of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → 201 { robot, pairing }; PATCH …/robots/:id and POST …/robots/:id/estop/clear → { robot }; DELETE …/robots/:id → 204 (no body); POST …/robots/:id/pairing-code → 201 { code, expires_at, installer }; POST …/robots/:id/operators → 201 { operators }; DELETE …/robots/:id/operators/:subject → { operators }; POST …/devices/:id/rotate and /revoke → bot.device-connect-result@1.",
++ "description": "bot.robot-manage-result@1: answers of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → 201 { robot, pairing }; PATCH …/robots/:id and DELETE …/robots/:id → 204 (no body); POST …/robots/:id/pairing-code → 201 { code, expires_at, installer }; POST …/robots/:id/operators → 201 { operators }; DELETE …/robots/:id/operators/:subject → { operators }; POST …/devices/:id/rotate and /revoke → bot.device-connect-result@1.",
"anyOf": [
{
"type": "object",
@@@ -81890,7 +81891,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/bot/robot-control-result.v1.json",
"title": "BotRobotControlResult",
-- "description": "bot.robot-control-result@1: answers of bot.robot.control on OpenVibe.Bot. POST /api/v1/robots/:id/estop → { robot } with its e-stop as it now stands.",
++ "description": "bot.robot-control-result@1: answers of bot.robot.control on OpenVibe.Bot. It now describes the shape of a robot answer; the old stop endpoint is gone.",
"type": "object",
"required": [
"robot"
@@@ -82494,7 +82495,7 @@
}
},
{
-- "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.",
++ "description": "Server to device: the effective limits (the owner's clamped by the profile's), the kinds the profile takes and the legacy latch field; sent again when any of them changes.",
"type": "object",
"required": [
"v",
@@@ -82561,6 +82562,7 @@
}
},
"estop_latched": {
++ "description": "The old latch field: Bot always sends false and no longer latches anything.",
"type": "boolean"
},
"voice": {
@@@ -82670,7 +82672,7 @@
}
},
{
-- "description": "Server to device: the latch moved (true sets it, false is the owner clearing it).",
++ "description": "Legacy server frame: the old stop latch moved. Bot no longer sends it; a device that still accepts it keeps working.",
"type": "object",
"required": [
"v",
@@@ -83201,7 +83203,7 @@
}
},
{
-- "description": "Device to server: the command was refused, with OpenVibe Link's fault code (bad_value, estopped, local_stop, expired, …).",
++ "description": "Device to server: the command was refused, with OpenVibe Link's fault code (bad_value, local_stop, expired, …).",
"type": "object",
"required": [
"v",
@@@ -83361,7 +83363,7 @@
}
},
{
-- "description": "Device to server: the device's own latch; a device may set the e-stop, never clear the owner's.",
++ "description": "Device to server: the old name of a machine's own Stop report; accepted and ignored. The tray's own Stop stays local.",
"type": "object",
"required": [
"v",
@@@ -84440,7 +84442,7 @@
"$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. An ack of a computer command (exec, fs, screen) carries its answer in data. delivery says whether the command reached the machine (delivered, not_delivered, uncertain); retry says whether repeating it is safe. Only not_delivered is safe to repeat: a delivered command already ran, and an uncertain one may have.",
++ "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.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. An ack of a computer command (exec, fs, screen) carries its answer in data. delivery says whether the command reached the machine (delivered, not_delivered, uncertain); retry says whether repeating it is safe. Only not_delivered is safe to repeat: a delivered command already ran, and an uncertain one may have.",
"type": "object",
"required": [
"v",
@@@ -85054,12 +85056,12 @@
"bot.estop.set": {
"version": "1.0.0",
"owner": "bot",
-- "status": "active",
++ "status": "retired",
"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.",
++ "description": "bot.estop.set v1 (OpenVibe.Bot server/domain/index.js writeEstop). Legacy event: the old stop latch. Bot no longer emits it; kept for the contract window. 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",
@@@ -85091,12 +85093,12 @@
"bot.estop.cleared": {
"version": "1.0.0",
"owner": "bot",
-- "status": "active",
++ "status": "retired",
"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.",
++ "description": "bot.estop.cleared v1 (OpenVibe.Bot server/domain/index.js writeEstop via clearEstop). Legacy event: the old stop latch was cleared. Bot no longer emits it; kept for the contract window. 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",
@@@ -85133,7 +85135,7 @@
"$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.",
++ "description": "bot.command.refused v1 (OpenVibe.Bot server/domain/index.js auditCommand). The gate refused a command (role, allowlist, limits, policy, cooldown, turn, device offline). 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",
@@@ -131276,7 +131278,7 @@
},
"required": {
"type": "boolean",
-- "description": "A required part is always visible and at its minimum size (Stop and the e-stop). A layout that hides or shrinks it is refused."
++ "description": "A required part is always visible and at its minimum size. A layout that hides or shrinks it is refused. Stop is not required: it is an ordinary part a person may add."
},
"pressed": {
"type": "boolean",
@@@ -131942,7 +131944,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/ui/action.v1.json",
"title": "UiAction",
-- "description": "ui.action@1: one action an Element's recipe may request, from the fixed catalog, with the product that authorises it. An element never has a power of its own: the host asks the product named here, and that product checks the presser's role, the element's declared capability, its cooldown and cost, and its own owner limits before it does anything. kind is also the capability id an element declares in ui.element@1, and product is fixed by kind: bot for robot.drive_pulse, robot.head_preset, robot.headlight, robot.say and robot.macro; chat for chat.message, chat.pin and chat.poll; live for the screen, stream and webhook actions; ai for ai.ask; actor for actor.task; billing for money.tip_goal. A robot action is a request through Bot's existing command path: turns, Stop, the e-stop and owner takeover are never bypassed. Money never moves from an action without Billing's own confirmation.",
++ "description": "ui.action@1: one action an Element's recipe may request, from the fixed catalog, with the product that authorises it. An element never has a power of its own: the host asks the product named here, and that product checks the presser's role, the element's declared capability, its cooldown and cost, and its own owner limits before it does anything. kind is also the capability id an element declares in ui.element@1, and product is fixed by kind: bot for robot.drive_pulse, robot.head_preset, robot.headlight, robot.say and robot.macro; chat for chat.message, chat.pin and chat.poll; live for the screen, stream and webhook actions; ai for ai.ask; actor for actor.task; billing for money.tip_goal. A robot action is a request through Bot's existing command path: turns, Stop and owner takeover are never bypassed. Money never moves from an action without Billing's own confirmation.",
"oneOf": [
{
"type": "object",
@@@ -133369,6 +133371,7 @@
},
"event": {
"enum": [
++ "stop",
"estop",
"stopped",
"disconnected",
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex c9db6f7d..cffb0fa0 100644
---- a/generated/openapi/ai.json
++++ b/generated/openapi/ai.json
@@@ -10751,7 +10751,7 @@
},
"required": {
"type": "boolean",
-- "description": "A required part is always visible and at its minimum size (Stop and the e-stop). A layout that hides or shrinks it is refused."
++ "description": "A required part is always visible and at its minimum size. A layout that hides or shrinks it is refused. Stop is not required: it is an ordinary part a person may add."
},
"pressed": {
"type": "boolean",
ddiff --git a/generated/openapi/bot.json b/generated/openapi/bot.json
iindex 5baa945d..6d401939 100644
---- a/generated/openapi/bot.json
++++ b/generated/openapi/bot.json
@@@ -20,7 +20,7 @@
"post": {
"operationId": "post_api_v1_devices_id_revoke",
"summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
-- "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
"tags": [
"bot.robot"
],
@@@ -86,7 +86,7 @@
"post": {
"operationId": "post_api_v1_devices_id_rotate",
"summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
-- "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
"tags": [
"bot.robot"
],
@@@ -1392,7 +1392,7 @@
"post": {
"operationId": "post_api_v1_resources_control",
"summary": "Control a person's robots through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-request@1 naming a ",
-- "description": "**bot.resource.control** (first-party): Control a person's robots through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-request@1 naming a bot.robot (id rob_<ULID>) from a first-party service (OpenVibe.Services) acting for the person in on_behalf_of. Actions: stop (latch the robot's remote e-stop, as POST /api/v1/robots/:id/estop: every actuator stops and nothing moves until it is cleared) and resume (clear that e-stop, as POST /api/v1/robots/:id/estop/clear; a local stop on the robot itself still holds); any other action is refused unsupported. Only the resource's owner may act: the request's owner must be the owner the authority holds. dry_run decides and explains without applying. The first answer per (caller, idempotency_key) is kept and returned for a repeat; the same key with another request is refused. Answers common.resource-control-result@1 without resource (person-owned resources have no OVRN).",
++ "description": "**bot.resource.control** (first-party): Control a person's robots through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-request@1 naming a bot.robot (id rob_<ULID>) from a first-party service (OpenVibe.Services) acting for the person in on_behalf_of. Actions: stop (send the robot's own Stop, the halt command: every actuator stops and nothing is latched) and resume (answer that nothing was latched, so there is nothing to clear; a stop on the machine itself stays local); any other action is refused unsupported. Only the resource's owner may act: the request's owner must be the owner the authority holds. dry_run decides and explains without applying. The first answer per (caller, idempotency_key) is kept and returned for a repeat; the same key with another request is refused. Answers common.resource-control-result@1 without resource (person-owned resources have no OVRN).",
"tags": [
"bot.resource"
],
@@@ -1448,7 +1448,7 @@
"get": {
"operationId": "get_api_v1_robots",
"summary": "Read robots with a Network service token.",
-- "description": "**bot.robot.read** (first-party): Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit (GET /robots/:id/audit, newest first, paged) is read too, for any robot id. Bot also requires this capability, beside bot.robot.control, to latch the e-stop.",
++ "description": "**bot.robot.read** (first-party): Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit (GET /robots/:id/audit, newest first, paged) is read too, for any robot id.",
"tags": [
"bot.robot"
],
@@@ -1525,7 +1525,7 @@
"post": {
"operationId": "post_api_v1_robots",
"summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
-- "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
"tags": [
"bot.robot"
],
@@@ -1581,7 +1581,7 @@
"delete": {
"operationId": "delete_api_v1_robots_id",
"summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
-- "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
"tags": [
"bot.robot"
],
@@@ -1638,7 +1638,7 @@
"get": {
"operationId": "get_api_v1_robots_id",
"summary": "Read robots with a Network service token.",
-- "description": "**bot.robot.read** (first-party): Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit (GET /robots/:id/audit, newest first, paged) is read too, for any robot id. Bot also requires this capability, beside bot.robot.control, to latch the e-stop.",
++ "description": "**bot.robot.read** (first-party): Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit (GET /robots/:id/audit, newest first, paged) is read too, for any robot id.",
"tags": [
"bot.robot"
],
@@@ -1723,7 +1723,7 @@
"patch": {
"operationId": "patch_api_v1_robots_id",
"summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
-- "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
"tags": [
"bot.robot"
],
@@@ -1789,7 +1789,7 @@
"get": {
"operationId": "get_api_v1_robots_id_audit",
"summary": "Read robots with a Network service token.",
-- "description": "**bot.robot.read** (first-party): Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit (GET /robots/:id/audit, newest first, paged) is read too, for any robot id. Bot also requires this capability, beside bot.robot.control, to latch the e-stop.",
++ "description": "**bot.robot.read** (first-party): Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit (GET /robots/:id/audit, newest first, paged) is read too, for any robot id.",
"tags": [
"bot.robot"
],
@@@ -1876,7 +1876,7 @@
"get": {
"operationId": "get_api_v1_robots_id_devices",
"summary": "Read robots with a Network service token.",
-- "description": "**bot.robot.read** (first-party): Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit (GET /robots/:id/audit, newest first, paged) is read too, for any robot id. Bot also requires this capability, beside bot.robot.control, to latch the e-stop.",
++ "description": "**bot.robot.read** (first-party): Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit (GET /robots/:id/audit, newest first, paged) is read too, for any robot id.",
"tags": [
"bot.robot"
],
@@@ -1959,133 +1959,11 @@
}
}
},
-- "/api/v1/robots/{id}/estop": {
-- "post": {
-- "operationId": "post_api_v1_robots_id_estop",
-- "summary": "Physically control a robot with a Network service token.",
-- "description": "**bot.robot.control** (first-party): Physically control a robot with a Network service token. POST /robots/:id/estop latches the e-stop for any robot id and needs bot.robot.read as well; Bot records the service as the actor and does not check X-OV-Subject. On the operator WebSocket (/control) a token with this capability must name a person in X-OV-Subject, and Bot applies that person's role (owner, operator or queue), allowlist and limits. Clearing the e-stop is bot.robot.manage. 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"
-- ],
-- "x-openvibe-capabilities": [
-- "bot.robot.control"
-- ],
-- "x-openvibe-visibility": [
-- "first-party"
-- ],
-- "x-openvibe-quota-class": "bot-control",
-- "security": [
-- {
-- "openvibeToken": [
-- "bot.robot.control"
-- ]
-- }
-- ],
-- "parameters": [
-- {
-- "name": "id",
-- "in": "path",
-- "required": true,
-- "schema": {
-- "type": "string"
-- }
-- }
-- ],
-- "responses": {
-- "2XX": {
-- "description": "Success",
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/bot.robot-control-result.v1"
-- }
-- }
-- }
-- },
-- "default": {
-- "description": "An error (RFC 9457 problem details)",
-- "content": {
-- "application/problem+json": {
-- "schema": {
-- "$ref": "#/components/schemas/errors.problem.v1"
-- }
-- }
-- }
-- }
-- }
-- }
-- },
-- "/api/v1/robots/{id}/estop/clear": {
-- "post": {
-- "operationId": "post_api_v1_robots_id_estop_clear",
-- "summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
-- "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
-- "tags": [
-- "bot.robot"
-- ],
-- "x-openvibe-capabilities": [
-- "bot.robot.manage"
-- ],
-- "x-openvibe-visibility": [
-- "first-party"
-- ],
-- "x-openvibe-quota-class": "bot-manage",
-- "security": [
-- {
-- "openvibeToken": [
-- "bot.robot.manage"
-- ]
-- }
-- ],
-- "requestBody": {
-- "required": false,
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/bot.robot-manage-request.v1"
-- }
-- }
-- }
-- },
-- "parameters": [
-- {
-- "name": "id",
-- "in": "path",
-- "required": true,
-- "schema": {
-- "type": "string"
-- }
-- }
-- ],
-- "responses": {
-- "2XX": {
-- "description": "Success",
-- "content": {
-- "application/json": {
-- "schema": {
-- "$ref": "#/components/schemas/bot.robot-manage-result.v1"
-- }
-- }
-- }
-- },
-- "default": {
-- "description": "An error (RFC 9457 problem details)",
-- "content": {
-- "application/problem+json": {
-- "schema": {
-- "$ref": "#/components/schemas/errors.problem.v1"
-- }
-- }
-- }
-- }
-- }
-- }
-- },
"/api/v1/robots/{id}/operators": {
"get": {
"operationId": "get_api_v1_robots_id_operators",
"summary": "Read robots with a Network service token.",
-- "description": "**bot.robot.read** (first-party): Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit (GET /robots/:id/audit, newest first, paged) is read too, for any robot id. Bot also requires this capability, beside bot.robot.control, to latch the e-stop.",
++ "description": "**bot.robot.read** (first-party): Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit (GET /robots/:id/audit, newest first, paged) is read too, for any robot id.",
"tags": [
"bot.robot"
],
@@@ -2170,7 +2048,7 @@
"post": {
"operationId": "post_api_v1_robots_id_operators",
"summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
-- "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
"tags": [
"bot.robot"
],
@@@ -2236,7 +2114,7 @@
"delete": {
"operationId": "delete_api_v1_robots_id_operators_subject",
"summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
-- "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
"tags": [
"bot.robot"
],
@@@ -2303,7 +2181,7 @@
"post": {
"operationId": "post_api_v1_robots_id_pairing_code",
"summary": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it",
-- "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "description": "**bot.robot.manage** (first-party): Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
"tags": [
"bot.robot"
],
@@@ -3058,19 +2936,6 @@
}
}
},
-- "bot.robot-control-result.v1": {
-- "title": "BotRobotControlResult",
-- "description": "bot.robot-control-result@1: answers of bot.robot.control on OpenVibe.Bot. POST /api/v1/robots/:id/estop → { robot } with its e-stop as it now stands.",
-- "type": "object",
-- "required": [
-- "robot"
-- ],
-- "properties": {
-- "robot": {
-- "$ref": "#/components/schemas/bot.robot.v1"
-- }
-- }
-- },
"bot.robot-manage-request.v1": {
"title": "BotRobotManageRequest",
"description": "bot.robot-manage-request@1: bodies of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → { owner (the person a service acts for), name, profile_id, access_policy?, limits? }; PATCH …/robots/:id → { name?, access_policy?, limits? }; POST …/robots/:id/operators → { subject, role? (operator by default) }. POST …/robots/:id/pairing-code and both DELETE routes take no body.",
@@@ -3150,7 +3015,7 @@
},
"bot.robot-manage-result.v1": {
"title": "BotRobotManageResult",
-- "description": "bot.robot-manage-result@1: answers of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → 201 { robot, pairing }; PATCH …/robots/:id and POST …/robots/:id/estop/clear → { robot }; DELETE …/robots/:id → 204 (no body); POST …/robots/:id/pairing-code → 201 { code, expires_at, installer }; POST …/robots/:id/operators → 201 { operators }; DELETE …/robots/:id/operators/:subject → { operators }; POST …/devices/:id/rotate and /revoke → bot.device-connect-result@1.",
++ "description": "bot.robot-manage-result@1: answers of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → 201 { robot, pairing }; PATCH …/robots/:id and DELETE …/robots/:id → 204 (no body); POST …/robots/:id/pairing-code → 201 { code, expires_at, installer }; POST …/robots/:id/operators → 201 { operators }; DELETE …/robots/:id/operators/:subject → { operators }; POST …/devices/:id/rotate and /revoke → bot.device-connect-result@1.",
"anyOf": [
{
"type": "object",
@@@ -3481,7 +3346,7 @@
},
"bot.robot.v1": {
"title": "BotRobot",
-- "description": "bot.robot@1: a robot on OpenVibe.Bot as its API presents it (domain.present.robot): its owner-chosen name, the device profile it runs, who may drive it (access_policy) and the latched e-stop.",
++ "description": "bot.robot@1: a robot on OpenVibe.Bot as its API presents it (domain.present.robot): its owner-chosen name, the device profile it runs and who may drive it (access_policy). The old stop-latch field stays for the contract window and is always false.",
"type": "object",
"required": [
"id",
@@@ -3518,6 +3383,7 @@
"type": "object"
},
"estop": {
++ "description": "The old stop-latch field, kept for the contract window: Bot sends latched false and never reads or writes it. A later contract migration removes it.",
"type": "object",
"required": [
"latched",
@@@ -4732,11 +4598,7 @@
},
{
"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)"
++ "binding": "WS /control {type:\"command\", kind:\"halt\"}"
}
]
}
ddiff --git a/generated/openapi/index.json b/generated/openapi/index.json
iindex 3190401e..199c7054 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -40,7 +40,7 @@
"service": "bot",
"name": "OpenVibe.Bot",
"origin": "https://openvibe.bot",
-- "operations": 32,
++ "operations": 30,
"capabilities": 12
},
{
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 9721efef..3c656bc8 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -217809,7 +217809,7 @@ export type JobFrame =
*/
id: string;
/**
-- * exited: the process ended by itself; cancelled: `job_cancel`; ttl: `ttl_ms` ran out; limit: a `limits` cap was hit; stopped: the local kill switch or e-stop; failed: the worker could not run it (spawn error, missing artifact)
++ * exited: the process ended by itself; cancelled: `job_cancel`; ttl: `ttl_ms` ran out; limit: a `limits` cap was hit; stopped: the local kill switch or Stop; failed: the worker could not run it (spawn error, missing artifact)
*/
reason: "exited" | "cancelled" | "ttl" | "limit" | "stopped" | "failed";
/**
@@@ -219071,7 +219071,7 @@ export interface ObjectZoneUsage {
/** [email protected] (owner: bot) */
/**
-- * bot.robot@1: a robot on OpenVibe.Bot as its API presents it (domain.present.robot): its owner-chosen name, the device profile it runs, who may drive it (access_policy) and the latched e-stop.
++ * bot.robot@1: a robot on OpenVibe.Bot as its API presents it (domain.present.robot): its owner-chosen name, the device profile it runs and who may drive it (access_policy). The old stop-latch field stays for the contract window and is always false.
*/
export interface BotRobot {
id: string;
@@@ -219080,6 +219080,9 @@ export interface BotRobot {
profile_version?: unknown;
access_policy: "private" | "invite" | "queue";
limits: {};
++ /**
++ * The old stop-latch field, kept for the contract window: Bot sends latched false and never reads or writes it. A later contract migration removes it.
++ */
estop: {
latched: boolean;
by: string | null;
@@@ -219181,7 +219184,7 @@ export type BotRobotManageRequest =
/** [email protected] (owner: bot) */
/**
-- * bot.robot-manage-result@1: answers of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → 201 { robot, pairing }; PATCH …/robots/:id and POST …/robots/:id/estop/clear → { robot }; DELETE …/robots/:id → 204 (no body); POST …/robots/:id/pairing-code → 201 { code, expires_at, installer }; POST …/robots/:id/operators → 201 { operators }; DELETE …/robots/:id/operators/:subject → { operators }; POST …/devices/:id/rotate and /revoke → bot.device-connect-result@1.
++ * bot.robot-manage-result@1: answers of bot.robot.manage on OpenVibe.Bot. POST /api/v1/robots → 201 { robot, pairing }; PATCH …/robots/:id and DELETE …/robots/:id → 204 (no body); POST …/robots/:id/pairing-code → 201 { code, expires_at, installer }; POST …/robots/:id/operators → 201 { operators }; DELETE …/robots/:id/operators/:subject → { operators }; POST …/devices/:id/rotate and /revoke → bot.device-connect-result@1.
*/
export type BotRobotManageResult =
| {
@@@ -219214,7 +219217,7 @@ export type BotRobotManageResult =
| BotDeviceConnectResult;
/** [email protected] (owner: bot) */
/**
-- * bot.robot-control-result@1: answers of bot.robot.control on OpenVibe.Bot. POST /api/v1/robots/:id/estop → { robot } with its e-stop as it now stands.
++ * bot.robot-control-result@1: answers of bot.robot.control on OpenVibe.Bot. It now describes the shape of a robot answer; the old stop endpoint is gone.
*/
export interface BotRobotControlResult {
robot: BotRobot;
@@@ -219435,6 +219438,9 @@ export type BotDeviceMessage =
heartbeat_ms?: number;
};
allowed_commands: ("drive" | "actuator" | "ptz" | "say" | "display" | "halt")[];
++ /**
++ * The old latch field: Bot always sends false and no longer latches anything.
++ */
estop_latched: boolean;
/**
* Vibe by voice: the owner turned it on for this machine, so it may listen for its call word and send voice_ask (absent or false: it does not).
@@@ -268733,7 +268739,7 @@ export type BotCommand = {
/** [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. An ack of a computer command (exec, fs, screen) carries its answer in data. delivery says whether the command reached the machine (delivered, not_delivered, uncertain); retry says whether repeating it is safe. Only not_delivered is safe to repeat: a delivered command already ran, and an uncertain one may have.
++ * 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.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. An ack of a computer command (exec, fs, screen) carries its answer in data. delivery says whether the command reached the machine (delivered, not_delivered, uncertain); retry says whether repeating it is safe. Only not_delivered is safe to repeat: a delivered command already ran, and an uncertain one may have.
*/
export interface BotCommandResult {
v: 1;
@@@ -269048,7 +269054,7 @@ export interface BotRobotOfflinePayload {
/** [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.
++ * bot.estop.set v1 (OpenVibe.Bot server/domain/index.js writeEstop). Legacy event: the old stop latch. Bot no longer emits it; kept for the contract window. 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;
@@@ -269058,7 +269064,7 @@ export interface BotEstopSetPayload {
/** [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.
++ * bot.estop.cleared v1 (OpenVibe.Bot server/domain/index.js writeEstop via clearEstop). Legacy event: the old stop latch was cleared. Bot no longer emits it; kept for the contract window. 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;
@@@ -269068,7 +269074,7 @@ export interface BotEstopClearedPayload {
/** [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.
++ * bot.command.refused v1 (OpenVibe.Bot server/domain/index.js auditCommand). The gate refused a command (role, allowlist, limits, policy, cooldown, turn, device offline). 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;
@@@ -338391,7 +338397,7 @@ export interface UiSurface {
name: string;
kind: "stick" | "button" | "slider" | "readout" | "video" | "list";
/**
-- * A required part is always visible and at its minimum size (Stop and the e-stop). A layout that hides or shrinks it is refused.
++ * A required part is always visible and at its minimum size. A layout that hides or shrinks it is refused. Stop is not required: it is an ordinary part a person may add.
*/
required: boolean;
/**
@@@ -338869,7 +338875,7 @@ export interface UiSurface {
name: string;
kind: "stick" | "button" | "slider" | "readout" | "video" | "list";
/**
-- * A required part is always visible and at its minimum size (Stop and the e-stop). A layout that hides or shrinks it is refused.
++ * A required part is always visible and at its minimum size. A layout that hides or shrinks it is refused. Stop is not required: it is an ordinary part a person may add.
*/
required: boolean;
/**
@@@ -339663,7 +339669,7 @@ export interface NetworkLayoutChangedPayload {
/** [email protected] (owner: contracts) */
/**
-- * ui.action@1: one action an Element's recipe may request, from the fixed catalog, with the product that authorises it. An element never has a power of its own: the host asks the product named here, and that product checks the presser's role, the element's declared capability, its cooldown and cost, and its own owner limits before it does anything. kind is also the capability id an element declares in ui.element@1, and product is fixed by kind: bot for robot.drive_pulse, robot.head_preset, robot.headlight, robot.say and robot.macro; chat for chat.message, chat.pin and chat.poll; live for the screen, stream and webhook actions; ai for ai.ask; actor for actor.task; billing for money.tip_goal. A robot action is a request through Bot's existing command path: turns, Stop, the e-stop and owner takeover are never bypassed. Money never moves from an action without Billing's own confirmation.
++ * ui.action@1: one action an Element's recipe may request, from the fixed catalog, with the product that authorises it. An element never has a power of its own: the host asks the product named here, and that product checks the presser's role, the element's declared capability, its cooldown and cost, and its own owner limits before it does anything. kind is also the capability id an element declares in ui.element@1, and product is fixed by kind: bot for robot.drive_pulse, robot.head_preset, robot.headlight, robot.say and robot.macro; chat for chat.message, chat.pin and chat.poll; live for the screen, stream and webhook actions; ai for ai.ask; actor for actor.task; billing for money.tip_goal. A robot action is a request through Bot's existing command path: turns, Stop and owner takeover are never bypassed. Money never moves from an action without Billing's own confirmation.
*/
export type UiAction =
| {
@@@ -340362,7 +340368,7 @@ export type UiTrigger =
}
| {
kind: "robot_event";
-- event: "estop" | "stopped" | "disconnected" | "command_refused" | "button";
++ event: "stop" | "estop" | "stopped" | "disconnected" | "command_refused" | "button";
}
| {
kind: "stream_start";
ddiff --git a/manifests/capabilities/bot.resource.control.json b/manifests/capabilities/bot.resource.control.json
iindex 46dfe090..c1e58176 100644
---- a/manifests/capabilities/bot.resource.control.json
++++ b/manifests/capabilities/bot.resource.control.json
@@@ -5,7 +5,7 @@
"status": "active",
"visibility": "first-party",
"sensitive": true,
-- "description": "Control a person's robots through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-request@1 naming a bot.robot (id rob_<ULID>) from a first-party service (OpenVibe.Services) acting for the person in on_behalf_of. Actions: stop (latch the robot's remote e-stop, as POST /api/v1/robots/:id/estop: every actuator stops and nothing moves until it is cleared) and resume (clear that e-stop, as POST /api/v1/robots/:id/estop/clear; a local stop on the robot itself still holds); any other action is refused unsupported. Only the resource's owner may act: the request's owner must be the owner the authority holds. dry_run decides and explains without applying. The first answer per (caller, idempotency_key) is kept and returned for a repeat; the same key with another request is refused. Answers common.resource-control-result@1 without resource (person-owned resources have no OVRN).",
++ "description": "Control a person's robots through the authority's control API (ADR-048 amendment 2026-10-15, step 8 phase 2): POST /api/v1/resources/control takes one common.owned-resource-control-request@1 naming a bot.robot (id rob_<ULID>) from a first-party service (OpenVibe.Services) acting for the person in on_behalf_of. Actions: stop (send the robot's own Stop, the halt command: every actuator stops and nothing is latched) and resume (answer that nothing was latched, so there is nothing to clear; a stop on the machine itself stays local); any other action is refused unsupported. Only the resource's owner may act: the request's owner must be the owner the authority holds. dry_run decides and explains without applying. The first answer per (caller, idempotency_key) is kept and returned for a repeat; the same key with another request is refused. Answers common.resource-control-result@1 without resource (person-owned resources have no OVRN).",
"permissions": [
"resources:control"
],
ddiff --git a/manifests/capabilities/bot.robot.control.json b/manifests/capabilities/bot.robot.control.json
iindex 2ca96409..a0af1b35 100644
---- a/manifests/capabilities/bot.robot.control.json
++++ b/manifests/capabilities/bot.robot.control.json
@@@ -5,7 +5,7 @@
"status": "active",
"visibility": "first-party",
"sensitive": true,
-- "description": "Physically control a robot with a Network service token. POST /robots/:id/estop latches the e-stop for any robot id and needs bot.robot.read as well; Bot records the service as the actor and does not check X-OV-Subject. On the operator WebSocket (/control) a token with this capability must name a person in X-OV-Subject, and Bot applies that person's role (owner, operator or queue), allowlist and limits. Clearing the e-stop is bot.robot.manage. The operator WebSocket frames are bot.command@1, answered by bot.command-result@1; a refused command is audited and emits bot.command.refused.",
++ "description": "Physically control a robot with a Network service token. POST /robots/:id/commands sends one command 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. A Stop is the ordinary halt command. 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,15 +14,11 @@
],
"quotaClass": "bot-control",
"events": [
-- "bot.estop.set",
-- "bot.estop.cleared",
"bot.command.refused"
],
"implementedBy": [
-- "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)"
++ "WS /control {type:\"command\", kind:\"halt\"}"
],
"inputSchema": "common.no-body@1",
"outputSchema": "bot.robot-control-result@1"
ddiff --git a/manifests/capabilities/bot.robot.manage.json b/manifests/capabilities/bot.robot.manage.json
iindex 01088769..09c932bc 100644
---- a/manifests/capabilities/bot.robot.manage.json
++++ b/manifests/capabilities/bot.robot.manage.json
@@@ -5,7 +5,7 @@
"status": "active",
"visibility": "first-party",
"sensitive": true,
-- "description": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, clear its e-stop, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
++ "description": "Manage robots with a Network service token: create one for the person the body's owner or X-OV-Subject names (a new robot and its pairing code), and, for any robot id, rename, reconfigure or delete it (irreversible), issue a one-time pairing code, add or remove operators, and rotate a paired device's credentials (the new credential and publish key are shown once) or revoke the device, which closes its socket at once. Bot does not check the acting person against the robot's owner for a service token (a person's own token must be the owner's).",
"permissions": [
"bot:robot:manage"
],
@@@ -14,7 +14,6 @@
],
"quotaClass": "bot-manage",
"events": [
-- "bot.estop.cleared"
],
"implementedBy": [
"POST /api/v1/robots",
@@@ -23,7 +22,6 @@
"POST /api/v1/robots/:id/pairing-code",
"POST /api/v1/robots/:id/operators",
"DELETE /api/v1/robots/:id/operators/:subject",
-- "POST /api/v1/robots/:id/estop/clear",
"POST /api/v1/devices/:id/rotate",
"POST /api/v1/devices/:id/revoke"
],
ddiff --git a/manifests/capabilities/bot.robot.read.json b/manifests/capabilities/bot.robot.read.json
iindex 90ac2f37..26efa519 100644
---- a/manifests/capabilities/bot.robot.read.json
++++ b/manifests/capabilities/bot.robot.read.json
@@@ -4,7 +4,7 @@
"owner": "bot",
"status": "active",
"visibility": "first-party",
-- "description": "Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit (GET /robots/:id/audit, newest first, paged) is read too, for any robot id. Bot also requires this capability, beside bot.robot.control, to latch the e-stop.",
++ "description": "Read robots with a Network service token. GET /robots needs ?owner= and lists that person's robots; the robot routes answer for any robot id with role \"service\": Bot does not check X-OV-Subject or any person's membership for a service token (a person's own token is held to owner, operator or viewer). Devices never carry a credential or key hash. The command audit (GET /robots/:id/audit, newest first, paged) is read too, for any robot id.",
"permissions": [
"bot:robot:read"
],
ddiff --git a/manifests/products/openvibe.bot.json b/manifests/products/openvibe.bot.json
iindex b5ec0217..a684ef49 100644
---- a/manifests/products/openvibe.bot.json
++++ b/manifests/products/openvibe.bot.json
@@@ -17,7 +17,7 @@
},
{
"title": "Safe on the device",
-- "text": "Dead-man stop, command limits and local emergency stop."
++ "text": "Dead-man stop, command limits and a plain optional Stop."
},
{
"title": "Stream it",
@@@ -47,7 +47,7 @@
},
{
"question": "How does it keep the robot safe?",
-- "answer": "The device stops its motors by itself when commands or the connection stop. There is an emergency stop in the panel, limits on speed, range and time, and every command is logged."
++ "answer": "The device stops its motors by itself when commands or the connection stop. Stop is one optional button that sends the machine's own stop, limits cap speed, range and time, and every command is logged."
},
{
"question": "What will it cost?",
ddiff --git a/manifests/services/bot.json b/manifests/services/bot.json
iindex dc2f3ee5..7bd3a4be 100644
---- a/manifests/services/bot.json
++++ b/manifests/services/bot.json
@@@ -22,7 +22,7 @@
"name": "Bot",
"icon": "bot",
"tagline": "Drive any robot from your browser",
-- "what": "Pair a robot with one pasted command and drive it from any browser, by keyboard, gamepad or on-screen joystick, from its own page or under a live stream, with roles, limits and an emergency stop on every command.",
++ "what": "Pair a robot with one pasted command and drive it from any browser, by keyboard, gamepad or on-screen joystick, from its own page or under a live stream, with roles and limits on every command.",
"legalProfile": "games",
"position": 22,
"neighbourhood": "make"
@@@ -45,8 +45,6 @@
"eventsProduced": [
"bot.robot.online",
"bot.robot.offline",
-- "bot.estop.set",
-- "bot.estop.cleared",
"bot.command.refused"
],
"eventsConsumed": [
ddiff --git a/test/bot-manifest.test.js b/test/bot-manifest.test.js
iindex 46e4dd5a..ef143faf 100644
---- a/test/bot-manifest.test.js
++++ b/test/bot-manifest.test.js
@@@ -16,7 +16,7 @@ const c = require('..');
// person consented to; Bot stays the authority and checks the machine is that person's own.
// bot.machine.watch_link: the owner's agent asks for a link that shows the desk it works on, inside a chat (plan L55).
const BOT_CAPABILITIES = ['bot.robot.read', 'bot.robot.manage', 'bot.robot.control', 'bot.computer.control', 'bot.machine.ai', 'bot.machine.manage', 'bot.machine.watch_link', 'bot.machine.relay', 'bot.desk.session', 'bot.device.connect', 'bot.job.dispatch', 'bot.resource.read', 'bot.resource.control'];
--const BOT_EVENTS = ['bot.robot.online', 'bot.robot.offline', 'bot.estop.set', 'bot.estop.cleared', 'bot.command.refused'];
++const BOT_EVENTS = ['bot.robot.online', 'bot.robot.offline', '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');
ddiff --git a/test/run.js b/test/run.js
iindex f3669f70..4f6965ba 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -1341,6 +1341,25 @@ sub(path.join(__dirname, 'exchange-c2.test.js'));
'actor.voice@1 does not call the hash a signature');
}
++// The words: these are small hobby robots. No page, doc or AGENTS.md says E-STOP, estop or
++// emergency stop; an inline code name for a legacy protocol field is skipped.
++{
++ const files = ['README.md', 'AGENTS.md', 'STATUS.json'].map((f) => path.join(ROOT, f));
++ const walk = (dir) => {
++ for (const name of fs.readdirSync(dir)) {
++ const p = path.join(dir, name);
++ if (fs.statSync(p).isDirectory()) walk(p);
++ else if (p.endsWith('.md')) files.push(p);
++ }
++ };
++ walk(path.join(ROOT, 'docs'));
++ const bad = /(^|[^A-Za-z])e-?stop([^A-Za-z]|$)|emergency\s+stop/i;
++ for (const file of files) {
++ let text = fs.readFileSync(file, 'utf8').replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '');
++ ok(!bad.test(text), `${path.relative(ROOT, file)} says E-STOP; write Stop`);
++ }
++}
++
console.log(skips.length
? `openvibe-contracts: ${n} checks passed, ${skips.length} skipped (${skips.join(' | ')})${strict ? ' — strict: skips fail the run' : ''}`
: `openvibe-contracts: ${n} checks passed`);