openvibers/openvibe.live Commits

Live sends Bot a Stop, not an E-STOP (#857)

OP OpenVibers committed 1 days ago · 9b35ba9b18 · 10 files, +37 −12

Changes

ddiff --git a/README.md b/README.md
iindex f61a8f23..e5f56980 100644
---- a/README.md
++++ b/README.md
@@@ -115,7 +115,7 @@ Live's own principal (`live`) holds grants to call:
API writes are limited per person (`server/net/actor-limits.js`, `LIVE_LIMITS_MINUTE` / `_HOUR`).
--**Robots are OpenVibe.Bot's (plan T15).** Live has no control socket, hardware API or ONVIF code. A bound channel's chat commands (`!forward`, `!backward`, `!left`, `!right`, `!headup`/`!headdown`, `!liftup`/`!liftdown`, `!say`) reach `POST /internal/chat-effects/hardware` from OpenVibe.Chat and go to Bot as the viewer who typed them (`server/bot/bot-client.js`): Bot's roles, queue, cooldowns and e-stop decide. On a robot made of buttons (Bot's generic relay or a local profile), which has no drive or actuator, the chat word is pressed as its button of the same name (`forward`, `turn_left`, `head_up` …; a hold button is pressed for the 600 ms pulse and released), and `button:<name>` (Chat's `!<word>` that is not a channel sound) presses any other button.
++**Robots are OpenVibe.Bot's (plan T15).** Live has no control socket, hardware API or ONVIF code. A bound channel's chat commands (`!forward`, `!backward`, `!left`, `!right`, `!headup`/`!headdown`, `!liftup`/`!liftdown`, `!say`) reach `POST /internal/chat-effects/hardware` from OpenVibe.Chat and go to Bot as the viewer who typed them (`server/bot/bot-client.js`): Bot's roles, queue and cooldowns decide. On a robot made of buttons (Bot's generic relay or a local profile), which has no drive or actuator, the chat word is pressed as its button of the same name (`forward`, `turn_left`, `head_up` …; a hold button is pressed for the 600 ms pulse and released), and `button:<name>` (Chat's `!<word>` that is not a channel sound) presses any other button.
**Bot panel embed (`LIVE_BOT_EMBED`, off by default; on in production).** A channel can show its OpenVibe.Bot robot's embeddable panel (`<LIVE_BOT_URL>/panel/<robot id>/embed`). With `LIVE_BOT_EMBED=1` the channel owner (not mods, not staff) binds a robot with `PUT /api/streams/channel/:username/bot` `{ "robot_id": "rob_…" | null }` (null or `""` unbinds; stored in `channels.bot_robot_id`; limited per person as `live.bot.bind`), and `GET /api/streams/channel/:username` carries `bot_embed: { enabled: true, robot_id, url }`. Live never calls Bot to bind: the robot owner's `embed_public` toggle in Bot is the gate, so binding someone else's robot shows nothing until its owner makes it public. With the flag off the route answers 404 and the channel JSON has no `bot_embed` key (the stored id is kept). Turn it on in this order: Bot deployed with `GET /panel/:id/embed` and its `BOT_EMBED_ORIGINS` including this site's origins → set `LIVE_BOT_EMBED=1` → bind a channel. The owner binds from the dashboard's Controls tab (a "Bot robot id" field shown only with the flag on), and a bound channel's page shows the panel in a sandboxed, lazy iframe under the player (`public/js/bot-embed.js`, its own `botEmbed` feature, loaded only for a bound channel; the frame is removed when the route ends). Unbound channels and the flag off load nothing new. The easier way to bind is from Bot: the robot page's **Put the buttons under my OpenVibe.Live stream** turns its embedding on and opens the owner's channel with `?bind_robot=rob_…`, where they confirm (`public/js/bot-bind.js`). The panel's Drive asks the page around it for a ticket (`postMessage` `ov-bot-ticket-request`, answered only to that frame on Bot's origin): `POST /api/streams/channel/:username/bot/ticket` `{ robot_id }` (signed in; the channel's own robot only; limited as `live.bot.ticket`) gets one from Bot as the viewer (`controlTicket`, `bot.robot.control`), so a viewer signed in here drives with no popup or second sign-in; Bot's refusal is said in the panel. When the robot's owner has not turned the panel on, Bot's framed refusal tells the page, which hides the box from viewers (the owner keeps it, with the link to the switch).
ddiff --git a/public/fragments/documentation.html b/public/fragments/documentation.html
iindex 8e55a4ab..93ec591d 100644
---- a/public/fragments/documentation.html
++++ b/public/fragments/documentation.html
@@@ -622,7 +622,7 @@ while True:
<!-- ═══════════════════════════════════════════════════════════ -->
<div class="doc-tab-content" data-doc-tab="robots" style="display:none">
<h2>5. ROBOTS — OPENVIBE.BOT</h2>
--<p>Robots live on <a href="https://openvibe.bot" target="_blank" rel="noopener">OpenVibe.Bot</a>, the open control panel for robots: pairing, the drive panel, roles and turn queues, limits, the latched e-stop, video and the command audit are all there. OpenVibe.Live shows a channel's robot and lets its chat drive it; it has no control socket or hardware API of its own.</p>
++<p>Robots live on <a href="https://openvibe.bot" target="_blank" rel="noopener">OpenVibe.Bot</a>, the open control panel for robots: pairing, the drive panel, roles and turn queues, limits, Stop, video and the command audit are all there. OpenVibe.Live shows a channel's robot and lets its chat drive it; it has no control socket or hardware API of its own.</p>
<h3>A robot on your channel</h3>
<ol>
@@@ -642,7 +642,7 @@ while True:
<tr><td><code>!say &lt;text&gt;</code></td><td>speech, on robots that speak</td></tr>
</tbody>
</table>
--<p>Each command goes to OpenVibe.Bot as the viewer who typed it, so the robot's rules decide: signed-in viewers only, the owner's access policy and turn queue, per-person cooldowns, and nothing while the e-stop is latched. A refusal is answered in chat ("It is not your turn at the robot yet").</p>
++<p>Each command goes to OpenVibe.Bot as the viewer who typed it, so the robot's rules decide: signed-in viewers only, the owner's access policy and turn queue, per-person cooldowns, and nothing while Stop is latched. A refusal is answered in chat ("It is not your turn at the robot yet").</p>
<h3>Building for robots</h3>
<p>Drivers, robot profiles, the device protocol, the REST API with the same gate, and a simulated rover to try it all are on OpenVibe.Bot: <a href="https://openvibe.bot/docs" target="_blank" rel="noopener">Build for OpenVibe.Bot</a> · <a href="https://openvibe.bot/try" target="_blank" rel="noopener">try the panel</a> · <a href="https://openvibe.bot/models" target="_blank" rel="noopener">supported models</a>.</p>
ddiff --git a/public/js/bot-bind.js b/public/js/bot-bind.js
iindex 39287621..792ed307 100644
---- a/public/js/bot-bind.js
++++ b/public/js/bot-bind.js
@@@ -59,7 +59,7 @@
const text = el('p', 'ch-bot-bind-text');
text.append('Let your chat drive the robot you just added on OpenVibe.Bot? ', el('code', null, robotId));
const note = el('p', 'ch-bot-bind-note',
-- 'Chat commands like !forward, !left and !say go to it, checked by its own rules on openvibe.bot (who may drive, limits, the e-stop). Its panel shows here. You can unbind it any time.');
++ 'Chat commands like !forward, !left and !say go to it, checked by its own rules on openvibe.bot (who may drive, limits, Stop). Its panel shows here. You can unbind it any time.');
const actions = el('div', 'ch-bot-bind-actions');
const yes = el('button', 'btn btn-sm btn-primary', 'Use it in my chat');
yes.type = 'button';
ddiff --git a/server/bot/bot-client.js b/server/bot/bot-client.js
iindex bf2cc3c0..5841c607 100644
---- a/server/bot/bot-client.js
++++ b/server/bot/bot-client.js
@@@ -11,7 +11,7 @@
* sign-in (Bot's POST /api/v1/robots/:id/control-ticket)
*
* The token is Live's client-credentials token for audience openvibe.bot (Network's grant to `live`:
-- * bot.robot.control); X-OV-Subject names the viewer, so Bot's roles, queue, cooldowns and e-stop apply to them.
++ * bot.robot.control); X-OV-Subject names the viewer, so Bot's roles, queue and cooldowns apply to them.
* In a restore drill (LIVE_DRILL) nothing is sent.
*/
'use strict';
ddiff --git a/server/bot/kill.js b/server/bot/kill.js
iindex ac596df5..c5abac8d 100644
---- a/server/bot/kill.js
++++ b/server/bot/kill.js
@@@ -3,7 +3,7 @@
* The kill switch's reach into OpenVibe.Bot (Agent Streams S7, design 2026-11-06 §6).
*
* When an agent stream ends — the owner's End, a moderator's end control, a ticket revoke or the sweep — and the
-- * stream's channel is bound to a robot (`channels.bot_robot_id`), Live tells Bot to e_stop that robot. Bot's own rules
++ * stream's channel is bound to a robot (`channels.bot_robot_id`), Live tells Bot to stop that robot. Bot's own rules
* still hold: its owner may always stop it, and Live's internal call carries the deployment's shared secret. With
* `BOT_KILL_URL` unset (and in tests) this is a no-op: a stream ending never fails because Bot is down.
*
@@@ -37,7 +37,7 @@ async function stop(row, { fetchImpl = globalThis.fetch } = {}) {
if (!robotId) return { stopped: false, robot_id: null };
if (!configured()) return { stopped: false, robot_id: robotId, reason: 'bot_kill_unconfigured' };
try {
-- const res = await fetchImpl(`${URL_BASE()}/internal/robots/${encodeURIComponent(robotId)}/estop`, {
++ const res = await fetchImpl(`${URL_BASE()}/internal/robots/${encodeURIComponent(robotId)}/stop`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-OV-Bot-Kill': SECRET() },
body: JSON.stringify({ reason: 'agent_stream_ended', stream_id: row.public_id || null }),
ddiff --git a/server/chat/live-context-routes.js b/server/chat/live-context-routes.js
iindex 5cc0e194..d549a1cd 100644
---- a/server/chat/live-context-routes.js
++++ b/server/chat/live-context-routes.js
@@@ -500,7 +500,7 @@ effectsRouter.post('/media-queue', async (req, res) => {
// !forward, !say … : a channel's chat commands drive the OpenVibe.Bot robot the channel is bound to (channels.bot_robot_id),
// through Bot's gate as the viewer who typed them (server/bot/bot-client.js): the robot's access policy, queue, turn
--// budget, cooldowns and e-stop all apply, and Bot audits every one. Live keeps no robot code and no control socket
++// budget and cooldowns all apply, and Bot audits every one. Live keeps no robot code and no control socket
// (plan T15). OpenVibe.Chat names the viewer by Network subject (`from_subject`); a viewer who is not signed in cannot
// drive a robot. Each motion is a short pulse: Bot clamps `ms` to the profile's max_command_ms and the device stops
// by itself after it. A per-viewer cooldown keeps a chat flood from queueing faster than a held key.
@@@ -544,7 +544,6 @@ const ROBOT_REFUSALS = {
'bot.not_your_turn': 'It is not your turn at the robot yet.',
'bot.turn_budget': 'Your turn at the robot is used up.',
'bot.cooldown': null,
-- 'bot.estop_latched': 'The robot\'s emergency stop is on.',
'bot.device_offline': 'The robot is offline.',
'bot.command_not_allowed': 'This robot does not do that.',
'bot.unknown_actuator': 'This robot does not do that.',
ddiff --git a/server/elements/press.js b/server/elements/press.js
iindex 15ad08ab..c985f21e 100644
---- a/server/elements/press.js
++++ b/server/elements/press.js
@@@ -6,7 +6,7 @@
* VIP or subscriber, follower, everyone), whether the channel is live, the element's cooldowns and
* daily caps, and its channel-points cost (Live's own ledger, debited atomically); then each action
* is sent to the product that owns it: Live's screen and stream actions here, Bot's command path
-- * for a robot (turns, operators, limits, Stop, the e-stop and owner takeover are Bot's), Chat's
++ * for a robot (turns, operators, limits, Stop and owner takeover are Bot's), Chat's
* ingress for a message. A refusal from any of them stops the run and refunds the points.
*
* The press is shown on stream through the existing overlay alert feed with the presser's name,
ddiff --git a/test/agent-streams-s7.test.js b/test/agent-streams-s7.test.js
iindex 61b3315f..9497f1fa 100644
---- a/test/agent-streams-s7.test.js
++++ b/test/agent-streams-s7.test.js
@@@ -238,7 +238,7 @@ async function check(name, fn) {
);
assert.deepStrictEqual(out, { stopped: true, robot_id: 'rob_sim123' });
assert.strictEqual(calls.length, 1);
-- assert.strictEqual(calls[0].url, 'https://bot.test/internal/robots/rob_sim123/estop');
++ assert.strictEqual(calls[0].url, 'https://bot.test/internal/robots/rob_sim123/stop');
assert.strictEqual(calls[0].opts.headers['X-OV-Bot-Kill'], 'kill-secret');
assert.strictEqual(JSON.parse(calls[0].opts.body).reason, 'agent_stream_ended');
} finally {
ddiff --git a/test/plain-stop-text.test.js b/test/plain-stop-text.test.js
nnew file mode 100644
iindex 00000000..6fe4a23a
---- /dev/null
++++ b/test/plain-stop-text.test.js
@@@ -0,0 +1,26 @@
++'use strict';
++// These are small hobby robots: no page, doc or AGENTS.md says E-STOP, estop or emergency stop. Inline code
++// names a legacy protocol field and is skipped.
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const root = path.join(__dirname, '..');
++const files = ['AGENTS.md', 'README.md', 'STATUS.json'].map((f) => path.join(root, f)).filter((f) => fs.existsSync(f));
++const walk = (dir, ext) => {
++ if (!fs.existsSync(dir)) return;
++ for (const name of fs.readdirSync(dir)) {
++ const p = path.join(dir, name);
++ if (fs.statSync(p).isDirectory()) walk(p, ext);
++ else if (p.endsWith(ext)) files.push(p);
++ }
++};
++walk(path.join(root, 'docs'), '.md');
++walk(path.join(root, 'public', 'fragments'), '.html');
++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');
++ text = text.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
++ .replace(/<pre[\s\S]*?<\/pre>/gi, '').replace(/<code[\s\S]*?<\/code>/gi, '');
++ assert.ok(!bad.test(text), `${path.relative(root, file)} says E-STOP; write Stop`);
++}
++console.log('plain-stop-text: no page, doc or AGENTS.md says E-STOP');
ddiff --git a/test/security-ssrf.test.js b/test/security-ssrf.test.js
iindex cd190798..c9f0ad94 100644
---- a/test/security-ssrf.test.js
++++ b/test/security-ssrf.test.js
@@@ -351,7 +351,7 @@ async function check(name, fn) {
'server/social/preview.js': 'OpenVibe.Network public profile API (configured OV_NETWORK_INTERNAL_URL, fixed path, username checked); every other preview goes through egress',
'server/monetization/inventory-client.js': 'OpenVibe.Inventory API for Live cosmetics (configured INVENTORY_URL)',
'server/bot/bot-client.js': 'OpenVibe.Bot command API for a bound channel\'s chat robot commands (configured LIVE_BOT_INTERNAL_URL)',
-- 'server/bot/kill.js': 'OpenVibe.Bot internal e-stop for the robot a killed agent stream is bound to (configured BOT_KILL_URL, fixed path, robot id checked)',
++ 'server/bot/kill.js': 'OpenVibe.Bot internal Stop for the robot a killed agent stream is bound to (configured BOT_KILL_URL, fixed path, robot id checked)',
'server/cam/cam-client.js': 'OpenVibe.Cam stills API for the cam button, as the viewer (configured CAM_URL)',
'server/chat/insight-client.js': 'OpenVibe.Chat public chat-AI reads (configured OV_CHAT_INTERNAL_URL, fixed paths, ids validated)',
'server/chat/chat-client.js': 'OpenVibe.Chat typed ingress (configured OV_CHAT_INTERNAL_URL, fixed /internal/chat/* paths)',