Skip to content

Commit 77ddc30

Browse files
committed
Sequence and redact cloud run updates
1 parent 7f35071 commit 77ddc30

2 files changed

Lines changed: 118 additions & 5 deletions

File tree

src/chrome/src/cloud-runs.js

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ const CLOUD_STRING_LIMIT = 16 * 1024;
66
const CLOUD_RUN_PERSIST_BYTES_LIMIT = 256 * 1024;
77
const CLOUD_PERSIST_BYTES_LIMIT = 4 * 1024 * 1024;
88
const TERMINAL_STATUSES = new Set(['completed', 'failed', 'aborted']);
9+
const SENSITIVE_CLOUD_KEY = /(?:authorization|cookie|password|passwd|passphrase|passcode|pin|secret|credential|privatekey|apikey|token)$/i;
10+
const LARGE_IMAGE_KEY = /(?:attachimage|screenshot|image|imagedata|dataurl)$/i;
11+
12+
function normalizedCloudKey(key) {
13+
return String(key || '').replace(/[^a-z0-9]/gi, '');
14+
}
915

1016
export function normalizeCloudBridgeUrl(value = DEFAULT_CLOUD_BRIDGE_URL) {
1117
const url = new URL(String(value || DEFAULT_CLOUD_BRIDGE_URL));
@@ -19,10 +25,14 @@ export function normalizeCloudBridgeUrl(value = DEFAULT_CLOUD_BRIDGE_URL) {
1925
function scrubCloudValue(value) {
2026
try {
2127
return JSON.parse(JSON.stringify(value, (key, item) => {
22-
if (typeof item === 'string' && item.startsWith('data:image/')) {
28+
const normalizedKey = normalizedCloudKey(key);
29+
if (normalizedKey && SENSITIVE_CLOUD_KEY.test(normalizedKey)) {
30+
return '[redacted]';
31+
}
32+
if (typeof item === 'string' && /^data:image\//i.test(item)) {
2333
return `[image omitted: ${item.length} chars]`;
2434
}
25-
if ((key === '_attachImage' || key === 'screenshot') && typeof item === 'string' && item.length > 500) {
35+
if (LARGE_IMAGE_KEY.test(normalizedKey) && typeof item === 'string' && item.length > 500) {
2636
return `[large payload omitted: ${item.length} chars]`;
2737
}
2838
if (typeof item === 'string' && item.length > CLOUD_STRING_LIMIT) {
@@ -169,7 +179,18 @@ export function createCloudRunController({
169179
let changed = false;
170180
for (const row of rows) {
171181
if (!row?.runId) continue;
172-
const restored = { ...row, updates: Array.isArray(row.updates) ? row.updates : [] };
182+
const rawUpdates = Array.isArray(row.updates) ? row.updates : [];
183+
let nextUpdateSeq = 0;
184+
const updates = rawUpdates.map((update, index) => {
185+
const candidate = Number(update?.seq);
186+
const seq = Number.isSafeInteger(candidate) && candidate > nextUpdateSeq
187+
? candidate
188+
: nextUpdateSeq + 1;
189+
if (seq !== candidate) changed = true;
190+
nextUpdateSeq = seq;
191+
return { ...update, seq };
192+
});
193+
const restored = { ...row, updates, nextUpdateSeq };
173194
if (!TERMINAL_STATUSES.has(restored.status)) {
174195
const at = isoNow();
175196
restored.status = restored.status === 'aborting' ? 'aborted' : 'failed';
@@ -230,7 +251,18 @@ export function createCloudRunController({
230251

231252
function pushUpdate(run, type, data) {
232253
run.updatedAt = isoNow();
233-
run.updates.push({ type, data: scrubCloudValue(data), ts: run.updatedAt });
254+
const previous = run.updates.at(-1);
255+
if (type === 'text_delta' && previous?.type === 'text_delta') {
256+
previous.data = {
257+
...previous.data,
258+
content: `${previous.data?.content || ''}${data?.content || ''}`,
259+
};
260+
previous.ts = run.updatedAt;
261+
schedulePersist();
262+
return;
263+
}
264+
run.nextUpdateSeq = (Number(run.nextUpdateSeq) || 0) + 1;
265+
run.updates.push({ seq: run.nextUpdateSeq, type, data: scrubCloudValue(data), ts: run.updatedAt });
234266
if (run.updates.length > CLOUD_UPDATE_LIMIT) {
235267
run.updates.splice(0, run.updates.length - CLOUD_UPDATE_LIMIT);
236268
}
@@ -274,6 +306,7 @@ export function createCloudRunController({
274306
finalUrl: '',
275307
error: '',
276308
updates: [],
309+
nextUpdateSeq: 0,
277310
createdAt,
278311
updatedAt: createdAt,
279312
completedAt: null,

test/run.js

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4169,6 +4169,31 @@ test('cloud run controller uses the visible tab and persists terminal status', a
41694169
assert.equal(processArgs[3], 'act');
41704170
assert.deepEqual(processArgs[4], []);
41714171
assert.equal(processArgs[5].cloudRun, true);
4172+
processArgs[2]('thinking', { step: 1, note: 'Opening the page' });
4173+
processArgs[2]('text_delta', { content: 'Opening ' });
4174+
processArgs[2]('text_delta', { content: 'the page' });
4175+
processArgs[2]('tool_call', {
4176+
name: 'fetch_url',
4177+
args: {
4178+
url: 'https://webbrain.one/',
4179+
authorization: 'Bearer secret-token',
4180+
nested: {
4181+
apiKey: 'sk-test',
4182+
sessionToken: 'session-secret',
4183+
headers: { 'x-api-key': 'header-secret' },
4184+
},
4185+
screenshot: `data:image/png;base64,${'a'.repeat(600)}`,
4186+
},
4187+
});
4188+
processArgs[2]('tool_result', { name: 'fetch_url', result: { success: true } });
4189+
const running = await controller.status({ run_id: 'run_test' });
4190+
assert.deepEqual(running.updates.map(update => update.seq), [1, 2, 3, 4]);
4191+
assert.equal(running.updates[1].data.content, 'Opening the page');
4192+
assert.equal(running.updates[2].data.args.authorization, '[redacted]');
4193+
assert.equal(running.updates[2].data.args.nested.apiKey, '[redacted]');
4194+
assert.equal(running.updates[2].data.args.nested.sessionToken, '[redacted]');
4195+
assert.equal(running.updates[2].data.args.nested.headers['x-api-key'], '[redacted]');
4196+
assert.match(running.updates[2].data.args.screenshot, /^\[image omitted:/);
41724197
finishRun('Google');
41734198
await new Promise(resolve => setTimeout(resolve, 0));
41744199
const completed = await controller.status({ run_id: 'run_test' });
@@ -4177,6 +4202,50 @@ test('cloud run controller uses the visible tab and persists terminal status', a
41774202
assert.equal(session.webbrainCloudRunSnapshots[0].status, 'completed');
41784203
});
41794204

4205+
test('cloud run controller keeps the newest 200 monotonically sequenced updates', async () => {
4206+
const session = {};
4207+
const tab = { id: 21, url: 'https://webbrain.one/', active: true, windowId: 3 };
4208+
let finishRun;
4209+
let emitUpdate;
4210+
const controller = createCloudRunController({
4211+
chromeApi: {
4212+
tabs: {
4213+
query: async () => [tab],
4214+
get: async () => tab,
4215+
update: async () => tab,
4216+
},
4217+
windows: { update: async () => ({}) },
4218+
storage: {
4219+
local: { get: async () => ({ webbrainCloudBridgeEnabled: false }) },
4220+
session: {
4221+
get: async key => ({ [key]: session[key] || [] }),
4222+
set: async value => Object.assign(session, value),
4223+
},
4224+
},
4225+
runtime: { sendMessage: async () => ({ connected: false }) },
4226+
},
4227+
agent: {
4228+
isRunning: () => false,
4229+
abort: () => {},
4230+
processMessage: (_tabId, _task, onUpdate) => {
4231+
emitUpdate = onUpdate;
4232+
return new Promise(resolve => { finishRun = resolve; });
4233+
},
4234+
},
4235+
ensureOffscreen: async () => {},
4236+
makeRunId: () => 'run_updates',
4237+
});
4238+
4239+
await controller.startRun({ task: 'Generate many updates' });
4240+
for (let i = 1; i <= 205; i += 1) emitUpdate('thinking', { step: i });
4241+
const running = await controller.status({ runId: 'run_updates' });
4242+
assert.equal(running.updates.length, 200);
4243+
assert.equal(running.updates[0].seq, 6);
4244+
assert.equal(running.updates.at(-1).seq, 205);
4245+
finishRun('Done');
4246+
await new Promise(resolve => setTimeout(resolve, 0));
4247+
});
4248+
41804249
test('cloud run controller fails immediately if an interactive plan review leaks through', async () => {
41814250
const session = {};
41824251
const tab = { id: 19, url: 'https://webbrain.one/', active: true, windowId: 3 };
@@ -4220,7 +4289,17 @@ test('cloud run controller fails immediately if an interactive plan review leaks
42204289
});
42214290

42224291
test('cloud run controller fails interrupted runs after service-worker restart', async () => {
4223-
const row = { runId: 'run_old', status: 'running', tabId: 2, task: 'Old task', updates: [], createdAt: '2020-01-01T00:00:00.000Z' };
4292+
const row = {
4293+
runId: 'run_old',
4294+
status: 'running',
4295+
tabId: 2,
4296+
task: 'Old task',
4297+
updates: [
4298+
{ type: 'thinking', data: { step: 1 }, ts: '2020-01-01T00:00:01.000Z' },
4299+
{ seq: 8, type: 'tool_call', data: { name: 'navigate' }, ts: '2020-01-01T00:00:02.000Z' },
4300+
],
4301+
createdAt: '2020-01-01T00:00:00.000Z',
4302+
};
42244303
const session = { webbrainCloudRunSnapshots: [row] };
42254304
const controller = createCloudRunController({
42264305
chromeApi: {
@@ -4238,6 +4317,7 @@ test('cloud run controller fails interrupted runs after service-worker restart',
42384317
const restored = await controller.status({ runId: 'run_old' });
42394318
assert.equal(restored.status, 'failed');
42404319
assert.match(restored.error, /service worker restarted/i);
4320+
assert.deepEqual(restored.updates.map(update => update.seq), [1, 8]);
42414321
});
42424322

42434323
test('cloud run persistence caps oversized strings, runs, and total snapshot bytes', () => {

0 commit comments

Comments
 (0)