mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-28 10:49:46 +02:00
[eric] browser: surface a redacted concrete route example so the agent can compose replay_route with {{value}}
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user