openvibers/openvibe.live Commits
Live sends Bot a Stop, not an E-STOP (#857)
Changes
@@@ -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).@@@ -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 <text></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>@@@ -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';@@@ -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';@@@ -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 }),@@@ -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.',@@@ -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,@@@ -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 {nnew file mode 100644@@@ -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');@@@ -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)',
@@ -115,7 +115,7 @@ Live's own principal (`live`) holds grants to call:
@@ -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`).
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).
**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).
@@ -622,7 +622,7 @@ while True:
@@ -622,7 +622,7 @@ while True:
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- ═══════════════════════════════════════════════════════════ -->
<div class="doc-tab-content" data-doc-tab="robots" style="display:none">
<div class="doc-tab-content" data-doc-tab="robots" style="display:none">
<h2>5. ROBOTS — OPENVIBE.BOT</h2>
<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>
<h3>A robot on your channel</h3>
<ol>
<ol>
@@ -642,7 +642,7 @@ while True:
@@ -642,7 +642,7 @@ while True:
<tr><td><code>!say <text></code></td><td>speech, on robots that speak</td></tr>
<tr><td><code>!say <text></code></td><td>speech, on robots that speak</td></tr>
</tbody>
</tbody>
</table>
</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>
<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>
<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>
@@ -59,7 +59,7 @@
@@ -59,7 +59,7 @@
const text = el('p', 'ch-bot-bind-text');
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));
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',
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 actions = el('div', 'ch-bot-bind-actions');
const yes = el('button', 'btn btn-sm btn-primary', 'Use it in my chat');
const yes = el('button', 'btn btn-sm btn-primary', 'Use it in my chat');
yes.type = 'button';
yes.type = 'button';
@@ -11,7 +11,7 @@
@@ -11,7 +11,7 @@
* sign-in (Bot's POST /api/v1/robots/:id/control-ticket)
* 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`:
* 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.
* In a restore drill (LIVE_DRILL) nothing is sent.
*/
*/
'use strict';
'use strict';
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
* The kill switch's reach into OpenVibe.Bot (Agent Streams S7, design 2026-11-06 §6).
* 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
* 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
* 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.
* `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 } = {}) {
@@ -37,7 +37,7 @@ async function stop(row, { fetchImpl = globalThis.fetch } = {}) {
if (!robotId) return { stopped: false, robot_id: null };
if (!robotId) return { stopped: false, robot_id: null };
if (!configured()) return { stopped: false, robot_id: robotId, reason: 'bot_kill_unconfigured' };
if (!configured()) return { stopped: false, robot_id: robotId, reason: 'bot_kill_unconfigured' };
try {
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',
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-OV-Bot-Kill': SECRET() },
headers: { 'Content-Type': 'application/json', 'X-OV-Bot-Kill': SECRET() },
body: JSON.stringify({ reason: 'agent_stream_ended', stream_id: row.public_id || null }),
body: JSON.stringify({ reason: 'agent_stream_ended', stream_id: row.public_id || null }),
@@ -500,7 +500,7 @@ effectsRouter.post('/media-queue', async (req, res) => {
@@ -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),
// !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
// 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
// (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
// 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.
// 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 = {
@@ -544,7 +544,6 @@ const ROBOT_REFUSALS = {
'bot.not_your_turn': 'It is not your turn at the robot yet.',
'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.turn_budget': 'Your turn at the robot is used up.',
'bot.cooldown': null,
'bot.cooldown': null,
- 'bot.estop_latched': 'The robot\'s emergency stop is on.',
'bot.device_offline': 'The robot is offline.',
'bot.device_offline': 'The robot is offline.',
'bot.command_not_allowed': 'This robot does not do that.',
'bot.command_not_allowed': 'This robot does not do that.',
'bot.unknown_actuator': 'This robot does not do that.',
'bot.unknown_actuator': 'This robot does not do that.',
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
* VIP or subscriber, follower, everyone), whether the channel is live, the element's cooldowns and
* 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
* 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
* 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.
* 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,
* The press is shown on stream through the existing overlay alert feed with the presser's name,
@@ -238,7 +238,7 @@ async function check(name, fn) {
@@ -238,7 +238,7 @@ async function check(name, fn) {
);
);
assert.deepStrictEqual(out, { stopped: true, robot_id: 'rob_sim123' });
assert.deepStrictEqual(out, { stopped: true, robot_id: 'rob_sim123' });
assert.strictEqual(calls.length, 1);
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(calls[0].opts.headers['X-OV-Bot-Kill'], 'kill-secret');
assert.strictEqual(JSON.parse(calls[0].opts.body).reason, 'agent_stream_ended');
assert.strictEqual(JSON.parse(calls[0].opts.body).reason, 'agent_stream_ended');
} finally {
} finally {
new file mode 100644
new file mode 100644
@@ -0,0 +1,26 @@
@@ -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');
@@ -351,7 +351,7 @@ async function check(name, fn) {
@@ -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/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/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/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/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/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)',
'server/chat/chat-client.js': 'OpenVibe.Chat typed ingress (configured OV_CHAT_INTERNAL_URL, fixed /internal/chat/* paths)',