@demigodmode/pi-web-agent 1.10.0 → 1.12.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +19 -4
  3. package/dist/backends/config.d.ts +40 -0
  4. package/dist/backends/config.js +140 -1
  5. package/dist/backends/factory.d.ts +17 -1
  6. package/dist/backends/factory.js +170 -85
  7. package/dist/backends/failure.d.ts +11 -0
  8. package/dist/backends/failure.js +34 -0
  9. package/dist/backends/fallback-policy.d.ts +33 -0
  10. package/dist/backends/fallback-policy.js +239 -0
  11. package/dist/backends/provider-failure.d.ts +21 -0
  12. package/dist/backends/provider-failure.js +111 -0
  13. package/dist/backends/provider-health.d.ts +29 -0
  14. package/dist/backends/provider-health.js +49 -0
  15. package/dist/commands/web-agent-config.d.ts +17 -1
  16. package/dist/commands/web-agent-config.js +131 -7
  17. package/dist/extension.d.ts +1 -0
  18. package/dist/extension.js +49 -3
  19. package/dist/fetch/destination-policy.d.ts +32 -0
  20. package/dist/fetch/destination-policy.js +24 -0
  21. package/dist/fetch/firecrawl-fetch.js +64 -45
  22. package/dist/fetch/guard-proxy-fetch.d.ts +17 -0
  23. package/dist/fetch/guard-proxy-fetch.js +82 -0
  24. package/dist/fetch/guard-proxy.d.ts +58 -0
  25. package/dist/fetch/guard-proxy.js +420 -0
  26. package/dist/fetch/guarded-fetch.d.ts +7 -0
  27. package/dist/fetch/guarded-fetch.js +75 -0
  28. package/dist/fetch/headless-fetch.d.ts +17 -2
  29. package/dist/fetch/headless-fetch.js +181 -9
  30. package/dist/fetch/http-fetch.js +16 -1
  31. package/dist/fetch/network-guard.d.ts +82 -0
  32. package/dist/fetch/network-guard.js +275 -0
  33. package/dist/fetch/proxy-fetch.d.ts +22 -0
  34. package/dist/fetch/proxy-fetch.js +46 -0
  35. package/dist/jiti-compat-run.d.ts +1 -0
  36. package/dist/jiti-compat-run.js +9 -0
  37. package/dist/jiti-compat.d.ts +32 -0
  38. package/dist/jiti-compat.js +215 -0
  39. package/dist/orchestration/answer-synthesizer.js +2 -0
  40. package/dist/orchestration/evidence-quality.d.ts +3 -2
  41. package/dist/orchestration/evidence-quality.js +2 -1
  42. package/dist/orchestration/index.d.ts +23 -0
  43. package/dist/orchestration/index.js +9 -2
  44. package/dist/orchestration/research-orchestrator.d.ts +21 -1
  45. package/dist/orchestration/research-orchestrator.js +40 -7
  46. package/dist/orchestration/research-types.d.ts +13 -1
  47. package/dist/orchestration/research-worker.js +38 -3
  48. package/dist/orchestration/stop-decider.js +3 -1
  49. package/dist/presentation/config-store.js +10 -0
  50. package/dist/presentation/explore-presentation.js +3 -1
  51. package/dist/presentation/fetch-presentation.js +16 -9
  52. package/dist/presentation/search-presentation.d.ts +2 -1
  53. package/dist/presentation/search-presentation.js +13 -1
  54. package/dist/readers/youtube-reader.d.ts +3 -1
  55. package/dist/readers/youtube-reader.js +11 -3
  56. package/dist/search/brave.d.ts +1 -2
  57. package/dist/search/brave.js +23 -80
  58. package/dist/search/duckduckgo.d.ts +7 -3
  59. package/dist/search/duckduckgo.js +17 -18
  60. package/dist/search/exa.d.ts +1 -2
  61. package/dist/search/exa.js +15 -76
  62. package/dist/search/fanout.d.ts +12 -0
  63. package/dist/search/fanout.js +86 -47
  64. package/dist/search/json-provider.d.ts +32 -0
  65. package/dist/search/json-provider.js +76 -0
  66. package/dist/search/searxng.d.ts +1 -2
  67. package/dist/search/searxng.js +15 -57
  68. package/dist/search/tavily.d.ts +1 -2
  69. package/dist/search/tavily.js +17 -74
  70. package/dist/search/youcom.d.ts +1 -2
  71. package/dist/search/youcom.js +15 -76
  72. package/dist/tools/web-explore.d.ts +9 -0
  73. package/dist/tools/web-explore.js +16 -2
  74. package/dist/tools/web-search.js +41 -103
  75. package/dist/types.d.ts +40 -0
  76. package/package.json +4 -3
  77. package/scripts/patch-jiti-compat.mjs +52 -8
package/CHANGELOG.md CHANGED
@@ -18,6 +18,33 @@ The format is intentionally simple and release-oriented.
18
18
  ### Breaking
19
19
  - None.
20
20
 
21
+ ## [1.12.0] - 2026-09-17
22
+ ### Added
23
+ - A network allow list, under Settings → Backends → Network allow list. web_explore now refuses private and local addresses when the link came from the model or a page it read, so if you genuinely want it to read something on your own network (an internal docs site, a service on localhost), add that range here as a CIDR like `10.0.0.0/24`. Entries are checked when you save, and ones that would allow everything (`0.0.0.0/0`, `::/0`) are rejected, since that would quietly turn the protection off. It is also the fix if every fetch suddenly fails: some proxy apps run in fake-IP mode and make every site look like it lives in `198.18.0.0/15`, and adding that range gets you going again. (#53)
24
+ - A "Trust the upstream proxy to enforce private-address restrictions" setting (`backends.network.trustProxyDns`). With a proxy configured, pi-web-agent normally looks each address up itself and asks the proxy to connect to that exact IP, so the proxy can't send a request somewhere else. Some proxies only accept hostnames, and some networks only resolve names inside the proxy. Turning this on hands the hostname to the proxy instead. It is off by default because it moves the address decision to your proxy, and even when it is on, localhost, private IPs typed straight into a link, and anything your own machine resolves to a private address are still refused. `/web-agent doctor` shows the allow list and this setting. (#53)
25
+
26
+ ### Changed
27
+ - Search and fetch fallback now act on why a backend failed. Rate limits pause that backend for the time the provider asks (capped at 15 minutes), exhausted quotas and bad API keys skip it until settings change, and timeouts or server errors get one retry. An empty result is treated as a real answer instead of a failure, a private-address refusal or an invalid shared proxy setting never falls back to another backend, and answers say when some search backends were unavailable. DuckDuckGo no longer retries on its own. (#55)
28
+
29
+ ### Fixed
30
+ - web_explore no longer fetches private, loopback, or link-local addresses when the link came from the model or a page it read. The check happens where connections are opened, through a local guard proxy, so it also covers redirects, DNS changes, and everything a headless page loads. A web page could previously steer it at cloud metadata endpoints or services on your network. Addresses you configured yourself (search backends, SearXNG, Firecrawl, the proxy) are unaffected. If you do need to reach a private range, add it under Settings → Backends → Network allow list. With an upstream proxy that only accepts hostnames, turn on "Trust the upstream proxy to enforce private-address restrictions". (#53)
31
+
32
+ ### Breaking
33
+ - None.
34
+
35
+ ## [1.11.0] - 2026-09-16
36
+ ### Added
37
+ - Optional proxy support. Point web_explore at an HTTP/HTTPS proxy and everything outbound goes through it: search, fetch, Firecrawl, the GitHub/PDF/YouTube readers, doctor health checks, and the headless browser. Off unless you set it, and nothing changes if you don't. Set it in Settings → Backends, and keep credentials in `PI_WEB_AGENT_PROXY_USERNAME` / `PI_WEB_AGENT_PROXY_PASSWORD` rather than in the URL. Thanks to @lo-tp for building this. (#50)
38
+
39
+ ### Changed
40
+ - `/web-agent doctor` reports whether the jsdom compat patch is in place, and prints the command to reapply it if it isn't.
41
+
42
+ ### Fixed
43
+ - Pi failing to start with `Cannot find module 'punycode/'` or `Set operation called on non-Set object`. The patch for this was only applied at install time, but every pi extension shares one node_modules tree, so installing or updating anything else quietly reverted it. It now reapplies every time the extension loads. Also fixes it picking the wrong copy of the dependency when the shared tree holds more than one. (#34)
44
+
45
+ ### Breaking
46
+ - None.
47
+
21
48
  ## [1.10.0] - 2026-08-25
22
49
  ### Added
23
50
  - The zero-config default has a safety net now. When DuckDuckGo blocks a search (which happens fast on VPS and datacenter IPs), web search falls back to Tavily's keyless endpoint, no account or API key needed, so a fresh install still returns results instead of an error. Set `PI_WEB_AGENT_DISABLE_KEYLESS_FALLBACK=1` if you'd rather it just fail. (#42)
package/README.md CHANGED
@@ -4,9 +4,24 @@
4
4
 
5
5
  # pi-web-agent
6
6
 
7
- [![CI](https://github.com/demigodmode/pi-web-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/demigodmode/pi-web-agent/actions/workflows/ci.yml)
8
- [![npm version](https://img.shields.io/npm/v/@demigodmode/pi-web-agent)](https://www.npmjs.com/package/@demigodmode/pi-web-agent)
9
- [![Docs](https://img.shields.io/badge/docs-github%20pages-blue)](https://demigodmode.github.io/pi-web-agent/)
7
+ <p align="center">
8
+ <a href="https://github.com/demigodmode/pi-web-agent/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/demigodmode/pi-web-agent/ci.yml?branch=main&style=flat-square&logo=github&label=CI" alt="CI"></a>
9
+ <a href="https://www.npmjs.com/package/@demigodmode/pi-web-agent"><img src="https://img.shields.io/npm/v/@demigodmode/pi-web-agent?style=flat-square&logo=npm&logoColor=white&color=CB3837" alt="npm version"></a>
10
+ <a href="https://www.npmjs.com/package/@demigodmode/pi-web-agent"><img src="https://img.shields.io/npm/dm/@demigodmode/pi-web-agent?style=flat-square&color=0A7BBB&label=downloads" alt="npm downloads"></a>
11
+ <img src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-475569?style=flat-square" alt="Platform: macOS, Linux, Windows">
12
+ <a href="https://demigodmode.github.io/pi-web-agent/"><img src="https://img.shields.io/badge/docs-github%20pages-2088FF?style=flat-square&logo=readthedocs&logoColor=white" alt="Docs"></a>
13
+ <a href="https://github.com/demigodmode/pi-web-agent/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@demigodmode/pi-web-agent?style=flat-square&color=6F42C1" alt="License"></a>
14
+ </p>
15
+
16
+ <p align="center">
17
+ <img src="https://img.shields.io/badge/search%20backends-475569?style=flat-square" alt="Search backends">
18
+ <img src="https://img.shields.io/badge/DuckDuckGo-475569?style=flat-square&logo=duckduckgo&logoColor=white" alt="DuckDuckGo">
19
+ <img src="https://img.shields.io/badge/SearXNG-475569?style=flat-square&logo=searxng&logoColor=white" alt="SearXNG">
20
+ <img src="https://img.shields.io/badge/Brave-475569?style=flat-square&logo=brave&logoColor=white" alt="Brave Search">
21
+ <img src="https://img.shields.io/badge/You.com-475569?style=flat-square" alt="You.com">
22
+ <img src="https://img.shields.io/badge/Exa-475569?style=flat-square" alt="Exa">
23
+ <img src="https://img.shields.io/badge/Tavily-475569?style=flat-square" alt="Tavily">
24
+ </p>
10
25
 
11
26
  One public tool, `web_explore`, that does bounded web research for Pi: search, fetch, targeted browser rendering, ranking, and honest caveats, all behind a single call.
12
27
 
@@ -26,7 +41,7 @@ One public tool, `web_explore`, that does bounded web research for Pi: search, f
26
41
 
27
42
  Compared to other web tooling for agents:
28
43
 
29
- - **Hands-off.** No curator, no browser windows to approve, no step that pops you out of your session. Ask `web_explore` once and the answer comes back with caveats. Nothing to babysit.
44
+ - **Hands-off.** No curator, no browser windows to approve, no step that pops you out of your session. Ask `web_explore` once and the answer comes back cleanly. Nothing to babysit.
30
45
  - **Keyless by default.** Search, page reads, and the GitHub/PDF/YouTube readers all work with no API keys. Add hosted providers only when you want them.
31
46
  - **Bounded and honest.** Compact output by default, and it says when a read was not good enough instead of returning fake confidence.
32
47
 
@@ -12,6 +12,11 @@ export type FirecrawlOptions = {
12
12
  formats?: string[];
13
13
  onlyMainContent?: boolean;
14
14
  };
15
+ export type ProxyConfig = {
16
+ url: string;
17
+ username?: string;
18
+ password?: string;
19
+ };
15
20
  export type SearchBackendConfig = {
16
21
  provider: 'duckduckgo' | 'searxng' | 'brave' | 'youcom' | 'exa' | 'tavily';
17
22
  baseUrl?: string;
@@ -29,15 +34,25 @@ export type FetchBackendConfig = {
29
34
  export type HeadlessBackendConfig = {
30
35
  provider: 'local-browser';
31
36
  };
37
+ export type NetworkConfig = {
38
+ /** CIDR ranges exempted from the private-address guard (#53). */
39
+ allowRanges?: string[];
40
+ /** Trust the upstream proxy to enforce private-address restrictions. */
41
+ trustProxyDns?: boolean;
42
+ };
32
43
  export type BackendConfig = {
33
44
  search: SearchBackendConfig;
34
45
  fetch: FetchBackendConfig;
35
46
  headless: HeadlessBackendConfig;
47
+ proxy?: ProxyConfig;
48
+ network?: NetworkConfig;
36
49
  };
37
50
  export type BackendConfigOverride = {
38
51
  search?: Partial<SearchBackendConfig>;
39
52
  fetch?: Partial<FetchBackendConfig>;
40
53
  headless?: Partial<HeadlessBackendConfig>;
54
+ proxy?: ProxyConfig;
55
+ network?: NetworkConfig;
41
56
  };
42
57
  export type BackendConfigFile = {
43
58
  backends?: {
@@ -58,9 +73,34 @@ export type BackendConfigFile = {
58
73
  headless?: {
59
74
  provider?: unknown;
60
75
  };
76
+ proxy?: {
77
+ url?: unknown;
78
+ username?: unknown;
79
+ password?: unknown;
80
+ };
81
+ network?: {
82
+ allowRanges?: unknown;
83
+ trustProxyDns?: unknown;
84
+ };
61
85
  };
62
86
  };
63
87
  export declare const DEFAULT_BACKEND_CONFIG: BackendConfig;
88
+ /**
89
+ * Remove any credentials (user:pass@) embedded in a proxy URL. pi-web-agent
90
+ * never reads or sends credentials from the URL; proxy auth comes from
91
+ * backends.proxy.username/password or PI_WEB_AGENT_PROXY_USERNAME /
92
+ * PI_WEB_AGENT_PROXY_PASSWORD. Stripping them here keeps them out of logs,
93
+ * doctor output, the settings UI, and the proxy connection itself.
94
+ */
95
+ export declare function stripProxyCredentials(url: string): string;
96
+ /**
97
+ * Whether a proxy url passes validation: a blank url is the "disable proxy"
98
+ * marker and passes; anything else must parse as an http(s) URL. This is a
99
+ * pure syntax check — no connectivity check is performed.
100
+ */
101
+ export declare function isValidProxyUrl(url: string): boolean;
102
+ export declare function extractProxyConfig(value: unknown): ProxyConfig | undefined;
103
+ export declare function extractNetworkConfig(value: unknown): NetworkConfig | undefined;
64
104
  export declare function usableSearchProviders(search: SearchBackendConfig, env?: NodeJS.ProcessEnv): SearchProviderName[];
65
105
  export declare function extractBackendConfigOverride(file: BackendConfigFile | null | undefined): BackendConfigOverride;
66
106
  export declare function validateBackendConfig(config: BackendConfig): string[];
@@ -1,3 +1,4 @@
1
+ import { parseCidr } from '../fetch/network-guard.js';
1
2
  export const DEFAULT_BACKEND_CONFIG = {
2
3
  search: { provider: 'duckduckgo' },
3
4
  fetch: { provider: 'http' },
@@ -9,6 +10,89 @@ function extractStringArray(value) {
9
10
  const strings = value.filter((item) => typeof item === 'string');
10
11
  return strings.length === value.length ? strings : undefined;
11
12
  }
13
+ /**
14
+ * Remove any credentials (user:pass@) embedded in a proxy URL. pi-web-agent
15
+ * never reads or sends credentials from the URL; proxy auth comes from
16
+ * backends.proxy.username/password or PI_WEB_AGENT_PROXY_USERNAME /
17
+ * PI_WEB_AGENT_PROXY_PASSWORD. Stripping them here keeps them out of logs,
18
+ * doctor output, the settings UI, and the proxy connection itself.
19
+ */
20
+ export function stripProxyCredentials(url) {
21
+ let parsed;
22
+ try {
23
+ parsed = new URL(url);
24
+ }
25
+ catch {
26
+ return url;
27
+ }
28
+ if (!parsed.username && !parsed.password)
29
+ return url; // already credential-free
30
+ parsed.username = '';
31
+ parsed.password = '';
32
+ return parsed.toString().replace(/\/$/, '');
33
+ }
34
+ /**
35
+ * Whether a proxy url passes validation: a blank url is the "disable proxy"
36
+ * marker and passes; anything else must parse as an http(s) URL. This is a
37
+ * pure syntax check — no connectivity check is performed.
38
+ */
39
+ export function isValidProxyUrl(url) {
40
+ if (url.trim() === '')
41
+ return true;
42
+ try {
43
+ const parsed = new URL(url);
44
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
50
+ export function extractProxyConfig(value) {
51
+ if (!value || typeof value !== 'object')
52
+ return undefined;
53
+ const raw = value;
54
+ // An explicitly present but blank url is the "disable proxy" marker: it lets a
55
+ // higher-priority layer (e.g. a project) clear a proxy set in a lower layer.
56
+ if (typeof raw.url === 'string' && raw.url.trim() === '') {
57
+ return { url: '' };
58
+ }
59
+ if (typeof raw.url !== 'string' || !raw.url.trim())
60
+ return undefined;
61
+ const url = raw.url.trim();
62
+ let parsed;
63
+ try {
64
+ parsed = new URL(url);
65
+ }
66
+ catch {
67
+ parsed = undefined;
68
+ }
69
+ // A valid url is normalized. A malformed url is kept as-is (rather than
70
+ // dropped) so validation can flag it and the backend factory can fail loudly
71
+ // instead of silently sending traffic direct to the websites.
72
+ const config = {
73
+ url: parsed && (parsed.protocol === 'http:' || parsed.protocol === 'https:')
74
+ ? parsed.toString().replace(/\/$/, '')
75
+ : url
76
+ };
77
+ if (typeof raw.username === 'string' && raw.username.trim())
78
+ config.username = raw.username;
79
+ if (typeof raw.password === 'string')
80
+ config.password = raw.password;
81
+ return config;
82
+ }
83
+ export function extractNetworkConfig(value) {
84
+ if (!value || typeof value !== 'object')
85
+ return undefined;
86
+ const raw = value;
87
+ const config = {};
88
+ if (Array.isArray(raw.allowRanges)) {
89
+ // Coerce non-string entries so validation flags them instead of dropping the list.
90
+ config.allowRanges = raw.allowRanges.map((entry) => (typeof entry === 'string' ? entry : String(entry)));
91
+ }
92
+ if (typeof raw.trustProxyDns === 'boolean')
93
+ config.trustProxyDns = raw.trustProxyDns;
94
+ return Object.keys(config).length > 0 ? config : undefined;
95
+ }
12
96
  function extractSearxngOptions(value) {
13
97
  if (!value || typeof value !== 'object')
14
98
  return undefined;
@@ -113,6 +197,14 @@ export function extractBackendConfigOverride(file) {
113
197
  if (backends?.headless?.provider === 'local-browser') {
114
198
  override.headless = { provider: 'local-browser' };
115
199
  }
200
+ const proxy = extractProxyConfig(backends?.proxy);
201
+ if (proxy) {
202
+ override.proxy = proxy;
203
+ }
204
+ const network = extractNetworkConfig(backends?.network);
205
+ if (network) {
206
+ override.network = network;
207
+ }
116
208
  return override;
117
209
  }
118
210
  export function validateBackendConfig(config) {
@@ -120,9 +212,38 @@ export function validateBackendConfig(config) {
120
212
  if (config.search.provider === 'searxng' && !config.search.baseUrl) {
121
213
  issues.push('search provider searxng requires backends.search.baseUrl');
122
214
  }
215
+ if (config.proxy && config.proxy.url.trim() !== '') {
216
+ let parsed;
217
+ try {
218
+ parsed = new URL(config.proxy.url);
219
+ }
220
+ catch {
221
+ parsed = undefined;
222
+ }
223
+ if (!isValidProxyUrl(config.proxy.url)) {
224
+ issues.push('backends.proxy.url must be an http or https URL');
225
+ }
226
+ else if (parsed && (parsed.username || parsed.password)) {
227
+ // Credentials belong in backends.proxy.username/password or the env vars
228
+ // below, never in the URL itself.
229
+ issues.push('backends.proxy.url must not include credentials (user:pass@); set PI_WEB_AGENT_PROXY_USERNAME and PI_WEB_AGENT_PROXY_PASSWORD (or backends.proxy.username / backends.proxy.password) instead');
230
+ }
231
+ }
123
232
  if (config.fetch.provider === 'firecrawl' && !config.fetch.baseUrl) {
124
233
  issues.push('fetch provider firecrawl requires backends.fetch.baseUrl');
125
234
  }
235
+ for (const range of config.network?.allowRanges ?? []) {
236
+ const cidr = parseCidr(range);
237
+ if (!cidr) {
238
+ issues.push(`backends.network.allowRanges entry "${range}" is not a valid CIDR range`);
239
+ }
240
+ else if (cidr.prefix === 0) {
241
+ issues.push(`backends.network.allowRanges entry "${range}" allows every address, which turns the guard off; list specific ranges instead`);
242
+ }
243
+ }
244
+ if (config.network?.trustProxyDns && !config.proxy?.url?.trim()) {
245
+ issues.push('backends.network.trustProxyDns has no effect without backends.proxy');
246
+ }
126
247
  if (config.search.fallback === 'duckduckgo' && config.search.provider !== 'searxng' && config.search.provider !== 'brave' && config.search.provider !== 'youcom' && config.search.provider !== 'exa' && config.search.provider !== 'tavily') {
127
248
  issues.push('search fallback duckduckgo is only supported when search provider is searxng, brave, youcom, exa, or tavily');
128
249
  }
@@ -169,10 +290,28 @@ function mergeFetchConfig(current, override) {
169
290
  }
170
291
  return { ...current, ...override };
171
292
  }
293
+ function mergeNetworkConfig(base, layer) {
294
+ if (!layer)
295
+ return base;
296
+ const next = { ...base };
297
+ // A layer's allow list replaces the lower one outright; trust is set independently.
298
+ if (layer.allowRanges)
299
+ next.allowRanges = [...layer.allowRanges];
300
+ if (layer.trustProxyDns !== undefined)
301
+ next.trustProxyDns = layer.trustProxyDns;
302
+ return next;
303
+ }
172
304
  export function mergeBackendConfigLayers(...layers) {
173
305
  return layers.reduce((merged, layer) => ({
174
306
  search: mergeSearchConfig(merged.search, layer?.search),
175
307
  fetch: mergeFetchConfig(merged.fetch, layer?.fetch),
176
- headless: { ...merged.headless, ...layer?.headless }
308
+ headless: { ...merged.headless, ...layer?.headless },
309
+ proxy: layer?.proxy
310
+ ? layer.proxy.url === ''
311
+ ? undefined // explicit disable overrides any proxy from lower layers
312
+ : { ...merged.proxy, ...layer.proxy }
313
+ : merged.proxy,
314
+ // Replace, don't union: a project list is the whole list for that project.
315
+ network: mergeNetworkConfig(merged.network, layer?.network)
177
316
  }), DEFAULT_BACKEND_CONFIG);
178
317
  }
@@ -1,14 +1,18 @@
1
1
  import { createFirecrawlFetcher } from '../fetch/firecrawl-fetch.js';
2
+ import { type GuardProxy, type GuardProxyOptions } from '../fetch/guard-proxy.js';
3
+ import { type NetworkGuard } from '../fetch/network-guard.js';
2
4
  import { createBraveSearchTool } from '../search/brave.js';
3
5
  import { createYouComSearchTool } from '../search/youcom.js';
4
6
  import { createExaSearchTool } from '../search/exa.js';
5
7
  import { createTavilySearchTool } from '../search/tavily.js';
6
8
  import { createSearxngSearchTool } from '../search/searxng.js';
9
+ import { type PolicyDeps } from './fallback-policy.js';
10
+ import { type ProviderHealth } from './provider-health.js';
7
11
  import { createWebFetchHeadlessTool } from '../tools/web-fetch-headless.js';
8
12
  import { createWebFetchTool } from '../tools/web-fetch.js';
9
13
  import { createWebSearchTool } from '../tools/web-search.js';
10
14
  import type { WebFetchHeadlessResponse, WebFetchResponse, WebSearchResponse } from '../types.js';
11
- import { type BackendConfig } from './config.js';
15
+ import { type BackendConfig, type ProxyConfig } from './config.js';
12
16
  export type BackendSet = {
13
17
  search: (input: {
14
18
  query: string;
@@ -19,6 +23,8 @@ export type BackendSet = {
19
23
  headlessFetch: (input: {
20
24
  url: string;
21
25
  }) => Promise<WebFetchHeadlessResponse>;
26
+ /** Releases the guard proxy and its agents. Idempotent; never starts the proxy. */
27
+ close: () => Promise<void>;
22
28
  };
23
29
  export type BackendFactoryDeps = {
24
30
  createDuckDuckGoSearch?: typeof createWebSearchTool;
@@ -30,5 +36,15 @@ export type BackendFactoryDeps = {
30
36
  createHttpFetch?: typeof createWebFetchTool;
31
37
  createFirecrawlFetch?: typeof createFirecrawlFetcher;
32
38
  createHeadlessFetch?: typeof createWebFetchHeadlessTool;
39
+ createProxyFetch?: (proxy: ProxyConfig) => typeof fetch;
40
+ networkGuard?: NetworkGuard;
41
+ /** Test seam: the fetch used for model-chosen URLs, before redirect handling. */
42
+ createModelFetch?: (guard: NetworkGuard) => typeof fetch;
43
+ createGuardProxy?: (options: GuardProxyOptions) => Promise<GuardProxy>;
44
+ providerHealth?: ProviderHealth;
45
+ /** Test seam for the retry sleep, jitter, and clock. */
46
+ policy?: Omit<PolicyDeps, 'health'>;
47
+ /** Test seam for the per-call fanout provider timeout. */
48
+ fanoutTimeoutMs?: number;
33
49
  };
34
50
  export declare function createBackendSet(config?: BackendConfig, deps?: BackendFactoryDeps): BackendSet;