openvibers/openvibe.actor Commits

See it work: redact typed desk text from task events (follow-up to #449) (#450)

OP OpenVibers committed 1 days ago · cecd8f0882 · 3 files, +19 −4

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex f652343..aca02b1 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -14,7 +14,8 @@ What changed in OpenVibe.Actor, newest first. Each site also publishes its patch
optional note that reaches the run as the person's message. A `hand_to_person` step is an Ask that opens the viewer
in control.
- **Replay**: every computer step keeps its still and its action. A finished task's card has a timeline to scrub, and
-- each step opens the picture it saw. Typed text is never stored in a replay; a click keeps only its x,y.
++ each step opens the picture it saw. Typed text is redacted before it reaches the task's events, so neither the replay
++ nor the task log carries it; a click keeps only its x,y.
- Migration 0070 (task stills); `GET /api/v1/tasks/:id/desks`, `/desk-still`, `/replay`, the desk take-over/hand-back
and the person's input. Tests: `test/see-it-work.test.js`; screenshots: `test/see-it-work-shots.js`.
ddiff --git a/server/runtime/loop.js b/server/runtime/loop.js
iindex 0afe708..483f303 100644
---- a/server/runtime/loop.js
++++ b/server/runtime/loop.js
@@@ -171,6 +171,16 @@ function planText(plan, brain = null) {
const clip = (s, n) => (String(s == null ? '' : s).length > n ? `${String(s).slice(0, n - 1)}…` : String(s == null ? '' : s));
++/** The args an event may carry: credentials stripped, and what a person or agent typed never stored verbatim. */
++function eventArgs(name, args) {
++ const safe = stripSecrets(args);
++ if (safe && typeof safe === 'object' && !Array.isArray(safe)) {
++ if (name === 'computer_input' && safe.action === 'type') safe.text = '[redacted: typed on the desk]';
++ if (name === 'computer_browser' && safe.action === 'fill') safe.text = '[redacted: typed on the desk]';
++ }
++ return safe;
++}
++
function summarize(args) {
const a = args || {};
const v = a.query || a.need || a.goal || a.url || a.instruction || (a.tool ? `${a.tool} ${JSON.stringify(a.input || {})}` : a.robot ? `${a.robot}: ${[].concat(a.steps || []).join(', ')}`
@@@ -410,7 +420,8 @@ function createLoop({ config, models, registry, log = console, now = () => Date.
let i = 0;
for (const p of planned) {
if (p.skipped) {
-- await ctx.emit({ step: 'tool_call', tool: p.name, call_id: p.call.id, args: stripSecrets(p.args), input: summarize(p.args) });
++ const skippedArgs = eventArgs(p.name, p.args);
++ await ctx.emit({ step: 'tool_call', tool: p.name, call_id: p.call.id, args: skippedArgs, input: summarize(skippedArgs) });
state.messages.push({ role: 'tool', tool_call_id: p.call.id, content: JSON.stringify({ error: p.skipped, tool: p.name }) });
await ctx.emit({ step: 'tool_result', tool: p.name, text: p.skipped, is_error: true });
errorResults += 1;
@@@ -420,7 +431,8 @@ function createLoop({ config, models, registry, log = console, now = () => Date.
// A tool that parks the run (plan A2): the wait is written and the loop stops here.
// No model runs until the event or the deadline; the run's place is its checkpoint.
if (out && out.park) state.parked = out.park;
-- await ctx.emit({ step: 'tool_call', tool: p.name, call_id: p.call.id, args: stripSecrets(p.args), input: summarize(p.args) });
++ const calledArgs = eventArgs(p.name, p.args);
++ await ctx.emit({ step: 'tool_call', tool: p.name, call_id: p.call.id, args: calledArgs, input: summarize(calledArgs) });
if (out && out.delegate) {
// A sub-agent's report: its answer and the evidence ids it brought into this run's ledger. The report
// itself is not evidence (it is a model's words), so the parent cites the ids, never the report.
@@@ -839,7 +851,7 @@ function createLoop({ config, models, registry, log = console, now = () => Date.
const name = (call.function && call.function.name) || 'tool';
let args = {};
try { args = JSON.parse((call.function && call.function.arguments) || '{}'); } catch { args = {}; }
-- await ctx.emit({ step: 'tool_call', tool: name, input: summarize(args) });
++ await ctx.emit({ step: 'tool_call', tool: name, input: summarize(eventArgs(name, args)) });
const choice = choices.get(id);
const redacted = /\[redacted|\[not stored/.test(JSON.stringify(args));
const startedAt = now();
ddiff --git a/test/see-it-work.test.js b/test/see-it-work.test.js
iindex 869ae27..ddab62a 100644
---- a/test/see-it-work.test.js
++++ b/test/see-it-work.test.js
@@@ -187,6 +187,8 @@ const PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwA
assert.match(js, /stopStream\(st\)/);
assert.match(js, /setInterval\(function \(\) \{ show\(img, tile\.getAttribute\('data-still'\)\); \}, 1000\)/);
assert.match(js, /data-ct-replay-step/);
++ const loop = require('fs').readFileSync(require('path').join(__dirname, '..', 'server', 'runtime', 'loop.js'), 'utf8');
++ assert.match(loop, /computer_input.*type.*\[redacted: typed on the desk\]/, 'typed desk text never reaches an event');
const css = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'css', 'chat.css'), 'utf8');
assert.match(css, /\.ct-desk-frame \{[^}]*aspect-ratio: 16 \/ 9/);
assert.match(css, /@media \(min-width: 900px\)[\s\S]*\.ct-viewer-side \{/);