@@ -35,6 +35,8 @@ import {
3535 killBackgroundCommandsForWorkspace ,
3636} from "./agent-tools/execution" ;
3737import { gitStatus , gitDiff , gitLog , gitCommit } from "./agent-tools/git" ;
38+ import { NETWORK_TOOLS , fetchUrl , httpRequest , webSearch } from "./agent-tools/network" ;
39+ import type { WebSearchResult } from "./agent-tools/network" ;
3840
3941export { AGENT_TOOLS } ;
4042export {
@@ -68,126 +70,8 @@ export {
6870 killBackgroundCommandsForWorkspace ,
6971} ;
7072export { gitStatus , gitDiff , gitLog , gitCommit } ;
71-
72- const WEB_FETCH_TIMEOUT_MS = 15_000 ;
73- const MAX_FETCH_CHARS = 30_000 ;
74- const MAX_SEARCH_RESULTS_WEB = 5 ;
75-
76- // Crude HTML-to-text: drop non-content tags outright, then strip remaining
77- // markup and collapse whitespace. Not a real HTML parser — good enough for
78- // giving a model readable page text without pulling in a DOM library in the
79- // main process.
80- function htmlToText ( html : string ) : string {
81- return html
82- . replace ( / < s c r i p t [ \s \S ] * ?< \/ s c r i p t > / gi, " " )
83- . replace ( / < s t y l e [ \s \S ] * ?< \/ s t y l e > / gi, " " )
84- . replace ( / < ! - - [ \s \S ] * ?- - > / g, " " )
85- . replace ( / < [ ^ > ] + > / g, " " )
86- . replace ( / & n b s p ; / g, " " )
87- . replace ( / & a m p ; / g, "&" )
88- . replace ( / & l t ; / g, "<" )
89- . replace ( / & g t ; / g, ">" )
90- . replace ( / & q u o t ; / g, '"' )
91- . replace ( / & # 3 9 ; / g, "'" )
92- . replace ( / [ \t ] + / g, " " )
93- . replace ( / \n \s * \n + / g, "\n\n" )
94- . trim ( ) ;
95- }
96-
97- export async function fetchUrl ( url : string ) : Promise < string > {
98- let parsed : URL ;
99- try {
100- parsed = new URL ( url ) ;
101- } catch {
102- throw new Error ( `"${ url } " is not a valid URL.` ) ;
103- }
104- if ( parsed . protocol !== "http:" && parsed . protocol !== "https:" ) {
105- throw new Error ( "Only http:// and https:// URLs can be fetched." ) ;
106- }
107-
108- const res = await fetch ( parsed , {
109- signal : AbortSignal . timeout ( WEB_FETCH_TIMEOUT_MS ) ,
110- headers : { "User-Agent" : "Mozilla/5.0 (compatible; Modelforge/1.0)" } ,
111- } ) ;
112- if ( ! res . ok ) throw new Error ( `Failed to fetch ${ url } : HTTP ${ res . status } ` ) ;
113- const contentType = res . headers . get ( "content-type" ) ?? "" ;
114- const raw = await res . text ( ) ;
115- const text = contentType . includes ( "html" ) ? htmlToText ( raw ) : raw ;
116- return text . length > MAX_FETCH_CHARS
117- ? `${ text . slice ( 0 , MAX_FETCH_CHARS ) } \n\n[truncated — page is ${ text . length } characters]`
118- : text ;
119- }
120-
121- const MAX_HTTP_BODY_CHARS = 20_000 ;
122-
123- export async function httpRequest (
124- url : string ,
125- method = "GET" ,
126- headers ?: Record < string , string > ,
127- body ?: string
128- ) : Promise < string > {
129- let parsed : URL ;
130- try {
131- parsed = new URL ( url ) ;
132- } catch {
133- throw new Error ( `"${ url } " is not a valid URL.` ) ;
134- }
135- if ( parsed . protocol !== "http:" && parsed . protocol !== "https:" ) {
136- throw new Error ( "Only http:// and https:// URLs can be requested." ) ;
137- }
138-
139- const res = await fetch ( parsed , {
140- method,
141- headers,
142- body : method !== "GET" && method !== "DELETE" ? body : undefined ,
143- signal : AbortSignal . timeout ( WEB_FETCH_TIMEOUT_MS ) ,
144- } ) ;
145- const text = await res . text ( ) ;
146- const truncated =
147- text . length > MAX_HTTP_BODY_CHARS
148- ? `${ text . slice ( 0 , MAX_HTTP_BODY_CHARS ) } \n[truncated — body is ${ text . length } characters]`
149- : text ;
150- return `HTTP ${ res . status } ${ res . statusText } \n--- body ---\n${ truncated || "(empty body)" } ` ;
151- }
152-
153- export interface WebSearchResult {
154- title : string ;
155- url : string ;
156- snippet : string ;
157- }
158-
159- // Uses DuckDuckGo's keyless HTML endpoint (no API key/signup needed, unlike
160- // most search APIs) and regex-scrapes the result markup — brittle if DDG
161- // changes its HTML, but keeps this tool usable out of the box with zero
162- // configuration, consistent with the rest of Agent mode's tools.
163- export async function webSearch ( query : string ) : Promise < WebSearchResult [ ] > {
164- const res = await fetch ( `https://html.duckduckgo.com/html/?q=${ encodeURIComponent ( query ) } ` , {
165- signal : AbortSignal . timeout ( WEB_FETCH_TIMEOUT_MS ) ,
166- headers : { "User-Agent" : "Mozilla/5.0 (compatible; Modelforge/1.0)" } ,
167- } ) ;
168- if ( ! res . ok ) throw new Error ( `Web search failed: HTTP ${ res . status } ` ) ;
169- const html = await res . text ( ) ;
170-
171- const results : WebSearchResult [ ] = [ ] ;
172- const linkPattern = / < a [ ^ > ] * c l a s s = " r e s u l t _ _ a " [ ^ > ] * h r e f = " ( [ ^ " ] + ) " [ ^ > ] * > ( [ \s \S ] * ?) < \/ a > / gi;
173- const snippetPattern = / < a [ ^ > ] * c l a s s = " r e s u l t _ _ s n i p p e t " [ ^ > ] * > ( [ \s \S ] * ?) < \/ a > / gi;
174- const links = [ ...html . matchAll ( linkPattern ) ] ;
175- const snippets = [ ...html . matchAll ( snippetPattern ) ] ;
176-
177- for ( let i = 0 ; i < links . length && results . length < MAX_SEARCH_RESULTS_WEB ; i ++ ) {
178- const href = links [ i ] [ 1 ] ;
179- // DuckDuckGo's HTML endpoint wraps result URLs in a redirect
180- // (/l/?uddg=<encoded target>) rather than linking straight to them.
181- const uddgMatch = href . match ( / [ ? & ] u d d g = ( [ ^ & ] + ) / ) ;
182- const url = uddgMatch ? decodeURIComponent ( uddgMatch [ 1 ] ) : href ;
183- results . push ( {
184- title : htmlToText ( links [ i ] [ 2 ] ) ,
185- url,
186- snippet : htmlToText ( snippets [ i ] ?. [ 1 ] ?? "" ) ,
187- } ) ;
188- }
189- return results ;
190- }
73+ export { fetchUrl , httpRequest , webSearch } ;
74+ export type { WebSearchResult } ;
19175
19276function requireGitHubToken ( ) : string {
19377 const token = getAccountToken ( "github" ) ;
@@ -267,21 +151,6 @@ export async function githubReadFile(repository: string, filePath: string, ref?:
267151 return content . length > MAX_READ_CHARS ? `${ content . slice ( 0 , MAX_READ_CHARS ) } \n\n[truncated]` : content ;
268152}
269153
270- // Tools that reach the network — gated by settings.networkToolsEnabled as a
271- // baseline that's 100% enforceable on every platform (refusing to run the
272- // tool at all, rather than trying to block network access after the fact,
273- // which is what command-sandbox.ts's per-call `network` argument does for
274- // run_command/run_code/start_background_command instead).
275- const NETWORK_TOOLS = new Set ( [
276- "web_search" ,
277- "fetch_url" ,
278- "http_request" ,
279- "capture_page_screenshot" ,
280- "github_list_repositories" ,
281- "github_repository_tree" ,
282- "github_read_file" ,
283- ] ) ;
284-
285154export async function executeTool ( workspaceRoot : string , name : string , args : Record < string , unknown > ) : Promise < unknown > {
286155 if ( NETWORK_TOOLS . has ( name ) && settingsStore . getSettings ( ) . networkToolsEnabled === false ) {
287156 throw new Error ( `Network access for agent tools is turned off in Settings — "${ name } " can't run.` ) ;
0 commit comments