@wrongstack/tools 0.276.4 → 0.277.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit.js +21 -8
- package/dist/audit.js.map +1 -1
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +648 -57
- package/dist/builtin.js.map +1 -1
- package/dist/exec.js +521 -9
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js +7 -3
- package/dist/fetch.js.map +1 -1
- package/dist/format.js +22 -9
- package/dist/format.js.map +1 -1
- package/dist/index.js +649 -58
- package/dist/index.js.map +1 -1
- package/dist/install.js +21 -8
- package/dist/install.js.map +1 -1
- package/dist/lint.js +21 -8
- package/dist/lint.js.map +1 -1
- package/dist/memory.js +1 -1
- package/dist/memory.js.map +1 -1
- package/dist/outdated.js +20 -7
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +648 -57
- package/dist/pack.js.map +1 -1
- package/dist/search.js +113 -36
- package/dist/search.js.map +1 -1
- package/dist/test.js +21 -8
- package/dist/test.js.map +1 -1
- package/dist/typecheck.js +21 -8
- package/dist/typecheck.js.map +1 -1
- package/package.json +9 -5
package/dist/search.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ToolValidationError, expectDefined, FetchError, isPrivateIPv4, isPrivateIPv6 } from '@wrongstack/core';
|
|
2
2
|
import * as dns from 'node:dns/promises';
|
|
3
3
|
import * as net from 'node:net';
|
|
4
|
-
import { Agent } from 'undici';
|
|
4
|
+
import { Agent, fetch } from 'undici';
|
|
5
5
|
import TurndownService from 'turndown';
|
|
6
6
|
import { toErrorMessage } from '@wrongstack/core/utils';
|
|
7
7
|
|
|
@@ -16,6 +16,7 @@ TD.addRule("stripDangerousElements", {
|
|
|
16
16
|
filter: ["script", "style", "noscript"],
|
|
17
17
|
replacement: () => ""
|
|
18
18
|
});
|
|
19
|
+
var nativeGlobalFetch = globalThis.fetch;
|
|
19
20
|
var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
|
|
20
21
|
if (ALLOW_PRIVATE && !process.env["CI"]) {
|
|
21
22
|
console.warn(
|
|
@@ -64,6 +65,9 @@ function getPinnedDispatcher() {
|
|
|
64
65
|
}
|
|
65
66
|
return pinnedAgent;
|
|
66
67
|
}
|
|
68
|
+
function dispatcherFetch() {
|
|
69
|
+
return globalThis.fetch === nativeGlobalFetch ? fetch : globalThis.fetch;
|
|
70
|
+
}
|
|
67
71
|
var _beforeExitRegistered = false;
|
|
68
72
|
if (!_beforeExitRegistered) {
|
|
69
73
|
_beforeExitRegistered = true;
|
|
@@ -99,7 +103,7 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
|
99
103
|
headers,
|
|
100
104
|
dispatcher: getPinnedDispatcher()
|
|
101
105
|
};
|
|
102
|
-
const res = await
|
|
106
|
+
const res = await dispatcherFetch()(currentUrl, init);
|
|
103
107
|
if (res.status < 300 || res.status > 399) {
|
|
104
108
|
return res;
|
|
105
109
|
}
|
|
@@ -173,7 +177,7 @@ var searchTool = {
|
|
|
173
177
|
category: "Search",
|
|
174
178
|
description: "Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL.",
|
|
175
179
|
usageHint: "Good for: API documentation, error messages, library usage examples, current best practices.\n\n- Prefer specific queries over very broad ones.\n- Results go through the guarded fetch system (same protections as the `fetch` tool).\n- Supports duckduckgo (default), google, and bing sources.\n- Set `skip_cache: true` to force a fresh search.\n- This is often better than the model trying to recall outdated knowledge.",
|
|
176
|
-
permission: "
|
|
180
|
+
permission: "auto",
|
|
177
181
|
mutating: false,
|
|
178
182
|
capabilities: ["net.outbound"],
|
|
179
183
|
icon: "search",
|
|
@@ -236,15 +240,15 @@ var searchTool = {
|
|
|
236
240
|
};
|
|
237
241
|
yield {
|
|
238
242
|
type: "partial_output",
|
|
239
|
-
text: `${results.length} cached results from ${source}`,
|
|
240
|
-
data: { count: results.length, cached: true }
|
|
243
|
+
text: `${results.length} cached results from ${entry.source}`,
|
|
244
|
+
data: { count: results.length, cached: true, source: entry.source }
|
|
241
245
|
};
|
|
242
246
|
yield {
|
|
243
247
|
type: "final",
|
|
244
248
|
output: {
|
|
245
249
|
query: input.query,
|
|
246
250
|
results: results.slice(0, num),
|
|
247
|
-
source,
|
|
251
|
+
source: entry.source,
|
|
248
252
|
truncated: results.length >= num,
|
|
249
253
|
cached: true
|
|
250
254
|
}
|
|
@@ -258,6 +262,7 @@ var searchTool = {
|
|
|
258
262
|
data: { source, query: input.query, cached: false }
|
|
259
263
|
};
|
|
260
264
|
let rawResults;
|
|
265
|
+
let effectiveSource = source;
|
|
261
266
|
switch (source) {
|
|
262
267
|
case "duckduckgo":
|
|
263
268
|
rawResults = await duckduckgoSearch(input.query, num, opts.signal);
|
|
@@ -274,24 +279,24 @@ var searchTool = {
|
|
|
274
279
|
field: "source"
|
|
275
280
|
});
|
|
276
281
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
282
|
+
let ranked = rankSearchResults(rawResults, input.query);
|
|
283
|
+
if (source !== "duckduckgo" && shouldFallbackToDuckDuckGo(ranked, input.query)) {
|
|
284
|
+
yield {
|
|
285
|
+
type: "log",
|
|
286
|
+
text: `${source} returned no relevant static results; falling back to duckduckgo`,
|
|
287
|
+
data: { source, fallback: "duckduckgo", query: input.query }
|
|
288
|
+
};
|
|
289
|
+
rawResults = await duckduckgoSearch(input.query, num, opts.signal);
|
|
290
|
+
ranked = rankSearchResults(rawResults, input.query);
|
|
291
|
+
effectiveSource = "duckduckgo";
|
|
286
292
|
}
|
|
287
|
-
const ranked = scoreResults(deduped, input.query);
|
|
288
293
|
const finalResults = ranked.slice(0, num);
|
|
289
|
-
cache.set(cacheKey, { results: ranked, timestamp: Date.now() });
|
|
294
|
+
cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
|
|
290
295
|
pruneStaleCacheEntries();
|
|
291
296
|
yield {
|
|
292
297
|
type: "partial_output",
|
|
293
|
-
text: `${finalResults.length} results from ${
|
|
294
|
-
data: { count: finalResults.length, cached: false }
|
|
298
|
+
text: `${finalResults.length} results from ${effectiveSource}`,
|
|
299
|
+
data: { count: finalResults.length, cached: false, source: effectiveSource }
|
|
295
300
|
};
|
|
296
301
|
yield {
|
|
297
302
|
type: "final",
|
|
@@ -302,7 +307,7 @@ var searchTool = {
|
|
|
302
307
|
url: r.url,
|
|
303
308
|
snippet: r.snippet
|
|
304
309
|
})),
|
|
305
|
-
source,
|
|
310
|
+
source: effectiveSource,
|
|
306
311
|
truncated: finalResults.length >= num,
|
|
307
312
|
cached: false
|
|
308
313
|
}
|
|
@@ -318,6 +323,19 @@ function pruneStaleCacheEntries() {
|
|
|
318
323
|
function __clearSearchCache() {
|
|
319
324
|
cache.clear();
|
|
320
325
|
}
|
|
326
|
+
function rankSearchResults(results, query) {
|
|
327
|
+
const seenUrls = /* @__PURE__ */ new Set();
|
|
328
|
+
const deduped = [];
|
|
329
|
+
for (const r of results) {
|
|
330
|
+
const noQuery = r.url.split("?")[0] ?? r.url;
|
|
331
|
+
const normalized = noQuery.split("#")[0] ?? r.url;
|
|
332
|
+
if (!seenUrls.has(normalized) && r.url.startsWith("http")) {
|
|
333
|
+
seenUrls.add(normalized);
|
|
334
|
+
deduped.push(r);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return scoreResults(deduped, query);
|
|
338
|
+
}
|
|
321
339
|
function scoreResults(results, query) {
|
|
322
340
|
const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
|
|
323
341
|
return results.map((r) => {
|
|
@@ -331,6 +349,15 @@ function scoreResults(results, query) {
|
|
|
331
349
|
return { ...r, score };
|
|
332
350
|
}).sort((a, b) => b.score - a.score);
|
|
333
351
|
}
|
|
352
|
+
function shouldFallbackToDuckDuckGo(results, query) {
|
|
353
|
+
if (results.length === 0) return true;
|
|
354
|
+
const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length >= 3);
|
|
355
|
+
if (terms.length === 0) return false;
|
|
356
|
+
return !results.some((r) => {
|
|
357
|
+
const haystack = `${r.title} ${r.url} ${r.snippet}`.toLowerCase();
|
|
358
|
+
return terms.some((term) => haystack.includes(term));
|
|
359
|
+
});
|
|
360
|
+
}
|
|
334
361
|
async function duckduckgoSearch(query, num, signal) {
|
|
335
362
|
const encoded = encodeURIComponent(query);
|
|
336
363
|
const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
|
|
@@ -355,14 +382,19 @@ function takeFrom(iter, max) {
|
|
|
355
382
|
}
|
|
356
383
|
function parseDuckDuckGo(html, num) {
|
|
357
384
|
const results = [];
|
|
358
|
-
const
|
|
359
|
-
const
|
|
385
|
+
const linkRegex = /<a\b([^>]*\bclass=(["'])[^"']*\bresult-link\b[^"']*\2[^>]*)>([\s\S]*?)<\/a>/gi;
|
|
386
|
+
const snippetRegex = /<([a-z0-9]+)\b([^>]*\bclass=(["'])[^"']*\bresult-snippet\b[^"']*\3[^>]*)>([\s\S]*?)<\/\1>/gi;
|
|
360
387
|
const linkMatches = takeFrom(
|
|
361
|
-
[...html.matchAll(
|
|
388
|
+
[...html.matchAll(linkRegex)].map((m) => {
|
|
389
|
+
const attrs = expectDefined(m[1]);
|
|
390
|
+
const href = getHtmlAttr(attrs, "href");
|
|
391
|
+
const title = stripTags(expectDefined(m[3]));
|
|
392
|
+
return href && title ? { url: normalizeDuckDuckGoUrl(href), title } : void 0;
|
|
393
|
+
}).filter((m) => m !== void 0),
|
|
362
394
|
num
|
|
363
395
|
);
|
|
364
396
|
const snippetMatches = takeFrom(
|
|
365
|
-
[...html.matchAll(
|
|
397
|
+
[...html.matchAll(snippetRegex)].filter((m) => m[4]).map((m) => stripTags(expectDefined(m[4]))),
|
|
366
398
|
num
|
|
367
399
|
);
|
|
368
400
|
for (let i = 0; i < linkMatches.length && i < num; i++) {
|
|
@@ -378,6 +410,24 @@ function parseDuckDuckGo(html, num) {
|
|
|
378
410
|
}
|
|
379
411
|
return results;
|
|
380
412
|
}
|
|
413
|
+
function getHtmlAttr(attrs, name) {
|
|
414
|
+
const quoted = new RegExp(`\\b${name}\\s*=\\s*(["'])(.*?)\\1`, "i").exec(attrs);
|
|
415
|
+
if (quoted?.[2]) return decodeHtmlEntities(quoted[2]);
|
|
416
|
+
const unquoted = new RegExp(`\\b${name}\\s*=\\s*([^\\s>]+)`, "i").exec(attrs);
|
|
417
|
+
return unquoted?.[1] ? decodeHtmlEntities(unquoted[1]) : void 0;
|
|
418
|
+
}
|
|
419
|
+
function normalizeDuckDuckGoUrl(raw) {
|
|
420
|
+
if (raw.startsWith("//")) return `https:${raw}`;
|
|
421
|
+
if (!raw.startsWith("/")) return raw;
|
|
422
|
+
if (!raw.startsWith("/l/")) return raw;
|
|
423
|
+
try {
|
|
424
|
+
const url = new URL(raw, "https://duckduckgo.com");
|
|
425
|
+
const uddg = url.searchParams.get("uddg");
|
|
426
|
+
return uddg?.startsWith("http") ? uddg : url.toString();
|
|
427
|
+
} catch {
|
|
428
|
+
return raw;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
381
431
|
async function googleSearch(query, num, signal) {
|
|
382
432
|
const encoded = encodeURIComponent(query);
|
|
383
433
|
const url = `https://www.google.com/search?q=${encoded}&hl=en`;
|
|
@@ -419,29 +469,53 @@ async function bingSearch(query, num, signal) {
|
|
|
419
469
|
}
|
|
420
470
|
function parseBingResults(html, num) {
|
|
421
471
|
const results = [];
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
const entries = takeFrom(
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
472
|
+
const blocks = [...html.matchAll(/<li\b[^>]*class=(["'])[^"']*\bb_algo\b[^"']*\1[^>]*>([\s\S]*?)(?=<li\b[^>]*class=(["'])[^"']*\bb_algo\b[^"']*\3|<\/ol>)/gi)].map((m) => expectDefined(m[2]));
|
|
473
|
+
const candidates = blocks.length > 0 ? blocks : [html];
|
|
474
|
+
const entries = takeFrom(candidates.flatMap((block) => {
|
|
475
|
+
const titleMatch = /<h2[^>]*>\s*<a\b([^>]*)>([\s\S]*?)<\/a>\s*<\/h2>/i.exec(block);
|
|
476
|
+
if (!titleMatch) return [];
|
|
477
|
+
const href = getHtmlAttr(expectDefined(titleMatch[1]), "href");
|
|
478
|
+
const title = stripTags(expectDefined(titleMatch[2]));
|
|
479
|
+
if (!href || !title) return [];
|
|
480
|
+
const snippetMatch = /<p\b[^>]*class=(["'])[^"']*\b(?:b_paractl|b_lineclamp\d*)\b[^"']*\1[^>]*>([\s\S]*?)<\/p>/i.exec(block) ?? /<p\b[^>]*>([\s\S]*?)<\/p>/i.exec(block);
|
|
481
|
+
const snippet = snippetMatch ? stripTags(expectDefined(snippetMatch.at(-1))) : "";
|
|
482
|
+
return [{ url: normalizeBingUrl(href), title, snippet, score: 1 }];
|
|
483
|
+
}), num);
|
|
432
484
|
for (let i = 0; i < entries.length; i++) {
|
|
433
485
|
const entry = entries[i];
|
|
434
486
|
if (entry) {
|
|
435
487
|
results.push({
|
|
436
488
|
title: entry.title ?? "",
|
|
437
489
|
url: entry.url ?? "",
|
|
438
|
-
snippet:
|
|
490
|
+
snippet: entry.snippet ?? "",
|
|
439
491
|
score: 1
|
|
440
492
|
});
|
|
441
493
|
}
|
|
442
494
|
}
|
|
443
495
|
return results;
|
|
444
496
|
}
|
|
497
|
+
function normalizeBingUrl(raw) {
|
|
498
|
+
try {
|
|
499
|
+
const url = new URL(raw);
|
|
500
|
+
if (url.hostname.endsWith("bing.com") && url.pathname.startsWith("/ck/")) {
|
|
501
|
+
const encoded = url.searchParams.get("u");
|
|
502
|
+
const decoded = decodeBingTarget(encoded);
|
|
503
|
+
if (decoded?.startsWith("http")) return decoded;
|
|
504
|
+
}
|
|
505
|
+
} catch {
|
|
506
|
+
}
|
|
507
|
+
return raw;
|
|
508
|
+
}
|
|
509
|
+
function decodeBingTarget(encoded) {
|
|
510
|
+
if (!encoded) return void 0;
|
|
511
|
+
const payload = encoded.startsWith("a1") ? encoded.slice(2) : encoded;
|
|
512
|
+
try {
|
|
513
|
+
const padded = payload.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payload.length / 4) * 4, "=");
|
|
514
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
515
|
+
} catch {
|
|
516
|
+
return void 0;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
445
519
|
async function fetchWithTimeout(url, signal, timeoutMs) {
|
|
446
520
|
const controller = new AbortController();
|
|
447
521
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -469,7 +543,10 @@ function anySignal(...signals) {
|
|
|
469
543
|
return AbortSignal.any(signals);
|
|
470
544
|
}
|
|
471
545
|
function stripTags(html) {
|
|
472
|
-
return html.replace(/<[^>]+>/g, "")
|
|
546
|
+
return decodeHtmlEntities(html.replace(/<[^>]+>/g, "")).trim();
|
|
547
|
+
}
|
|
548
|
+
function decodeHtmlEntities(text) {
|
|
549
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
473
550
|
}
|
|
474
551
|
|
|
475
552
|
export { __clearSearchCache, searchTool };
|
package/dist/search.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fetch.ts","../src/search.ts"],"names":["ToolValidationError","FetchError"],"mappings":"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","file":"search.js","sourcesContent":["import * as dns from 'node:dns/promises';\nimport * as net from 'node:net';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport {\n FetchError,\n ToolError,\n ToolValidationError,\n isPrivateIPv4,\n isPrivateIPv6,\n} from '@wrongstack/core';\nimport { Agent } from 'undici';\nimport TurndownService from 'turndown';\nimport { truncateMiddle } from './_util.js';\n\n/**\n * Singleton Turndown instance for HTML→Markdown conversion.\n * Pre-configured with sensible defaults; code blocks are handled via the\n * default fenced code rule. Reused across all fetch calls.\n */\nconst TD = new TurndownService({\n // Use `# Title` for headings, not setext underline style (`Title\\n=====`).\n headingStyle: 'atx',\n // Don't wrap code blocks in <pre> — render them as triple-backtick blocks.\n codeBlockStyle: 'fenced',\n});\n\n// Strip <script>/<style>/<noscript> before turndown sees them. The old\n// hand-rolled converter did this via regex; turndown's DOM-based approach\n// may keep their text content unless we remove the elements first.\n// Using turndown's own addRule mechanism keeps the logic co-located.\nTD.addRule('stripDangerousElements', {\n filter: ['script', 'style', 'noscript'],\n replacement: () => '',\n});\n\ninterface FetchInput {\n url: string;\n format?: 'markdown' | 'text' | 'raw' | undefined;\n}\n\ninterface FetchOutput {\n content: string;\n status: number;\n content_type: string;\n url: string;\n}\n\nconst MAX_BYTES = 131_072;\nconst TIMEOUT_MS = 20_000;\n\nconst ALLOW_PRIVATE = process.env['WRONGSTACK_FETCH_ALLOW_PRIVATE'] === '1';\n/* v8 ignore next 8 -- module-load-time opt-in warning; gated on an env var not set during tests. */\nif (ALLOW_PRIVATE && !process.env['CI']) {\n console.warn(\n '[WrongStack] WARNING: WRONGSTACK_FETCH_ALLOW_PRIVATE=1 is active —\\n' +\n ' fetch tool can now access private IPs (10.x, 192.168.x, 169.254.x),\\n' +\n ' cloud metadata endpoints, and plaintext HTTP. Use only on isolated networks.',\n );\n}\n\n/** Abort when any of the signals abort (Node 22+ — AbortSignal.any shipped in Node 20). */\nconst combineSignals = (signals: AbortSignal[]): AbortSignal => AbortSignal.any(signals);\n\ntype LookupCallback = (\n err: NodeJS.ErrnoException | null,\n address?: string | Array<{ address: string | undefined; family: number }>,\n family?: number | undefined,\n) => void;\n\n/**\n * DNS lookup used by the undici dispatcher below. It performs the SINGLE name\n * resolution that the TCP connection actually uses, and rejects if any\n * resolved address is private/loopback/link-local. Because the connection\n * reuses exactly this result, there is no DNS-rebinding TOCTOU window between\n * the security check and the connect — closing the gap the old code documented\n * (validate with one dns.lookup, then let fetch re-resolve independently).\n * TLS still validates the certificate against the hostname (SNI is set by\n * undici from the URL), so pinning the IP does not weaken cert checking.\n */\nexport function guardedLookup(\n hostname: string,\n options: { all?: boolean | undefined; family?: number | undefined },\n callback: LookupCallback,\n): void {\n dns\n .lookup(hostname, { all: true })\n .then((records) => {\n const family = options?.family;\n const byFamily =\n family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;\n const list = byFamily.length > 0 ? byFamily : records;\n if (!ALLOW_PRIVATE) {\n for (const r of list) {\n const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);\n if (bad) {\n callback(\n Object.assign(new Error(`fetch: resolved to private address ${r.address}`), {\n code: 'EAI_FAIL',\n }),\n );\n return;\n }\n }\n }\n if (options?.all) {\n callback(\n null,\n list.map((r) => ({ address: r.address, family: r.family })),\n );\n return;\n }\n const first = list.at(0);\n if (!first) {\n callback(\n Object.assign(new Error(`fetch: no address for ${hostname}`), { code: 'ENOTFOUND' }),\n );\n return;\n }\n callback(null, first.address, first.family);\n })\n .catch((err) => callback(err as NodeJS.ErrnoException));\n}\n\n// Reused across requests; guardedLookup re-validates on every new connection,\n// so connection pooling is safe. Literal-IP targets bypass lookup entirely and\n// are caught by assertNotPrivate's pre-check instead.\n// Destroyed on process exit so long-running processes (eternal autonomy,\n// MCP server mode) don't let the connection pool grow unboundedly.\nlet pinnedAgent: Agent | undefined;\nfunction getPinnedDispatcher(): Agent {\n if (!pinnedAgent) {\n pinnedAgent = new Agent({ connect: { lookup: guardedLookup as never } });\n }\n return pinnedAgent;\n}\n// Clean up the global dispatcher on exit — undici Agents maintain connection\n// pools and DNS caches that should be torn down in long-running processes.\n// Guard against duplicate registration (module reload/HMR would otherwise\n// accumulate listeners).\nlet _beforeExitRegistered = false;\nif (!_beforeExitRegistered) {\n _beforeExitRegistered = true;\n /* v8 ignore next 4 -- process 'beforeExit' cleanup; not deterministically triggerable in-test. */\n process.on('beforeExit', () => {\n pinnedAgent?.destroy();\n pinnedAgent = undefined;\n });\n}\n\n/**\n * SSRF-guarded fetch with manual, per-hop-revalidated redirects, exported so\n * other builtin tools (e.g. `search`) get the same protections instead of a\n * weaker `redirect: 'follow'`. Every hop is re-checked against private/loopback\n * ranges and the connection is pinned to the validated IP via the undici\n * dispatcher (no DNS-rebinding TOCTOU). `headers` defaults to the plain `fetch`\n * tool's; callers may override (e.g. a browser User-Agent for search engines).\n */\nexport async function guardedFetch(\n url: string,\n maxRedirects: number,\n signal: AbortSignal,\n headers: Record<string, string> = {\n 'user-agent': 'WrongStack/1.0 (+https://wrongstack.com)',\n accept: 'text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.1',\n },\n): Promise<Response> {\n let redirectCount = 0;\n let currentUrl = url;\n for (;;) {\n // Re-validate every hop. A public host can 302 to 169.254.169.254 (cloud metadata),\n // or DNS can rebind between hops; checking only the initial URL is insufficient.\n const parsed = new URL(currentUrl);\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n throw new ToolValidationError({\n message: `fetch: redirect to unsupported protocol \"${parsed.protocol}\"`,\n field: 'url',\n });\n }\n if (parsed.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new ToolValidationError({\n message: 'fetch: redirect to http:// blocked (HTTPS required by default)',\n field: 'url',\n });\n }\n await assertNotPrivate(parsed.hostname);\n\n // The dispatcher pins the connection to the IP guardedLookup validated —\n // no independent re-resolution, so DNS rebinding can't swap in a private\n // address between check and connect. `dispatcher` is a runtime option of\n // Node's undici-backed global fetch but isn't in lib.dom's RequestInit, and\n // our undici Agent's type differs from the @types/node copy — hence the\n // cast. (Verified: global fetch invokes the Agent's custom lookup.)\n const init = {\n redirect: 'manual' as const,\n signal,\n headers,\n dispatcher: getPinnedDispatcher(),\n };\n const res = await fetch(currentUrl, init as never as RequestInit);\n if (res.status < 300 || res.status > 399) {\n return res;\n }\n redirectCount++;\n if (redirectCount > maxRedirects) {\n throw new FetchError({\n message: `fetch: exceeded ${maxRedirects} redirects`,\n status: res.status,\n context: { url: currentUrl, maxRedirects, redirectCount },\n });\n }\n const location = res.headers.get('location');\n if (!location) {\n throw new FetchError({\n message: 'fetch: redirect status with no location header',\n status: res.status,\n context: { url: currentUrl, redirectCount },\n });\n }\n currentUrl = new URL(location, currentUrl).toString();\n }\n}\n\nexport const fetchTool: Tool<FetchInput, FetchOutput> = {\n name: 'fetch',\n category: 'Network',\n description:\n 'Fetch a URL and return its content. HTML pages are automatically converted to clean markdown. ' +\n 'This tool has strong SSRF protections (private IPs, localhost, and cloud metadata endpoints are blocked by default).',\n usageHint:\n 'Use this when you need external information (documentation, API responses, web pages, etc.).\\n\\n' +\n 'Security notes:\\n' +\n '- Only HTTPS is allowed by default.\\n' +\n '- Internal/private networks are blocked unless explicitly enabled via environment variable.\\n' +\n '- Redirects are followed but re-validated at each hop.\\n' +\n '- Output is capped (128KB by default) to avoid flooding context.\\n' +\n 'Prefer this over raw `bash curl` or `bash wget`.',\n permission: 'confirm',\n mutating: false,\n capabilities: ['net.outbound'],\n icon: 'web',\n // Trust rules for fetch match on the literal URL — declare it explicitly\n // so a user can trust `https://api.example.com/*` without accidentally\n // matching that pattern on any other tool that happens to have a `url`\n // input field.\n subjectKey: 'url',\n timeoutMs: TIMEOUT_MS,\n maxOutputBytes: MAX_BYTES,\n inputSchema: {\n type: 'object',\n properties: {\n url: {\n type: 'string',\n description: 'The target URL (must use https://).',\n },\n format: {\n type: 'string',\n enum: ['markdown', 'text', 'raw'],\n description: 'Output format. \"markdown\" is recommended for HTML pages.',\n },\n },\n required: ['url'],\n },\n async execute(input, ctx, opts) {\n let final: FetchOutput | undefined;\n const executeStream = fetchTool.executeStream;\n if (!executeStream) {\n throw new ToolError({\n message: 'fetchTool: stream execution unavailable',\n code: 'TOOL_EXECUTION_FAILED',\n toolName: 'fetch',\n });\n }\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) {\n throw new ToolError({\n message: 'fetch: stream ended without final event',\n code: 'TOOL_EXECUTION_FAILED',\n toolName: 'fetch',\n });\n }\n return final;\n },\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<FetchOutput>> {\n if (!input?.url) {\n throw new ToolValidationError({\n message: 'fetch: url is required',\n field: 'url',\n });\n }\n const u = new URL(input.url);\n if (u.protocol !== 'https:' && u.protocol !== 'http:') {\n throw new ToolValidationError({\n message: `fetch: unsupported protocol \"${u.protocol}\"`,\n field: 'url',\n });\n }\n if (u.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new ToolValidationError({\n message: 'fetch: http:// blocked (HTTPS required by default)',\n field: 'url',\n });\n }\n await assertNotPrivate(u.hostname);\n\n yield { type: 'log', text: `GET ${input.url}` };\n\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(new ToolError({\n message: 'fetch timeout',\n code: 'TOOL_TIMEOUT',\n toolName: 'fetch',\n })), TIMEOUT_MS);\n const combined = combineSignals([opts.signal, ctrl.signal]);\n\n try {\n let res: Response;\n try {\n res = await guardedFetch(input.url, 5, combined);\n } catch (err) {\n // A user-initiated cancel propagates unchanged. Our own timeout and any\n // transport failure get a diagnostic message: undici throws an opaque\n // `TypeError: fetch failed` whose real reason (ENOTFOUND, ECONNREFUSED,\n // UND_ERR_CONNECT_TIMEOUT, a TLS/cert error, …) lives only on `.cause`.\n // Surfacing just `.message` left users with \"fetch failed\" and no clue\n // why HTTPS broke (see #100), so unwrap the cause chain here.\n if (opts.signal.aborted) throw err;\n throw describeFetchError(err, input.url, ctrl.signal.aborted);\n }\n\n const ct = res.headers.get('content-type') ?? 'application/octet-stream';\n if (/^image\\/|^audio\\/|^video\\/|application\\/octet-stream/.test(ct)) {\n throw new FetchError({\n message: `fetch: refusing to read binary content-type \"${ct}\"`,\n status: res.status,\n context: { url: res.url, contentType: ct },\n });\n }\n\n yield {\n type: 'log',\n text: `HTTP ${res.status} ${ct}`,\n data: { status: res.status, contentType: ct },\n };\n\n const reader = res.body?.getReader();\n let received = 0;\n const chunks: Uint8Array[] = [];\n let pendingBytes = 0;\n const FLUSH_AT = 4 * 1024;\n if (reader) {\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n if (!value) continue;\n received += value.byteLength;\n pendingBytes += value.byteLength;\n chunks.push(value);\n if (pendingBytes >= FLUSH_AT) {\n // Snapshot recent bytes for the partial_output. Keep it cheap —\n // don't try to decode UTF-8 boundaries; the TUI just needs a\n // \"things are happening\" signal.\n const recent = Buffer.from(value).toString('utf-8');\n yield {\n type: 'partial_output',\n text: recent,\n data: { received },\n };\n pendingBytes = 0;\n }\n if (received > MAX_BYTES) break;\n }\n }\n const text = Buffer.concat(chunks.map((c) => Buffer.from(c))).toString('utf8');\n\n const format = input.format ?? (ct.includes('text/html') ? 'markdown' : 'text');\n let content: string;\n if (format === 'raw') content = text;\n else if (format === 'markdown' && ct.includes('text/html')) content = TD.turndown(text);\n else if (ct.includes('application/json')) content = prettyJson(text);\n else content = text;\n\n yield {\n type: 'final',\n output: {\n content: truncateMiddle(content, MAX_BYTES),\n status: res.status,\n content_type: ct,\n url: res.url,\n },\n };\n // P2 #5: record the network request as a structured side effect.\n ctx.recordSideEffect?.({\n toolUseId: `fetch-${Date.now()}`,\n toolName: 'fetch',\n ts: new Date().toISOString(),\n input: { url: input.url, format: input.format },\n outcome: `HTTP ${res.status} (${ct})`,\n risk: 'network',\n });\n } finally {\n clearTimeout(timer);\n }\n },\n};\n\nasync function assertNotPrivate(hostname: string): Promise<void> {\n if (ALLOW_PRIVATE) return;\n\n const host =\n hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;\n\n if (host === 'localhost' || host.endsWith('.localhost')) {\n throw new ToolValidationError({\n message: 'fetch: blocked localhost target',\n field: 'url',\n });\n }\n\n const ipVersion = net.isIP(host);\n if (ipVersion === 4) {\n if (isPrivateIPv4(host)) {\n throw new ToolValidationError({\n message: `fetch: blocked private/loopback address \"${host}\"`,\n field: 'url',\n });\n }\n } else if (ipVersion === 6) {\n if (isPrivateIPv6(host)) {\n throw new ToolValidationError({\n message: `fetch: blocked private/loopback address \"${host}\"`,\n field: 'url',\n });\n }\n } else {\n // Hostname — pre-flight check: resolve and reject if any record is private,\n // so we fail fast with a clear error before opening a socket. The\n // authoritative anti-rebinding control is guardedLookup on the pinned\n // undici dispatcher (see getPinnedDispatcher): it performs the single\n // resolution the connection actually uses, so there is no TOCTOU between\n // this check and the connect. Each redirect target is re-checked too.\n try {\n // Use dns.lookup for async hostname resolution (matches guardedLookup below).\n const records = await dns.lookup(host, { all: true });\n for (const r of records) {\n const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);\n if (bad) {\n throw new ToolValidationError({\n message: `fetch: resolved to private address ${r.address}`,\n field: 'url',\n });\n }\n }\n } catch (err) {\n if (err instanceof Error && err.message.startsWith('fetch:')) throw err;\n // DNS failure — let fetch handle it\n }\n }\n}\n\n/**\n * Turn an opaque undici `TypeError: fetch failed` into an actionable message by\n * walking its `.cause` chain. undici buries the transport reason (a DNS/socket\n * errno or a TLS handshake failure) one or more `.cause` hops down, so the bare\n * `.message` is always just \"fetch failed\". We join each distinct\n * `code: message` link so the user sees, e.g.,\n * `fetch: GET https://x failed — UND_ERR_CONNECT_TIMEOUT: Connect Timeout Error`.\n */\nfunction describeFetchError(err: unknown, url: string, timedOut: boolean): FetchError | ToolError {\n if (timedOut) {\n return new ToolError({\n message: `fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`,\n code: 'TOOL_TIMEOUT',\n toolName: 'fetch',\n context: { url, timedOut: true, timeoutMs: TIMEOUT_MS },\n cause: err,\n });\n }\n const parts: string[] = [];\n const seen = new Set<unknown>();\n let cur: unknown = err;\n while (cur instanceof Error && !seen.has(cur)) {\n seen.add(cur);\n const code = (cur as NodeJS.ErrnoException).code;\n const label = code ? `${code}: ${cur.message}` : cur.message;\n // Skip undici's uninformative top-level \"fetch failed\" wrapper, but keep it\n // as a fallback if it turns out to be the only thing we have.\n if (label && label !== 'fetch failed' && !parts.includes(label)) parts.push(label);\n cur = (cur as { cause?: unknown }).cause;\n }\n const detail = parts.length > 0 ? parts.join(' → ') : 'fetch failed';\n return new FetchError({\n message: `fetch: GET ${url} failed — ${detail}`,\n status: 502,\n context: { url, timedOut: false, transportErrors: parts },\n // Preserve the original undici / DNS / TLS chain so callers can inspect\n // it via `err.cause` and structured `instanceof` checks. The flattened\n // text version stays in the message for human readability.\n cause: err,\n });\n}\n\nfunction prettyJson(s: string): string {\n try {\n return JSON.stringify(JSON.parse(s), null, 2);\n } catch {\n return s;\n }\n}\n","import { expectDefined, FetchError, ToolValidationError } from '@wrongstack/core';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { guardedFetch } from './fetch.js';\nimport { toErrorMessage } from '@wrongstack/core/utils';\n\ninterface SearchResult {\n title: string;\n url: string;\n snippet: string;\n score: number;\n}\n\ninterface CacheEntry {\n results: SearchResult[];\n timestamp: number;\n}\n\ninterface SearchInput {\n query: string;\n num_results?: number | undefined;\n source?: 'duckduckgo' | 'google' | 'bing' | undefined;\n skip_cache?: boolean | undefined;\n}\n\ninterface SearchOutput {\n query: string;\n results: { title: string; url: string; snippet: string }[];\n source: string;\n truncated: boolean;\n cached: boolean;\n}\n\nconst DEFAULT_NUM = 10;\nconst MAX_RESULTS = 50;\nconst TIMEOUT_MS = 15_000;\nconst CACHE_TTL_MS = 300_000; // 5 minutes — matches the former web-search plugin default\n\n// Module-level cache shared across calls within a single agent run. Keyed by\n// `<source>:<query>`. This is intentionally process-local (not persisted) —\n// it exists to avoid hammering a search engine when the model rephrases the\n// same query a few turns apart.\nconst cache = new Map<string, CacheEntry>();\n\nexport const searchTool: Tool<SearchInput, SearchOutput> = {\n name: 'search',\n category: 'Search',\n description:\n 'Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL.',\n usageHint:\n 'Good for: API documentation, error messages, library usage examples, current best practices.\\n\\n' +\n '- Prefer specific queries over very broad ones.\\n' +\n '- Results go through the guarded fetch system (same protections as the `fetch` tool).\\n' +\n '- Supports duckduckgo (default), google, and bing sources.\\n' +\n '- Set `skip_cache: true` to force a fresh search.\\n' +\n '- This is often better than the model trying to recall outdated knowledge.',\n permission: 'confirm',\n mutating: false,\n capabilities: ['net.outbound'],\n icon: 'search',\n timeoutMs: TIMEOUT_MS,\n inputSchema: {\n type: 'object',\n properties: {\n query: { type: 'string', description: 'Search query' },\n num_results: {\n type: 'integer',\n description: 'Number of results (1-50, default 10)',\n minimum: 1,\n maximum: MAX_RESULTS,\n },\n source: {\n type: 'string',\n enum: ['duckduckgo', 'google', 'bing'],\n description: 'Search engine to use (default: duckduckgo)',\n },\n skip_cache: {\n type: 'boolean',\n description: 'Skip the in-memory cache and force a fresh search (default: false)',\n },\n },\n required: ['query'],\n },\n async execute(input, ctx, opts) {\n let final: SearchOutput | undefined;\n const executeStream = searchTool.executeStream;\n if (!executeStream) throw new Error('searchTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('search: stream ended without final event');\n return final;\n },\n async *executeStream(input, _ctx, opts): AsyncGenerator<ToolStreamEvent<SearchOutput>> {\n if (!input?.query || input.query.trim() === '') {\n throw new ToolValidationError({\n message: 'search: query is required and must be a non-empty string',\n field: 'query',\n });\n }\n\n const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));\n const source = input.source ?? 'duckduckgo';\n const skipCache = input.skip_cache ?? false;\n const cacheKey = `${source}:${input.query}`;\n\n // --- Cache hit ---\n if (!skipCache) {\n const entry = cache.get(cacheKey);\n if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) {\n const results = entry.results.map((r) => ({\n title: r.title,\n url: r.url,\n snippet: r.snippet,\n }));\n yield {\n type: 'log',\n text: `Cache hit for \"${input.query}\" (${source})`,\n data: { source, query: input.query, cached: true },\n };\n yield {\n type: 'partial_output',\n text: `${results.length} cached results from ${source}`,\n data: { count: results.length, cached: true },\n };\n yield {\n type: 'final',\n output: {\n query: input.query,\n results: results.slice(0, num),\n source,\n truncated: results.length >= num,\n cached: true,\n },\n };\n return;\n }\n }\n\n yield {\n type: 'log',\n text: `Querying ${source} for \"${input.query}\"…`,\n data: { source, query: input.query, cached: false },\n };\n\n let rawResults: SearchResult[];\n switch (source) {\n case 'duckduckgo':\n rawResults = await duckduckgoSearch(input.query, num, opts.signal);\n break;\n case 'google':\n rawResults = await googleSearch(input.query, num, opts.signal);\n break;\n case 'bing':\n rawResults = await bingSearch(input.query, num, opts.signal);\n break;\n default:\n throw new ToolValidationError({\n message: `search: unknown source \"${source}\"`,\n field: 'source',\n });\n }\n\n // --- Deduplicate by normalized URL, drop non-http ---\n const seenUrls = new Set<string>();\n const deduped: SearchResult[] = [];\n for (const r of rawResults) {\n const noQuery = r.url.split('?')[0] ?? r.url;\n const normalized = noQuery.split('#')[0] ?? r.url;\n if (!seenUrls.has(normalized) && r.url.startsWith('http')) {\n seenUrls.add(normalized);\n deduped.push(r);\n }\n }\n\n // --- Rank by query-term overlap (title > snippet) ---\n const ranked = scoreResults(deduped, input.query);\n const finalResults = ranked.slice(0, num);\n\n // --- Store in cache ---\n cache.set(cacheKey, { results: ranked, timestamp: Date.now() });\n pruneStaleCacheEntries();\n\n yield {\n type: 'partial_output',\n text: `${finalResults.length} results from ${source}`,\n data: { count: finalResults.length, cached: false },\n };\n yield {\n type: 'final',\n output: {\n query: input.query,\n results: finalResults.map((r) => ({\n title: r.title,\n url: r.url,\n snippet: r.snippet,\n })),\n source,\n truncated: finalResults.length >= num,\n cached: false,\n },\n };\n },\n};\n\n// ---------------------------------------------------------------------------\n// Cache helpers\n// ---------------------------------------------------------------------------\n\n/** Drop entries older than 2× the TTL — bounded growth, cheap to run per write. */\nfunction pruneStaleCacheEntries(): void {\n const cutoff = Date.now() - CACHE_TTL_MS * 2;\n for (const [key, entry] of cache.entries()) {\n if (entry.timestamp < cutoff) cache.delete(key);\n }\n}\n\n/** Exposed for tests so they can reset the module-level cache between cases. */\nexport function __clearSearchCache(): void {\n cache.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Ranking\n// ---------------------------------------------------------------------------\n\nfunction scoreResults(results: SearchResult[], query: string): SearchResult[] {\n const terms = query.toLowerCase().split(/\\s+/).filter((t) => t.length > 0);\n return results\n .map((r) => {\n const titleLower = r.title.toLowerCase();\n const snippetLower = r.snippet.toLowerCase();\n let score = r.score;\n for (const term of terms) {\n if (titleLower.includes(term)) score += 2;\n if (snippetLower.includes(term)) score += 1;\n }\n return { ...r, score };\n })\n .sort((a, b) => b.score - a.score);\n}\n\n// ---------------------------------------------------------------------------\n// Search engines\n// ---------------------------------------------------------------------------\n\nasync function duckduckgoSearch(\n query: string,\n num: number,\n signal: AbortSignal,\n): Promise<SearchResult[]> {\n const encoded = encodeURIComponent(query);\n const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;\n\n try {\n const response = await fetchWithTimeout(url, signal, TIMEOUT_MS);\n const html = await response.text();\n return parseDuckDuckGo(html, num);\n } catch (err) {\n console.log(\n JSON.stringify({ level: 'debug', event: 'search_failed', query, error: toErrorMessage(err) }),\n );\n // Return a sentinel result that survives the dedup filter (which drops\n // non-http URLs). Using a placeholder http URL keeps it visible to the\n // caller so they know the search failed rather than silently getting [].\n return [{ title: 'Search unavailable', url: 'https://duckduckgo.com/unavailable', snippet: 'Could not reach DuckDuckGo', score: 0 }];\n }\n}\n\nfunction takeFrom<T>(iter: Iterable<T>, max: number): T[] {\n const out: T[] = [];\n for (const item of iter) {\n if (out.length >= max) break;\n out.push(item);\n }\n return out;\n}\n\nfunction parseDuckDuckGo(html: string, num: number): SearchResult[] {\n const results: SearchResult[] = [];\n const snippetRegex = /<a class=\"result-link\"[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\/a>/gi;\n const snippet2Regex = /<a class=\"result-snippet\"[^>]*>([^<]+)<\\/a>/gi;\n\n const linkMatches = takeFrom(\n [...html.matchAll(snippetRegex)]\n .filter((m) => m[1] && m[2])\n .map((m) => ({ url: expectDefined(m[1]), title: stripTags(expectDefined(m[2])) })),\n num,\n );\n\n const snippetMatches = takeFrom(\n [...html.matchAll(snippet2Regex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n for (let i = 0; i < linkMatches.length && i < num; i++) {\n const entry = linkMatches[i];\n if (entry) {\n results.push({\n title: entry.title ?? '',\n url: entry.url ?? '',\n snippet: snippetMatches[i] ?? '',\n score: 1,\n });\n }\n }\n\n return results;\n}\n\nasync function googleSearch(query: string, num: number, signal: AbortSignal): Promise<SearchResult[]> {\n const encoded = encodeURIComponent(query);\n const url = `https://www.google.com/search?q=${encoded}&hl=en`;\n\n const html = await fetchWithTimeout(url, signal, TIMEOUT_MS)\n .then((r) => r.text())\n .catch(() => '');\n\n return parseGoogleResults(html, num);\n}\n\nfunction parseGoogleResults(html: string, num: number): SearchResult[] {\n const results: SearchResult[] = [];\n const titleRegex = /<h3[^>]*class=\"[^\"]*DKV84\"[^>]*>([^<]+)<\\/h3>/gi;\n const urlRegex = /<cite[^>]*>([^<]+)<\\/cite>/gi;\n const snippetRegex = /<span[^>]*class=\"[^\"]*aXCZ0b[^>]*>([^<]+)<\\/span>/gi;\n\n const titles = takeFrom(\n [...html.matchAll(titleRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n const urls = takeFrom(\n [...html.matchAll(urlRegex)]\n .filter((m) => m[1])\n .map((m) => stripTags(expectDefined(m[1])).replace(/^\\*(https?:\\/\\/[^\\s]+).*$/, '$1'))\n .filter((u) => u.startsWith('http')),\n num,\n );\n\n const snippets = takeFrom(\n [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n for (let i = 0; i < Math.min(titles.length, num); i++) {\n results.push({\n title: titles[i] ?? '',\n url: urls[i] ?? '',\n snippet: snippets[i] ?? '',\n score: 1,\n });\n }\n\n return results;\n}\n\nasync function bingSearch(query: string, num: number, signal: AbortSignal): Promise<SearchResult[]> {\n const encoded = encodeURIComponent(query);\n const url = `https://www.bing.com/search?q=${encoded}`;\n\n const html = await fetchWithTimeout(url, signal, TIMEOUT_MS)\n .then((r) => r.text())\n .catch(() => '');\n\n return parseBingResults(html, num);\n}\n\nfunction parseBingResults(html: string, num: number): SearchResult[] {\n const results: SearchResult[] = [];\n const titleRegex = /<h2[^>]*>\\s*<a[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\/a>\\s*<\\/h2>/gi;\n const snippetRegex = /<p[^>]*class=\"[^\"]*b_paractl[^\"]*\"[^>]*>([^<]+)<\\/p>/gi;\n\n const entries = takeFrom(\n [...html.matchAll(titleRegex)]\n .filter((m) => m[1] && m[2])\n .map((m) => ({ url: expectDefined(m[1]), title: stripTags(expectDefined(m[2])) })),\n num,\n );\n\n const snippets = takeFrom(\n [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n if (entry) {\n results.push({\n title: entry.title ?? '',\n url: entry.url ?? '',\n snippet: snippets[i] ?? '',\n score: 1,\n });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// HTTP helper\n// ---------------------------------------------------------------------------\n\nasync function fetchWithTimeout(\n url: string,\n signal: AbortSignal,\n timeoutMs: number,\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n const fetchSignal = anySignal(signal, controller.signal);\n try {\n // F-05: route through the SSRF-guarded fetch (private-IP blocking, HTTPS,\n // DNS-pinned dispatcher, per-hop redirect re-validation) instead of a bare\n // `fetch` with `redirect: 'follow'`. Search hosts are fixed/trusted, but\n // this closes the residual \"engine 30x → internal address\" redirect risk.\n const res = await guardedFetch(url, 5, fetchSignal, {\n 'user-agent':\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\n });\n clearTimeout(timer);\n return res;\n } catch (e) {\n clearTimeout(timer);\n if (e instanceof FetchError) {\n throw e;\n }\n throw new FetchError({\n message: `search: failed to fetch ${url}`,\n status: 0,\n context: { url },\n cause: e,\n });\n }\n}\n\nfunction anySignal(...signals: AbortSignal[]): AbortSignal {\n // Native combinator (Node ≥ 20.3; this repo requires ≥ 22). The previous\n // hand-rolled version registered a non-once 'abort' listener on every\n // input signal and never removed it — the run-level signal outlives each\n // request, so listeners (and their closures) accumulated one per search\n // call for the life of the agent run.\n return AbortSignal.any(signals);\n}\n\nfunction stripTags(html: string): string {\n return html\n .replace(/<[^>]+>/g, '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .trim();\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/fetch.ts","../src/search.ts"],"names":["undiciFetch","ToolValidationError","FetchError"],"mappings":"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","file":"search.js","sourcesContent":["import * as dns from 'node:dns/promises';\nimport * as net from 'node:net';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport {\n FetchError,\n ToolError,\n ToolValidationError,\n isPrivateIPv4,\n isPrivateIPv6,\n} from '@wrongstack/core';\nimport { Agent, fetch as undiciFetch } from 'undici';\nimport TurndownService from 'turndown';\nimport { truncateMiddle } from './_util.js';\n\n/**\n * Singleton Turndown instance for HTML→Markdown conversion.\n * Pre-configured with sensible defaults; code blocks are handled via the\n * default fenced code rule. Reused across all fetch calls.\n */\nconst TD = new TurndownService({\n // Use `# Title` for headings, not setext underline style (`Title\\n=====`).\n headingStyle: 'atx',\n // Don't wrap code blocks in <pre> — render them as triple-backtick blocks.\n codeBlockStyle: 'fenced',\n});\n\n// Strip <script>/<style>/<noscript> before turndown sees them. The old\n// hand-rolled converter did this via regex; turndown's DOM-based approach\n// may keep their text content unless we remove the elements first.\n// Using turndown's own addRule mechanism keeps the logic co-located.\nTD.addRule('stripDangerousElements', {\n filter: ['script', 'style', 'noscript'],\n replacement: () => '',\n});\n\ninterface FetchInput {\n url: string;\n format?: 'markdown' | 'text' | 'raw' | undefined;\n}\n\ninterface FetchOutput {\n content: string;\n status: number;\n content_type: string;\n url: string;\n}\n\nconst MAX_BYTES = 131_072;\nconst TIMEOUT_MS = 20_000;\nconst nativeGlobalFetch = globalThis.fetch;\n\nconst ALLOW_PRIVATE = process.env['WRONGSTACK_FETCH_ALLOW_PRIVATE'] === '1';\n/* v8 ignore next 8 -- module-load-time opt-in warning; gated on an env var not set during tests. */\nif (ALLOW_PRIVATE && !process.env['CI']) {\n console.warn(\n '[WrongStack] WARNING: WRONGSTACK_FETCH_ALLOW_PRIVATE=1 is active —\\n' +\n ' fetch tool can now access private IPs (10.x, 192.168.x, 169.254.x),\\n' +\n ' cloud metadata endpoints, and plaintext HTTP. Use only on isolated networks.',\n );\n}\n\n/** Abort when any of the signals abort (Node 22+ — AbortSignal.any shipped in Node 20). */\nconst combineSignals = (signals: AbortSignal[]): AbortSignal => AbortSignal.any(signals);\n\ntype LookupCallback = (\n err: NodeJS.ErrnoException | null,\n address?: string | Array<{ address: string | undefined; family: number }>,\n family?: number | undefined,\n) => void;\n\n/**\n * DNS lookup used by the undici dispatcher below. It performs the SINGLE name\n * resolution that the TCP connection actually uses, and rejects if any\n * resolved address is private/loopback/link-local. Because the connection\n * reuses exactly this result, there is no DNS-rebinding TOCTOU window between\n * the security check and the connect — closing the gap the old code documented\n * (validate with one dns.lookup, then let fetch re-resolve independently).\n * TLS still validates the certificate against the hostname (SNI is set by\n * undici from the URL), so pinning the IP does not weaken cert checking.\n */\nexport function guardedLookup(\n hostname: string,\n options: { all?: boolean | undefined; family?: number | undefined },\n callback: LookupCallback,\n): void {\n dns\n .lookup(hostname, { all: true })\n .then((records) => {\n const family = options?.family;\n const byFamily =\n family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;\n const list = byFamily.length > 0 ? byFamily : records;\n if (!ALLOW_PRIVATE) {\n for (const r of list) {\n const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);\n if (bad) {\n callback(\n Object.assign(new Error(`fetch: resolved to private address ${r.address}`), {\n code: 'EAI_FAIL',\n }),\n );\n return;\n }\n }\n }\n if (options?.all) {\n callback(\n null,\n list.map((r) => ({ address: r.address, family: r.family })),\n );\n return;\n }\n const first = list.at(0);\n if (!first) {\n callback(\n Object.assign(new Error(`fetch: no address for ${hostname}`), { code: 'ENOTFOUND' }),\n );\n return;\n }\n callback(null, first.address, first.family);\n })\n .catch((err) => callback(err as NodeJS.ErrnoException));\n}\n\n// Reused across requests; guardedLookup re-validates on every new connection,\n// so connection pooling is safe. Literal-IP targets bypass lookup entirely and\n// are caught by assertNotPrivate's pre-check instead.\n// Destroyed on process exit so long-running processes (eternal autonomy,\n// MCP server mode) don't let the connection pool grow unboundedly.\nlet pinnedAgent: Agent | undefined;\nfunction getPinnedDispatcher(): Agent {\n if (!pinnedAgent) {\n pinnedAgent = new Agent({ connect: { lookup: guardedLookup as never } });\n }\n return pinnedAgent;\n}\n\nfunction dispatcherFetch(): typeof globalThis.fetch {\n // Node's built-in global fetch is backed by its own bundled undici version.\n // Passing an Agent from the workspace's `undici` package to that different\n // dispatcher ABI fails on recent Node with:\n // UND_ERR_INVALID_ARG: invalid onRequestStart method\n // Use the matching package fetch+Agent pair in real runs, but keep honoring\n // test/user fetch shims that replace globalThis.fetch after this module loads.\n return globalThis.fetch === nativeGlobalFetch\n ? (undiciFetch as unknown as typeof globalThis.fetch)\n : globalThis.fetch;\n}\n// Clean up the global dispatcher on exit — undici Agents maintain connection\n// pools and DNS caches that should be torn down in long-running processes.\n// Guard against duplicate registration (module reload/HMR would otherwise\n// accumulate listeners).\nlet _beforeExitRegistered = false;\nif (!_beforeExitRegistered) {\n _beforeExitRegistered = true;\n /* v8 ignore next 4 -- process 'beforeExit' cleanup; not deterministically triggerable in-test. */\n process.on('beforeExit', () => {\n pinnedAgent?.destroy();\n pinnedAgent = undefined;\n });\n}\n\n/**\n * SSRF-guarded fetch with manual, per-hop-revalidated redirects, exported so\n * other builtin tools (e.g. `search`) get the same protections instead of a\n * weaker `redirect: 'follow'`. Every hop is re-checked against private/loopback\n * ranges and the connection is pinned to the validated IP via the undici\n * dispatcher (no DNS-rebinding TOCTOU). `headers` defaults to the plain `fetch`\n * tool's; callers may override (e.g. a browser User-Agent for search engines).\n */\nexport async function guardedFetch(\n url: string,\n maxRedirects: number,\n signal: AbortSignal,\n headers: Record<string, string> = {\n 'user-agent': 'WrongStack/1.0 (+https://wrongstack.com)',\n accept: 'text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.1',\n },\n): Promise<Response> {\n let redirectCount = 0;\n let currentUrl = url;\n for (;;) {\n // Re-validate every hop. A public host can 302 to 169.254.169.254 (cloud metadata),\n // or DNS can rebind between hops; checking only the initial URL is insufficient.\n const parsed = new URL(currentUrl);\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n throw new ToolValidationError({\n message: `fetch: redirect to unsupported protocol \"${parsed.protocol}\"`,\n field: 'url',\n });\n }\n if (parsed.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new ToolValidationError({\n message: 'fetch: redirect to http:// blocked (HTTPS required by default)',\n field: 'url',\n });\n }\n await assertNotPrivate(parsed.hostname);\n\n // The dispatcher pins the connection to the IP guardedLookup validated —\n // no independent re-resolution, so DNS rebinding can't swap in a private\n // address between check and connect. `dispatcher` is a runtime option of\n // Node's undici-backed global fetch but isn't in lib.dom's RequestInit, and\n // our undici Agent's type differs from the @types/node copy — hence the\n // cast. (Verified: global fetch invokes the Agent's custom lookup.)\n const init = {\n redirect: 'manual' as const,\n signal,\n headers,\n dispatcher: getPinnedDispatcher(),\n };\n const res = await dispatcherFetch()(currentUrl, init as never as RequestInit);\n if (res.status < 300 || res.status > 399) {\n return res;\n }\n redirectCount++;\n if (redirectCount > maxRedirects) {\n throw new FetchError({\n message: `fetch: exceeded ${maxRedirects} redirects`,\n status: res.status,\n context: { url: currentUrl, maxRedirects, redirectCount },\n });\n }\n const location = res.headers.get('location');\n if (!location) {\n throw new FetchError({\n message: 'fetch: redirect status with no location header',\n status: res.status,\n context: { url: currentUrl, redirectCount },\n });\n }\n currentUrl = new URL(location, currentUrl).toString();\n }\n}\n\nexport const fetchTool: Tool<FetchInput, FetchOutput> = {\n name: 'fetch',\n category: 'Network',\n description:\n 'Fetch a URL and return its content. HTML pages are automatically converted to clean markdown. ' +\n 'This tool has strong SSRF protections (private IPs, localhost, and cloud metadata endpoints are blocked by default).',\n usageHint:\n 'Use this when you need external information (documentation, API responses, web pages, etc.).\\n\\n' +\n 'Security notes:\\n' +\n '- Only HTTPS is allowed by default.\\n' +\n '- Internal/private networks are blocked unless explicitly enabled via environment variable.\\n' +\n '- Redirects are followed but re-validated at each hop.\\n' +\n '- Output is capped (128KB by default) to avoid flooding context.\\n' +\n 'Prefer this over raw `bash curl` or `bash wget`.',\n permission: 'auto',\n mutating: false,\n capabilities: ['net.outbound'],\n icon: 'web',\n // Trust rules for fetch match on the literal URL — declare it explicitly\n // so a user can trust `https://api.example.com/*` without accidentally\n // matching that pattern on any other tool that happens to have a `url`\n // input field.\n subjectKey: 'url',\n timeoutMs: TIMEOUT_MS,\n maxOutputBytes: MAX_BYTES,\n inputSchema: {\n type: 'object',\n properties: {\n url: {\n type: 'string',\n description: 'The target URL (must use https://).',\n },\n format: {\n type: 'string',\n enum: ['markdown', 'text', 'raw'],\n description: 'Output format. \"markdown\" is recommended for HTML pages.',\n },\n },\n required: ['url'],\n },\n async execute(input, ctx, opts) {\n let final: FetchOutput | undefined;\n const executeStream = fetchTool.executeStream;\n if (!executeStream) {\n throw new ToolError({\n message: 'fetchTool: stream execution unavailable',\n code: 'TOOL_EXECUTION_FAILED',\n toolName: 'fetch',\n });\n }\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) {\n throw new ToolError({\n message: 'fetch: stream ended without final event',\n code: 'TOOL_EXECUTION_FAILED',\n toolName: 'fetch',\n });\n }\n return final;\n },\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<FetchOutput>> {\n if (!input?.url) {\n throw new ToolValidationError({\n message: 'fetch: url is required',\n field: 'url',\n });\n }\n const u = new URL(input.url);\n if (u.protocol !== 'https:' && u.protocol !== 'http:') {\n throw new ToolValidationError({\n message: `fetch: unsupported protocol \"${u.protocol}\"`,\n field: 'url',\n });\n }\n if (u.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new ToolValidationError({\n message: 'fetch: http:// blocked (HTTPS required by default)',\n field: 'url',\n });\n }\n await assertNotPrivate(u.hostname);\n\n yield { type: 'log', text: `GET ${input.url}` };\n\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(new ToolError({\n message: 'fetch timeout',\n code: 'TOOL_TIMEOUT',\n toolName: 'fetch',\n })), TIMEOUT_MS);\n const combined = combineSignals([opts.signal, ctrl.signal]);\n\n try {\n let res: Response;\n try {\n res = await guardedFetch(input.url, 5, combined);\n } catch (err) {\n // A user-initiated cancel propagates unchanged. Our own timeout and any\n // transport failure get a diagnostic message: undici throws an opaque\n // `TypeError: fetch failed` whose real reason (ENOTFOUND, ECONNREFUSED,\n // UND_ERR_CONNECT_TIMEOUT, a TLS/cert error, …) lives only on `.cause`.\n // Surfacing just `.message` left users with \"fetch failed\" and no clue\n // why HTTPS broke (see #100), so unwrap the cause chain here.\n if (opts.signal.aborted) throw err;\n throw describeFetchError(err, input.url, ctrl.signal.aborted);\n }\n\n const ct = res.headers.get('content-type') ?? 'application/octet-stream';\n if (/^image\\/|^audio\\/|^video\\/|application\\/octet-stream/.test(ct)) {\n throw new FetchError({\n message: `fetch: refusing to read binary content-type \"${ct}\"`,\n status: res.status,\n context: { url: res.url, contentType: ct },\n });\n }\n\n yield {\n type: 'log',\n text: `HTTP ${res.status} ${ct}`,\n data: { status: res.status, contentType: ct },\n };\n\n const reader = res.body?.getReader();\n let received = 0;\n const chunks: Uint8Array[] = [];\n let pendingBytes = 0;\n const FLUSH_AT = 4 * 1024;\n if (reader) {\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n if (!value) continue;\n received += value.byteLength;\n pendingBytes += value.byteLength;\n chunks.push(value);\n if (pendingBytes >= FLUSH_AT) {\n // Snapshot recent bytes for the partial_output. Keep it cheap —\n // don't try to decode UTF-8 boundaries; the TUI just needs a\n // \"things are happening\" signal.\n const recent = Buffer.from(value).toString('utf-8');\n yield {\n type: 'partial_output',\n text: recent,\n data: { received },\n };\n pendingBytes = 0;\n }\n if (received > MAX_BYTES) break;\n }\n }\n const text = Buffer.concat(chunks.map((c) => Buffer.from(c))).toString('utf8');\n\n const format = input.format ?? (ct.includes('text/html') ? 'markdown' : 'text');\n let content: string;\n if (format === 'raw') content = text;\n else if (format === 'markdown' && ct.includes('text/html')) content = TD.turndown(text);\n else if (ct.includes('application/json')) content = prettyJson(text);\n else content = text;\n\n yield {\n type: 'final',\n output: {\n content: truncateMiddle(content, MAX_BYTES),\n status: res.status,\n content_type: ct,\n url: res.url,\n },\n };\n // P2 #5: record the network request as a structured side effect.\n ctx.recordSideEffect?.({\n toolUseId: `fetch-${Date.now()}`,\n toolName: 'fetch',\n ts: new Date().toISOString(),\n input: { url: input.url, format: input.format },\n outcome: `HTTP ${res.status} (${ct})`,\n risk: 'network',\n });\n } finally {\n clearTimeout(timer);\n }\n },\n};\n\nasync function assertNotPrivate(hostname: string): Promise<void> {\n if (ALLOW_PRIVATE) return;\n\n const host =\n hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;\n\n if (host === 'localhost' || host.endsWith('.localhost')) {\n throw new ToolValidationError({\n message: 'fetch: blocked localhost target',\n field: 'url',\n });\n }\n\n const ipVersion = net.isIP(host);\n if (ipVersion === 4) {\n if (isPrivateIPv4(host)) {\n throw new ToolValidationError({\n message: `fetch: blocked private/loopback address \"${host}\"`,\n field: 'url',\n });\n }\n } else if (ipVersion === 6) {\n if (isPrivateIPv6(host)) {\n throw new ToolValidationError({\n message: `fetch: blocked private/loopback address \"${host}\"`,\n field: 'url',\n });\n }\n } else {\n // Hostname — pre-flight check: resolve and reject if any record is private,\n // so we fail fast with a clear error before opening a socket. The\n // authoritative anti-rebinding control is guardedLookup on the pinned\n // undici dispatcher (see getPinnedDispatcher): it performs the single\n // resolution the connection actually uses, so there is no TOCTOU between\n // this check and the connect. Each redirect target is re-checked too.\n try {\n // Use dns.lookup for async hostname resolution (matches guardedLookup below).\n const records = await dns.lookup(host, { all: true });\n for (const r of records) {\n const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);\n if (bad) {\n throw new ToolValidationError({\n message: `fetch: resolved to private address ${r.address}`,\n field: 'url',\n });\n }\n }\n } catch (err) {\n if (err instanceof Error && err.message.startsWith('fetch:')) throw err;\n // DNS failure — let fetch handle it\n }\n }\n}\n\n/**\n * Turn an opaque undici `TypeError: fetch failed` into an actionable message by\n * walking its `.cause` chain. undici buries the transport reason (a DNS/socket\n * errno or a TLS handshake failure) one or more `.cause` hops down, so the bare\n * `.message` is always just \"fetch failed\". We join each distinct\n * `code: message` link so the user sees, e.g.,\n * `fetch: GET https://x failed — UND_ERR_CONNECT_TIMEOUT: Connect Timeout Error`.\n */\nfunction describeFetchError(err: unknown, url: string, timedOut: boolean): FetchError | ToolError {\n if (timedOut) {\n return new ToolError({\n message: `fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`,\n code: 'TOOL_TIMEOUT',\n toolName: 'fetch',\n context: { url, timedOut: true, timeoutMs: TIMEOUT_MS },\n cause: err,\n });\n }\n const parts: string[] = [];\n const seen = new Set<unknown>();\n let cur: unknown = err;\n while (cur instanceof Error && !seen.has(cur)) {\n seen.add(cur);\n const code = (cur as NodeJS.ErrnoException).code;\n const label = code ? `${code}: ${cur.message}` : cur.message;\n // Skip undici's uninformative top-level \"fetch failed\" wrapper, but keep it\n // as a fallback if it turns out to be the only thing we have.\n if (label && label !== 'fetch failed' && !parts.includes(label)) parts.push(label);\n cur = (cur as { cause?: unknown }).cause;\n }\n const detail = parts.length > 0 ? parts.join(' → ') : 'fetch failed';\n return new FetchError({\n message: `fetch: GET ${url} failed — ${detail}`,\n status: 502,\n context: { url, timedOut: false, transportErrors: parts },\n // Preserve the original undici / DNS / TLS chain so callers can inspect\n // it via `err.cause` and structured `instanceof` checks. The flattened\n // text version stays in the message for human readability.\n cause: err,\n });\n}\n\nfunction prettyJson(s: string): string {\n try {\n return JSON.stringify(JSON.parse(s), null, 2);\n } catch {\n return s;\n }\n}\n","import { expectDefined, FetchError, ToolValidationError } from '@wrongstack/core';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { guardedFetch } from './fetch.js';\nimport { toErrorMessage } from '@wrongstack/core/utils';\n\ninterface SearchResult {\n title: string;\n url: string;\n snippet: string;\n score: number;\n}\n\ninterface CacheEntry {\n results: SearchResult[];\n source: string;\n timestamp: number;\n}\n\ninterface SearchInput {\n query: string;\n num_results?: number | undefined;\n source?: 'duckduckgo' | 'google' | 'bing' | undefined;\n skip_cache?: boolean | undefined;\n}\n\ninterface SearchOutput {\n query: string;\n results: { title: string; url: string; snippet: string }[];\n source: string;\n truncated: boolean;\n cached: boolean;\n}\n\nconst DEFAULT_NUM = 10;\nconst MAX_RESULTS = 50;\nconst TIMEOUT_MS = 15_000;\nconst CACHE_TTL_MS = 300_000; // 5 minutes — matches the former web-search plugin default\n\n// Module-level cache shared across calls within a single agent run. Keyed by\n// `<source>:<query>`. This is intentionally process-local (not persisted) —\n// it exists to avoid hammering a search engine when the model rephrases the\n// same query a few turns apart.\nconst cache = new Map<string, CacheEntry>();\n\nexport const searchTool: Tool<SearchInput, SearchOutput> = {\n name: 'search',\n category: 'Search',\n description:\n 'Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL.',\n usageHint:\n 'Good for: API documentation, error messages, library usage examples, current best practices.\\n\\n' +\n '- Prefer specific queries over very broad ones.\\n' +\n '- Results go through the guarded fetch system (same protections as the `fetch` tool).\\n' +\n '- Supports duckduckgo (default), google, and bing sources.\\n' +\n '- Set `skip_cache: true` to force a fresh search.\\n' +\n '- This is often better than the model trying to recall outdated knowledge.',\n permission: 'auto',\n mutating: false,\n capabilities: ['net.outbound'],\n icon: 'search',\n timeoutMs: TIMEOUT_MS,\n inputSchema: {\n type: 'object',\n properties: {\n query: { type: 'string', description: 'Search query' },\n num_results: {\n type: 'integer',\n description: 'Number of results (1-50, default 10)',\n minimum: 1,\n maximum: MAX_RESULTS,\n },\n source: {\n type: 'string',\n enum: ['duckduckgo', 'google', 'bing'],\n description: 'Search engine to use (default: duckduckgo)',\n },\n skip_cache: {\n type: 'boolean',\n description: 'Skip the in-memory cache and force a fresh search (default: false)',\n },\n },\n required: ['query'],\n },\n async execute(input, ctx, opts) {\n let final: SearchOutput | undefined;\n const executeStream = searchTool.executeStream;\n if (!executeStream) throw new Error('searchTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('search: stream ended without final event');\n return final;\n },\n async *executeStream(input, _ctx, opts): AsyncGenerator<ToolStreamEvent<SearchOutput>> {\n if (!input?.query || input.query.trim() === '') {\n throw new ToolValidationError({\n message: 'search: query is required and must be a non-empty string',\n field: 'query',\n });\n }\n\n const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));\n const source = input.source ?? 'duckduckgo';\n const skipCache = input.skip_cache ?? false;\n const cacheKey = `${source}:${input.query}`;\n\n // --- Cache hit ---\n if (!skipCache) {\n const entry = cache.get(cacheKey);\n if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) {\n const results = entry.results.map((r) => ({\n title: r.title,\n url: r.url,\n snippet: r.snippet,\n }));\n yield {\n type: 'log',\n text: `Cache hit for \"${input.query}\" (${source})`,\n data: { source, query: input.query, cached: true },\n };\n yield {\n type: 'partial_output',\n text: `${results.length} cached results from ${entry.source}`,\n data: { count: results.length, cached: true, source: entry.source },\n };\n yield {\n type: 'final',\n output: {\n query: input.query,\n results: results.slice(0, num),\n source: entry.source,\n truncated: results.length >= num,\n cached: true,\n },\n };\n return;\n }\n }\n\n yield {\n type: 'log',\n text: `Querying ${source} for \"${input.query}\"…`,\n data: { source, query: input.query, cached: false },\n };\n\n let rawResults: SearchResult[];\n let effectiveSource: SearchOutput['source'] = source;\n switch (source) {\n case 'duckduckgo':\n rawResults = await duckduckgoSearch(input.query, num, opts.signal);\n break;\n case 'google':\n rawResults = await googleSearch(input.query, num, opts.signal);\n break;\n case 'bing':\n rawResults = await bingSearch(input.query, num, opts.signal);\n break;\n default:\n throw new ToolValidationError({\n message: `search: unknown source \"${source}\"`,\n field: 'source',\n });\n }\n\n let ranked = rankSearchResults(rawResults, input.query);\n if (source !== 'duckduckgo' && shouldFallbackToDuckDuckGo(ranked, input.query)) {\n yield {\n type: 'log',\n text: `${source} returned no relevant static results; falling back to duckduckgo`,\n data: { source, fallback: 'duckduckgo', query: input.query },\n };\n rawResults = await duckduckgoSearch(input.query, num, opts.signal);\n ranked = rankSearchResults(rawResults, input.query);\n effectiveSource = 'duckduckgo';\n }\n\n const finalResults = ranked.slice(0, num);\n\n // --- Store in cache ---\n cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });\n pruneStaleCacheEntries();\n\n yield {\n type: 'partial_output',\n text: `${finalResults.length} results from ${effectiveSource}`,\n data: { count: finalResults.length, cached: false, source: effectiveSource },\n };\n yield {\n type: 'final',\n output: {\n query: input.query,\n results: finalResults.map((r) => ({\n title: r.title,\n url: r.url,\n snippet: r.snippet,\n })),\n source: effectiveSource,\n truncated: finalResults.length >= num,\n cached: false,\n },\n };\n },\n};\n\n// ---------------------------------------------------------------------------\n// Cache helpers\n// ---------------------------------------------------------------------------\n\n/** Drop entries older than 2× the TTL — bounded growth, cheap to run per write. */\nfunction pruneStaleCacheEntries(): void {\n const cutoff = Date.now() - CACHE_TTL_MS * 2;\n for (const [key, entry] of cache.entries()) {\n if (entry.timestamp < cutoff) cache.delete(key);\n }\n}\n\n/** Exposed for tests so they can reset the module-level cache between cases. */\nexport function __clearSearchCache(): void {\n cache.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Ranking\n// ---------------------------------------------------------------------------\n\nfunction rankSearchResults(results: SearchResult[], query: string): SearchResult[] {\n const seenUrls = new Set<string>();\n const deduped: SearchResult[] = [];\n for (const r of results) {\n const noQuery = r.url.split('?')[0] ?? r.url;\n const normalized = noQuery.split('#')[0] ?? r.url;\n if (!seenUrls.has(normalized) && r.url.startsWith('http')) {\n seenUrls.add(normalized);\n deduped.push(r);\n }\n }\n return scoreResults(deduped, query);\n}\n\nfunction scoreResults(results: SearchResult[], query: string): SearchResult[] {\n const terms = query.toLowerCase().split(/\\s+/).filter((t) => t.length > 0);\n return results\n .map((r) => {\n const titleLower = r.title.toLowerCase();\n const snippetLower = r.snippet.toLowerCase();\n let score = r.score;\n for (const term of terms) {\n if (titleLower.includes(term)) score += 2;\n if (snippetLower.includes(term)) score += 1;\n }\n return { ...r, score };\n })\n .sort((a, b) => b.score - a.score);\n}\n\nfunction shouldFallbackToDuckDuckGo(results: SearchResult[], query: string): boolean {\n if (results.length === 0) return true;\n const terms = query.toLowerCase().split(/\\s+/).filter((t) => t.length >= 3);\n if (terms.length === 0) return false;\n return !results.some((r) => {\n const haystack = `${r.title} ${r.url} ${r.snippet}`.toLowerCase();\n return terms.some((term) => haystack.includes(term));\n });\n}\n\n// ---------------------------------------------------------------------------\n// Search engines\n// ---------------------------------------------------------------------------\n\nasync function duckduckgoSearch(\n query: string,\n num: number,\n signal: AbortSignal,\n): Promise<SearchResult[]> {\n const encoded = encodeURIComponent(query);\n const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;\n\n try {\n const response = await fetchWithTimeout(url, signal, TIMEOUT_MS);\n const html = await response.text();\n return parseDuckDuckGo(html, num);\n } catch (err) {\n console.log(\n JSON.stringify({ level: 'debug', event: 'search_failed', query, error: toErrorMessage(err) }),\n );\n // Return a sentinel result that survives the dedup filter (which drops\n // non-http URLs). Using a placeholder http URL keeps it visible to the\n // caller so they know the search failed rather than silently getting [].\n return [{ title: 'Search unavailable', url: 'https://duckduckgo.com/unavailable', snippet: 'Could not reach DuckDuckGo', score: 0 }];\n }\n}\n\nfunction takeFrom<T>(iter: Iterable<T>, max: number): T[] {\n const out: T[] = [];\n for (const item of iter) {\n if (out.length >= max) break;\n out.push(item);\n }\n return out;\n}\n\nfunction parseDuckDuckGo(html: string, num: number): SearchResult[] {\n const results: SearchResult[] = [];\n const linkRegex = /<a\\b([^>]*\\bclass=([\"'])[^\"']*\\bresult-link\\b[^\"']*\\2[^>]*)>([\\s\\S]*?)<\\/a>/gi;\n const snippetRegex =\n /<([a-z0-9]+)\\b([^>]*\\bclass=([\"'])[^\"']*\\bresult-snippet\\b[^\"']*\\3[^>]*)>([\\s\\S]*?)<\\/\\1>/gi;\n\n const linkMatches = takeFrom(\n [...html.matchAll(linkRegex)]\n .map((m) => {\n const attrs = expectDefined(m[1]);\n const href = getHtmlAttr(attrs, 'href');\n const title = stripTags(expectDefined(m[3]));\n return href && title ? { url: normalizeDuckDuckGoUrl(href), title } : undefined;\n })\n .filter((m): m is { url: string; title: string } => m !== undefined),\n num,\n );\n\n const snippetMatches = takeFrom(\n [...html.matchAll(snippetRegex)]\n .filter((m) => m[4])\n .map((m) => stripTags(expectDefined(m[4]))),\n num,\n );\n\n for (let i = 0; i < linkMatches.length && i < num; i++) {\n const entry = linkMatches[i];\n if (entry) {\n results.push({\n title: entry.title ?? '',\n url: entry.url ?? '',\n snippet: snippetMatches[i] ?? '',\n score: 1,\n });\n }\n }\n\n return results;\n}\n\nfunction getHtmlAttr(attrs: string, name: string): string | undefined {\n const quoted = new RegExp(`\\\\b${name}\\\\s*=\\\\s*([\"'])(.*?)\\\\1`, 'i').exec(attrs);\n if (quoted?.[2]) return decodeHtmlEntities(quoted[2]);\n const unquoted = new RegExp(`\\\\b${name}\\\\s*=\\\\s*([^\\\\s>]+)`, 'i').exec(attrs);\n return unquoted?.[1] ? decodeHtmlEntities(unquoted[1]) : undefined;\n}\n\nfunction normalizeDuckDuckGoUrl(raw: string): string {\n if (raw.startsWith('//')) return `https:${raw}`;\n if (!raw.startsWith('/')) return raw;\n if (!raw.startsWith('/l/')) return raw;\n try {\n const url = new URL(raw, 'https://duckduckgo.com');\n const uddg = url.searchParams.get('uddg');\n return uddg?.startsWith('http') ? uddg : url.toString();\n } catch {\n return raw;\n }\n}\n\nasync function googleSearch(query: string, num: number, signal: AbortSignal): Promise<SearchResult[]> {\n const encoded = encodeURIComponent(query);\n const url = `https://www.google.com/search?q=${encoded}&hl=en`;\n\n const html = await fetchWithTimeout(url, signal, TIMEOUT_MS)\n .then((r) => r.text())\n .catch(() => '');\n\n return parseGoogleResults(html, num);\n}\n\nfunction parseGoogleResults(html: string, num: number): SearchResult[] {\n const results: SearchResult[] = [];\n const titleRegex = /<h3[^>]*class=\"[^\"]*DKV84\"[^>]*>([^<]+)<\\/h3>/gi;\n const urlRegex = /<cite[^>]*>([^<]+)<\\/cite>/gi;\n const snippetRegex = /<span[^>]*class=\"[^\"]*aXCZ0b[^>]*>([^<]+)<\\/span>/gi;\n\n const titles = takeFrom(\n [...html.matchAll(titleRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n const urls = takeFrom(\n [...html.matchAll(urlRegex)]\n .filter((m) => m[1])\n .map((m) => stripTags(expectDefined(m[1])).replace(/^\\*(https?:\\/\\/[^\\s]+).*$/, '$1'))\n .filter((u) => u.startsWith('http')),\n num,\n );\n\n const snippets = takeFrom(\n [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n for (let i = 0; i < Math.min(titles.length, num); i++) {\n results.push({\n title: titles[i] ?? '',\n url: urls[i] ?? '',\n snippet: snippets[i] ?? '',\n score: 1,\n });\n }\n\n return results;\n}\n\nasync function bingSearch(query: string, num: number, signal: AbortSignal): Promise<SearchResult[]> {\n const encoded = encodeURIComponent(query);\n const url = `https://www.bing.com/search?q=${encoded}`;\n\n const html = await fetchWithTimeout(url, signal, TIMEOUT_MS)\n .then((r) => r.text())\n .catch(() => '');\n\n return parseBingResults(html, num);\n}\n\nfunction parseBingResults(html: string, num: number): SearchResult[] {\n const results: SearchResult[] = [];\n const blocks = [...html.matchAll(/<li\\b[^>]*class=([\"'])[^\"']*\\bb_algo\\b[^\"']*\\1[^>]*>([\\s\\S]*?)(?=<li\\b[^>]*class=([\"'])[^\"']*\\bb_algo\\b[^\"']*\\3|<\\/ol>)/gi)]\n .map((m) => expectDefined(m[2]));\n const candidates = blocks.length > 0 ? blocks : [html];\n\n const entries = takeFrom(candidates.flatMap((block) => {\n const titleMatch = /<h2[^>]*>\\s*<a\\b([^>]*)>([\\s\\S]*?)<\\/a>\\s*<\\/h2>/i.exec(block);\n if (!titleMatch) return [];\n const href = getHtmlAttr(expectDefined(titleMatch[1]), 'href');\n const title = stripTags(expectDefined(titleMatch[2]));\n if (!href || !title) return [];\n const snippetMatch = /<p\\b[^>]*class=([\"'])[^\"']*\\b(?:b_paractl|b_lineclamp\\d*)\\b[^\"']*\\1[^>]*>([\\s\\S]*?)<\\/p>/i.exec(block)\n ?? /<p\\b[^>]*>([\\s\\S]*?)<\\/p>/i.exec(block);\n const snippet = snippetMatch ? stripTags(expectDefined(snippetMatch.at(-1))) : '';\n return [{ url: normalizeBingUrl(href), title, snippet, score: 1 }];\n }), num);\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n if (entry) {\n results.push({\n title: entry.title ?? '',\n url: entry.url ?? '',\n snippet: entry.snippet ?? '',\n score: 1,\n });\n }\n }\n\n return results;\n}\n\nfunction normalizeBingUrl(raw: string): string {\n try {\n const url = new URL(raw);\n if (url.hostname.endsWith('bing.com') && url.pathname.startsWith('/ck/')) {\n const encoded = url.searchParams.get('u');\n const decoded = decodeBingTarget(encoded);\n if (decoded?.startsWith('http')) return decoded;\n }\n } catch {\n // Fall through to the raw URL.\n }\n return raw;\n}\n\nfunction decodeBingTarget(encoded: string | null): string | undefined {\n if (!encoded) return undefined;\n const payload = encoded.startsWith('a1') ? encoded.slice(2) : encoded;\n try {\n const padded = payload.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(payload.length / 4) * 4, '=');\n return Buffer.from(padded, 'base64').toString('utf8');\n } catch {\n return undefined;\n }\n}\n\n// ---------------------------------------------------------------------------\n// HTTP helper\n// ---------------------------------------------------------------------------\n\nasync function fetchWithTimeout(\n url: string,\n signal: AbortSignal,\n timeoutMs: number,\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n const fetchSignal = anySignal(signal, controller.signal);\n try {\n // F-05: route through the SSRF-guarded fetch (private-IP blocking, HTTPS,\n // DNS-pinned dispatcher, per-hop redirect re-validation) instead of a bare\n // `fetch` with `redirect: 'follow'`. Search hosts are fixed/trusted, but\n // this closes the residual \"engine 30x → internal address\" redirect risk.\n const res = await guardedFetch(url, 5, fetchSignal, {\n 'user-agent':\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\n });\n clearTimeout(timer);\n return res;\n } catch (e) {\n clearTimeout(timer);\n if (e instanceof FetchError) {\n throw e;\n }\n throw new FetchError({\n message: `search: failed to fetch ${url}`,\n status: 0,\n context: { url },\n cause: e,\n });\n }\n}\n\nfunction anySignal(...signals: AbortSignal[]): AbortSignal {\n // Native combinator (Node ≥ 20.3; this repo requires ≥ 22). The previous\n // hand-rolled version registered a non-once 'abort' listener on every\n // input signal and never removed it — the run-level signal outlives each\n // request, so listeners (and their closures) accumulated one per search\n // call for the life of the agent run.\n return AbortSignal.any(signals);\n}\n\nfunction stripTags(html: string): string {\n return decodeHtmlEntities(html.replace(/<[^>]+>/g, '')).trim();\n}\n\nfunction decodeHtmlEntities(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\");\n}\n"]}
|
package/dist/test.js
CHANGED
|
@@ -687,16 +687,28 @@ function resolveWin32Command(cmd) {
|
|
|
687
687
|
}
|
|
688
688
|
return cmd;
|
|
689
689
|
}
|
|
690
|
-
var WIN32_SHELL_META = /[
|
|
690
|
+
var WIN32_SHELL_META = /[&|<>"\r\n\0]/;
|
|
691
691
|
function assertSafeWin32ShellArgs(args) {
|
|
692
|
-
for (const
|
|
693
|
-
if (typeof
|
|
692
|
+
for (const arg of args) {
|
|
693
|
+
if (typeof arg === "string" && WIN32_SHELL_META.test(arg)) {
|
|
694
694
|
throw new Error(
|
|
695
|
-
|
|
695
|
+
'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > ", or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
|
|
696
696
|
);
|
|
697
697
|
}
|
|
698
698
|
}
|
|
699
699
|
}
|
|
700
|
+
function buildWin32CmdShimInvocation(command, args = []) {
|
|
701
|
+
assertSafeWin32ShellArgs([command, ...args]);
|
|
702
|
+
const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
|
|
703
|
+
return {
|
|
704
|
+
command: process.env["COMSPEC"] ?? "cmd.exe",
|
|
705
|
+
args: ["/d", "/c", line],
|
|
706
|
+
windowsVerbatimArguments: true
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function quoteWin32CmdArg(arg) {
|
|
710
|
+
return `"${arg}"`;
|
|
711
|
+
}
|
|
700
712
|
|
|
701
713
|
// src/_spawn-stream.ts
|
|
702
714
|
var isWin = process.platform === "win32";
|
|
@@ -711,15 +723,16 @@ async function* spawnStream(opts) {
|
|
|
711
723
|
const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
|
|
712
724
|
const resolved = resolveWin32Command(opts.cmd);
|
|
713
725
|
const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
const
|
|
726
|
+
const shim = needsShell ? buildWin32CmdShimInvocation(resolved, opts.args) : null;
|
|
727
|
+
const cmd = shim?.command ?? resolved;
|
|
728
|
+
const args = shim?.args ?? opts.args;
|
|
729
|
+
const child = spawn(cmd, args, {
|
|
717
730
|
cwd: opts.cwd,
|
|
718
731
|
env: buildChildEnv(),
|
|
719
732
|
stdio: ["ignore", "pipe", "pipe"],
|
|
720
733
|
windowsHide: true,
|
|
721
734
|
...isWin ? {} : { signal: opts.signal },
|
|
722
|
-
...
|
|
735
|
+
...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
|
|
723
736
|
});
|
|
724
737
|
const registry = getProcessRegistry();
|
|
725
738
|
const pid = child.pid;
|