@wrongstack/tools 0.267.0 → 0.269.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fetch.ts","../src/search.ts"],"names":[],"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 { isPrivateIPv4, isPrivateIPv6 } 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 Error(`fetch: redirect to unsupported protocol \"${parsed.protocol}\"`);\n }\n if (parsed.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new Error('fetch: redirect to http:// blocked (HTTPS required by default)');\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 unknown as RequestInit);\n if (res.status < 300 || res.status > 399) {\n return res;\n }\n redirectCount++;\n if (redirectCount > maxRedirects) {\n throw new Error(`fetch: exceeded ${maxRedirects} redirects`);\n }\n const location = res.headers.get('location');\n if (!location) {\n throw new Error('fetch: redirect status with no location header');\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) throw new Error('fetchTool: 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('fetch: stream ended without final event');\n return final;\n },\n async *executeStream(input, _ctx, opts): AsyncGenerator<ToolStreamEvent<FetchOutput>> {\n if (!input?.url) throw new Error('fetch: url is required');\n const u = new URL(input.url);\n if (u.protocol !== 'https:' && u.protocol !== 'http:') {\n throw new Error(`fetch: unsupported protocol \"${u.protocol}\"`);\n }\n if (u.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new Error('fetch: http:// blocked (HTTPS required by default)');\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 Error('fetch timeout')), TIMEOUT_MS);\n const combined = combineSignals([opts.signal, ctrl.signal]);\n\n try {\n const res = await guardedFetch(input.url, 5, combined);\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 Error(`fetch: refusing to read binary content-type \"${ct}\"`);\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 } 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 Error('fetch: blocked localhost target');\n }\n\n const ipVersion = net.isIP(host);\n if (ipVersion === 4) {\n if (isPrivateIPv4(host)) {\n throw new Error(`fetch: blocked private/loopback address \"${host}\"`);\n }\n } else if (ipVersion === 6) {\n if (isPrivateIPv6(host)) {\n throw new Error(`fetch: blocked private/loopback address \"${host}\"`);\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 Error(`fetch: resolved to private address ${r.address}`);\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\nfunction prettyJson(s: string): string {\n try {\n return JSON.stringify(JSON.parse(s), null, 2);\n } catch {\n return s;\n }\n}\n\n","import { expectDefined } from '@wrongstack/core';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { guardedFetch } from './fetch.js';\nimport { toErrorMessage } from '@wrongstack/core/utils';\ninterface SearchInput {\n query: string;\n num_results?: number | undefined;\n source?: 'duckduckgo' | 'google' | 'bing' | undefined;\n}\n\ninterface SearchOutput {\n query: string;\n results: { title: string; url: string; snippet: string }[];\n source: string;\n truncated: boolean;\n}\n\nconst DEFAULT_NUM = 10;\nconst MAX_RESULTS = 50;\nconst TIMEOUT_MS = 15_000;\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.',\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 '- 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 },\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) throw new Error('search: query is required');\n\n const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));\n const source = input.source ?? 'duckduckgo';\n\n yield {\n type: 'log',\n text: `Querying ${source} for \"${input.query}\"…`,\n data: { source, query: input.query },\n };\n\n let output: SearchOutput;\n switch (source) {\n case 'duckduckgo':\n output = await duckduckgoSearch(input.query, num, opts.signal);\n break;\n case 'google':\n output = await googleSearch(input.query, num, opts.signal);\n break;\n case 'bing':\n output = await bingSearch(input.query, num, opts.signal);\n break;\n default:\n throw new Error(`search: unknown source \"${source}\"`);\n }\n\n yield {\n type: 'partial_output',\n text: `${output.results.length} results from ${output.source}`,\n data: { count: output.results.length },\n };\n yield { type: 'final', output };\n },\n};\n\nasync function duckduckgoSearch(\n query: string,\n num: number,\n signal: AbortSignal,\n): Promise<SearchOutput> {\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 const results = parseDuckDuckGo(html, num);\n return {\n query,\n results,\n source: 'duckduckgo',\n truncated: results.length >= num,\n };\n } catch (err) {\n console.log(JSON.stringify({ level: 'debug', event: 'search_failed', query, error: toErrorMessage(err) }));\n return {\n query,\n results: [{ title: 'Search unavailable', url: '', snippet: 'Could not reach DuckDuckGo' }],\n source: 'duckduckgo',\n truncated: false,\n };\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): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\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 results.push({\n title: entry?.title ?? '',\n url: entry?.url ?? '',\n snippet: snippetMatches[i] ?? '',\n });\n }\n\n return results;\n}\n\nasync function googleSearch(\n query: string,\n num: number,\n signal: AbortSignal,\n): Promise<SearchOutput> {\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 const results = parseGoogleResults(html, num);\n\n return {\n query,\n results,\n source: 'google',\n truncated: results.length >= num,\n };\n}\n\nfunction parseGoogleResults(html: string, num: number): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\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 });\n }\n\n return results;\n}\n\nasync function bingSearch(query: string, num: number, signal: AbortSignal): Promise<SearchOutput> {\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 const results = parseBingResults(html, num);\n\n return {\n query,\n results,\n source: 'bing',\n truncated: results.length >= num,\n };\n}\n\nfunction parseBingResults(html: string, num: number): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\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 results.push({\n title: entries[i]?.title ?? '',\n url: entries[i]?.url ?? '',\n snippet: snippets[i] ?? '',\n });\n }\n\n return results;\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 throw e;\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(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .trim();\n}\n"]}
1
+ {"version":3,"sources":["../src/fetch.ts","../src/search.ts"],"names":[],"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 { isPrivateIPv4, isPrivateIPv6 } 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 Error(`fetch: redirect to unsupported protocol \"${parsed.protocol}\"`);\n }\n if (parsed.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new Error('fetch: redirect to http:// blocked (HTTPS required by default)');\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 Error(`fetch: exceeded ${maxRedirects} redirects`);\n }\n const location = res.headers.get('location');\n if (!location) {\n throw new Error('fetch: redirect status with no location header');\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) throw new Error('fetchTool: 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('fetch: stream ended without final event');\n return final;\n },\n async *executeStream(input, _ctx, opts): AsyncGenerator<ToolStreamEvent<FetchOutput>> {\n if (!input?.url) throw new Error('fetch: url is required');\n const u = new URL(input.url);\n if (u.protocol !== 'https:' && u.protocol !== 'http:') {\n throw new Error(`fetch: unsupported protocol \"${u.protocol}\"`);\n }\n if (u.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new Error('fetch: http:// blocked (HTTPS required by default)');\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 Error('fetch timeout')), TIMEOUT_MS);\n const combined = combineSignals([opts.signal, ctrl.signal]);\n\n try {\n const res = await guardedFetch(input.url, 5, combined);\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 Error(`fetch: refusing to read binary content-type \"${ct}\"`);\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 } 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 Error('fetch: blocked localhost target');\n }\n\n const ipVersion = net.isIP(host);\n if (ipVersion === 4) {\n if (isPrivateIPv4(host)) {\n throw new Error(`fetch: blocked private/loopback address \"${host}\"`);\n }\n } else if (ipVersion === 6) {\n if (isPrivateIPv6(host)) {\n throw new Error(`fetch: blocked private/loopback address \"${host}\"`);\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 Error(`fetch: resolved to private address ${r.address}`);\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\nfunction prettyJson(s: string): string {\n try {\n return JSON.stringify(JSON.parse(s), null, 2);\n } catch {\n return s;\n }\n}\n\n","import { expectDefined } from '@wrongstack/core';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { guardedFetch } from './fetch.js';\nimport { toErrorMessage } from '@wrongstack/core/utils';\ninterface SearchInput {\n query: string;\n num_results?: number | undefined;\n source?: 'duckduckgo' | 'google' | 'bing' | undefined;\n}\n\ninterface SearchOutput {\n query: string;\n results: { title: string; url: string; snippet: string }[];\n source: string;\n truncated: boolean;\n}\n\nconst DEFAULT_NUM = 10;\nconst MAX_RESULTS = 50;\nconst TIMEOUT_MS = 15_000;\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.',\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 '- 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 },\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) throw new Error('search: query is required');\n\n const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));\n const source = input.source ?? 'duckduckgo';\n\n yield {\n type: 'log',\n text: `Querying ${source} for \"${input.query}\"…`,\n data: { source, query: input.query },\n };\n\n let output: SearchOutput;\n switch (source) {\n case 'duckduckgo':\n output = await duckduckgoSearch(input.query, num, opts.signal);\n break;\n case 'google':\n output = await googleSearch(input.query, num, opts.signal);\n break;\n case 'bing':\n output = await bingSearch(input.query, num, opts.signal);\n break;\n default:\n throw new Error(`search: unknown source \"${source}\"`);\n }\n\n yield {\n type: 'partial_output',\n text: `${output.results.length} results from ${output.source}`,\n data: { count: output.results.length },\n };\n yield { type: 'final', output };\n },\n};\n\nasync function duckduckgoSearch(\n query: string,\n num: number,\n signal: AbortSignal,\n): Promise<SearchOutput> {\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 const results = parseDuckDuckGo(html, num);\n return {\n query,\n results,\n source: 'duckduckgo',\n truncated: results.length >= num,\n };\n } catch (err) {\n console.log(JSON.stringify({ level: 'debug', event: 'search_failed', query, error: toErrorMessage(err) }));\n return {\n query,\n results: [{ title: 'Search unavailable', url: '', snippet: 'Could not reach DuckDuckGo' }],\n source: 'duckduckgo',\n truncated: false,\n };\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): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\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 results.push({\n title: entry?.title ?? '',\n url: entry?.url ?? '',\n snippet: snippetMatches[i] ?? '',\n });\n }\n\n return results;\n}\n\nasync function googleSearch(\n query: string,\n num: number,\n signal: AbortSignal,\n): Promise<SearchOutput> {\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 const results = parseGoogleResults(html, num);\n\n return {\n query,\n results,\n source: 'google',\n truncated: results.length >= num,\n };\n}\n\nfunction parseGoogleResults(html: string, num: number): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\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 });\n }\n\n return results;\n}\n\nasync function bingSearch(query: string, num: number, signal: AbortSignal): Promise<SearchOutput> {\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 const results = parseBingResults(html, num);\n\n return {\n query,\n results,\n source: 'bing',\n truncated: results.length >= num,\n };\n}\n\nfunction parseBingResults(html: string, num: number): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\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 results.push({\n title: entries[i]?.title ?? '',\n url: entries[i]?.url ?? '',\n snippet: snippets[i] ?? '',\n });\n }\n\n return results;\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 throw e;\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(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .trim();\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/tools",
3
- "version": "0.267.0",
3
+ "version": "0.269.0",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack built-in tools: read/write/edit, bash/exec, grep/glob, git, fetch, test, lint, and more.",
6
6
  "repository": {
@@ -180,7 +180,7 @@
180
180
  "turndown": "^7.2.0",
181
181
  "typescript": "^6.0.3",
182
182
  "undici": "^8.4.1",
183
- "@wrongstack/core": "0.267.0"
183
+ "@wrongstack/core": "0.269.0"
184
184
  },
185
185
  "devDependencies": {
186
186
  "@types/node": "^25.9.3",