fix(browser): redact observed event URLs

Squash merge PR #1296 into dev.
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-05-19 08:54:57 -04:00
1 parent a2a51c6128
commit 9a2a1dde31
4 files changed
+194 -12

No files matched your search

+75 -7
View File
@@ -1209,7 +1209,18 @@ function getTools() {
},
event: {
type: 'object',
description: 'Required event selector.',
description:
'Required event selector. request events require urlIncludes; download events require urlIncludes or suggestedFilenameIncludes. URLs in returned details are redacted.',
properties: {
kind: { type: 'string', enum: ['dialog', 'navigation', 'request', 'download'] },
dialogType: { type: 'string' },
messageIncludes: { type: 'string' },
urlIncludes: { type: 'string' },
method: { type: 'string' },
suggestedFilenameIncludes: { type: 'string' },
},
required: ['kind'],
additionalProperties: false,
},
},
required: ['event'],
@@ -3348,24 +3359,80 @@ function parseEventCondition(value) {
};
}
if (value.kind === 'request') {
const urlIncludes = value.urlIncludes ? String(value.urlIncludes) : undefined;
if (!urlIncludes) {
throw new Error('request events require urlIncludes to limit network metadata exposure');
}
return {
kind: 'request',
urlIncludes: value.urlIncludes ? String(value.urlIncludes) : undefined,
urlIncludes,
method: value.method ? String(value.method) : undefined,
};
}
if (value.kind === 'download') {
const urlIncludes = value.urlIncludes ? String(value.urlIncludes) : undefined;
const suggestedFilenameIncludes = value.suggestedFilenameIncludes
? String(value.suggestedFilenameIncludes)
: undefined;
if (!urlIncludes && !suggestedFilenameIncludes) {
throw new Error(
'download events require urlIncludes or suggestedFilenameIncludes to limit metadata exposure'
);
}
return {
kind: 'download',
urlIncludes: value.urlIncludes ? String(value.urlIncludes) : undefined,
suggestedFilenameIncludes: value.suggestedFilenameIncludes
? String(value.suggestedFilenameIncludes)
: undefined,
urlIncludes,
suggestedFilenameIncludes,
};
}
throw new Error(`unknown event kind: ${String(value.kind || '')}`);
}
function redactUrlForModel(value, options = {}) {
const rawUrl = String(value || '');
if (!rawUrl) {
return '';
}
try {
const url = new URL(rawUrl);
if (options.originOnly) {
return url.origin;
}
url.username = '';
url.password = '';
url.search = '';
url.hash = '';
return url.toString();
} catch {
return '[redacted-url]';
}
}
function redactObservedEvent(event, observed) {
if (!observed || typeof observed !== 'object') {
return observed;
}
if (event.kind === 'request') {
return {
url: redactUrlForModel(observed.url, { originOnly: true }),
method: observed.method || '',
};
}
if (event.kind === 'download') {
return {
url: redactUrlForModel(observed.url, { originOnly: true }),
suggestedFilename: observed.suggestedFilename || '',
};
}
if (event.kind === 'navigation' && Object.prototype.hasOwnProperty.call(observed, 'url')) {
return {
...observed,
url: redactUrlForModel(observed.url, { originOnly: true }),
};
}
return observed;
}
function matchesObservedEvent(event, observed) {
if (event.kind === 'dialog') {
return (
@@ -4819,7 +4886,8 @@ async function handleWaitForEvent(toolArgs) {
);
const event = parseEventCondition(toolArgs.event);
const observed = await waitForMatchingEvent({ page, pageIndex, timeoutMs, event });
return `pageIndex: ${pageIndex}\nevent: ${event.kind}\nstatus: observed\ndetail: ${JSON.stringify(observed)}`;
const safeObserved = redactObservedEvent(event, observed);
return `pageIndex: ${pageIndex}\nevent: ${event.kind}\nstatus: observed\ndetail: ${JSON.stringify(safeObserved)}`;
}
function findInterceptRuleIndex(ruleId) {