[eric] browser: surface a redacted concrete route example so the agent can compose replay_route with {{value}}

This commit is contained in:
ciregenz
2026-06-03 13:17:57 -07:00
parent 61826d441a
commit f121fa442c
3 changed files with 75 additions and 2 deletions
+31
View File
@@ -38,6 +38,34 @@ function redactHeaders(headers) {
return out;
}
// A concrete EXAMPLE url (real values) so the agent can see the param shape and
// template its own input in. The template alone (value-stripped) isn't composable
// for multi-param endpoints. We redact token-shaped query VALUES so a session
// token / api key in the query never lands in the agent's context. The user's own
// search terms stay (the agent typed them; not secret). Path stays as-is.
const SECRET_PARAM = /token|secret|sig|session|password|pwd|jwt|bearer|auth|access|refresh|csrf|api[-_]?key/i;
const TOKEN_PREFIX = /^(sk-|ghp_|gho_|pk_|xox[bap]-|AIza|eyJ|Bearer )/;
function looksSecretValue(v) {
if (!v) return false;
if (TOKEN_PREFIX.test(v)) return true;
return v.length >= 20 && /[A-Za-z]/.test(v) && /[0-9]/.test(v) && !/\s/.test(v);
}
function redactExampleUrl(raw) {
try {
const u = new URL(raw);
for (const [k, v] of [...u.searchParams.entries()]) {
if (SECRET_PARAM.test(k) || looksSecretValue(v)) {
u.searchParams.set(k, '<redacted>');
}
}
return u.origin + u.pathname + (u.search || '');
} catch {
return raw;
}
}
// Keep the JSON body's key skeleton with value TYPES, never values.
function bodyShape(postData) {
if (!postData) return null;
@@ -73,6 +101,7 @@ function makeRouteEntry(request, resourceType) {
return {
method,
template,
example: redactExampleUrl(request.url),
resourceType,
headers: redactHeaders(request.headers),
bodyShape: bodyShape(request.postData),
@@ -109,6 +138,8 @@ function recordRoute(routesMap, request, resourceType, now = Date.now()) {
module.exports = {
templateUrl,
redactExampleUrl,
looksSecretValue,
redactHeaders,
bodyShape,
isSafeMethod,
+39
View File
@@ -97,3 +97,42 @@ test('recordRoute evicts least-recently-seen past the cap', () => {
// the earliest few paths should have been evicted
assert.ok(!m.has('GET https://x.com/p0/a'));
});
test('redactExampleUrl keeps normal search terms (agent typed them)', () => {
assert.equal(
R.redactExampleUrl('https://x.com/api/search?q=design+engineer&page=2'),
'https://x.com/api/search?q=design+engineer&page=2',
);
});
test('redactExampleUrl redacts token-shaped param VALUES', () => {
// sensitive param name
assert.equal(
R.redactExampleUrl('https://x.com/api/feed?access_token=abc123xyz&q=cats'),
'https://x.com/api/feed?access_token=%3Credacted%3E&q=cats',
);
// high-entropy value even under a benign key
assert.equal(
R.redactExampleUrl('https://x.com/api?sid=aB3xK9mQ2pL7wR4tY8nZ&q=hi'),
'https://x.com/api?sid=%3Credacted%3E&q=hi',
);
// known token prefix
assert.ok(
R.redactExampleUrl('https://x.com/api?key=sk-ant-api03-abc').includes('redacted'),
);
});
test('looksSecretValue: tokens yes, plain words no', () => {
assert.ok(R.looksSecretValue('eyJhbGciOiJIUzI1NiIsInR5cCI6'));
assert.ok(R.looksSecretValue('aB3xK9mQ2pL7wR4tY8nZ'));
assert.ok(!R.looksSecretValue('shoes'));
assert.ok(!R.looksSecretValue('design engineer'));
assert.ok(!R.looksSecretValue('2'));
});
test('makeRouteEntry carries a redacted example url', () => {
const e = R.makeRouteEntry({ method: 'GET', url: 'https://x.com/api/p?q=ada&token=secretAbc123Long', headers: {} }, 'XHR');
assert.ok(e.example.includes('q=ada'));
assert.ok(e.example.includes('redacted'));
assert.ok(!e.example.includes('secretAbc123Long'));
});
+5 -2
View File
@@ -762,9 +762,12 @@ async function handleListRoutes(wv: BrowserWebview): Promise<Record<string, any>
if (!safe.length) {
return { text: 'No replayable (GET) API routes captured for this site yet. Use the page first so they get recorded, then try again.', url: wv.getURL() };
}
const lines = safe.slice(0, 40).map((r) => `${r.method} ${r.template} (x${r.hits})`);
const lines = safe.slice(0, 40).map((r) => `${r.method} ${r.example || r.template} (seen ${r.hits}x)`);
return {
text: `Replayable API routes for this site (safe GETs, call with BrowserReplayRoute):\n${lines.join('\n')}`,
text: `Replayable API routes for this site (safe GETs). To READ the same kind of `
+ `data for many inputs fast: swap the varying value in the URL with {{value}} `
+ `and use a replay_route step in BrowserRepeatFlow (or call BrowserReplayRoute `
+ `per item). Far cheaper than navigating + scraping each page:\n${lines.join('\n')}`,
routes: safe.slice(0, 40),
url: wv.getURL(),
};