openvibers/openvibe.actor Commits
See it work: redact typed desk text from task events (follow-up to #449) (#450)
Changes
@@@ -14,7 +14,8 @@ What changed in OpenVibe.Actor, newest first. Each site also publishes its patchoptional note that reaches the run as the person's message. A `hand_to_person` step is an Ask that opens the viewerin 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-backand the person's input. Tests: `test/see-it-work.test.js`; screenshots: `test/see-it-work-shots.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();@@@ -187,6 +187,8 @@ const PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAassert.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 \{/);
@@ -14,7 +14,8 @@ What changed in OpenVibe.Actor, newest first. Each site also publishes its patch
@@ -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
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.
in control.
- **Replay**: every computer step keeps its still and its action. A finished task's card has a timeline to scrub, and
- **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
- 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`.
and the person's input. Tests: `test/see-it-work.test.js`; screenshots: `test/see-it-work-shots.js`.
@@ -171,6 +171,16 @@ function planText(plan, brain = null) {
@@ -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));
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) {
function summarize(args) {
const a = 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(', ')}`
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.
@@ -410,7 +420,8 @@ function createLoop({ config, models, registry, log = console, now = () => Date.
let i = 0;
let i = 0;
for (const p of planned) {
for (const p of planned) {
if (p.skipped) {
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 }) });
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 });
await ctx.emit({ step: 'tool_result', tool: p.name, text: p.skipped, is_error: true });
errorResults += 1;
errorResults += 1;
@@ -420,7 +431,8 @@ function createLoop({ config, models, registry, log = console, now = () => Date.
@@ -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.
// 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.
// No model runs until the event or the deadline; the run's place is its checkpoint.
if (out && out.park) state.parked = out.park;
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) {
if (out && out.delegate) {
// A sub-agent's report: its answer and the evidence ids it brought into this run's ledger. The report
// 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.
// 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.
@@ -839,7 +851,7 @@ function createLoop({ config, models, registry, log = console, now = () => Date.
const name = (call.function && call.function.name) || 'tool';
const name = (call.function && call.function.name) || 'tool';
let args = {};
let args = {};
try { args = JSON.parse((call.function && call.function.arguments) || '{}'); } catch { 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 choice = choices.get(id);
const redacted = /\[redacted|\[not stored/.test(JSON.stringify(args));
const redacted = /\[redacted|\[not stored/.test(JSON.stringify(args));
const startedAt = now();
const startedAt = now();
@@ -187,6 +187,8 @@ const PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwA
@@ -187,6 +187,8 @@ const PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwA
assert.match(js, /stopStream\(st\)/);
assert.match(js, /stopStream\(st\)/);
assert.match(js, /setInterval\(function \(\) \{ show\(img, tile\.getAttribute\('data-still'\)\); \}, 1000\)/);
assert.match(js, /setInterval\(function \(\) \{ show\(img, tile\.getAttribute\('data-still'\)\); \}, 1000\)/);
assert.match(js, /data-ct-replay-step/);
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');
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, /\.ct-desk-frame \{[^}]*aspect-ratio: 16 \/ 9/);
assert.match(css, /@media \(min-width: 900px\)[\s\S]*\.ct-viewer-side \{/);
assert.match(css, /@media \(min-width: 900px\)[\s\S]*\.ct-viewer-side \{/);