Skip to content

Commit 810a620

Browse files
committed
Scrub coalesced cloud text_delta and tighten key redaction
Apply scrubCloudValue on upserted text_delta rows so live status hits the same size/image limits, and broaden sensitive-key matching for AWS-style credentials without over-matching pin suffixes.
1 parent 77ddc30 commit 810a620

2 files changed

Lines changed: 80 additions & 5 deletions

File tree

src/chrome/src/cloud-runs.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ 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;
9+
// Suffix match on normalized keys (non-alnum stripped). Avoid bare `pin` as a
10+
// suffix — it over-matches `spin`, `mapPin`, etc. Short exact keys live in the set.
11+
const SENSITIVE_CLOUD_KEY = /(?:authorization|cookie|password|passwd|passphrase|passcode|pincode|secret|credential|privatekey|apikey|token|accesskeyid|secretaccesskey)$/i;
12+
const SENSITIVE_CLOUD_KEY_EXACT = new Set(['pin', 'otp', 'cvv', 'cvc', 'ssn']);
1013
const LARGE_IMAGE_KEY = /(?:attachimage|screenshot|image|imagedata|dataurl)$/i;
1114

1215
function normalizedCloudKey(key) {
@@ -22,11 +25,17 @@ export function normalizeCloudBridgeUrl(value = DEFAULT_CLOUD_BRIDGE_URL) {
2225
return url.href;
2326
}
2427

28+
function isSensitiveCloudKey(key) {
29+
const normalizedKey = normalizedCloudKey(key);
30+
if (!normalizedKey) return false;
31+
return SENSITIVE_CLOUD_KEY.test(normalizedKey) || SENSITIVE_CLOUD_KEY_EXACT.has(normalizedKey);
32+
}
33+
2534
function scrubCloudValue(value) {
2635
try {
2736
return JSON.parse(JSON.stringify(value, (key, item) => {
2837
const normalizedKey = normalizedCloudKey(key);
29-
if (normalizedKey && SENSITIVE_CLOUD_KEY.test(normalizedKey)) {
38+
if (normalizedKey && isSensitiveCloudKey(key)) {
3039
return '[redacted]';
3140
}
3241
if (typeof item === 'string' && /^data:image\//i.test(item)) {
@@ -181,7 +190,7 @@ export function createCloudRunController({
181190
if (!row?.runId) continue;
182191
const rawUpdates = Array.isArray(row.updates) ? row.updates : [];
183192
let nextUpdateSeq = 0;
184-
const updates = rawUpdates.map((update, index) => {
193+
const updates = rawUpdates.map((update) => {
185194
const candidate = Number(update?.seq);
186195
const seq = Number.isSafeInteger(candidate) && candidate > nextUpdateSeq
187196
? candidate
@@ -252,11 +261,15 @@ export function createCloudRunController({
252261
function pushUpdate(run, type, data) {
253262
run.updatedAt = isoNow();
254263
const previous = run.updates.at(-1);
264+
// Consecutive text_delta events upsert the same seq: content grows in place
265+
// and ts advances. Full-array pollers are fine; append-only / seq-cursor
266+
// clients must re-read that row (or take a full snapshot) rather than
267+
// assuming each seq is immutable.
255268
if (type === 'text_delta' && previous?.type === 'text_delta') {
256-
previous.data = {
269+
previous.data = scrubCloudValue({
257270
...previous.data,
258271
content: `${previous.data?.content || ''}${data?.content || ''}`,
259-
};
272+
});
260273
previous.ts = run.updatedAt;
261274
schedulePersist();
262275
return;

test/run.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4180,6 +4180,12 @@ test('cloud run controller uses the visible tab and persists terminal status', a
41804180
nested: {
41814181
apiKey: 'sk-test',
41824182
sessionToken: 'session-secret',
4183+
accessKeyId: 'AKIAEXAMPLE',
4184+
secretAccessKey: 'aws-secret',
4185+
pin: '1234',
4186+
pinCode: '9999',
4187+
spin: 'keep-me',
4188+
mapPin: 'also-keep',
41834189
headers: { 'x-api-key': 'header-secret' },
41844190
},
41854191
screenshot: `data:image/png;base64,${'a'.repeat(600)}`,
@@ -4192,6 +4198,12 @@ test('cloud run controller uses the visible tab and persists terminal status', a
41924198
assert.equal(running.updates[2].data.args.authorization, '[redacted]');
41934199
assert.equal(running.updates[2].data.args.nested.apiKey, '[redacted]');
41944200
assert.equal(running.updates[2].data.args.nested.sessionToken, '[redacted]');
4201+
assert.equal(running.updates[2].data.args.nested.accessKeyId, '[redacted]');
4202+
assert.equal(running.updates[2].data.args.nested.secretAccessKey, '[redacted]');
4203+
assert.equal(running.updates[2].data.args.nested.pin, '[redacted]');
4204+
assert.equal(running.updates[2].data.args.nested.pinCode, '[redacted]');
4205+
assert.equal(running.updates[2].data.args.nested.spin, 'keep-me');
4206+
assert.equal(running.updates[2].data.args.nested.mapPin, 'also-keep');
41954207
assert.equal(running.updates[2].data.args.nested.headers['x-api-key'], '[redacted]');
41964208
assert.match(running.updates[2].data.args.screenshot, /^\[image omitted:/);
41974209
finishRun('Google');
@@ -4202,6 +4214,56 @@ test('cloud run controller uses the visible tab and persists terminal status', a
42024214
assert.equal(session.webbrainCloudRunSnapshots[0].status, 'completed');
42034215
});
42044216

4217+
test('cloud run text_delta coalesce scrubs live status payloads', async () => {
4218+
const session = {};
4219+
const tab = { id: 22, url: 'https://webbrain.one/', active: true, windowId: 3 };
4220+
let finishRun;
4221+
let emitUpdate;
4222+
const controller = createCloudRunController({
4223+
chromeApi: {
4224+
tabs: {
4225+
query: async () => [tab],
4226+
get: async () => tab,
4227+
update: async () => tab,
4228+
},
4229+
windows: { update: async () => ({}) },
4230+
storage: {
4231+
local: { get: async () => ({ webbrainCloudBridgeEnabled: false }) },
4232+
session: {
4233+
get: async key => ({ [key]: session[key] || [] }),
4234+
set: async value => Object.assign(session, value),
4235+
},
4236+
},
4237+
runtime: { sendMessage: async () => ({ connected: false }) },
4238+
},
4239+
agent: {
4240+
isRunning: () => false,
4241+
abort: () => {},
4242+
processMessage: (_tabId, _task, onUpdate) => {
4243+
emitUpdate = onUpdate;
4244+
return new Promise(resolve => { finishRun = resolve; });
4245+
},
4246+
},
4247+
ensureOffscreen: async () => {},
4248+
makeRunId: () => 'run_delta_scrub',
4249+
});
4250+
4251+
await controller.startRun({ task: 'Stream a large reply' });
4252+
emitUpdate('text_delta', { content: 'a'.repeat(10 * 1024) });
4253+
emitUpdate('text_delta', { content: 'b'.repeat(10 * 1024) });
4254+
emitUpdate('text_delta', { content: 'c'.repeat(10 * 1024) });
4255+
const running = await controller.status({ runId: 'run_delta_scrub' });
4256+
assert.equal(running.updates.length, 1, 'consecutive text_delta events should upsert one seq');
4257+
assert.equal(running.updates[0].seq, 1);
4258+
assert.ok(
4259+
running.updates[0].data.content.length <= 16 * 1024 + 80,
4260+
'coalesced text_delta must re-apply CLOUD_STRING_LIMIT on the live status row',
4261+
);
4262+
assert.match(running.updates[0].data.content, /truncated .* chars for cloud persistence/);
4263+
finishRun('done');
4264+
await new Promise(resolve => setTimeout(resolve, 0));
4265+
});
4266+
42054267
test('cloud run controller keeps the newest 200 monotonically sequenced updates', async () => {
42064268
const session = {};
42074269
const tab = { id: 21, url: 'https://webbrain.one/', active: true, windowId: 3 };

0 commit comments

Comments
 (0)