openvibers/openvibe.actor Commits

Actor: /console?ask= fills the command box (for the Frame's Ask Actor); nothing runs until you do (#79)

OP OpenVibers committed 29 days ago · 4e7ed8c97d · 2 files, +37 −4

The Console's command box can be filled from a link, for the OpenVibe Frame's "Ask Actor" (plan T17, "the Frame button").

- `/console?ask=<text>` fills the box with up to 2000 characters, control characters removed, escaped.
- `&from=<page>` adds "(On this page: <url>)" on a line after the text, but only for an https address on an OpenVibe site. Any other address is dropped.
- Nothing runs: the person still presses Run it. A filled box gets 4 rows and the focus.
- Signed out, the sign-in link comes back to the same filled box when the address fits the 500-character `next` rule, and to `/console` otherwise.

`test/console.test.js` checks the fill and its escaping, the `from` line, a foreign address dropped, no `ask` meaning an empty box, no task created, and the signed-out `next`.

Changes

ddiff --git a/server/http/console-pages.js b/server/http/console-pages.js
iindex e6a41f3..9a16070 100644
---- a/server/http/console-pages.js
++++ b/server/http/console-pages.js
@@@ -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>
ddiff --git a/test/console.test.js b/test/console.test.js
iindex 0a99fcf..463169a 100644
---- a/test/console.test.js
++++ b/test/console.test.js
@@@ -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 &lt;this&gt; &amp; 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);