openvibers/openvibe.actor Commits
Actor: /console?ask= fills the command box (for the Frame's Ask Actor); nothing runs until you do (#79)
Changes
@@@ -51,11 +51,29 @@ function createConsolePages(ctx, { robots = null, computers = null } = {}) {} catch { return { ok: false, why: 'OpenVibe.Bot did not answer in time.' }; }}-- function commandBox() {++ /**++ * What the command box starts with: ?ask= (the OpenVibe Frame's "Ask Actor", any site: the text the person had++ * selected), at most 2000 characters, and ?from= (the page they were on: an https address on an OpenVibe site)++ * as a line after it. Only ever text in the box: nothing runs until they press Run it.++ */++ function prefill(req) {++ const ask = typeof req.query.ask === 'string' ? req.query.ask.replace(/\r\n?/g, '\n').replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, '').trim().slice(0, 2000) : '';++ let from = '';++ if (typeof req.query.from === 'string' && req.query.from.length <= 500) {++ try {++ const u = new URL(req.query.from);++ if (u.protocol === 'https:' && /(^|\.)(openvibe\.[a-z]+|openre\.stream)$/.test(u.hostname)) from = u.href;++ } catch { from = ''; }++ }++ if (!ask && !from) return '';++ return from ? `${ask}${ask ? '\n\n' : ''}(On this page: ${from})` : ask;++ }++++ function commandBox(start = '') {return html`<form class="cn-command" method="post" action="/tasks" aria-labelledby="cn-command-h"><h2 id="cn-command-h" class="cn-sr">Give Actor a task</h2><label class="cn-sr" for="cn-task">What should Actor do?</label>--<textarea id="cn-task" name="task" rows="2" maxlength="20000" required placeholder="Tell Actor what to do: “check my site's certificate”, “what changed in my repo today?”, “drive my rover to the door”"></textarea>++<textarea id="cn-task" name="task" rows="${start ? '4' : '2'}" maxlength="20000" required${start ? raw(' autofocus') : ''} placeholder="Tell Actor what to do: “check my site's certificate”, “what changed in my repo today?”, “drive my rover to the door”">${start}</textarea><div class="cn-command-row"><label class="cn-mode">Mode <select name="mode">${MODES.map((m) => html`<option value="${m}"${m === 'balanced' ? raw(' selected') : ''}>${MODE_LABEL[m]}</option>`)}</select></label><button class="cn-run" type="submit">Run it</button></div></form>`;@@@ -120,7 +138,7 @@ ${c.online && c.can.includes('screen') ? html`<a class="cn-link" href="${pages}/title: 'Console', crumbs: [{ label: 'Console' }],body: html`<header class="cn-head"><p class="cn-eyebrow">OpenVibe.Actor · console</p><h1>Console</h1><p class="cn-lede">One place for everything your agents do: the tasks running now, what waits for your approval, your agents, your own computers and robots, and what it all cost today.</p></header>--<p><a class="cn-run" href="/auth/login?next=%2Fconsole">Sign in to open your console</a></p>`,++<p><a class="cn-run" href="/auth/login?next=${encodeURIComponent(req.originalUrl.startsWith('/console') && req.originalUrl.length <= 500 ? req.originalUrl : '/console')}">Sign in to open your console</a></p>`,});}const requester = `user:${req.viewer.subject}`;@@@ -144,7 +162,7 @@ ${c.online && c.can.includes('screen') ? html`<a class="cn-link" href="${pages}/scripts: live.length ? ['js/console.js'] : [],body: html`${live.length ? raw('<noscript><meta http-equiv="refresh" content="10"></noscript>') : ''}<header class="cn-head"${live.length ? raw(' data-live="10"') : ''}><p class="cn-eyebrow">OpenVibe.Actor · console · alpha</p><h1>Console</h1><p class="cn-lede">What your agents, computers and robots are doing, and one box to tell them what to do next.</p></header>--${commandBox()}++${commandBox(prefill(req))}<div class="cn-grid"><section class="cn-card cn-wide" aria-labelledby="cn-now"><h2 id="cn-now">Now${live.length ? html` <span class="cn-count">${String(live.length)}</span>` : ''}</h2>${live.length ? html`<ul class="cn-tasks">${live.map(taskRow)}</ul><p class="cn-note">This page refreshes every 10 seconds while something runs.</p>` : html`<p class="cn-empty">Nothing is running.</p>`}</section>@@@ -128,6 +128,21 @@ const approvals = require('../server/agents/approvals');assert.match((await t.get('/console', { as: ada })).text, /href="\/me\/spend"/, 'the console links it');});++ await check('?ask= and ?from= fill the command box (the Frame\'s "Ask Actor"); nothing runs; other sites\' addresses are dropped', async () => {++ const before = (await t.ctx.s.db.maybe('SELECT count(*)::int AS n FROM tasks')).n;++ const q = `?ask=${encodeURIComponent('Summarise <this> & explain')}&from=${encodeURIComponent('https://openvibe.wiki/s/openvibe/actor')}`;++ const r = await t.get(`/console${q}`, { as: ada });++ assert.strictEqual(r.status, 200);++ assert.ok(r.text.includes('autofocus placeholder=') || r.text.includes(' autofocus '), 'the box takes the focus');++ assert.ok(r.text.includes('>Summarise <this> & explain\n\n(On this page: https://openvibe.wiki/s/openvibe/actor)</textarea>'), 'escaped, with the page it came from');++ const foreign = await t.get(`/console?ask=hi&from=${encodeURIComponent('https://evil.example/x')}`, { as: ada });++ assert.ok(foreign.text.includes('>hi</textarea>'), 'another site\'s address is not carried');++ assert.ok((await t.get('/console', { as: ada })).text.includes('"></textarea>'), 'without ?ask= the box is empty');++ assert.strictEqual((await t.ctx.s.db.maybe('SELECT count(*)::int AS n FROM tasks')).n, before, 'nothing ran');++ const out = await t.get(`/console?ask=${encodeURIComponent('hello there')}`);++ assert.ok(out.text.includes(`/auth/login?next=${encodeURIComponent('/console?ask=hello%20there')}`), 'signed out: sign-in comes back to the filled box');++ });++await check('nothing running: no refresh; nothing waiting says so', async () => {const r = await t.get('/console', { as: bo });assert.strictEqual(r.status, 200);
@@ -51,11 +51,29 @@ function createConsolePages(ctx, { robots = null, computers = null } = {}) {
@@ -51,11 +51,29 @@ function createConsolePages(ctx, { robots = null, computers = null } = {}) {
} catch { return { ok: false, why: 'OpenVibe.Bot did not answer in time.' }; }
} catch { return { ok: false, why: 'OpenVibe.Bot did not answer in time.' }; }
}
}
- function commandBox() {
+ /**
+ * What the command box starts with: ?ask= (the OpenVibe Frame's "Ask Actor", any site: the text the person had
+ * selected), at most 2000 characters, and ?from= (the page they were on: an https address on an OpenVibe site)
+ * as a line after it. Only ever text in the box: nothing runs until they press Run it.
+ */
+ function prefill(req) {
+ const ask = typeof req.query.ask === 'string' ? req.query.ask.replace(/\r\n?/g, '\n').replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, '').trim().slice(0, 2000) : '';
+ let from = '';
+ if (typeof req.query.from === 'string' && req.query.from.length <= 500) {
+ try {
+ const u = new URL(req.query.from);
+ if (u.protocol === 'https:' && /(^|\.)(openvibe\.[a-z]+|openre\.stream)$/.test(u.hostname)) from = u.href;
+ } catch { from = ''; }
+ }
+ if (!ask && !from) return '';
+ return from ? `${ask}${ask ? '\n\n' : ''}(On this page: ${from})` : ask;
+ }
+
+ function commandBox(start = '') {
return html`<form class="cn-command" method="post" action="/tasks" aria-labelledby="cn-command-h">
return html`<form class="cn-command" method="post" action="/tasks" aria-labelledby="cn-command-h">
<h2 id="cn-command-h" class="cn-sr">Give Actor a task</h2>
<h2 id="cn-command-h" class="cn-sr">Give Actor a task</h2>
<label class="cn-sr" for="cn-task">What should Actor do?</label>
<label class="cn-sr" for="cn-task">What should Actor do?</label>
-<textarea id="cn-task" name="task" rows="2" maxlength="20000" required placeholder="Tell Actor what to do: “check my site's certificate”, “what changed in my repo today?”, “drive my rover to the door”"></textarea>
+<textarea id="cn-task" name="task" rows="${start ? '4' : '2'}" maxlength="20000" required${start ? raw(' autofocus') : ''} placeholder="Tell Actor what to do: “check my site's certificate”, “what changed in my repo today?”, “drive my rover to the door”">${start}</textarea>
<div class="cn-command-row"><label class="cn-mode">Mode <select name="mode">${MODES.map((m) => html`<option value="${m}"${m === 'balanced' ? raw(' selected') : ''}>${MODE_LABEL[m]}</option>`)}</select></label>
<div class="cn-command-row"><label class="cn-mode">Mode <select name="mode">${MODES.map((m) => html`<option value="${m}"${m === 'balanced' ? raw(' selected') : ''}>${MODE_LABEL[m]}</option>`)}</select></label>
<button class="cn-run" type="submit">Run it</button></div>
<button class="cn-run" type="submit">Run it</button></div>
</form>`;
</form>`;
@@ -120,7 +138,7 @@ ${c.online && c.can.includes('screen') ? html`<a class="cn-link" href="${pages}/
@@ -120,7 +138,7 @@ ${c.online && c.can.includes('screen') ? html`<a class="cn-link" href="${pages}/
title: 'Console', crumbs: [{ label: 'Console' }],
title: 'Console', crumbs: [{ label: 'Console' }],
body: html`<header class="cn-head"><p class="cn-eyebrow">OpenVibe.Actor · console</p><h1>Console</h1>
body: html`<header class="cn-head"><p class="cn-eyebrow">OpenVibe.Actor · console</p><h1>Console</h1>
<p class="cn-lede">One place for everything your agents do: the tasks running now, what waits for your approval, your agents, your own computers and robots, and what it all cost today.</p></header>
<p class="cn-lede">One place for everything your agents do: the tasks running now, what waits for your approval, your agents, your own computers and robots, and what it all cost today.</p></header>
-<p><a class="cn-run" href="/auth/login?next=%2Fconsole">Sign in to open your console</a></p>`,
+<p><a class="cn-run" href="/auth/login?next=${encodeURIComponent(req.originalUrl.startsWith('/console') && req.originalUrl.length <= 500 ? req.originalUrl : '/console')}">Sign in to open your console</a></p>`,
});
});
}
}
const requester = `user:${req.viewer.subject}`;
const requester = `user:${req.viewer.subject}`;
@@ -144,7 +162,7 @@ ${c.online && c.can.includes('screen') ? html`<a class="cn-link" href="${pages}/
@@ -144,7 +162,7 @@ ${c.online && c.can.includes('screen') ? html`<a class="cn-link" href="${pages}/
scripts: live.length ? ['js/console.js'] : [],
scripts: live.length ? ['js/console.js'] : [],
body: html`${live.length ? raw('<noscript><meta http-equiv="refresh" content="10"></noscript>') : ''}<header class="cn-head"${live.length ? raw(' data-live="10"') : ''}><p class="cn-eyebrow">OpenVibe.Actor · console · alpha</p><h1>Console</h1>
body: html`${live.length ? raw('<noscript><meta http-equiv="refresh" content="10"></noscript>') : ''}<header class="cn-head"${live.length ? raw(' data-live="10"') : ''}><p class="cn-eyebrow">OpenVibe.Actor · console · alpha</p><h1>Console</h1>
<p class="cn-lede">What your agents, computers and robots are doing, and one box to tell them what to do next.</p></header>
<p class="cn-lede">What your agents, computers and robots are doing, and one box to tell them what to do next.</p></header>
-${commandBox()}
+${commandBox(prefill(req))}
<div class="cn-grid">
<div class="cn-grid">
<section class="cn-card cn-wide" aria-labelledby="cn-now"><h2 id="cn-now">Now${live.length ? html` <span class="cn-count">${String(live.length)}</span>` : ''}</h2>
<section class="cn-card cn-wide" aria-labelledby="cn-now"><h2 id="cn-now">Now${live.length ? html` <span class="cn-count">${String(live.length)}</span>` : ''}</h2>
${live.length ? html`<ul class="cn-tasks">${live.map(taskRow)}</ul><p class="cn-note">This page refreshes every 10 seconds while something runs.</p>` : html`<p class="cn-empty">Nothing is running.</p>`}</section>
${live.length ? html`<ul class="cn-tasks">${live.map(taskRow)}</ul><p class="cn-note">This page refreshes every 10 seconds while something runs.</p>` : html`<p class="cn-empty">Nothing is running.</p>`}</section>
@@ -128,6 +128,21 @@ const approvals = require('../server/agents/approvals');
@@ -128,6 +128,21 @@ const approvals = require('../server/agents/approvals');
assert.match((await t.get('/console', { as: ada })).text, /href="\/me\/spend"/, 'the console links it');
assert.match((await t.get('/console', { as: ada })).text, /href="\/me\/spend"/, 'the console links it');
});
});
+ await check('?ask= and ?from= fill the command box (the Frame\'s "Ask Actor"); nothing runs; other sites\' addresses are dropped', async () => {
+ const before = (await t.ctx.s.db.maybe('SELECT count(*)::int AS n FROM tasks')).n;
+ const q = `?ask=${encodeURIComponent('Summarise <this> & explain')}&from=${encodeURIComponent('https://openvibe.wiki/s/openvibe/actor')}`;
+ const r = await t.get(`/console${q}`, { as: ada });
+ assert.strictEqual(r.status, 200);
+ assert.ok(r.text.includes('autofocus placeholder=') || r.text.includes(' autofocus '), 'the box takes the focus');
+ assert.ok(r.text.includes('>Summarise <this> & explain\n\n(On this page: https://openvibe.wiki/s/openvibe/actor)</textarea>'), 'escaped, with the page it came from');
+ const foreign = await t.get(`/console?ask=hi&from=${encodeURIComponent('https://evil.example/x')}`, { as: ada });
+ assert.ok(foreign.text.includes('>hi</textarea>'), 'another site\'s address is not carried');
+ assert.ok((await t.get('/console', { as: ada })).text.includes('"></textarea>'), 'without ?ask= the box is empty');
+ assert.strictEqual((await t.ctx.s.db.maybe('SELECT count(*)::int AS n FROM tasks')).n, before, 'nothing ran');
+ const out = await t.get(`/console?ask=${encodeURIComponent('hello there')}`);
+ assert.ok(out.text.includes(`/auth/login?next=${encodeURIComponent('/console?ask=hello%20there')}`), 'signed out: sign-in comes back to the filled box');
+ });
+
await check('nothing running: no refresh; nothing waiting says so', async () => {
await check('nothing running: no refresh; nothing waiting says so', async () => {
const r = await t.get('/console', { as: bo });
const r = await t.get('/console', { as: bo });
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);