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)

OP OpenVibers committed 16 days ago · 5322439629 · 7 files, +28 −9

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex efe7145..aa1cf1f 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -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
ddiff --git a/README.md b/README.md
iindex a00e107..b25b5fb 100644
---- a/README.md
++++ b/README.md
@@@ -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 deployed
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
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex 6b1bbf0..a6e308a 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -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)",
ddiff --git a/agent-session.js b/agent-session.js
iindex 2ee4605..622a9ca 100644
---- a/agent-session.js
++++ b/agent-session.js
@@@ -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();
}
ddiff --git a/docs/agent-session.md b/docs/agent-session.md
iindex adfd8a3..6cc09ff 100644
---- a/docs/agent-session.md
++++ b/docs/agent-session.md
@@@ -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`:
ddiff --git a/package.json b/package.json
iindex f651871..2e54a03 100644
---- a/package.json
++++ b/package.json
@@@ -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": {
ddiff --git a/test/agent-session-chrome.test.js b/test/agent-session-chrome.test.js
iindex c5a3d04..df5209e 100644
---- a/test/agent-session-chrome.test.js
++++ b/test/agent-session-chrome.test.js
@@@ -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');