@warlock.js/ai-tools 4.15.0 → 5.0.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/CHANGELOG.md +13 -0
- package/cjs/index.cjs +44 -14
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/http.type.d.mts +13 -1
- package/esm/contracts/http.type.d.mts.map +1 -1
- package/esm/contracts/web.type.d.mts +13 -1
- package/esm/contracts/web.type.d.mts.map +1 -1
- package/esm/errors.d.mts +10 -4
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs.map +1 -1
- package/esm/http/http-request.d.mts +9 -1
- package/esm/http/http-request.d.mts.map +1 -1
- package/esm/http/http-request.mjs +25 -13
- package/esm/http/http-request.mjs.map +1 -1
- package/esm/web/fetch-url.d.mts +9 -1
- package/esm/web/fetch-url.d.mts.map +1 -1
- package/esm/web/fetch-url.mjs +21 -3
- package/esm/web/fetch-url.mjs.map +1 -1
- package/llms-full.txt +6 -2
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/skills/use-web-and-http-tools/SKILL.md +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to `@warlock.js/ai-tools` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
6
|
|
|
7
|
+
## 5.0.0 - 2026-08-25
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
|
|
12
|
+
|
|
13
|
+
## 4.16.0 - 2026-08-18
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- **`fetch_url` and `http_request` now deny private-network targets by default and route through `@warlock.js/ai`'s hardened `guardedFetch` instead of a local host check.** Previously `allowHosts` was the *only* SSRF guardrail and it was opt-in — a bare `ai.tools.fetchUrl()` / `ai.tools.http()` would fetch any `http(s)` URL the model supplied, including `http://169.254.169.254/latest/meta-data/...`, `localhost`, and RFC1918 addresses, and neither tool re-validated redirect targets, so even a configured allowlist could be 302'd into an internal endpoint. Both tools now issue every request through the core outbound policy, which by default refuses private / loopback / link-local / CGNAT / cloud-metadata addresses — resolving hostnames through DNS and checking every returned address, failing closed on resolution failure — and re-validates every redirect `Location` (scheme, allowlist, private-IP deny) before following it, with a hop cap and cross-origin credential-header stripping. Blocks surface as the existing typed errors (`WebToolError` `type: "denied-host"`, `HttpPolicyError` `type: "host-not-allowed"`), so agents still read them as `{ error }` data
|
|
18
|
+
- **New `allowPrivateNetwork` option (default `false`) on both tools** for the deliberate case of a tool that must call an internal service (e.g. a local dev server); `allowHosts` still works and now also constrains redirect targets
|
|
19
|
+
|
|
7
20
|
## 4.12.0
|
|
8
21
|
|
|
9
22
|
### Changed
|
package/cjs/index.cjs
CHANGED
|
@@ -370,8 +370,16 @@ function isJsonContentType(contentType) {
|
|
|
370
370
|
* - **Method allowlist** — `allowMethods` (default `["GET"]`). A method
|
|
371
371
|
* outside the list is rejected with a typed
|
|
372
372
|
* {@link HttpPolicyError} (`type: "method-not-allowed"`).
|
|
373
|
+
* - **Private-network deny (default).** The request — and every redirect
|
|
374
|
+
* hop — goes through the framework's `guardedFetch` outbound policy,
|
|
375
|
+
* which refuses private / loopback / link-local / cloud-metadata
|
|
376
|
+
* addresses (and hostnames resolving to them) unless
|
|
377
|
+
* `allowPrivateNetwork: true` is set. This applies even when
|
|
378
|
+
* `allowHosts` is not configured, so a bare `ai.tools.http()` is not
|
|
379
|
+
* an SSRF primitive (`type: "host-not-allowed"`).
|
|
373
380
|
* - **Host allowlist** — when `allowHosts` is set, any other host is
|
|
374
|
-
* rejected (`type: "host-not-allowed"`), an SSRF guardrail
|
|
381
|
+
* rejected (`type: "host-not-allowed"`), an SSRF guardrail; redirect
|
|
382
|
+
* targets are held to the same allowlist.
|
|
375
383
|
* - **`baseUrl` join** — when configured, the model passes a path that
|
|
376
384
|
* is resolved against `baseUrl`; otherwise it must pass an absolute
|
|
377
385
|
* `http(s)` URL. An unresolvable URL is rejected
|
|
@@ -413,6 +421,7 @@ function httpRequestTool(options = {}) {
|
|
|
413
421
|
const allowMethods = options.allowMethods ?? ["GET"];
|
|
414
422
|
const allowedMethodSet = new Set(allowMethods);
|
|
415
423
|
const allowHostSet = options.allowHosts ? new Set(options.allowHosts) : void 0;
|
|
424
|
+
const allowPrivateNetwork = options.allowPrivateNetwork ?? false;
|
|
416
425
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
|
|
417
426
|
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES$1;
|
|
418
427
|
const staticHeaders = options.headers;
|
|
@@ -436,22 +445,25 @@ function httpRequestTool(options = {}) {
|
|
|
436
445
|
body = JSON.stringify(input.body);
|
|
437
446
|
if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) headers["content-type"] = "application/json";
|
|
438
447
|
}
|
|
439
|
-
const controller = new AbortController();
|
|
440
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
441
|
-
const onAbort = () => controller.abort();
|
|
442
|
-
if (ctx?.signal) if (ctx.signal.aborted) controller.abort();
|
|
443
|
-
else ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
444
448
|
let response;
|
|
445
449
|
try {
|
|
446
|
-
response = await
|
|
450
|
+
response = await (0, _warlock_js_ai.guardedFetch)(url.toString(), {
|
|
451
|
+
allowedSchemes: ["http", "https"],
|
|
452
|
+
hostAllowlist: options.allowHosts,
|
|
453
|
+
denyPrivateIPsAfterDNS: !allowPrivateNetwork,
|
|
454
|
+
timeoutMs,
|
|
455
|
+
signal: ctx?.signal
|
|
456
|
+
}, {
|
|
447
457
|
method,
|
|
448
458
|
headers,
|
|
449
|
-
body
|
|
450
|
-
|
|
459
|
+
body
|
|
460
|
+
});
|
|
461
|
+
} catch (cause) {
|
|
462
|
+
if (cause instanceof _warlock_js_ai.OutboundPolicyError) throw new HttpPolicyError(`http_request blocked: ${cause.message}`, {
|
|
463
|
+
type: "host-not-allowed",
|
|
464
|
+
cause
|
|
451
465
|
});
|
|
452
|
-
|
|
453
|
-
clearTimeout(timer);
|
|
454
|
-
ctx?.signal?.removeEventListener("abort", onAbort);
|
|
466
|
+
throw cause;
|
|
455
467
|
}
|
|
456
468
|
const responseHeaders = {};
|
|
457
469
|
response.headers.forEach((value, key) => {
|
|
@@ -2267,8 +2279,16 @@ function stripTags(html) {
|
|
|
2267
2279
|
* `fetch` (Node 18+) and hand the model back rendered `content`.
|
|
2268
2280
|
*
|
|
2269
2281
|
* Guardrails, applied in order before/around the network call:
|
|
2282
|
+
* - **Private-network deny (default).** Every request — and every
|
|
2283
|
+
* redirect hop — goes through the framework's `guardedFetch` outbound
|
|
2284
|
+
* policy, which refuses private / loopback / link-local /
|
|
2285
|
+
* cloud-metadata addresses (and hostnames resolving to them) unless
|
|
2286
|
+
* `allowPrivateNetwork: true` is set. This applies even when
|
|
2287
|
+
* `allowHosts` is not configured, so a bare `ai.tools.fetchUrl()` is
|
|
2288
|
+
* not an SSRF primitive.
|
|
2270
2289
|
* - **`allowHosts`** — when set, a request to any host not in the list is
|
|
2271
|
-
* rejected before the fetch
|
|
2290
|
+
* rejected before the fetch, and redirect targets are held to the same
|
|
2291
|
+
* allowlist.
|
|
2272
2292
|
* - **`timeoutMs`** — the request is aborted via `AbortSignal.timeout`
|
|
2273
2293
|
* (default {@link DEFAULT_TIMEOUT_MS}).
|
|
2274
2294
|
* - **`maxBytes`** — the response body is read up to this cap and the
|
|
@@ -2298,6 +2318,7 @@ function fetchUrlTool(options) {
|
|
|
2298
2318
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2299
2319
|
const extract = options?.extract ?? DEFAULT_EXTRACT;
|
|
2300
2320
|
const allowHosts = options?.allowHosts;
|
|
2321
|
+
const allowPrivateNetwork = options?.allowPrivateNetwork ?? false;
|
|
2301
2322
|
if (extract !== "html") loadReadability();
|
|
2302
2323
|
return (0, _warlock_js_ai.tool)({
|
|
2303
2324
|
name: options?.name ?? DEFAULT_NAME$1,
|
|
@@ -2308,8 +2329,17 @@ function fetchUrlTool(options) {
|
|
|
2308
2329
|
assertHostAllowed(url, allowHosts);
|
|
2309
2330
|
let response;
|
|
2310
2331
|
try {
|
|
2311
|
-
response = await
|
|
2332
|
+
response = await (0, _warlock_js_ai.guardedFetch)(url.toString(), {
|
|
2333
|
+
allowedSchemes: ["http", "https"],
|
|
2334
|
+
hostAllowlist: allowHosts,
|
|
2335
|
+
denyPrivateIPsAfterDNS: !allowPrivateNetwork,
|
|
2336
|
+
timeoutMs
|
|
2337
|
+
});
|
|
2312
2338
|
} catch (cause) {
|
|
2339
|
+
if (cause instanceof _warlock_js_ai.OutboundPolicyError) throw new WebToolError(`fetch_url blocked: ${cause.message}`, {
|
|
2340
|
+
type: "denied-host",
|
|
2341
|
+
cause
|
|
2342
|
+
});
|
|
2313
2343
|
throw new WebToolError(`fetch_url request failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
2314
2344
|
type: "request-failed",
|
|
2315
2345
|
cause
|