openvibers/openvibe.shared Commits
4.24.2: agent-session's link(url) decides which addresses in an agent's text become links (Actor links only the person's desk) (#152)
Changes
@@@ -6,6 +6,12 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).## Unreleased++## 4.24.2 — 2026-10-28++++- **`agent-session.js` takes `link(url)`**, which decides which https addresses in an agent's text become links; the++ rest stay text. OpenVibe.Actor's agents browse untrusted pages, so its task page links only the person's own desk, as++ its server-drawn timeline always did. Without `link`, every https address links, as before.++## 4.24.1 — 2026-10-28- **`agent-session.js` reads OpenVibe.Actor's task events** (`actor.task-event@1`), so Actor's agents show in the same@@@ -2,7 +2,7 @@> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.--**Status:** alpha, v4.24.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.24.2 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.24.1",++ "version": "4.24.2","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","deployed": false,"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",@@@ -7,7 +7,9 @@* unavailable"): "OpenVibe.Codes should… actually show the output from the agent… more user friendly… handle that* between all agents… in a smart way… way better than this idea."*-- * const view = OpenVibeAgentSession.mount(el, { title, status: 'running', startedAt })++ * const view = OpenVibeAgentSession.mount(el, { title, status: 'running', startedAt, link })++ * link(url) → boolean: which https addresses in the agents' text become links (default:++ * all; a site whose agents read untrusted pages links only its own, e.g. a person's desk)* view.push(event) one event: a Codes job line ({ t: 'tool', … }), any Claude Code stream-json event* ({ type: 'assistant', … }) or an OpenVibe.Actor task event ({ kind: 'output', step, … },* actor.task-event@1); every OpenVibe.Codes harness adapter (Claude Code, Codex, OpenCode,@@@ -141,8 +143,9 @@}const base = (p) => String(p || '').split('/').filter(Boolean).pop() || String(p || '');-- /** Light Markdown, text-safe: paragraphs, bullet and numbered lists, `code`, ```blocks```, **bold**, https links. */-- function markdown(text) {++ /** Light Markdown, text-safe: paragraphs, bullet and numbered lists, `code`, ```blocks```, **bold**, https links (only++ * those `linkOk(url)` allows; the others stay text). */++ function markdown(text, linkOk) {const frag = document.createDocumentFragment();const inline = (s, into) => {const re = /(`[^`]+`|\*\*[^*]+\*\*|https:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/g;@@@ -152,6 +155,7 @@const tok = m[0];if (tok[0] === '`') into.appendChild(el('code', '', tok.slice(1, -1)));else if (tok.startsWith('**')) into.appendChild(el('strong', '', tok.slice(2, -2)));++ else if (linkOk && !linkOk(tok)) into.appendChild(document.createTextNode(tok));else { const a = el('a', '', tok); a.href = tok; a.target = '_blank'; a.rel = 'noopener noreferrer'; into.appendChild(a); }at = m.index + tok.length;}@@@ -333,6 +337,7 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }// ── The view ──────────────────────────────────────────────function mount(host, opts = {}) {styles();++ const linkOk = typeof opts.link === 'function' ? (u) => { try { return !!opts.link(u); } catch { return false; } } : null;const S = { status: opts.status || 'running', label: opts.label || '', startedAt: opts.startedAt || Date.now(), endedAt: null, agents: [], steps: new Map(), plan: null, tokens: { in: 0, out: 0 }, cost: 0, durationMs: 0, problems: 0, filter: 'all', follow: opts.follow !== false, lastGroup: null };host.textContent = '';const box = el('div', 'ovas');@@@ -381,7 +386,7 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }const section = (label, node, copy) => { const l = el('div', 'ovas-label'); l.appendChild(el('span', '', label)); if (copy) { const c = el('button', 'ovas-copy'); c.type = 'button'; c.innerHTML = `${svg('copy')}<span>Copy</span>`; l.appendChild(c); } detail.append(l, node); };if (step.kind === 'command' && info.command) { const pre = el('pre', 'ovas-code'); pre.textContent = info.command; section('Command', pre, true); }if (info.diff) { const pre = el('pre', 'ovas-diff'); info.diff.slice(0, 600).forEach(([op, line]) => { const s = el('span', op === '+' ? 'a' : op === '-' ? 'r' : op === '@' ? 'h' : '', `${op === '=' ? ' ' : op === '@' ? '' : op} ${line}`); pre.appendChild(s); }); section(step.kind === 'write' ? 'New file' : 'Change', pre, false); }-- if (step.kind === 'agent' && info.preview) { const p = el('div', 'ovas-msg'); p.appendChild(markdown(info.preview.slice(0, 1600))); section('Asked to', p, false); }++ if (step.kind === 'agent' && info.preview) { const p = el('div', 'ovas-msg'); p.appendChild(markdown(info.preview.slice(0, 1600), linkOk)); section('Asked to', p, false); }if (step.kind === 'agent') { const sub = el('div', 'ovas-sub'); step.subEl = sub; d.appendChild(sub); d.open = true; }if (step.kind !== 'agent' && step.kind !== 'plan') {const pre = el('pre', 'ovas-out'); pre.hidden = true; step.outEl = pre;@@@ -449,7 +454,7 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }paintHead(); return;}if (x.t === 'text') {-- const li = el('li', 'ovas-msg'); li.dataset.k = 'msg'; li.appendChild(markdown(x.text)); appendLi(target, li); return;++ const li = el('li', 'ovas-msg'); li.dataset.k = 'msg'; li.appendChild(markdown(x.text, linkOk)); appendLi(target, li); return;}if (x.t === 'tool') {const kind = kindOf(x.name);@@@ -504,7 +509,7 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }const li = el('li', 'ovas-done'); li.dataset.k = 'done'; li.dataset.ok = x.ok ? '1' : '0';const b = el('b'); b.innerHTML = svg(x.ok ? 'ok' : 'fail'); b.appendChild(document.createTextNode(`${agentName((S.agents[S.agents.length - 1] || {}).agent)} ${x.ok ? 'finished' : 'stopped'}${x.duration_ms ? ` in ${dur(x.duration_ms)}` : ''}`));li.appendChild(b);-- if (x.text) { const m = el('div', 'ovas-msg'); m.appendChild(markdown(x.text)); li.appendChild(m); }++ if (x.text) { const m = el('div', 'ovas-msg'); m.appendChild(markdown(x.text, linkOk)); li.appendChild(m); }if (!x.ok) li.dataset.p = '1';appendLi(feed, li); paintHead();}@@@ -59,6 +59,9 @@ agents… in a smart way… way better than this idea."**Theme:** the colours come from the page's theme tokens, so it reads in light and dark.++**Links:** `mount(el, { link: (url) => … })` decides which https addresses in the agents' text become links; the rest++stay text. A site whose agents read untrusted pages should link only its own (Actor links only the person's desk).++## The shapes it reads**1. Claude Code's stream-json**, the shape every Codes harness adapter emits. See OpenVibe.Codes' `harness/events.js`:@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.24.1",++ "version": "4.24.2","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -109,6 +109,11 @@ const server = http.createServer((req, res) => {assert.strictEqual(v.info, 'info');assert.strictEqual(v.pill, 'Finished');assert.strictEqual(v.problems, 1, 'only the hand-off counts as a problem, not the passed check');++ // link(url): a site whose agents read untrusted pages links only its own addresses; the rest stay text.++ await p.evaluate(`(() => { const d = document.createElement('div'); d.id = 'own'; document.body.appendChild(d); const w = OpenVibeAgentSession.mount(d, { title: 'x', link: (u) => u.startsWith('https://openvibe.bot/') }); w.push({ t: 'text', text: 'Your desk: https://openvibe.bot/machines/m/screen and https://evil.example/login' }); return 1; })()`);++ v = await p.evaluate(`[...document.querySelectorAll('#own .ovas-msg a')].map((a) => a.href)`);++ assert.deepStrictEqual(v, ['https://openvibe.bot/machines/m/screen'], 'only the site\'s own address links');++ assert.ok(await p.evaluate(`document.querySelector('#own .ovas-msg').textContent.includes('https://evil.example/login')`), 'the other stays as text');const errors = p.state.errors.filter((e) => !/Failed to load resource/.test(e.text));assert.deepStrictEqual(errors, [], JSON.stringify(errors));console.log('agent-session-chrome: groups, live output, failures, helpers, hand-offs, filters and safe text ok');
@@ -6,6 +6,12 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
@@ -6,6 +6,12 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## Unreleased
## Unreleased
+## 4.24.2 — 2026-10-28
+
+- **`agent-session.js` takes `link(url)`**, which decides which https addresses in an agent's text become links; the
+ rest stay text. OpenVibe.Actor's agents browse untrusted pages, so its task page links only the person's own desk, as
+ its server-drawn timeline always did. Without `link`, every https address links, as before.
+
## 4.24.1 — 2026-10-28
## 4.24.1 — 2026-10-28
- **`agent-session.js` reads OpenVibe.Actor's task events** (`actor.task-event@1`), so Actor's agents show in the same
- **`agent-session.js` reads OpenVibe.Actor's task events** (`actor.task-event@1`), so Actor's agents show in the same
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
-**Status:** alpha, v4.24.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.24.2 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.24.1",
+ "version": "4.24.2",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
@@ -7,7 +7,9 @@
@@ -7,7 +7,9 @@
* unavailable"): "OpenVibe.Codes should… actually show the output from the agent… more user friendly… handle that
* unavailable"): "OpenVibe.Codes should… actually show the output from the agent… more user friendly… handle that
* between all agents… in a smart way… way better than this idea."
* between all agents… in a smart way… way better than this idea."
*
*
- * const view = OpenVibeAgentSession.mount(el, { title, status: 'running', startedAt })
+ * const view = OpenVibeAgentSession.mount(el, { title, status: 'running', startedAt, link })
+ * link(url) → boolean: which https addresses in the agents' text become links (default:
+ * all; a site whose agents read untrusted pages links only its own, e.g. a person's desk)
* view.push(event) one event: a Codes job line ({ t: 'tool', … }), any Claude Code stream-json event
* view.push(event) one event: a Codes job line ({ t: 'tool', … }), any Claude Code stream-json event
* ({ type: 'assistant', … }) or an OpenVibe.Actor task event ({ kind: 'output', step, … },
* ({ type: 'assistant', … }) or an OpenVibe.Actor task event ({ kind: 'output', step, … },
* actor.task-event@1); every OpenVibe.Codes harness adapter (Claude Code, Codex, OpenCode,
* actor.task-event@1); every OpenVibe.Codes harness adapter (Claude Code, Codex, OpenCode,
@@ -141,8 +143,9 @@
@@ -141,8 +143,9 @@
}
}
const base = (p) => String(p || '').split('/').filter(Boolean).pop() || String(p || '');
const base = (p) => String(p || '').split('/').filter(Boolean).pop() || String(p || '');
- /** Light Markdown, text-safe: paragraphs, bullet and numbered lists, `code`, ```blocks```, **bold**, https links. */
- function markdown(text) {
+ /** Light Markdown, text-safe: paragraphs, bullet and numbered lists, `code`, ```blocks```, **bold**, https links (only
+ * those `linkOk(url)` allows; the others stay text). */
+ function markdown(text, linkOk) {
const frag = document.createDocumentFragment();
const frag = document.createDocumentFragment();
const inline = (s, into) => {
const inline = (s, into) => {
const re = /(`[^`]+`|\*\*[^*]+\*\*|https:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/g;
const re = /(`[^`]+`|\*\*[^*]+\*\*|https:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/g;
@@ -152,6 +155,7 @@
@@ -152,6 +155,7 @@
const tok = m[0];
const tok = m[0];
if (tok[0] === '`') into.appendChild(el('code', '', tok.slice(1, -1)));
if (tok[0] === '`') into.appendChild(el('code', '', tok.slice(1, -1)));
else if (tok.startsWith('**')) into.appendChild(el('strong', '', tok.slice(2, -2)));
else if (tok.startsWith('**')) into.appendChild(el('strong', '', tok.slice(2, -2)));
+ else if (linkOk && !linkOk(tok)) into.appendChild(document.createTextNode(tok));
else { const a = el('a', '', tok); a.href = tok; a.target = '_blank'; a.rel = 'noopener noreferrer'; into.appendChild(a); }
else { const a = el('a', '', tok); a.href = tok; a.target = '_blank'; a.rel = 'noopener noreferrer'; into.appendChild(a); }
at = m.index + tok.length;
at = m.index + tok.length;
}
}
@@ -333,6 +337,7 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }
@@ -333,6 +337,7 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }
// ── The view ──────────────────────────────────────────────
// ── The view ──────────────────────────────────────────────
function mount(host, opts = {}) {
function mount(host, opts = {}) {
styles();
styles();
+ const linkOk = typeof opts.link === 'function' ? (u) => { try { return !!opts.link(u); } catch { return false; } } : null;
const S = { status: opts.status || 'running', label: opts.label || '', startedAt: opts.startedAt || Date.now(), endedAt: null, agents: [], steps: new Map(), plan: null, tokens: { in: 0, out: 0 }, cost: 0, durationMs: 0, problems: 0, filter: 'all', follow: opts.follow !== false, lastGroup: null };
const S = { status: opts.status || 'running', label: opts.label || '', startedAt: opts.startedAt || Date.now(), endedAt: null, agents: [], steps: new Map(), plan: null, tokens: { in: 0, out: 0 }, cost: 0, durationMs: 0, problems: 0, filter: 'all', follow: opts.follow !== false, lastGroup: null };
host.textContent = '';
host.textContent = '';
const box = el('div', 'ovas');
const box = el('div', 'ovas');
@@ -381,7 +386,7 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }
@@ -381,7 +386,7 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }
const section = (label, node, copy) => { const l = el('div', 'ovas-label'); l.appendChild(el('span', '', label)); if (copy) { const c = el('button', 'ovas-copy'); c.type = 'button'; c.innerHTML = `${svg('copy')}<span>Copy</span>`; l.appendChild(c); } detail.append(l, node); };
const section = (label, node, copy) => { const l = el('div', 'ovas-label'); l.appendChild(el('span', '', label)); if (copy) { const c = el('button', 'ovas-copy'); c.type = 'button'; c.innerHTML = `${svg('copy')}<span>Copy</span>`; l.appendChild(c); } detail.append(l, node); };
if (step.kind === 'command' && info.command) { const pre = el('pre', 'ovas-code'); pre.textContent = info.command; section('Command', pre, true); }
if (step.kind === 'command' && info.command) { const pre = el('pre', 'ovas-code'); pre.textContent = info.command; section('Command', pre, true); }
if (info.diff) { const pre = el('pre', 'ovas-diff'); info.diff.slice(0, 600).forEach(([op, line]) => { const s = el('span', op === '+' ? 'a' : op === '-' ? 'r' : op === '@' ? 'h' : '', `${op === '=' ? ' ' : op === '@' ? '' : op} ${line}`); pre.appendChild(s); }); section(step.kind === 'write' ? 'New file' : 'Change', pre, false); }
if (info.diff) { const pre = el('pre', 'ovas-diff'); info.diff.slice(0, 600).forEach(([op, line]) => { const s = el('span', op === '+' ? 'a' : op === '-' ? 'r' : op === '@' ? 'h' : '', `${op === '=' ? ' ' : op === '@' ? '' : op} ${line}`); pre.appendChild(s); }); section(step.kind === 'write' ? 'New file' : 'Change', pre, false); }
- if (step.kind === 'agent' && info.preview) { const p = el('div', 'ovas-msg'); p.appendChild(markdown(info.preview.slice(0, 1600))); section('Asked to', p, false); }
+ if (step.kind === 'agent' && info.preview) { const p = el('div', 'ovas-msg'); p.appendChild(markdown(info.preview.slice(0, 1600), linkOk)); section('Asked to', p, false); }
if (step.kind === 'agent') { const sub = el('div', 'ovas-sub'); step.subEl = sub; d.appendChild(sub); d.open = true; }
if (step.kind === 'agent') { const sub = el('div', 'ovas-sub'); step.subEl = sub; d.appendChild(sub); d.open = true; }
if (step.kind !== 'agent' && step.kind !== 'plan') {
if (step.kind !== 'agent' && step.kind !== 'plan') {
const pre = el('pre', 'ovas-out'); pre.hidden = true; step.outEl = pre;
const pre = el('pre', 'ovas-out'); pre.hidden = true; step.outEl = pre;
@@ -449,7 +454,7 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }
@@ -449,7 +454,7 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }
paintHead(); return;
paintHead(); return;
}
}
if (x.t === 'text') {
if (x.t === 'text') {
- const li = el('li', 'ovas-msg'); li.dataset.k = 'msg'; li.appendChild(markdown(x.text)); appendLi(target, li); return;
+ const li = el('li', 'ovas-msg'); li.dataset.k = 'msg'; li.appendChild(markdown(x.text, linkOk)); appendLi(target, li); return;
}
}
if (x.t === 'tool') {
if (x.t === 'tool') {
const kind = kindOf(x.name);
const kind = kindOf(x.name);
@@ -504,7 +509,7 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }
@@ -504,7 +509,7 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }
const li = el('li', 'ovas-done'); li.dataset.k = 'done'; li.dataset.ok = x.ok ? '1' : '0';
const li = el('li', 'ovas-done'); li.dataset.k = 'done'; li.dataset.ok = x.ok ? '1' : '0';
const b = el('b'); b.innerHTML = svg(x.ok ? 'ok' : 'fail'); b.appendChild(document.createTextNode(`${agentName((S.agents[S.agents.length - 1] || {}).agent)} ${x.ok ? 'finished' : 'stopped'}${x.duration_ms ? ` in ${dur(x.duration_ms)}` : ''}`));
const b = el('b'); b.innerHTML = svg(x.ok ? 'ok' : 'fail'); b.appendChild(document.createTextNode(`${agentName((S.agents[S.agents.length - 1] || {}).agent)} ${x.ok ? 'finished' : 'stopped'}${x.duration_ms ? ` in ${dur(x.duration_ms)}` : ''}`));
li.appendChild(b);
li.appendChild(b);
- if (x.text) { const m = el('div', 'ovas-msg'); m.appendChild(markdown(x.text)); li.appendChild(m); }
+ if (x.text) { const m = el('div', 'ovas-msg'); m.appendChild(markdown(x.text, linkOk)); li.appendChild(m); }
if (!x.ok) li.dataset.p = '1';
if (!x.ok) li.dataset.p = '1';
appendLi(feed, li); paintHead();
appendLi(feed, li); paintHead();
}
}
@@ -59,6 +59,9 @@ agents… in a smart way… way better than this idea."
@@ -59,6 +59,9 @@ agents… in a smart way… way better than this idea."
**Theme:** the colours come from the page's theme tokens, so it reads in light and dark.
**Theme:** the colours come from the page's theme tokens, so it reads in light and dark.
+**Links:** `mount(el, { link: (url) => … })` decides which https addresses in the agents' text become links; the rest
+stay text. A site whose agents read untrusted pages should link only its own (Actor links only the person's desk).
+
## The shapes it reads
## The shapes it reads
**1. Claude Code's stream-json**, the shape every Codes harness adapter emits. See OpenVibe.Codes' `harness/events.js`:
**1. Claude Code's stream-json**, the shape every Codes harness adapter emits. See OpenVibe.Codes' `harness/events.js`:
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.24.1",
+ "version": "4.24.2",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -109,6 +109,11 @@ const server = http.createServer((req, res) => {
@@ -109,6 +109,11 @@ const server = http.createServer((req, res) => {
assert.strictEqual(v.info, 'info');
assert.strictEqual(v.info, 'info');
assert.strictEqual(v.pill, 'Finished');
assert.strictEqual(v.pill, 'Finished');
assert.strictEqual(v.problems, 1, 'only the hand-off counts as a problem, not the passed check');
assert.strictEqual(v.problems, 1, 'only the hand-off counts as a problem, not the passed check');
+ // link(url): a site whose agents read untrusted pages links only its own addresses; the rest stay text.
+ await p.evaluate(`(() => { const d = document.createElement('div'); d.id = 'own'; document.body.appendChild(d); const w = OpenVibeAgentSession.mount(d, { title: 'x', link: (u) => u.startsWith('https://openvibe.bot/') }); w.push({ t: 'text', text: 'Your desk: https://openvibe.bot/machines/m/screen and https://evil.example/login' }); return 1; })()`);
+ v = await p.evaluate(`[...document.querySelectorAll('#own .ovas-msg a')].map((a) => a.href)`);
+ assert.deepStrictEqual(v, ['https://openvibe.bot/machines/m/screen'], 'only the site\'s own address links');
+ assert.ok(await p.evaluate(`document.querySelector('#own .ovas-msg').textContent.includes('https://evil.example/login')`), 'the other stays as text');
const errors = p.state.errors.filter((e) => !/Failed to load resource/.test(e.text));
const errors = p.state.errors.filter((e) => !/Failed to load resource/.test(e.text));
assert.deepStrictEqual(errors, [], JSON.stringify(errors));
assert.deepStrictEqual(errors, [], JSON.stringify(errors));
console.log('agent-session-chrome: groups, live output, failures, helpers, hand-offs, filters and safe text ok');
console.log('agent-session-chrome: groups, live output, failures, helpers, hand-offs, filters and safe text ok');